blob: 052da890ea209f76cd5105fd49d48e69d81cb88d [file] [log] [blame]
/**
* @module modules/resources-sk
* @description A view of the shared images that are present in an MSKP file.
* Contains a scrollable area suitable for viewing images with transparency
* and an image selection function so different parts of the app can send you
* to an image here, or you can pick one and view details about it.
*/
import { define } from 'elements-sk/define';
import { html, TemplateResult } from 'lit-html';
import { ElementDocSk } from '../element-doc-sk/element-doc-sk';
import { DefaultMap } from '../default-map';
import { SkpDebugPlayer } from '../debugger';
import {
JumpCommandEventDetail,
JumpCommandEvent,
JumpInspectLayerEvent,
ToggleBackgroundEventDetail, InspectLayerEventDetail, ToggleBackgroundEvent, BackgroundStyle,
} from '../events';
interface ImageItem {
// The image indices provided from the debugger are contiguous
index: number;
width: number;
height: number;
pngUri: string;
// A map from commands to lists of frames
uses: DefaultMap<number, number[]>;
// An image use map for every layer id.
layeruses: DefaultMap<number, DefaultMap<number, number[]>>;
}
function displayName(item: ImageItem): string {
return `${item.index} (${item.width}, ${item.height})`;
}
export class ResourcesSk extends ElementDocSk {
private static template = (ele: ResourcesSk) => html`
<p>${ele.list.length} images were stored in this file. Note that image indices here are
file indices, which corresponded 1:1 to the gen ids that the images had during recording.
If an image appears more than once, that indicates there were multiple copies of it in
memory at record time. These indices appear in the 'imageIndex' field of commands using
them. This metadata is only recorded for multi frame (mskp) files from android, so
nothing is shown here for other skps. All images in SKPs are serialized as PNG regardless
of their original encoding.
</p>
<div class="main-box ${ele.backdropStyle}">
${ele.list.map((item: ImageItem) => ResourcesSk.templateImage(ele, item))}
</div>
<div class="selection-detail">
Selected: ${ele.selection !== null
? ResourcesSk.templateSelectionDetail(ele, ele.list[ele.selection])
: 'none'
}
</div>`;
private static templateImage = (ele: ResourcesSk, item: ImageItem) => html`
<div class="image-box">
<span class="resource-name ${ele.textContrast()}"
@click=${() => { ele.selectItem(item.index); } /* makes it easier to select 1x1 images */}>
${displayName(item)}
</span><br>
<img src="${item.pngUri}" id="res-img-${item.index}"
class="outline-on-hover ${ele.selection === item.index ? 'selected-image' : ''}"
@click=${() => { ele.selectItem(item.index); }}/>
</div>`;
private static templateSelectionDetail = (ele: ResourcesSk, item: ImageItem) => html`
<b>${item.index}</b><br>
size: (${item.width}, ${item.height})<br>
Usage in top-level skp
${item.uses.size > 0
? html`<table class="usage-table">${ele.usageTable(item.uses)}</table>`
: html`<p>No uses found in any drawImage* commands. May occur in shaders.</p>`
}
Usage in offscreen buffers
${item.layeruses.size > 0
? html`<table class="usage-table">${ele.layerUsageTable(item.layeruses)}</table>`
: html`<p>Not used</p>`
}`;
private list: ImageItem[] = [];
private selection: number | null = null;
private backdropStyle: BackgroundStyle = 'light-checkerboard';
constructor() {
super(ResourcesSk.template);
}
connectedCallback(): void {
super.connectedCallback();
this._render();
this.addDocumentEventListener(ToggleBackgroundEvent, (e) => {
this.backdropStyle = (e as CustomEvent<ToggleBackgroundEventDetail>).detail.mode;
this._render();
});
}
reset(): void {
this.list = [];
this.selection = null;
this._render();
}
// Load resource data for current file from player and build the array that
// drives the resource grid template
// To be called once after file load.
// resources-sk will not save any reference to the player.
update(player: SkpDebugPlayer): void {
// At the time of this writing, only MSKP files recorded on android have the necessary metadata
// to show shared image use across the file.
const imageCount = player.getImageCount();
this.list = [];
for (let i = 0; i < imageCount; i++) {
const info = player.getImageInfo(i);
this.list.push({
index: i,
width: info.width,
height: info.height,
pngUri: player.getImageResource(i),
// this will be populated below
uses: new DefaultMap<number, number[]>(() => []),
// one use map for every layer.
layeruses: new DefaultMap<number, DefaultMap<number, number[]>>(
() => new DefaultMap<number, number[]>(() => []),
),
});
}
// Collect uses at top level
for (let fp = 0; fp < player.getFrameCount(); fp++) {
const oneFrameUseMap = player.imageUseInfo(fp, -1);
for (const [imageIdStr, listOfCommands] of Object.entries(oneFrameUseMap)) {
const id = parseInt(imageIdStr, 10);
for (const com of listOfCommands) {
this.list[id].uses.get(com).push(fp);
}
}
}
// collect uses for every layer
const keys = player.getLayerKeys();
for (const key of keys) {
const useMap = player.imageUseInfo(key.frame, key.nodeId);
for (const [imageIdStr, listOfCommands] of Object.entries(useMap)) {
const id = parseInt(imageIdStr, 10);
for (const com of listOfCommands) {
this.list[id].layeruses.get(key.nodeId).get(com).push(key.frame);
}
}
}
this._render();
}
selectItem(i: number, scroll: boolean = false): void {
this.selection = i;
this._render();
if (scroll) {
this.querySelector<HTMLImageElement>(`#res-img-${i}`)?.scrollIntoView({ block: 'nearest' });
}
}
private textContrast() {
if (this.backdropStyle === 'light-checkerboard') {
return 'dark-text';
}
return 'light-text';
}
// Supply a non-negative nodeId to make jump actions go to a particular layer
private usageTable(uses: Map<number, number[]>, nodeId: number = -1): TemplateResult[] {
const out: TemplateResult[] = [];
uses.forEach((frames: number[], key: number) => {
out.push(html`
<tr>
<td class="command-cell"> Command <b>${key}</b> on frames: </td>
${frames.map((f: number) => html`
<td title="Jump to command ${key} frame ${f} ${
nodeId >= 0 ? `on layer ${nodeId}` : ''
}"
class="clickable-cell"
@click=${() => { this.jump(f, key, nodeId); }}>${f}</td>`)}
</tr>`);
});
return out;
}
private layerUsageTable(layeruses: DefaultMap<number, DefaultMap<number, number[]>>): TemplateResult[] {
const out: TemplateResult[] = [];
layeruses.forEach((usemap: DefaultMap<number, number[]>, nodeid: number) => {
out.push(html`
<tr><td class="layer-cell"><b>Layer ${nodeid}</b></td></tr>
${this.usageTable(usemap, nodeid)}`);
});
return out;
}
private jump(frame: number, command: number, nodeId: number): void {
// Note that the app may already be in the inspector for a layer.
// even for non-layer jumps, this is an appropriate way to get there,
// becuase it will close the inspector if needed.
// debugger-page-sk will move the frame when handling this event.
this.dispatchEvent(
new CustomEvent<InspectLayerEventDetail>(
JumpInspectLayerEvent, {
detail: { id: nodeId, frame: frame },
bubbles: true,
},
),
);
this.dispatchEvent(
new CustomEvent<JumpCommandEventDetail>(
JumpCommandEvent, {
detail: { unfilteredIndex: command },
bubbles: true,
},
),
);
}
}
define('resources-sk', ResourcesSk);