GIF: slow, but rendering GIFs
This commit is contained in:
parent
8f6bb74e17
commit
7f4c90178d
|
@ -35,6 +35,8 @@ export default (rootNode, getNodeUi) => {
|
||||||
let hoveringOver = null;
|
let hoveringOver = null;
|
||||||
|
|
||||||
const rerender = () => {
|
const rerender = () => {
|
||||||
|
console.time("rerender");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback IsActivatedFn
|
* @callback IsActivatedFn
|
||||||
* @param {NodePath} path
|
* @param {NodePath} path
|
||||||
|
@ -75,6 +77,8 @@ export default (rootNode, getNodeUi) => {
|
||||||
if (!path) continue;
|
if (!path) continue;
|
||||||
treeEl.classList.toggle("activated", isTreeElActivated(path));
|
treeEl.classList.toggle("activated", isTreeElActivated(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.timeEnd("rerender");
|
||||||
};
|
};
|
||||||
|
|
||||||
outerBytesEl.addEventListener("click", (event) => {
|
outerBytesEl.addEventListener("click", (event) => {
|
||||||
|
@ -87,11 +91,9 @@ export default (rootNode, getNodeUi) => {
|
||||||
if (explorerEl.dataset.path) explorerEl.removeAttribute("open");
|
if (explorerEl.dataset.path) explorerEl.removeAttribute("open");
|
||||||
}
|
}
|
||||||
|
|
||||||
const leafEl = outerTreeEl.querySelector(
|
const leafEl = outerTreeEl.querySelector(`details[data-path="${rawPath}"]`);
|
||||||
`details[data-path="${rawPath}"]`,
|
|
||||||
);
|
|
||||||
let currentTreeEl = leafEl;
|
let currentTreeEl = leafEl;
|
||||||
while (currentTreeEl && (currentTreeEl !== outerTreeEl)) {
|
while (currentTreeEl && currentTreeEl !== outerTreeEl) {
|
||||||
currentTreeEl.setAttribute("open", "open");
|
currentTreeEl.setAttribute("open", "open");
|
||||||
currentTreeEl = currentTreeEl.parentElement;
|
currentTreeEl = currentTreeEl.parentElement;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +126,7 @@ export default (rootNode, getNodeUi) => {
|
||||||
if (event.target.classList.contains("children")) return;
|
if (event.target.classList.contains("children")) return;
|
||||||
|
|
||||||
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
||||||
while (nodeToCheckForPath && (nodeToCheckForPath !== outerTreeEl)) {
|
while (nodeToCheckForPath && nodeToCheckForPath !== outerTreeEl) {
|
||||||
if (nodeToCheckForPath.dataset.path) break;
|
if (nodeToCheckForPath.dataset.path) break;
|
||||||
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
||||||
}
|
}
|
||||||
|
@ -137,10 +139,12 @@ export default (rootNode, getNodeUi) => {
|
||||||
/** @param {UIEvent} event */
|
/** @param {UIEvent} event */
|
||||||
const onBlurExplorerTree = (event) => {
|
const onBlurExplorerTree = (event) => {
|
||||||
if (
|
if (
|
||||||
(event.target !== outerTreeEl) &&
|
event.target !== outerTreeEl &&
|
||||||
(event.target !== outerTreeEl.children[0]) &&
|
event.target !== outerTreeEl.children[0] &&
|
||||||
(hoveringOver?.side !== "tree")
|
hoveringOver?.side !== "tree"
|
||||||
) return;
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
hoveringOver = null;
|
hoveringOver = null;
|
||||||
rerender();
|
rerender();
|
||||||
};
|
};
|
||||||
|
@ -153,7 +157,7 @@ export default (rootNode, getNodeUi) => {
|
||||||
if (!(event.target instanceof HTMLElement)) return;
|
if (!(event.target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
/** @type {null | HTMLElement} */ let nodeToCheckForPath = event.target;
|
||||||
while (nodeToCheckForPath && (nodeToCheckForPath !== outerTreeEl)) {
|
while (nodeToCheckForPath && nodeToCheckForPath !== outerTreeEl) {
|
||||||
if (nodeToCheckForPath.dataset.path) break;
|
if (nodeToCheckForPath.dataset.path) break;
|
||||||
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
nodeToCheckForPath = nodeToCheckForPath.parentElement;
|
||||||
}
|
}
|
||||||
|
@ -184,10 +188,10 @@ const traverse = (node, path, getNodeUi) => {
|
||||||
crel(
|
crel(
|
||||||
"summary",
|
"summary",
|
||||||
{},
|
{},
|
||||||
crel("span", { "class": "title" }, title),
|
crel("span", { class: "title" }, title),
|
||||||
crel(
|
crel(
|
||||||
"span",
|
"span",
|
||||||
{ "class": "bytecount" },
|
{ class: "bytecount" },
|
||||||
pluralize(node.bytes.byteLength, "byte"),
|
pluralize(node.bytes.byteLength, "byte"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -39,6 +39,36 @@ export const GifNodeType = [
|
||||||
|
|
||||||
"extensionBlockIntroducer",
|
"extensionBlockIntroducer",
|
||||||
"extensionBlockSize",
|
"extensionBlockSize",
|
||||||
|
"extensionBlockTerminator",
|
||||||
|
|
||||||
|
"applicationExtensionBlock",
|
||||||
|
"applicationIdentifier",
|
||||||
|
"applicationAuthenticationCode",
|
||||||
|
"applicationSubBlock",
|
||||||
|
"applicationSubBlockSize",
|
||||||
|
"applicationSubBlockData",
|
||||||
|
|
||||||
|
"commentSubBlock",
|
||||||
|
"commentSubBlockSize",
|
||||||
|
"commentSubBlockData",
|
||||||
|
|
||||||
|
"graphicControlExtensionBlock",
|
||||||
|
"graphicControlExtensionPackedFields",
|
||||||
|
"graphicControlExtensionDelayTime",
|
||||||
|
"graphicControlExtensionTransparentColorIndex",
|
||||||
|
|
||||||
|
"plainTextExtensionBlock",
|
||||||
|
"plainTextExtensionBlockTextGridLeftPosition",
|
||||||
|
"plainTextExtensionBlockTextGridTopPosition",
|
||||||
|
"plainTextExtensionBlockTextGridWidth",
|
||||||
|
"plainTextExtensionBlockTextGridHeight",
|
||||||
|
"plainTextExtensionBlockCharacterCellWidth",
|
||||||
|
"plainTextExtensionBlockCharacterCellHeight",
|
||||||
|
"plainTextExtensionBlockTextForegroundColorIndex",
|
||||||
|
"plainTextExtensionBlockTextBackgroundColorIndex",
|
||||||
|
"plainTextSubBlock",
|
||||||
|
"plainTextSubBlockSize",
|
||||||
|
"plainTextSubBlockData",
|
||||||
|
|
||||||
"unknownExtensionBlock",
|
"unknownExtensionBlock",
|
||||||
"unknownExtensionBlockLabel",
|
"unknownExtensionBlockLabel",
|
||||||
|
|
|
@ -56,7 +56,9 @@ const NODE_UI_FNS = {
|
||||||
[GifNodeType.header]: () => ({
|
[GifNodeType.header]: () => ({
|
||||||
title: "GIF header",
|
title: "GIF header",
|
||||||
description: fragment(
|
description: fragment(
|
||||||
p('GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.'),
|
p(
|
||||||
|
'GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
[GifNodeType.headerSignature]: () => ({
|
[GifNodeType.headerSignature]: () => ({
|
||||||
|
@ -92,7 +94,10 @@ const NODE_UI_FNS = {
|
||||||
title: "Logical Screen Width",
|
title: "Logical Screen Width",
|
||||||
description: p(
|
description: p(
|
||||||
`The width of the image stored as a 16-bit little endian unsigned integer. In this case, the width is ${
|
`The width of the image stored as a 16-bit little endian unsigned integer. In this case, the width is ${
|
||||||
pluralize(readGifUint(bytes), "byte")
|
pluralize(
|
||||||
|
readGifUint(bytes),
|
||||||
|
"byte",
|
||||||
|
)
|
||||||
}.`,
|
}.`,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -100,7 +105,10 @@ const NODE_UI_FNS = {
|
||||||
title: "Logical Screen Height",
|
title: "Logical Screen Height",
|
||||||
description: p(
|
description: p(
|
||||||
`The height of the image stored as a 16-bit little endian unsigned integer. In this case, the height is ${
|
`The height of the image stored as a 16-bit little endian unsigned integer. In this case, the height is ${
|
||||||
pluralize(readGifUint(bytes), "byte")
|
pluralize(
|
||||||
|
readGifUint(bytes),
|
||||||
|
"byte",
|
||||||
|
)
|
||||||
}.`,
|
}.`,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -111,7 +119,9 @@ const NODE_UI_FNS = {
|
||||||
return {
|
return {
|
||||||
title: "Logical Screen Descriptor packed fields",
|
title: "Logical Screen Descriptor packed fields",
|
||||||
description: fragment(
|
description: fragment(
|
||||||
p("This byte contains several flags that control how the GIF is displayed. In binary, it looks like this:"),
|
p(
|
||||||
|
"This byte contains several flags that control how the GIF is displayed. In binary, it looks like this:",
|
||||||
|
),
|
||||||
p(bytes[0].toString(2).padStart(8, "0")),
|
p(bytes[0].toString(2).padStart(8, "0")),
|
||||||
crel(
|
crel(
|
||||||
"ul",
|
"ul",
|
||||||
|
@ -123,7 +133,12 @@ const NODE_UI_FNS = {
|
||||||
),
|
),
|
||||||
li(
|
li(
|
||||||
`The next three bits (${
|
`The next three bits (${
|
||||||
packedField.colorResolution.toString(2).padStart(3, "0")
|
packedField.colorResolution
|
||||||
|
.toString(2)
|
||||||
|
.padStart(
|
||||||
|
3,
|
||||||
|
"0",
|
||||||
|
)
|
||||||
}) encode the color resolution. The bigger this number, the more colors this GIF can represent. This value is decoded as binary and then incremented by one, so the color resolution is ${packedField.colorResolution}.`,
|
}) encode the color resolution. The bigger this number, the more colors this GIF can represent. This value is decoded as binary and then incremented by one, so the color resolution is ${packedField.colorResolution}.`,
|
||||||
),
|
),
|
||||||
li(
|
li(
|
||||||
|
@ -135,9 +150,17 @@ const NODE_UI_FNS = {
|
||||||
),
|
),
|
||||||
li(
|
li(
|
||||||
`The last three bits (${
|
`The last three bits (${
|
||||||
(byte & 0b111).toString(2).padStart(3, "0")
|
(byte & 0b111)
|
||||||
|
.toString(2)
|
||||||
|
.padStart(
|
||||||
|
3,
|
||||||
|
"0",
|
||||||
|
)
|
||||||
}) encode the number of colors of the global color table. To decode this field, you add 1 and then raise that to the power of 2, so the size of the global color table is ${
|
}) encode the number of colors of the global color table. To decode this field, you add 1 and then raise that to the power of 2, so the size of the global color table is ${
|
||||||
pluralize(packedField.sizeOfGlobalColorTable, "color")
|
pluralize(
|
||||||
|
packedField.sizeOfGlobalColorTable,
|
||||||
|
"color",
|
||||||
|
)
|
||||||
}. `,
|
}. `,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -159,7 +182,10 @@ const NODE_UI_FNS = {
|
||||||
title: "Global Color Table",
|
title: "Global Color Table",
|
||||||
description: p(
|
description: p(
|
||||||
`The Global Color Table is a list of colors. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
|
`The Global Color Table is a list of colors. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
|
||||||
pluralize(children?.length || 0, "color")
|
pluralize(
|
||||||
|
children?.length || 0,
|
||||||
|
"color",
|
||||||
|
)
|
||||||
}.`,
|
}.`,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -170,9 +196,11 @@ const NODE_UI_FNS = {
|
||||||
crel(
|
crel(
|
||||||
"span",
|
"span",
|
||||||
{
|
{
|
||||||
"style":
|
style:
|
||||||
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
|
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
|
||||||
bytes.join(",")
|
bytes.join(
|
||||||
|
",",
|
||||||
|
)
|
||||||
})`,
|
})`,
|
||||||
},
|
},
|
||||||
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
|
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
|
||||||
|
@ -223,7 +251,10 @@ const NODE_UI_FNS = {
|
||||||
title: "Local Color Table",
|
title: "Local Color Table",
|
||||||
description: p(
|
description: p(
|
||||||
`The Local Color Table is a list of colors for this frame. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
|
`The Local Color Table is a list of colors for this frame. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
|
||||||
pluralize(children?.length || 0, "color")
|
pluralize(
|
||||||
|
children?.length || 0,
|
||||||
|
"color",
|
||||||
|
)
|
||||||
}.`,
|
}.`,
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -235,9 +266,11 @@ const NODE_UI_FNS = {
|
||||||
crel(
|
crel(
|
||||||
"span",
|
"span",
|
||||||
{
|
{
|
||||||
"style":
|
style:
|
||||||
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
|
`display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb(${
|
||||||
bytes.join(",")
|
bytes.join(
|
||||||
|
",",
|
||||||
|
)
|
||||||
})`,
|
})`,
|
||||||
},
|
},
|
||||||
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
|
`#${formatByte(bytes[0])}${formatByte(bytes[1])}${
|
||||||
|
@ -284,6 +317,135 @@ const NODE_UI_FNS = {
|
||||||
title: "Extension Block Size",
|
title: "Extension Block Size",
|
||||||
description: p("TODO"),
|
description: p("TODO"),
|
||||||
}),
|
}),
|
||||||
|
[GifNodeType.extensionBlockTerminator]: () => ({
|
||||||
|
title: "Extension Block Terminator",
|
||||||
|
description: p("A zero byte indicating the end of this extension block."),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Application extension block
|
||||||
|
|
||||||
|
[GifNodeType.applicationExtensionBlock]: () => ({
|
||||||
|
title: "Application Extension Block",
|
||||||
|
description: p(
|
||||||
|
'A block for application-specific extensions. A very common extension is the "NETSCAPE2.0" extension which specifies the number of loops.',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[GifNodeType.applicationIdentifier]: ({ bytes }) => ({
|
||||||
|
title: "Application Identifier",
|
||||||
|
description: p(
|
||||||
|
"An 8-byte string identifying the application. In this case, the identifier is ",
|
||||||
|
crel("code", {}, new TextDecoder().decode(bytes)),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[GifNodeType.applicationAuthenticationCode]: () => ({
|
||||||
|
title: "Application Authentication Code",
|
||||||
|
description: p("Three bytes identifying the application."),
|
||||||
|
}),
|
||||||
|
[GifNodeType.applicationSubBlock]: ({ bytes }) => {
|
||||||
|
const isTerminator = areBytesEqual(bytes, new Uint8Array([0]));
|
||||||
|
return {
|
||||||
|
title: "Application Sub-Block",
|
||||||
|
description: p(
|
||||||
|
"A sub-block of data for this application." +
|
||||||
|
(isTerminator
|
||||||
|
? " This byte is empty, meaning it's the end of the sub-blocks and this application extension block."
|
||||||
|
: ""),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[GifNodeType.applicationSubBlockSize]: ({ bytes }) => ({
|
||||||
|
title: "Application Sub-Block Size",
|
||||||
|
description: p(`The size of this application sub-block: ${bytes[0]}.`),
|
||||||
|
}),
|
||||||
|
[GifNodeType.applicationSubBlockData]: () => ({
|
||||||
|
title: "Application Sub-Block Data",
|
||||||
|
description: p("The data for this application sub-block."),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Comment extension block
|
||||||
|
|
||||||
|
[GifNodeType.commentSubBlock]: () => ({
|
||||||
|
title: "Comment Sub-Block",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.commentSubBlockSize]: () => ({
|
||||||
|
title: "Comment Sub-Block Size",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.commentSubBlockData]: () => ({
|
||||||
|
title: "Comment Sub-Block Data",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Graphic control extension block
|
||||||
|
|
||||||
|
[GifNodeType.graphicControlExtensionBlock]: () => ({
|
||||||
|
title: "Graphic Control Extension Block",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.graphicControlExtensionPackedFields]: () => ({
|
||||||
|
title: "Graphic Control Extension packed fields",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.graphicControlExtensionDelayTime]: () => ({
|
||||||
|
title: "Graphic Control Extension Delay Time",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.graphicControlExtensionTransparentColorIndex]: () => ({
|
||||||
|
title: "Graphic Control Extension Transparent Color Index",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Plain text extension block
|
||||||
|
|
||||||
|
[GifNodeType.plainTextExtensionBlock]: () => ({
|
||||||
|
title: "Plain Text Extension Block",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextGridLeftPosition]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextGridTopPosition]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextGridWidth]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextGridHeight]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockCharacterCellWidth]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockCharacterCellHeight]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextForegroundColorIndex]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextExtensionBlockTextBackgroundColorIndex]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextSubBlock]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextSubBlockSize]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
[GifNodeType.plainTextSubBlockData]: () => ({
|
||||||
|
title: "TODO",
|
||||||
|
description: p("TODO"),
|
||||||
|
}),
|
||||||
|
|
||||||
// Unknown extension block
|
// Unknown extension block
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,18 @@ const errorEl = document.getElementById("error");
|
||||||
const explorerEl = document.getElementById("explorer");
|
const explorerEl = document.getElementById("explorer");
|
||||||
if (!errorEl || !explorerEl) throw new Error("HTML is not set up correctly");
|
if (!errorEl || !explorerEl) throw new Error("HTML is not set up correctly");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const totalElementChildren = (element) => {
|
||||||
|
let result = 1;
|
||||||
|
for (const child of element.children) {
|
||||||
|
result += totalElementChildren(child);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
// TODO: We may want a better UI here.
|
// TODO: We may want a better UI here.
|
||||||
// TODO: Handle errors.
|
// TODO: Handle errors.
|
||||||
|
@ -19,16 +31,22 @@ const main = () => {
|
||||||
}
|
}
|
||||||
const bytes = base64.toByteArray(fileDataBase64);
|
const bytes = base64.toByteArray(fileDataBase64);
|
||||||
|
|
||||||
|
console.time("parseGif");
|
||||||
const rootNode = parseGif(bytes);
|
const rootNode = parseGif(bytes);
|
||||||
|
console.timeEnd("parseGif");
|
||||||
if (!rootNode) {
|
if (!rootNode) {
|
||||||
// TODO: Is there better UI than this?
|
// TODO: Is there better UI than this?
|
||||||
errorEl.removeAttribute("hidden");
|
errorEl.removeAttribute("hidden");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.time("render");
|
||||||
explorerEl.innerHTML = "";
|
explorerEl.innerHTML = "";
|
||||||
explorerEl.append(explorer(rootNode, getNodeUi));
|
explorerEl.append(explorer(rootNode, getNodeUi));
|
||||||
explorerEl.removeAttribute("hidden");
|
explorerEl.removeAttribute("hidden");
|
||||||
|
console.timeEnd("render");
|
||||||
|
|
||||||
|
console.log(`Root node has ${totalElementChildren(explorerEl)} children`);
|
||||||
};
|
};
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
|
@ -7,6 +7,11 @@ const BLOCK_TYPE_IMAGE = 0x2c;
|
||||||
const BLOCK_TYPE_EXTENSION = 0x21;
|
const BLOCK_TYPE_EXTENSION = 0x21;
|
||||||
const BLOCK_TYPE_TRAILER = 0x3b;
|
const BLOCK_TYPE_TRAILER = 0x3b;
|
||||||
|
|
||||||
|
const EXTENSION_LABEL_FOR_APPLICATION = 0xff;
|
||||||
|
const EXTENSION_LABEL_FOR_COMMENT = 0xfe;
|
||||||
|
const EXTENSION_LABEL_FOR_GRAPHIC_CONTROL = 0xf9;
|
||||||
|
const EXTENSION_LABEL_FOR_PLAIN_TEXT = 0x01;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} LogicalScreenDescriptorPackedField
|
* @typedef {object} LogicalScreenDescriptorPackedField
|
||||||
* @prop {boolean} globalColorTableFlag
|
* @prop {boolean} globalColorTableFlag
|
||||||
|
@ -15,6 +20,14 @@ const BLOCK_TYPE_TRAILER = 0x3b;
|
||||||
* @prop {number} sizeOfGlobalColorTable
|
* @prop {number} sizeOfGlobalColorTable
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} condition
|
||||||
|
* @param {string} [message]
|
||||||
|
*/
|
||||||
|
const assert = (condition, message = "Assertion failed") => {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} byte
|
* @param {number} byte
|
||||||
* @returns {LogicalScreenDescriptorPackedField}
|
* @returns {LogicalScreenDescriptorPackedField}
|
||||||
|
@ -55,10 +68,12 @@ export const parseImageDescriptorPackedField = (byte) => ({
|
||||||
const readImageBlock = (bytes, startingOffset) => {
|
const readImageBlock = (bytes, startingOffset) => {
|
||||||
let offset = startingOffset;
|
let offset = startingOffset;
|
||||||
|
|
||||||
/** @type {GifNode[]} */ const children = [{
|
/** @type {GifNode[]} */ const children = [
|
||||||
type: GifNodeType.imageSeparator,
|
{
|
||||||
bytes: bytes.subarray(offset, offset + 1),
|
type: GifNodeType.imageSeparator,
|
||||||
}];
|
bytes: bytes.subarray(offset, offset + 1),
|
||||||
|
},
|
||||||
|
];
|
||||||
offset++;
|
offset++;
|
||||||
|
|
||||||
// Image descriptor
|
// Image descriptor
|
||||||
|
@ -102,17 +117,13 @@ const readImageBlock = (bytes, startingOffset) => {
|
||||||
for (let i = 0; i < packedField.sizeOfLocalColorTable; i++) {
|
for (let i = 0; i < packedField.sizeOfLocalColorTable; i++) {
|
||||||
colorNodes.push({
|
colorNodes.push({
|
||||||
type: GifNodeType.localColorTableColor,
|
type: GifNodeType.localColorTableColor,
|
||||||
bytes: bytes.subarray(offset + (i * 3), offset + (i * 3) + 3),
|
bytes: bytes.subarray(offset + i * 3, offset + i * 3 + 3),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const sizeOfLocalColorTableInBytes = packedField.sizeOfLocalColorTable *
|
const sizeOfLocalColorTableInBytes = packedField.sizeOfLocalColorTable * 3;
|
||||||
3;
|
|
||||||
children.push({
|
children.push({
|
||||||
type: GifNodeType.localColorTable,
|
type: GifNodeType.localColorTable,
|
||||||
bytes: bytes.subarray(
|
bytes: bytes.subarray(offset, offset + sizeOfLocalColorTableInBytes),
|
||||||
offset,
|
|
||||||
offset + sizeOfLocalColorTableInBytes,
|
|
||||||
),
|
|
||||||
children: colorNodes,
|
children: colorNodes,
|
||||||
});
|
});
|
||||||
offset += sizeOfLocalColorTableInBytes;
|
offset += sizeOfLocalColorTableInBytes;
|
||||||
|
@ -183,12 +194,49 @@ const readImageBlock = (bytes, startingOffset) => {
|
||||||
* @returns {{ node: GifNode, newOffset: number }}
|
* @returns {{ node: GifNode, newOffset: number }}
|
||||||
*/
|
*/
|
||||||
const readExtensionBlock = (bytes, startingOffset) => {
|
const readExtensionBlock = (bytes, startingOffset) => {
|
||||||
|
/** @type {GifNodeType} */ let type;
|
||||||
|
|
||||||
let offset = startingOffset;
|
let offset = startingOffset;
|
||||||
|
|
||||||
/** @type {GifNode[]} */ const children = [{
|
/**
|
||||||
type: GifNodeType.extensionBlockIntroducer,
|
* @param {GifNodeType} outerType
|
||||||
bytes: bytes.subarray(offset, offset + 1),
|
* @param {GifNodeType} sizeType
|
||||||
}];
|
* @param {GifNodeType} dataType
|
||||||
|
*/
|
||||||
|
const readSubBlocks = (outerType, sizeType, dataType) => {
|
||||||
|
let subblockSize = -1;
|
||||||
|
do {
|
||||||
|
subblockSize = bytes[offset++];
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: outerType,
|
||||||
|
bytes: bytes.subarray(offset, offset + subblockSize),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: sizeType,
|
||||||
|
bytes: new Uint8Array([subblockSize]),
|
||||||
|
},
|
||||||
|
...(subblockSize
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: dataType,
|
||||||
|
bytes: bytes.subarray(offset + 1, offset + 1 + subblockSize),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += subblockSize;
|
||||||
|
} while (subblockSize !== 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {GifNode[]} */ const children = [
|
||||||
|
{
|
||||||
|
type: GifNodeType.extensionBlockIntroducer,
|
||||||
|
bytes: bytes.subarray(offset, offset + 1),
|
||||||
|
},
|
||||||
|
];
|
||||||
offset++;
|
offset++;
|
||||||
|
|
||||||
const extensionLabel = bytes[offset++];
|
const extensionLabel = bytes[offset++];
|
||||||
|
@ -200,9 +248,142 @@ const readExtensionBlock = (bytes, startingOffset) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (extensionLabel) {
|
switch (extensionLabel) {
|
||||||
// TODO: handle various documented extensions
|
case EXTENSION_LABEL_FOR_APPLICATION: {
|
||||||
|
assert(extensionSize === 11);
|
||||||
|
|
||||||
|
type = GifNodeType.applicationExtensionBlock;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.applicationIdentifier,
|
||||||
|
bytes: bytes.subarray(offset, offset + 8),
|
||||||
|
});
|
||||||
|
offset += 8;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.applicationAuthenticationCode,
|
||||||
|
bytes: bytes.subarray(offset, offset + 3),
|
||||||
|
});
|
||||||
|
offset += 3;
|
||||||
|
|
||||||
|
readSubBlocks(
|
||||||
|
GifNodeType.applicationSubBlock,
|
||||||
|
GifNodeType.applicationSubBlockSize,
|
||||||
|
GifNodeType.applicationSubBlockData,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXTENSION_LABEL_FOR_COMMENT: {
|
||||||
|
type = GifNodeType.commentExtensionBlock;
|
||||||
|
|
||||||
|
readSubBlocks(
|
||||||
|
GifNodeType.commentSubBlock,
|
||||||
|
GifNodeType.commentSubBlockSize,
|
||||||
|
GifNodeType.commentSubBlockData,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXTENSION_LABEL_FOR_GRAPHIC_CONTROL: {
|
||||||
|
assert(extensionSize === 4);
|
||||||
|
|
||||||
|
type = GifNodeType.graphicControlExtensionBlock;
|
||||||
|
|
||||||
|
const packedField = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.graphicControlExtensionPackedFields,
|
||||||
|
bytes: new Uint8Array([packedField]),
|
||||||
|
});
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.graphicControlExtensionDelayTime,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
});
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
const transparentColorIndex = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.graphicControlExtensionTransparentColorIndex,
|
||||||
|
bytes: new Uint8Array([transparentColorIndex]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const blockTerminator = bytes[offset++];
|
||||||
|
assert(blockTerminator === 0, "Expected block terminator");
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.extensionBlockTerminator,
|
||||||
|
bytes: new Uint8Array([blockTerminator]),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EXTENSION_LABEL_FOR_PLAIN_TEXT: {
|
||||||
|
assert(extensionSize === 12);
|
||||||
|
|
||||||
|
type = GifNodeType.plainTextExtensionBlock;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextGridLeftPosition,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
});
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextGridTopPosition,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
});
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextGridWidth,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
});
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextGridHeight,
|
||||||
|
bytes: bytes.subarray(offset, offset + 2),
|
||||||
|
});
|
||||||
|
offset += 2;
|
||||||
|
|
||||||
|
const characterCellWidth = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockCharacterCellWidth,
|
||||||
|
bytes: new Uint8Array([characterCellWidth]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const characterCellHeight = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockCharacterCellHeight,
|
||||||
|
bytes: new Uint8Array([characterCellHeight]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const textForegroundColorIndex = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextForegroundColorIndex,
|
||||||
|
bytes: new Uint8Array([textForegroundColorIndex]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const textBackgroundColorIndex = bytes[offset++];
|
||||||
|
children.push({
|
||||||
|
type: GifNodeType.plainTextExtensionBlockTextBackgroundColorIndex,
|
||||||
|
bytes: new Uint8Array([textBackgroundColorIndex]),
|
||||||
|
});
|
||||||
|
|
||||||
|
readSubBlocks(
|
||||||
|
GifNodeType.plainTextSubBlock,
|
||||||
|
GifNodeType.plainTextSubBlockSize,
|
||||||
|
GifNodeType.plainTextSubBlockData,
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
type = GifNodeType.unknownExtensionBlock;
|
||||||
|
|
||||||
children.push(
|
children.push(
|
||||||
{
|
{
|
||||||
type: GifNodeType.unknownExtensionBlockLabel,
|
type: GifNodeType.unknownExtensionBlockLabel,
|
||||||
|
@ -214,16 +395,20 @@ const readExtensionBlock = (bytes, startingOffset) => {
|
||||||
bytes: bytes.subarray(offset, offset + extensionSize),
|
bytes: bytes.subarray(offset, offset + extensionSize),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
offset += extensionSize;
|
offset += extensionSize;
|
||||||
return {
|
|
||||||
newOffset: offset,
|
break;
|
||||||
node: {
|
|
||||||
type: GifNodeType.unknownExtensionBlock,
|
|
||||||
bytes: bytes.subarray(startingOffset, offset),
|
|
||||||
children,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newOffset: offset,
|
||||||
|
node: {
|
||||||
|
type,
|
||||||
|
bytes: bytes.subarray(startingOffset, offset),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -302,7 +487,7 @@ export default (bytes) => {
|
||||||
for (let i = 0; i < packedField.sizeOfGlobalColorTable; i++) {
|
for (let i = 0; i < packedField.sizeOfGlobalColorTable; i++) {
|
||||||
colorNodes.push({
|
colorNodes.push({
|
||||||
type: GifNodeType.globalColorTableColor,
|
type: GifNodeType.globalColorTableColor,
|
||||||
bytes: bytes.subarray(13 + (i * 3), 13 + (i * 3) + 3),
|
bytes: bytes.subarray(13 + i * 3, 13 + i * 3 + 3),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const sizeOfGlobalColorTableInBytes = packedField.sizeOfGlobalColorTable *
|
const sizeOfGlobalColorTableInBytes = packedField.sizeOfGlobalColorTable *
|
||||||
|
|
Loading…
Reference in New Issue