/** * * This File holds methods for creating the gif related UI elements to be put into the DOM, as well as their functionality * */ /** * Bundled functions that create Documentfragments for given blocks to be placed in the DOM */ namespace Visuals { /** * Creates the main DOM Elements for some blocktypes. * Due to the way blocks are displayed this fucntion only returns a DocumentFragment on the following Blocktypes: * Header, ImageDescriptor, PlainTextExtension, CommentExtension, ApplicationExtension, Trailer * @returns DocumentFragment corresponding to the given block. */ export function createMainRenderedBlock(block: GifBlock): DocumentFragment | undefined { switch (block.blockType) { case BlockType.Header: return completeMetaVisualizer(block as Header) case BlockType.TableBasedImageData: return completeImageVisualizer(block as TableBasedImageData) case BlockType.Trailer: return trailerVisualizer(block); case BlockType.CommentExtension: return commentVisualizer(block as CommentExtension); case BlockType.PlainTextExtension: return plainTextExtensionVisualizer(block as PlainTextExtension); case BlockType.ApplicationExtension: return applicationExtensionGenericVisualizer(block as ApplicationExtension); default: return undefined } } /** * Creates Document fragments for each individual blocktype. * No generalized big blocks like we want, but can be used for internal replacement */ export function createRenderedBlock(block: GifBlock) { switch (block.blockType) { case BlockType.LogicalScreenDescriptor: return logicalScreenDescriptorVisualizer(block as LogicalScreenDescriptor); case BlockType.Header: return headerVisualizer(block as Header); case BlockType.GlobalColorTable: return colormapVisualizer(block as ColorTable, 4, 50); case BlockType.TableBasedImageData: return imageVisualizer(gifView!.bundleImage(block)!); case BlockType.ImageDescriptor: return imageDescriptorVisualizer(block as ImageDescriptor) case BlockType.LocalColorTable: return colormapVisualizer(block as ColorTable, 4, 50); case BlockType.Trailer: return trailerVisualizer(block); case BlockType.GraphicControlExtension: return graphicControlExtensionVisualizer(block as GraphicControlExtension); case BlockType.CommentExtension: return commentVisualizer(block as CommentExtension); case BlockType.PlainTextExtension: return plainTextExtensionVisualizer(block as PlainTextExtension); case BlockType.ApplicationExtension: return applicationExtensionGenericVisualizer(block as ApplicationExtension); default: return undefined } } /** * Creates a document fragments to add a color table to the DOM. * @param colormapBlockPointer The ColorTable object holding the colors * @param widthPerColor how many pixel should each color be wide * @param heightTotal how many pixel should each color be high */ function colormapVisualizer(colormapBlockPointer: ColorTable, widthPerColor = 4, heightTotal = 50) { const template = (document.getElementById("colortable-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const canvas = template.getElementById("innercanvas-template") as HTMLCanvasElement; const ctx = canvas.getContext("2d"); const colors = colormapBlockPointer.data!; const coloramount = colormapBlockPointer.getColorAmount(); const width = widthPerColor * coloramount canvas.setAttribute("width", "" + width); canvas.setAttribute("height", "" + heightTotal); let pixels = new Array(); for (let j = 0; j < heightTotal; j++) { for (let i = 0; i < colors.length / 3; i++) { for (let k = 0; k < widthPerColor; k++) { pixels.push(colors[i * 3]); pixels.push(colors[i * 3 + 1]); pixels.push(colors[i * 3 + 2]); pixels.push(255); } } } const newimgdata = new ImageData(Uint8ClampedArray.from(pixels), width, heightTotal); ctx?.putImageData(newimgdata, 0, 0); canvas.addEventListener("click", () => { const dialog = imageDrawDialogVisualizer(gifView!.bundleImage(colormapBlockPointer)!) document.getElementById("canvas-div")!.appendChild(dialog) dialog.showModal() }) const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = colormapBlockPointer.blockID.toString() return template; } function logicalScreenDescriptorVisualizer(LSDBlock: LogicalScreenDescriptor) { const template = (document.getElementById("logicalscreendescriptor-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment { // Screen Width const iwidth = template.getElementById("lsd-sw-template")! as HTMLInputElement iwidth.max = "65535" iwidth.min = "0"; iwidth.valueAsNumber = LSDBlock.screenWidth() const updateScreenWidth = function (this: HTMLInputElement, event: Event) { this.valueAsNumber = isNaN(this.valueAsNumber) ? 250 : Math.min(Math.max(this.valueAsNumber, 0), 65535) LSDBlock.screenWidth(this.valueAsNumber) } iwidth.addEventListener("change", updateScreenWidth) } { // Screen Height const iheight = template.getElementById("lsd-sh-template")! as HTMLInputElement iheight.max = "65535" iheight.min = "0"; iheight.valueAsNumber = LSDBlock.screenHeight() const updateScreenHeight = function (this: HTMLInputElement, event: Event) { let newNumber = parseInt(this.value) newNumber = isNaN(newNumber) ? 250 : Math.min(Math.max(newNumber, 0), 65535) LSDBlock.screenHeight(newNumber) } iheight.addEventListener("change", updateScreenHeight) } { // Global Color Table Flag const igctflag = template.getElementById("lsd-gctflag-template")! as HTMLInputElement igctflag.checked = LSDBlock.gctFlag() const updateGCTFlag = function (this: HTMLInputElement, ev: Event) { LSDBlock.gctFlag(this.checked) if (this.checked) { //need to add a new gct const gctColorAmount = 1 << (LSDBlock.gctSize() + 1) const newColortableColor = BlockAdder.makeRGBColor() if (256 !== gctColorAmount) { for (let i = 0; i < 256 - gctColorAmount; i++) { newColortableColor.pop() newColortableColor.pop() newColortableColor.pop() } } const newgct = new ColorTable({ colors: newColortableColor, type: BlockType.GlobalColorTable }) gifView!.globalColorTable = newgct gifView!.addBlocks([newgct], gifView!.getBlockPosition(LSDBlock)) } else { // bye bye gct gifView!.removeBlocks([gifView!.globalColorTable!.blockID]); gifView!.globalColorTable = undefined; } rerenderMainBlock(gifView!.bundleMeta()!.header.blockID) // pool currently rendered images that need rerendering because they use the gct. If the gct is deleted, they will be rendered with the default colortable set in the state document.querySelectorAll((`[data-renderblockid]`)).forEach( (value) => { const block = gifView!.getBlockFromID(parseInt((value as HTMLElement).dataset.renderblockid!)) if (block!.blockType === BlockType.TableBasedImageData && !gifView!.bundleImage(block!)!.imagedescriptor.lctFlag()) { rerenderMainBlock(block!.blockID) } } ) } igctflag.addEventListener("change", updateGCTFlag) } { // Color Resolution const icolorres = template.getElementById("lsd-colorresolution-template")! as HTMLInputElement icolorres.min = "0" icolorres.max = "7" icolorres.valueAsNumber = LSDBlock.colorResolution() const updateColorResolution = function (this: HTMLInputElement, ev: Event) { this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) LSDBlock.colorResolution(this.valueAsNumber) } icolorres.addEventListener("change", updateColorResolution) } { // Sort Flag const isortflag = template.getElementById("lsd-sortflag-template")! as HTMLInputElement isortflag.checked = LSDBlock.gctSortedFlag() const updateSortFlag = function (this: HTMLInputElement, ev: Event) { LSDBlock.gctSortedFlag(this.checked) } isortflag.addEventListener("change", updateSortFlag) } { // Global Color Table Size const igctsize = template.getElementById("lsd-gctsize-template")! as HTMLInputElement igctsize.min = "0" igctsize.max = "7" igctsize.valueAsNumber = LSDBlock.gctSize() const updateGlobalColorTableSize = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) LSDBlock.gctSize(this.valueAsNumber) if (LSDBlock.gctFlag()) { const newAmount = 1 << (this.valueAsNumber + 1) const bundled = gifView!.bundleMeta()! const colortable = bundled.globalColorTable! if (colortable.getColorAmount() < newAmount) { const newcolor = BlockAdder.makeRGBColor().slice(3 * colortable.getColorAmount(), 3 * newAmount) colortable.data = Uint8Array.from(Array.from(colortable.data).concat(newcolor)) } else if (colortable.getColorAmount() > newAmount) { colortable.data = colortable.data.slice(0, newAmount * 3) } } rerenderMainBlock(gifView!.bundleMeta()!.header.blockID) document.querySelectorAll((`[data-renderblockid]`)).forEach( (value) => { const block = gifView!.getBlockFromID(parseInt((value as HTMLElement).dataset.renderblockid!)) //instead of bunding we could do some nitty gritty work here. we know where in the gif thiss tuff starts and ends etc. if (block!.blockType === BlockType.TableBasedImageData && !gifView!.bundleImage(block!)!.imagedescriptor.lctFlag()) { rerenderMainBlock(block!.blockID) } } ) } } igctsize.addEventListener("change", updateGlobalColorTableSize) } { //Background Color Index const ibci = template.getElementById("lsd-bci-template")! as HTMLInputElement ibci.valueAsNumber = LSDBlock.backgroundColorIndex() ibci.min = "0" ibci.max = "255" const updateBackgroundColorIndex = function (this: HTMLInputElement, ev: Event) { isNaN(this.valueAsNumber) ? noop() : LSDBlock.backgroundColorIndex(this.valueAsNumber) } ibci.addEventListener("change", updateBackgroundColorIndex) } { // Pixel Aspect Ratio const ipar = template.getElementById("lsd-par-template")! as HTMLInputElement ipar.valueAsNumber = LSDBlock.pixelAspectRatio() ipar.min = "0" ipar.max = "255" const updatePixelApsectRatio = function (this: HTMLInputElement, ev: Event) { isNaN(this.valueAsNumber) ? noop() : LSDBlock.pixelAspectRatio(this.valueAsNumber) } ipar.addEventListener("change", updatePixelApsectRatio) } const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = LSDBlock.blockID.toString() //was moved to the bigger block return template; } function imageVisualizer(gifImage: GifBlockImage) { const template = (document.getElementById("tablebasedImageData-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const lct = gifImage.colortable; const width = gifImage.imagedescriptor.width() const height = gifImage.imagedescriptor.height() const colortable = lct!.data! const canvas = template.getElementById("innercanvas-template") as HTMLCanvasElement; canvas.setAttribute("id", "test-canvas-" + gifImage.tableBasedImageData.blockID); //we use the pointer as unique ID because every tbid has a different one canvas.setAttribute("width", "" + width); canvas.setAttribute("height", "" + height); const ctx = canvas.getContext("2d"); // dry image aka the index sequence in cache so we decompress only once const decompressed = cachedImageDecompression(gifImage.tableBasedImageData) const picstream = indexStreamToPicture(decompressed, colortable) canvas.addEventListener("click", () => { const dialog = imageDrawDialogVisualizer(gifImage) document.getElementById("canvas-div")!.appendChild(dialog) dialog.showModal() }) const newimgdata = picStreamToImageData(picstream, width, height) ctx?.putImageData(newimgdata, 0, 0); const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = gifImage.tableBasedImageData.blockID.toString() return template; } function graphicControlExtensionVisualizer(gceblock: GraphicControlExtension) { const template = (document.getElementById("graphiccontrolextension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const disposalMethodSelection = template.getElementById("disposalMethodSelection-template")! as HTMLSelectElement for (let key of Object.entries(DisposalMethod).filter((v) => isNaN(Number(v[1])))) { let option = document.createElement("option") option.textContent = key[1].toString() option.value = key[0].toString() disposalMethodSelection.appendChild(option) } disposalMethodSelection.value = gceblock.disposalMethod().toString() const updateDisposalMethod = function (this: HTMLSelectElement, ev: Event) { gceblock.disposalMethod(parseInt(this.value)) } disposalMethodSelection.addEventListener("change", updateDisposalMethod) const userInputFlagCheckbox = template.getElementById("userInputFlagCheckbox-template")! as HTMLInputElement userInputFlagCheckbox.id = gceblock.blockID + "-userInputFlagCheckbox" // blockpointers should be unqiue so we can use that for label targeting userInputFlagCheckbox.checked = gceblock.userInputFlag() const updateUserInput = function (this: HTMLInputElement, ev: Event) { gceblock.userInputFlag(this.checked) } userInputFlagCheckbox.addEventListener("change", updateUserInput) const transparentColorFlagCheckbox = template.getElementById("transparentColorFlag-template")! as HTMLInputElement transparentColorFlagCheckbox.checked = gceblock.transparencyFlag() const updatetransparentColorFlagCheckbox = function (this: HTMLInputElement, ev: Event) { gceblock.transparencyFlag(this.checked) } transparentColorFlagCheckbox.addEventListener("change", updatetransparentColorFlagCheckbox) const delayInput = template.getElementById("delay-template")! as HTMLInputElement delayInput.valueAsNumber = gceblock.delay() const updateDelay = function (this: HTMLInputElement, event: Event) { let newNumber = parseInt(this.value) newNumber = isNaN(newNumber) ? 0 : Math.min(Math.max(newNumber, 0), ((1 << 16) - 1)) // 0 as default on wrong user inputs gceblock.delay(newNumber) } delayInput.addEventListener("change", updateDelay) const transparencyColorSelection = template.getElementById("transparentcolorindex-template")! as HTMLInputElement { // adding base values transparencyColorSelection.max = "255" transparencyColorSelection.valueAsNumber = gceblock.transparencyIndex() } const updateTransparencyColor = function (this: HTMLSelectElement, ev: Event) { const newIndex = isNaN(parseInt(this.value)) ? 0 : parseInt(this.value) gceblock.transparencyIndex(newIndex) } transparencyColorSelection.addEventListener("change", updateTransparencyColor) const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = gceblock.blockID.toString() return template } function applicationExtensionGenericVisualizer(applicationBlock: ApplicationExtension) { const template = (document.getElementById("applicationExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const appId = applicationBlock.identifier(); const appAuth = applicationBlock.authentification(); const appData = applicationBlock.getApplicationdata(); template.getElementById("applicationIdentifier")!.textContent = appId; template.getElementById("applicationAuthentication")!.textContent = String.fromCharCode(...appAuth); //TODO maybe make this its own special thingy and have the bigger function decide which subclass to use and so on if (appId === "NETSCAPE" && String.fromCharCode(...appAuth) === "2.0") { const loopinput = document.createElement("input") loopinput.type = "number" loopinput.min = "0" loopinput.max = "65535" loopinput.id = "Netscape-loops" //data in the subdatablock is [Sub-block ID|LoppData1|LoopData2] so we want bytes 16 and 17 loopinput.valueAsNumber = applicationBlock.data![16] | (applicationBlock.data![17] << 8) //loopcount const updateLoopCount = function (this: HTMLInputElement, ev: Event) { applicationBlock.data![16] = this.valueAsNumber & 0b11111111 applicationBlock.data![17] = (this.valueAsNumber >> 8) & 0b11111111 } loopinput.addEventListener("change", updateLoopCount) const loopLabel = document.createElement("label") loopLabel.textContent = "Loop Count (0 = infinit): " loopLabel.appendChild(loopinput) template.getElementById("applicationData")!.appendChild(loopLabel) } else { template.getElementById("applicationData")!.textContent = String.fromCharCode(...appData); } const addbtn = contextDependentAddButtonVisualizer(applicationBlock); const removebtn = removeButtonVisualizer(applicationBlock); template.getElementById("removeButton-template")!.appendChild(removebtn); template.getElementById("addButton-template")!.appendChild(addbtn); const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = applicationBlock.blockID.toString() wrapdiv.dataset.renderblockid = applicationBlock.blockID.toString() //special purpose id for the main block sued to render this element return template } function commentVisualizer(commentBlock: CommentExtension) { const template = (document.getElementById("commentExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const commentTextField = template.getElementById("comment-text-template")! as HTMLParagraphElement commentTextField.textContent = commentBlock.getComment(); commentTextField.addEventListener("input", () => { commentBlock.setComment(commentTextField.textContent) }) const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = commentBlock.blockID.toString() wrapdiv.dataset.renderblockid = commentBlock.blockID.toString() const addButton = contextDependentAddButtonVisualizer(commentBlock) template.getElementById("addButton-template")!.appendChild(addButton) const deleteButton = removeButtonVisualizer(commentBlock) template.getElementById("removeButton-template")!.appendChild(deleteButton) return template } function headerVisualizer(headerBlock: Header) { const template = (document.getElementById("header-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const versionP = template.getElementById("gifVersion-template")! versionP.textContent = String.fromCharCode(...headerBlock.data!.slice(3)); const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = headerBlock.blockID.toString() return template; } function imageDescriptorVisualizer(idBlock: ImageDescriptor) { const template = (document.getElementById("imagedescriptor-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment { // Left Position const leftpos = template.getElementById("id-leftpos-template") as HTMLInputElement leftpos.valueAsNumber = idBlock.leftPosition() leftpos.max = "65535" leftpos.min = "0" const updateLeftPos = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { idBlock.leftPosition(this.valueAsNumber) } } leftpos.addEventListener("change", updateLeftPos) } { // Top Position const topPos = template.getElementById("id-toppos-template") as HTMLInputElement topPos.valueAsNumber = idBlock.topPosition() topPos.max = "65535" topPos.min = "0" const updateLeftPos = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { idBlock.topPosition(this.valueAsNumber) } } topPos.addEventListener("change", updateLeftPos) } { // Width const width = template.getElementById("id-width-template") as HTMLInputElement width.valueAsNumber = idBlock.width() width.max = "65535" width.min = "1" // in theory a gif image could have a 0 width but we don't allow it here const updateLeftPos = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { const prevWidth = idBlock.width(); const height = idBlock.height() const diff = this.valueAsNumber - prevWidth const tbid = gifView!.bundleImage(idBlock)!.tableBasedImageData const indexstream = cachedImageDecompression(tbid) if (diff < 0) { for (let i = 1; i <= height; i++) { indexstream.splice((i * prevWidth - ((i) * Math.abs(diff))), Math.abs(diff)); } } else if (diff > 0) { const replace = [] for (let j = 0; j < diff; j++) { replace.push(0) } for (let i = 1; i <= height; i++) { indexstream.splice((i * prevWidth + (i - 1) * diff), 0, ...replace); } } else { return } const newtbiddata = compressor.compress(indexstream, tbid.LZWMinimumCodeSize()) tbid.setImageData(newtbiddata); //tbid.setDryImage(indexstream) idBlock.width(this.valueAsNumber) rerenderMainBlock(tbid.blockID) } } width.addEventListener("change", updateLeftPos) } { // Height const height = template.getElementById("id-height-template") as HTMLInputElement height.valueAsNumber = idBlock.height() height.max = "65535" height.min = "1" const updateLeftPos = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { const prevHeight = idBlock.height(); const width = idBlock.width(); const diff = this.valueAsNumber - prevHeight const tbid = gifView!.bundleImage(idBlock)!.tableBasedImageData const indexstream = cachedImageDecompression(tbid) if (diff == 0) { return; } if (diff < 0) { indexstream.splice(indexstream.length - 1 + (diff * width)) } else if (diff > 0) { for (let j = 0; j < diff * width; j++) { indexstream.push(0) } } const newtbiddata = compressor.compress(indexstream, tbid.LZWMinimumCodeSize()) tbid.setImageData(newtbiddata); //tbid.setDryImage(indexstream) idBlock.height(this.valueAsNumber) rerenderMainBlock(tbid.blockID) } } height.addEventListener("change", updateLeftPos) } { // LCT Flag const lctflag = template.getElementById("id-lctflag-template") as HTMLInputElement lctflag.checked = idBlock.lctFlag() const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { const bundled = gifView!.bundleImage(idBlock) idBlock.lctFlag(this.checked) if (this.checked) { //need to add a new local Color Table const lctColorAmount = 1 << (idBlock.lctSize() + 1) const newColortableColor = BlockAdder.makeRGBColor() if (256 !== lctColorAmount) { for (let i = 0; i < 256 - lctColorAmount; i++) { newColortableColor.pop() newColortableColor.pop() newColortableColor.pop() } } gifView!.addBlocks([new ColorTable({ colors: newColortableColor, type: BlockType.LocalColorTable })], gifView!.getBlockPosition(idBlock)) } else { //bye bye lct gifView!.removeBlocks([bundled!.colortable.blockID]); } rerenderMainBlock(bundled!.tableBasedImageData.blockID) } lctflag.addEventListener("change", updateLCTFlag) } { // Interlace Flag const interlaceFlag = template.getElementById("id-interlaceflag-template") as HTMLInputElement interlaceFlag.checked = idBlock.interlacedFlag() const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { idBlock.interlacedFlag(this.checked) } interlaceFlag.addEventListener("change", updateLCTFlag) } { // Sort Flag const sortFlag = template.getElementById("id-sortflag-template") as HTMLInputElement sortFlag.checked = idBlock.lctSortedFlag() const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { idBlock.lctSortedFlag(this.checked) } sortFlag.addEventListener("change", updateLCTFlag) } { // LCT Size const lctSize = template.getElementById("id-lctsize-template") as HTMLInputElement lctSize.valueAsNumber = idBlock.lctSize() lctSize.min = "0" lctSize.max = "7" const updateLCTSize = function (this: HTMLInputElement, ev: Event) { if (!isNaN(this.valueAsNumber)) { this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) idBlock.lctSize(this.valueAsNumber) if (idBlock.lctFlag()) { const newAmount = 1 << (this.valueAsNumber + 1) const bundled = gifView!.bundleImage(idBlock)! const colortable = bundled.colortable! if (colortable.getColorAmount() < newAmount) { const newcolor = BlockAdder.makeRGBColor().slice(3 * colortable.getColorAmount(), 3 * newAmount) colortable.setColors((Array.from(colortable.data).concat(newcolor))) } else if (colortable.getColorAmount() > newAmount) { colortable.data = colortable.data.slice(0, newAmount * 3) } rerenderMainBlock(bundled!.tableBasedImageData.blockID) } } } lctSize.addEventListener("change", updateLCTSize) } const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = idBlock.blockID.toString() return template; } function trailerVisualizer(trailerBlock: Trailer) { const template = (document.getElementById("imagetrailer-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = trailerBlock.blockID.toString() wrapdiv.dataset.renderblockid = trailerBlock.blockID.toString() return template; } function plainTextExtensionVisualizer(plainTextBlock: PlainTextExtension) { const template = (document.getElementById("plainTextExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const leftpos = template.getElementById("pte-leftpos-template") as HTMLInputElement const toppos = template.getElementById("pte-toppos-template") as HTMLInputElement const gridwidth = template.getElementById("pte-gridwidth-template") as HTMLInputElement const gridheight = template.getElementById("pte-gridheight-template") as HTMLInputElement const cellwidth = template.getElementById("pte-cellwidth-template") as HTMLInputElement const cellheight = template.getElementById("pte-cellheight-template") as HTMLInputElement const foregroundindex = template.getElementById("pte-foreground-template") as HTMLInputElement const backgroundindex = template.getElementById("pte-background-template") as HTMLInputElement const plaintext = template.getElementById("pte-plaintext-template") as HTMLInputElement //set initial values leftpos.valueAsNumber = plainTextBlock.gridLeftPosition() toppos.valueAsNumber = plainTextBlock.gridTopPosition() gridwidth.valueAsNumber = plainTextBlock.gridWidth() gridheight.valueAsNumber = plainTextBlock.gridHeight() cellwidth.valueAsNumber = plainTextBlock.cellWidth() cellheight.valueAsNumber = plainTextBlock.cellHeight() foregroundindex.valueAsNumber = plainTextBlock.textForegorundColorIndex() backgroundindex.valueAsNumber = plainTextBlock.textBackgroundColorIndex() plaintext.textContent = plainTextBlock.getPlainTextData() //event listeners on change leftpos.addEventListener("change", () => { plainTextBlock.gridLeftPosition(leftpos.valueAsNumber) }) toppos.addEventListener("change", () => { plainTextBlock.gridTopPosition(toppos.valueAsNumber) }) gridwidth.addEventListener("change", () => { plainTextBlock.gridWidth(gridwidth.valueAsNumber) }) gridheight.addEventListener("change", () => { plainTextBlock.gridHeight(gridheight.valueAsNumber) }) cellwidth.addEventListener("change", () => { plainTextBlock.cellWidth(cellwidth.valueAsNumber) }) cellheight.addEventListener("change", () => { plainTextBlock.cellHeight(cellheight.valueAsNumber) }) foregroundindex.addEventListener("change", () => { plainTextBlock.textForegorundColorIndex(foregroundindex.valueAsNumber) }) backgroundindex.addEventListener("change", () => { plainTextBlock.textBackgroundColorIndex(backgroundindex.valueAsNumber) }) plaintext.addEventListener("input", () => { plainTextBlock.setPlainTextData(plaintext.textContent) }) //set graphic control extension const bundle = gifView!.bundlePlainText(plainTextBlock)! if (bundle.graphicextension) { template.getElementById("pte-gce-template")!.appendChild(graphicControlExtensionVisualizer(bundle.graphicextension)) } // set data so we can rerender the block if needed const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement wrapdiv.dataset.blockid = plainTextBlock.blockID.toString() wrapdiv.dataset.renderblockid = plainTextBlock.blockID.toString() //add and delete buttons const addbtn = contextDependentAddButtonVisualizer(plainTextBlock) const removebtn = removeButtonVisualizer(plainTextBlock) template.getElementById("removeButton-template")?.appendChild(removebtn) template.getElementById("addButton-template")?.appendChild(addbtn) return template } /** * Creates the dialog DOM element that allows a user to paint over a specified image. * Drawing works via a secondary canvas layer and using the colortable indices. * @param imageBundle the bundled information for the image which is to be used in the dialog * @param initColorindex intial draw color index. default is 0 * @param initLineWidth initial draw thickness in pixel. default is 10px * @returns The dialog object to be displayed as modal */ export function imageDrawDialogVisualizer(imageBundle: GifBlockImage, initColorindex = 0, initLineWidth = 3) { //Not as efficient as things could be, but good enough const template = (document.getElementById("imagedraw-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const dryImage = Array.from(cachedImageDecompression(imageBundle!.tableBasedImageData)) //base symbols for the image which we can hydrate with the colortable const imgwidth = imageBundle!.imagedescriptor.width(); const imgheight = imageBundle!.imagedescriptor.height(); const colortableWidthPerColor = 4; const colortableWidth = colortableWidthPerColor * imageBundle!.colortable!.getColorAmount(); const colortableHeight = 50; const dialog = template.getElementById("draw-dialog")! as HTMLDialogElement const closespan = template.getElementById("closespan-template")! as HTMLSpanElement closespan.addEventListener("click", () => dialog.close()) const colortablecanvas = template.getElementById("colortablecanvas-template")! as HTMLCanvasElement colortablecanvas.width = colortableWidth colortablecanvas.height = colortableHeight; const imagecanvas = template.getElementById("imagelayer")! as HTMLCanvasElement imagecanvas.width = imgwidth; imagecanvas.height = imgheight; const drawcanvas = template.getElementById("drawlayer")! as HTMLCanvasElement; //we will draw on this drawcanvas.width = imgwidth; drawcanvas.height = imgheight; if (initColorindex >= imageBundle!.colortable!.getColorAmount()) { initColorindex = 0; } const colorindexInput = template.getElementById("choosencolorindex-template")! as HTMLInputElement colorindexInput.min = "0" colorindexInput.valueAsNumber = initColorindex colorindexInput.max = (imageBundle!.colortable!.getColorAmount() - 1).toString() const drawmodeButton = template.getElementById("drawmodebutton-template")! as HTMLButtonElement //TODO const colorpicker = template.getElementById("draw-colorpicker")! as HTMLInputElement { //set initial values colorpicker.dataset.colorIndex = initColorindex.toString(); // colortable index of the color that is going to be influenced by the picker let r = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3].toString(16); let g = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3 + 1].toString(16); let b = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3 + 2].toString(16); colorpicker.value = `#${r.length == 1 ? "0" + r : r}${g.length == 1 ? "0" + g : g}${b.length == 1 ? "0" + b : b}` drawcanvas.getContext("2d")!.strokeStyle = colorpicker.value drawcanvas.getContext("2d")!.fillStyle = colorpicker.value } const updatePickerandInput = (colorindex: number) => { colorpicker.dataset.colorIndex = colorindex.toString() colorindexInput.valueAsNumber = colorindex; const r = imageBundle!.colortable!.data![colorindex * 3].toString(16); const g = imageBundle!.colortable!.data![colorindex * 3 + 1].toString(16); const b = imageBundle!.colortable!.data![colorindex * 3 + 2].toString(16); colorpicker.value = `#${r.length == 1 ? "0" + r : r}${g.length == 1 ? "0" + g : g}${b.length == 1 ? "0" + b : b}`; drawcanvas.getContext("2d")!.strokeStyle = colorpicker.value; drawcanvas.getContext("2d")!.fillStyle = colorpicker.value; } // sets the colorindex that the picker influences and changes its default value to the color selected on the colortable function selectColorFromTableSelection(this: HTMLCanvasElement, ptr: PointerEvent) { const index = Math.min(Math.floor((ptr.x - this.getBoundingClientRect().left) / colortableWidthPerColor), 255); updatePickerandInput(index) } const selectColorFromInput = function (this: HTMLInputElement, e: Event) { if (isNaN(this.valueAsNumber)) { this.valueAsNumber = 0 } this.valueAsNumber = Math.min(Math.max(parseInt(this.min), this.valueAsNumber), parseInt(this.max)) updatePickerandInput(this.valueAsNumber) } // takes the drawn pixel from the draw canvas and updates the dry image indices with the currently selected colortable index function transposeDrawingToImage() { const drawctx = drawcanvas.getContext("2d")! const drawdata = drawctx.getImageData(0, 0, drawcanvas.width, drawcanvas.height).data for (let i = 0; i < drawcanvas.width * drawcanvas.height; i++) { if (drawdata[i * 4 + 3] != 0) { dryImage[i] = parseInt(colorpicker.dataset.colorIndex!); } } drawctx.clearRect(0, 0, drawcanvas.width, drawcanvas.height) renderImage() } //values to track during drawing let drawing = false; let lineWidth = initLineWidth; let imageChanged = false; let drawmode = true; // flag to switch between drawing and color selection function changeColor(this: HTMLInputElement, ev: Event) { //called when the color picker has a change/input event. updates the colormap and the image const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.value) let newColor = { r: 0, g: 0, b: 0 } if (result) { newColor = { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } } const index = this.dataset.colorIndex ? parseInt(this.dataset.colorIndex) : 0 imageBundle!.colortable!.data![3 * index] = newColor.r; imageBundle!.colortable!.data![3 * index + 1] = newColor.g; imageBundle!.colortable!.data![3 * index + 2] = newColor.b; renderColortable(); renderImage(); drawcanvas.getContext("2d")!.strokeStyle = this.value drawcanvas.getContext("2d")!.fillStyle = colorpicker.value; } function renderColortable() { const pixels = []; const ctx = colortablecanvas.getContext("2d"); const colors = imageBundle!.colortable!.data!; for (let j = 0; j < colortableHeight; j++) { for (let i = 0; i < imageBundle!.colortable!.getColorAmount(); i++) { for (let k = 0; k < colortableWidthPerColor; k++) { pixels.push(colors[i * 3]); pixels.push(colors[i * 3 + 1]); pixels.push(colors[i * 3 + 2]); pixels.push(255); } } } ctx?.putImageData( new ImageData(Uint8ClampedArray.from(pixels), colortableWidth, colortableHeight) , 0, 0); } function renderImage() { //hydrate image data with the colortable from memory const picstream = indexStreamToPicture(dryImage, imageBundle!.colortable!.data!) const ctx = imagecanvas.getContext("2d"); const newimgdata = picStreamToImageData(picstream, imgwidth, imgheight)// new ImageData(Uint8ClampedArray.from(picstream), imgwidth, imgheight) ctx?.putImageData(newimgdata, 0, 0); } //attach all listeners colortablecanvas.addEventListener("click", selectColorFromTableSelection) drawmodeButton.addEventListener("click", function(){ drawmode = !drawmode //change button text/image to indicate mode drawmodeButton.classList.toggle("colorselect-button") }) // This will become problematic if we ever allow image sizes to change on the fly within this element. So we better do not do that. template.getElementById("resetbtn-template")!.addEventListener("click", () => { const backupDryImage = cachedImageDecompression(imageBundle!.tableBasedImageData) for (let index = 0; index < dryImage.length; index++) { dryImage[index] = backupDryImage[index] } renderImage(); }) drawcanvas.addEventListener("mousedown", function (mouse: MouseEvent) { if (drawmode) { drawing = true; imageChanged = true; } else { const y = Math.round(mouse.clientY) - Math.round(this.getBoundingClientRect().top) const x = Math.round(mouse.clientX) - Math.round(this.getBoundingClientRect().left) const index = Math.max(0, Math.min(y * imgwidth + x, dryImage.length - 1)) updatePickerandInput(dryImage[index]) } }); drawcanvas.addEventListener("mouseup", function(){ if (drawing) { drawing = false; const drawctx = this.getContext("2d")! drawctx.stroke(); drawctx.beginPath(); transposeDrawingToImage(); } }); drawcanvas.addEventListener("mousemove", function(mouse: MouseEvent){ if (drawing) { const drawctx = this.getContext("2d")! drawctx.lineWidth = lineWidth; drawctx.lineCap = 'round'; drawctx.lineTo(mouse.clientX - this.getBoundingClientRect().left, mouse.y - this.getBoundingClientRect().top); drawctx.stroke(); } }); drawcanvas.addEventListener("mouseleave", function (mouse: MouseEvent){ if (drawing) { const drawctx = this.getContext("2d")! drawctx.lineTo(mouse.clientX - this.getBoundingClientRect().left, mouse.y - this.getBoundingClientRect().top); drawctx.stroke(); drawctx.beginPath(); transposeDrawingToImage(); } }); drawcanvas.addEventListener("mouseenter", function (mouse: MouseEvent){ if (drawing && (mouse.buttons & 0b1) == 0) { drawing = false; const drawctx = this.getContext("2d")! drawctx.stroke(); drawctx.beginPath(); } }); colorpicker.addEventListener("change", changeColor) //We so not change the images colors on input cause that's better for performance on big pictures. colorindexInput.addEventListener("change", selectColorFromInput) dialog.addEventListener("close", () => { // update image data based on the changes made if (imageChanged) { if (imageBundle!.colortable.getBitAmount() > imageBundle!.tableBasedImageData.LZWMinimumCodeSize()) { const compressedData = compressor.compress(clampIndices(dryImage, (1 << imageBundle!.colortable.getBitAmount()) - 1), imageBundle!.colortable.getBitAmount()) imageBundle!.tableBasedImageData.LZWMinimumCodeSize(imageBundle!.colortable.getBitAmount()) imageBundle!.tableBasedImageData.setImageData(compressedData) //imageBundle!.tableBasedImageData.setDryImage(dryImage) } else { const compressedData = compressor.compress(dryImage, imageBundle!.tableBasedImageData.LZWMinimumCodeSize()) imageBundle!.tableBasedImageData.setImageData(compressedData) //imageBundle!.tableBasedImageData.setDryImage(dryImage) } } dialog.remove(); rerenderMainBlock(imageBundle!.tableBasedImageData!.blockID); //we rerender the associcated and possibly changed blocks instead of everything }) const linethicknessInput = template.getElementById("linethickness-template")! as HTMLInputElement linethicknessInput.valueAsNumber = lineWidth linethicknessInput.addEventListener("change", () => { lineWidth = linethicknessInput.valueAsNumber }) { // set the previous and next image buttons const prevbutton = template.getElementById("prevImagebtn-template") as HTMLButtonElement const nextbutton = template.getElementById("nextImagebtn-template") as HTMLButtonElement const currimageIndex = gifView!.getBlockPositionFromID(imageBundle.tableBasedImageData.blockID) const nextImage = gifView!.blocks.find((value, index) => value.blockType == BlockType.TableBasedImageData && index > currimageIndex) if (nextImage) { nextbutton.addEventListener("click", () => { const newDialog = imageDrawDialogVisualizer(gifView!.bundleImage(nextImage)!, parseInt(colorpicker.dataset.colorIndex!), lineWidth) document.body.appendChild(newDialog) dialog.close() newDialog.showModal() }) } else { nextbutton.disabled = true } const prevImage = gifView!.blocks.findLast((value, index) => value.blockType == BlockType.TableBasedImageData && index < currimageIndex) if (prevImage) { prevbutton.addEventListener("click", () => { const newDialog = imageDrawDialogVisualizer(gifView!.bundleImage(prevImage)!, parseInt(colorpicker.dataset.colorIndex!), lineWidth) document.body.appendChild(newDialog) dialog.close() newDialog.showModal() }) } else { prevbutton.disabled = true } } renderColortable(); renderImage(); return dialog } /** * Creates the dialog DOM element that allows a user to download the gif from memory. * @returns The dialog object to be displayed as modal */ export function downloadDialogVisualizer() { const template = (document.getElementById("download-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const naming = template.getElementById("download-filename-template") as HTMLInputElement if (typeof state.currFileName !== "undefined") { naming.value = state.currFileName } const dialog = template.getElementById("download-dialog-template") as HTMLDialogElement dialog.addEventListener("close", () => { dialog.remove(); }) const dwlButton = template.getElementById("download-button-template") as HTMLButtonElement dwlButton.addEventListener("click", () => { downloadCurrentGif(naming.value); dialog.close(); }) const closespan = template.getElementById("closespan-template") as HTMLSpanElement closespan.addEventListener("click", () => dialog.close()) return dialog } /** * Creates a menue element whose entries depends on the given context Block to allow users to add elements to the gif while not breaking it * @param contextBlock * @returns */ function contextDependentAddButtonVisualizer(contextBlock: LogicalScreenDescriptor | TableBasedImageData | CommentExtension | ApplicationExtension | PlainTextExtension) { const template = (document.getElementById("blockAdder-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const addButton = template.getElementById("addbutton-template") as HTMLButtonElement const blockId = contextBlock.blockID addButton.dataset.id = blockId.toString() { // Populate option handlers if (contextBlock.blockType !== BlockType.LogicalScreenDescriptor && contextBlock.blockType !== BlockType.ApplicationExtension) { template.getElementById("choice-duplicate")?.addEventListener("click", () => { BlockAdder.contextSensitiveDuplicateBlock(contextBlock) state.maxBlockCount++ //TODO there should be like a call to a function or a hook or smth. this is not good switchPage(0) }) } else { template.getElementById("choice-duplicate")!.remove(); } template.getElementById("choice-image")?.addEventListener("click", () => { let newImageDialog; if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { newImageDialog = newImageDialogVisualizer(gifView!.globalColorTable) //we need to place the image after the gct } else { newImageDialog = newImageDialogVisualizer(contextBlock) } document.body.appendChild(newImageDialog) newImageDialog.showModal(); }) template.getElementById("choice-uploadimage")?.addEventListener("click", () => { let newImageDialog; if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { newImageDialog = newImageUploadDialogVisualizer(gifView!.globalColorTable) //we need to place the image after the gct } else { newImageDialog = newImageUploadDialogVisualizer(contextBlock) } document.body.appendChild(newImageDialog) newImageDialog.showModal(); }) template.getElementById("choice-comment")?.addEventListener("click", () => { const commentE = new CommentExtension() commentE.setComment("Edit me!") if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { BlockAdder.addCommentBlock(commentE, gifView!.getBlockPositionFromID(gifView!.globalColorTable.blockID)) } else { BlockAdder.addCommentBlock(commentE, gifView!.getBlockPositionFromID(contextBlock.blockID)) } state.maxBlockCount++ switchPage(0) }) template.getElementById("choice-plainText")?.addEventListener("click", () => { //TODO open the plaintext dialog const newPlaintext = new PlainTextExtension({}); const gce = new GraphicControlExtension({}); if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { gifView!.addBlocks([gce, newPlaintext], gifView!.getBlockPositionFromID(gifView!.globalColorTable.blockID)) } else { gifView!.addBlocks([gce, newPlaintext], gifView!.getBlockPositionFromID(contextBlock.blockID)) } state.maxBlockCount++ switchPage(0) }) if (contextBlock.blockType === BlockType.LogicalScreenDescriptor) { // According to some old sources, the loop application should occur directly after the global colortable template.getElementById("choice-application")!.addEventListener("click", () => { const newApplicationDialog = newApplicationDialogVisualizer(gifView!.globalColorTable ? gifView!.globalColorTable : contextBlock) document.body.appendChild(newApplicationDialog) newApplicationDialog.showModal() }) } else { template.getElementById("choice-application")!.remove() } } return template } /** * Creates an element that allows the deletion of a block or multiple blocks from the gif. * The removal is context dependent, such that the gif is still correct afterwards. */ function removeButtonVisualizer(block: GifBlock) { const template = (document.getElementById("blockDeleter-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const deletebutton = template.getElementById("deletebutton-template")! as HTMLButtonElement deletebutton.addEventListener("click", () => { BlockRemover.contextSensitiveRemoveBlock(block) //removeRenderedBlock(block.blockID) switchPage(0) UpdatePageFromTotal() }) return template } /** * Creates the dialog DOM element that allows a user to add a new image to their gif. */ function newImageDialogVisualizer(contextBlock: GifBlock): HTMLDialogElement { const blockID = contextBlock.blockID const template = (document.getElementById("newImage-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const dialog = template.getElementById("newImage-dialog")! as HTMLDialogElement const leftposInput = template.getElementById("id-leftpos-template")! as HTMLInputElement const topposInput = template.getElementById("id-toppos-template")! as HTMLInputElement const widthInput = template.getElementById("id-width-template")! as HTMLInputElement const heightInput = template.getElementById("id-height-template")! as HTMLInputElement const baseCheckbox = template.getElementById("descriptor-choice-template")! as HTMLInputElement baseCheckbox.addEventListener("change", () => { if (baseCheckbox.checked) { leftposInput.disabled = false leftposInput.valueAsNumber = 0 topposInput.disabled = false topposInput.valueAsNumber = 0 widthInput.disabled = false widthInput.valueAsNumber = 100 heightInput.disabled = false heightInput.valueAsNumber = 100 } else { leftposInput.disabled = true topposInput.disabled = true widthInput.disabled = true heightInput.disabled = true } }) const colortableOptions = template.getElementById("color-values-template")! as HTMLDivElement const colortableCheckbox = template.getElementById("color-choice-template")! as HTMLInputElement const colorbitsInput = template.getElementById("colorbits-template")! as HTMLInputElement const lctPresetSelect = template.getElementById("colortable-select-template")! as HTMLSelectElement const gceOptions = template.getElementById("gce-values-template")! as HTMLDivElement const gceCheckbox = template.getElementById("gce-choice-template")! as HTMLInputElement const disposalSelection = template.getElementById("disposalMethodSelection-template")! as HTMLSelectElement for (let key of Object.entries(DisposalMethod).filter((v) => isNaN(Number(v[1])))) { let option = document.createElement("option") option.textContent = key[1].toString() option.value = key[0].toString() disposalSelection.appendChild(option) } const transparentFlagInput = template.getElementById("delay-template")! as HTMLInputElement const delayInput = template.getElementById("delay-template")! as HTMLInputElement const transparentIndexInput = template.getElementById("delay-template")! as HTMLInputElement const addButton = template.getElementById("addImage-button-template")! as HTMLButtonElement addButton.addEventListener("click", () => { // Actually pool the inputs and create the image blocks before adding them let graphicalControl: GraphicControlExtension | undefined; let imagedescriptor: ImageDescriptor; let colortable: ColorTable | undefined; let tableBasedImageData: TableBasedImageData; const idprops: ImageDescriptorProperties = { leftPosition: leftposInput.valueAsNumber, topPosition: topposInput.valueAsNumber, width: widthInput.valueAsNumber, height: heightInput.valueAsNumber } if (colortableCheckbox.checked) { idprops.lctFlag = true; idprops.lctSize = colorbitsInput.valueAsNumber let colors: number[]; switch (lctPresetSelect.value) { case "rgb": colors = BlockAdder.makeRGBColor() break; case "grey": colors = BlockAdder.makeBWColor() break; case "funky": colors = BlockAdder.makeFunkyolor() break; default: colors = BlockAdder.makeRGBColor() break; } if (255 !== (1 << colorbitsInput.valueAsNumber + 1) - 1) { for (let i = 0; i < 255 - ((1 << colorbitsInput.valueAsNumber + 1) - 1); i++) { colors.pop(); colors.pop(); colors.pop(); } } colortable = new ColorTable({ type: BlockType.LocalColorTable, colors: colors }) } imagedescriptor = new ImageDescriptor(idprops) if (gceCheckbox.checked) { graphicalControl = new GraphicControlExtension({ delay: delayInput.valueAsNumber, transparencyFlag: transparentFlagInput.checked, transparencyIndex: transparentIndexInput.valueAsNumber, disposalMethod: parseInt(disposalSelection.value) }) } const tbiData = compressor.compress(Array(widthInput.valueAsNumber * heightInput.valueAsNumber).fill(1), 8) tableBasedImageData = new TableBasedImageData({ lzwMinSize: 8, imageData: tbiData }); const toAdd = [] graphicalControl ? toAdd.push(graphicalControl) : noop(); toAdd.push(imagedescriptor) colortable ? toAdd.push(colortable) : noop(); toAdd.push(tableBasedImageData) gifView!.addBlocks(toAdd, gifView!.getBlockPositionFromID(blockID)) state.maxBlockCount++ dialog.close() switchPage(0) }); const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement exitSpan!.addEventListener("click", () => dialog.close()) dialog.addEventListener("close", () => { dialog.remove(); }) return dialog } /** * Creates the dialog DOM element that allows a user to upload an image into the gif. */ function newImageUploadDialogVisualizer(contextBlock: GifBlock): HTMLDialogElement { const blockID = contextBlock.blockID const template = (document.getElementById("newImageUpload-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const dialog = template.getElementById("newImageUpload-dialog")! as HTMLDialogElement const coloramountInput = template.getElementById("coloramount-input-template")! as HTMLInputElement const fileInput = template.getElementById("image-upload-template")! as HTMLInputElement fileInput.addEventListener("change", () => { handleImageOnUpload(fileInput, gifView?.getBlockPositionFromID(blockID)!, coloramountInput.valueAsNumber).then( () => dialog.close() ) }) const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement exitSpan!.addEventListener("click", () => dialog.close()) dialog.addEventListener("close", () => { dialog.remove(); }) return dialog } /** * Creates the dialog DOM element that allows users to add Application extensions to the gif * For now only Netscape loops are supported */ function newApplicationDialogVisualizer(contextBlock: GifBlock) { const template = (document.getElementById("newApplication-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const dialog = template.getElementById("newApplication-dialog")! as HTMLDialogElement dialog.addEventListener("close", () => dialog.remove()) const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement exitSpan!.addEventListener("click", () => dialog.close()) const loops = template.getElementById("loopAmount")! as HTMLInputElement const addbtn = template.getElementById("addApplication-button-template")! as HTMLButtonElement addbtn.addEventListener("click", () => { const application = new NetscapeLoopApplication(); application.setLoops(loops.valueAsNumber) BlockAdder.addApplicationBlock(application, gifView!.getBlockPosition(contextBlock)) state.maxBlockCount++ dialog.close() switchPage(0) }) return dialog } /** * Creates a documentfragment with all the things needed to display and manipulate an image's related meta data */ function completeImageVisualizer(tbid: TableBasedImageData) { const template = (document.getElementById("bigImageDisplay-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const bundle = gifView!.bundleImage(tbid) const gceTemplate = bundle!.graphicextension ? graphicControlExtensionVisualizer(bundle!.graphicextension) : undefined const lctTemplate = bundle!.colortable.blockType === BlockType.LocalColorTable ? colormapVisualizer(bundle!.colortable) : undefined const idTemplate = imageDescriptorVisualizer(bundle!.imagedescriptor); const tbidTempale = imageVisualizer(bundle!) const addbtn = contextDependentAddButtonVisualizer(tbid) const delbtn = removeButtonVisualizer(tbid) template.getElementById("removeButton-template")?.appendChild(delbtn) template.getElementById("addButton-template")?.appendChild(addbtn) gceTemplate ? template.getElementById("image-gce-elements")?.appendChild(gceTemplate) : noop(); template.getElementById("image-tbid")?.appendChild(tbidTempale) lctTemplate ? template.getElementById("image-lct")?.appendChild(lctTemplate) : noop(); template.getElementById("image-descriptor-elements")?.appendChild(idTemplate) const mainwrapper = template.getElementById("imageDisplay-mainwrapper-template")! mainwrapper.dataset.renderblockid = bundle!.tableBasedImageData.blockID.toString(); return template } /** * Creates a documentfragment with all the things needed to display and manipulate a gif's related meta data. * Version changes are currently not supported tho... */ function completeMetaVisualizer(headerBlock: Header) { const template = (document.getElementById("GIFInfoDisplay-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const header = template.getElementById("GIFInfoDisplay-header-template")! const lsd = template.getElementById("GIFInfoDisplay-LSD-template")! const gct = template.getElementById("GIFInfoDisplay-gct-template")! const addbutton = template.getElementById("addButton-template")! header.appendChild(headerVisualizer(headerBlock)) lsd.appendChild(logicalScreenDescriptorVisualizer(gifView!.logicalScreen!)) gifView!.globalColorTable ? gct.appendChild(colormapVisualizer(gifView!.globalColorTable)) : noop addbutton.appendChild(contextDependentAddButtonVisualizer(gifView!.logicalScreen!)) template.getElementById("GIFInfoDisplay-mainwrapper-template")!.dataset.renderblockid = headerBlock.blockID.toString() return template } /** * Creates a dialog element to display the current gif in an img tag. */ export async function gifPreviewDialog() { const template = (document.getElementById("GifPreview-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment const dialog = template.getElementById("GifPreview-dialog")! as HTMLDialogElement dialog.addEventListener("close", () => { dialog.remove() }) const closespan = template.getElementById("closespan-dialog-template")! closespan.addEventListener("click", () => dialog.close()) const img = template.getElementById("gif-preview-image-template")! as HTMLImageElement const blob = await gifView!.exportToBlob() const reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = function () { img.src = reader.result!.toString(); } return dialog } }