Code for a in-browser gif editor project
at master 1390 lines 58 kB view raw
1/** 2 * 3 * This File holds methods for creating the gif related UI elements to be put into the DOM, as well as their functionality 4 * 5 */ 6 7 8/** 9 * Bundled functions that create Documentfragments for given blocks to be placed in the DOM 10 */ 11namespace Visuals { 12 13 /** 14 * Creates the main DOM Elements for some blocktypes. 15 * Due to the way blocks are displayed this fucntion only returns a DocumentFragment on the following Blocktypes: 16 * Header, ImageDescriptor, PlainTextExtension, CommentExtension, ApplicationExtension, Trailer 17 * @returns DocumentFragment corresponding to the given block. 18 */ 19 export function createMainRenderedBlock(block: GifBlock): DocumentFragment | undefined { 20 switch (block.blockType) { 21 case BlockType.Header: 22 return completeMetaVisualizer(block as Header) 23 case BlockType.TableBasedImageData: 24 return completeImageVisualizer(block as TableBasedImageData) 25 case BlockType.Trailer: 26 return trailerVisualizer(block); 27 case BlockType.CommentExtension: 28 return commentVisualizer(block as CommentExtension); 29 case BlockType.PlainTextExtension: 30 return plainTextExtensionVisualizer(block as PlainTextExtension); 31 case BlockType.ApplicationExtension: 32 return applicationExtensionGenericVisualizer(block as ApplicationExtension); 33 default: 34 return undefined 35 } 36 37 } 38 39 /** 40 * Creates Document fragments for each individual blocktype. 41 * No generalized big blocks like we want, but can be used for internal replacement 42 */ 43 export function createRenderedBlock(block: GifBlock) { 44 switch (block.blockType) { 45 case BlockType.LogicalScreenDescriptor: 46 return logicalScreenDescriptorVisualizer(block as LogicalScreenDescriptor); 47 case BlockType.Header: 48 return headerVisualizer(block as Header); 49 case BlockType.GlobalColorTable: 50 return colormapVisualizer(block as ColorTable, 4, 50); 51 case BlockType.TableBasedImageData: 52 return imageVisualizer(gifView!.bundleImage(block)!); 53 case BlockType.ImageDescriptor: 54 return imageDescriptorVisualizer(block as ImageDescriptor) 55 case BlockType.LocalColorTable: 56 return colormapVisualizer(block as ColorTable, 4, 50); 57 case BlockType.Trailer: 58 return trailerVisualizer(block); 59 case BlockType.GraphicControlExtension: 60 return graphicControlExtensionVisualizer(block as GraphicControlExtension); 61 case BlockType.CommentExtension: 62 return commentVisualizer(block as CommentExtension); 63 case BlockType.PlainTextExtension: 64 return plainTextExtensionVisualizer(block as PlainTextExtension); 65 case BlockType.ApplicationExtension: 66 return applicationExtensionGenericVisualizer(block as ApplicationExtension); 67 default: 68 return undefined 69 } 70 } 71 72 /** 73 * Creates a document fragments to add a color table to the DOM. 74 * @param colormapBlockPointer The ColorTable object holding the colors 75 * @param widthPerColor how many pixel should each color be wide 76 * @param heightTotal how many pixel should each color be high 77 */ 78 function colormapVisualizer(colormapBlockPointer: ColorTable, widthPerColor = 4, heightTotal = 50) { 79 const template = (document.getElementById("colortable-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 80 81 const canvas = template.getElementById("innercanvas-template") as HTMLCanvasElement; 82 const ctx = canvas.getContext("2d"); 83 const colors = colormapBlockPointer.data!; 84 const coloramount = colormapBlockPointer.getColorAmount(); 85 const width = widthPerColor * coloramount 86 canvas.setAttribute("width", "" + width); 87 canvas.setAttribute("height", "" + heightTotal); 88 let pixels = new Array(); 89 90 for (let j = 0; j < heightTotal; j++) { 91 for (let i = 0; i < colors.length / 3; i++) { 92 for (let k = 0; k < widthPerColor; k++) { 93 pixels.push(colors[i * 3]); 94 pixels.push(colors[i * 3 + 1]); 95 pixels.push(colors[i * 3 + 2]); 96 pixels.push(255); 97 } 98 } 99 } 100 101 const newimgdata = new ImageData(Uint8ClampedArray.from(pixels), width, heightTotal); 102 ctx?.putImageData(newimgdata, 0, 0); 103 104 canvas.addEventListener("click", () => { 105 const dialog = imageDrawDialogVisualizer(gifView!.bundleImage(colormapBlockPointer)!) 106 document.getElementById("canvas-div")!.appendChild(dialog) 107 dialog.showModal() 108 }) 109 110 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 111 wrapdiv.dataset.blockid = colormapBlockPointer.blockID.toString() 112 113 return template; 114 } 115 116 function logicalScreenDescriptorVisualizer(LSDBlock: LogicalScreenDescriptor) { 117 const template = (document.getElementById("logicalscreendescriptor-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 118 { // Screen Width 119 const iwidth = template.getElementById("lsd-sw-template")! as HTMLInputElement 120 iwidth.max = "65535" 121 iwidth.min = "0"; 122 iwidth.valueAsNumber = LSDBlock.screenWidth() 123 const updateScreenWidth = function (this: HTMLInputElement, event: Event) { 124 this.valueAsNumber = isNaN(this.valueAsNumber) ? 250 : Math.min(Math.max(this.valueAsNumber, 0), 65535) 125 LSDBlock.screenWidth(this.valueAsNumber) 126 } 127 iwidth.addEventListener("change", updateScreenWidth) 128 } 129 130 { // Screen Height 131 const iheight = template.getElementById("lsd-sh-template")! as HTMLInputElement 132 iheight.max = "65535" 133 iheight.min = "0"; 134 iheight.valueAsNumber = LSDBlock.screenHeight() 135 const updateScreenHeight = function (this: HTMLInputElement, event: Event) { 136 let newNumber = parseInt(this.value) 137 newNumber = isNaN(newNumber) ? 250 : Math.min(Math.max(newNumber, 0), 65535) 138 LSDBlock.screenHeight(newNumber) 139 } 140 iheight.addEventListener("change", updateScreenHeight) 141 } 142 143 { // Global Color Table Flag 144 const igctflag = template.getElementById("lsd-gctflag-template")! as HTMLInputElement 145 igctflag.checked = LSDBlock.gctFlag() 146 const updateGCTFlag = function (this: HTMLInputElement, ev: Event) { 147 LSDBlock.gctFlag(this.checked) 148 149 if (this.checked) { //need to add a new gct 150 const gctColorAmount = 1 << (LSDBlock.gctSize() + 1) 151 const newColortableColor = BlockAdder.makeRGBColor() 152 if (256 !== gctColorAmount) { 153 for (let i = 0; i < 256 - gctColorAmount; i++) { 154 newColortableColor.pop() 155 newColortableColor.pop() 156 newColortableColor.pop() 157 } 158 } 159 const newgct = new ColorTable({ colors: newColortableColor, type: BlockType.GlobalColorTable }) 160 gifView!.globalColorTable = newgct 161 gifView!.addBlocks([newgct], gifView!.getBlockPosition(LSDBlock)) 162 } else { // bye bye gct 163 gifView!.removeBlocks([gifView!.globalColorTable!.blockID]); 164 gifView!.globalColorTable = undefined; 165 } 166 167 rerenderMainBlock(gifView!.bundleMeta()!.header.blockID) 168 169 // 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 170 document.querySelectorAll((`[data-renderblockid]`)).forEach( 171 (value) => { 172 const block = gifView!.getBlockFromID(parseInt((value as HTMLElement).dataset.renderblockid!)) 173 if (block!.blockType === BlockType.TableBasedImageData && !gifView!.bundleImage(block!)!.imagedescriptor.lctFlag()) { 174 rerenderMainBlock(block!.blockID) 175 } 176 } 177 ) 178 179 } 180 igctflag.addEventListener("change", updateGCTFlag) 181 } 182 183 { // Color Resolution 184 const icolorres = template.getElementById("lsd-colorresolution-template")! as HTMLInputElement 185 icolorres.min = "0" 186 icolorres.max = "7" 187 icolorres.valueAsNumber = LSDBlock.colorResolution() 188 const updateColorResolution = function (this: HTMLInputElement, ev: Event) { 189 this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) 190 LSDBlock.colorResolution(this.valueAsNumber) 191 } 192 icolorres.addEventListener("change", updateColorResolution) 193 } 194 195 { // Sort Flag 196 const isortflag = template.getElementById("lsd-sortflag-template")! as HTMLInputElement 197 isortflag.checked = LSDBlock.gctSortedFlag() 198 const updateSortFlag = function (this: HTMLInputElement, ev: Event) { 199 LSDBlock.gctSortedFlag(this.checked) 200 } 201 isortflag.addEventListener("change", updateSortFlag) 202 } 203 204 { // Global Color Table Size 205 const igctsize = template.getElementById("lsd-gctsize-template")! as HTMLInputElement 206 igctsize.min = "0" 207 igctsize.max = "7" 208 igctsize.valueAsNumber = LSDBlock.gctSize() 209 const updateGlobalColorTableSize = function (this: HTMLInputElement, ev: Event) { 210 if (!isNaN(this.valueAsNumber)) { 211 this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) 212 LSDBlock.gctSize(this.valueAsNumber) 213 if (LSDBlock.gctFlag()) { 214 const newAmount = 1 << (this.valueAsNumber + 1) 215 const bundled = gifView!.bundleMeta()! 216 const colortable = bundled.globalColorTable! 217 if (colortable.getColorAmount() < newAmount) { 218 const newcolor = BlockAdder.makeRGBColor().slice(3 * colortable.getColorAmount(), 3 * newAmount) 219 colortable.data = Uint8Array.from(Array.from(colortable.data).concat(newcolor)) 220 } else if (colortable.getColorAmount() > newAmount) { 221 colortable.data = colortable.data.slice(0, newAmount * 3) 222 } 223 } 224 225 rerenderMainBlock(gifView!.bundleMeta()!.header.blockID) 226 document.querySelectorAll((`[data-renderblockid]`)).forEach( 227 (value) => { 228 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. 229 if (block!.blockType === BlockType.TableBasedImageData && !gifView!.bundleImage(block!)!.imagedescriptor.lctFlag()) { 230 rerenderMainBlock(block!.blockID) 231 } 232 } 233 ) 234 235 } 236 } 237 igctsize.addEventListener("change", updateGlobalColorTableSize) 238 } 239 240 { //Background Color Index 241 const ibci = template.getElementById("lsd-bci-template")! as HTMLInputElement 242 ibci.valueAsNumber = LSDBlock.backgroundColorIndex() 243 ibci.min = "0" 244 ibci.max = "255" 245 const updateBackgroundColorIndex = function (this: HTMLInputElement, ev: Event) { 246 isNaN(this.valueAsNumber) ? noop() : LSDBlock.backgroundColorIndex(this.valueAsNumber) 247 } 248 ibci.addEventListener("change", updateBackgroundColorIndex) 249 } 250 251 { // Pixel Aspect Ratio 252 const ipar = template.getElementById("lsd-par-template")! as HTMLInputElement 253 ipar.valueAsNumber = LSDBlock.pixelAspectRatio() 254 ipar.min = "0" 255 ipar.max = "255" 256 const updatePixelApsectRatio = function (this: HTMLInputElement, ev: Event) { 257 isNaN(this.valueAsNumber) ? noop() : LSDBlock.pixelAspectRatio(this.valueAsNumber) 258 } 259 ipar.addEventListener("change", updatePixelApsectRatio) 260 } 261 262 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 263 wrapdiv.dataset.blockid = LSDBlock.blockID.toString() //was moved to the bigger block 264 265 return template; 266 } 267 268 function imageVisualizer(gifImage: GifBlockImage) { 269 const template = (document.getElementById("tablebasedImageData-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 270 271 const lct = gifImage.colortable; 272 273 const width = gifImage.imagedescriptor.width() 274 const height = gifImage.imagedescriptor.height() 275 const colortable = lct!.data! 276 277 const canvas = template.getElementById("innercanvas-template") as HTMLCanvasElement; 278 canvas.setAttribute("id", "test-canvas-" + gifImage.tableBasedImageData.blockID); //we use the pointer as unique ID because every tbid has a different one 279 canvas.setAttribute("width", "" + width); 280 canvas.setAttribute("height", "" + height); 281 282 const ctx = canvas.getContext("2d"); 283 284 // dry image aka the index sequence in cache so we decompress only once 285 const decompressed = cachedImageDecompression(gifImage.tableBasedImageData) 286 287 const picstream = indexStreamToPicture(decompressed, colortable) 288 289 canvas.addEventListener("click", () => { 290 const dialog = imageDrawDialogVisualizer(gifImage) 291 document.getElementById("canvas-div")!.appendChild(dialog) 292 dialog.showModal() 293 }) 294 295 const newimgdata = picStreamToImageData(picstream, width, height) 296 ctx?.putImageData(newimgdata, 0, 0); 297 298 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 299 wrapdiv.dataset.blockid = gifImage.tableBasedImageData.blockID.toString() 300 return template; 301 } 302 303 function graphicControlExtensionVisualizer(gceblock: GraphicControlExtension) { 304 const template = (document.getElementById("graphiccontrolextension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 305 306 const disposalMethodSelection = template.getElementById("disposalMethodSelection-template")! as HTMLSelectElement 307 for (let key of Object.entries(DisposalMethod).filter((v) => isNaN(Number(v[1])))) { 308 let option = document.createElement("option") 309 option.textContent = key[1].toString() 310 option.value = key[0].toString() 311 disposalMethodSelection.appendChild(option) 312 } 313 disposalMethodSelection.value = gceblock.disposalMethod().toString() 314 const updateDisposalMethod = function (this: HTMLSelectElement, ev: Event) { 315 gceblock.disposalMethod(parseInt(this.value)) 316 } 317 disposalMethodSelection.addEventListener("change", updateDisposalMethod) 318 319 const userInputFlagCheckbox = template.getElementById("userInputFlagCheckbox-template")! as HTMLInputElement 320 userInputFlagCheckbox.id = gceblock.blockID + "-userInputFlagCheckbox" // blockpointers should be unqiue so we can use that for label targeting 321 userInputFlagCheckbox.checked = gceblock.userInputFlag() 322 const updateUserInput = function (this: HTMLInputElement, ev: Event) { 323 gceblock.userInputFlag(this.checked) 324 } 325 userInputFlagCheckbox.addEventListener("change", updateUserInput) 326 327 const transparentColorFlagCheckbox = template.getElementById("transparentColorFlag-template")! as HTMLInputElement 328 transparentColorFlagCheckbox.checked = gceblock.transparencyFlag() 329 const updatetransparentColorFlagCheckbox = function (this: HTMLInputElement, ev: Event) { 330 gceblock.transparencyFlag(this.checked) 331 } 332 transparentColorFlagCheckbox.addEventListener("change", updatetransparentColorFlagCheckbox) 333 334 const delayInput = template.getElementById("delay-template")! as HTMLInputElement 335 delayInput.valueAsNumber = gceblock.delay() 336 const updateDelay = function (this: HTMLInputElement, event: Event) { 337 let newNumber = parseInt(this.value) 338 newNumber = isNaN(newNumber) ? 0 : Math.min(Math.max(newNumber, 0), ((1 << 16) - 1)) // 0 as default on wrong user inputs 339 gceblock.delay(newNumber) 340 } 341 delayInput.addEventListener("change", updateDelay) 342 343 const transparencyColorSelection = template.getElementById("transparentcolorindex-template")! as HTMLInputElement 344 { // adding base values 345 transparencyColorSelection.max = "255" 346 transparencyColorSelection.valueAsNumber = gceblock.transparencyIndex() 347 } 348 349 const updateTransparencyColor = function (this: HTMLSelectElement, ev: Event) { 350 const newIndex = isNaN(parseInt(this.value)) ? 0 : parseInt(this.value) 351 gceblock.transparencyIndex(newIndex) 352 } 353 transparencyColorSelection.addEventListener("change", updateTransparencyColor) 354 355 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 356 wrapdiv.dataset.blockid = gceblock.blockID.toString() 357 358 return template 359 } 360 361 function applicationExtensionGenericVisualizer(applicationBlock: ApplicationExtension) { 362 const template = (document.getElementById("applicationExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 363 const appId = applicationBlock.identifier(); 364 const appAuth = applicationBlock.authentification(); 365 const appData = applicationBlock.getApplicationdata(); 366 367 template.getElementById("applicationIdentifier")!.textContent = appId; 368 template.getElementById("applicationAuthentication")!.textContent = String.fromCharCode(...appAuth); 369 370 //TODO maybe make this its own special thingy and have the bigger function decide which subclass to use and so on 371 if (appId === "NETSCAPE" && String.fromCharCode(...appAuth) === "2.0") { 372 const loopinput = document.createElement("input") 373 loopinput.type = "number" 374 loopinput.min = "0" 375 loopinput.max = "65535" 376 loopinput.id = "Netscape-loops" 377 //data in the subdatablock is [Sub-block ID|LoppData1|LoopData2] so we want bytes 16 and 17 378 loopinput.valueAsNumber = applicationBlock.data![16] | (applicationBlock.data![17] << 8) //loopcount 379 const updateLoopCount = function (this: HTMLInputElement, ev: Event) { 380 applicationBlock.data![16] = this.valueAsNumber & 0b11111111 381 applicationBlock.data![17] = (this.valueAsNumber >> 8) & 0b11111111 382 } 383 loopinput.addEventListener("change", updateLoopCount) 384 385 const loopLabel = document.createElement("label") 386 loopLabel.textContent = "Loop Count (0 = infinit): " 387 loopLabel.appendChild(loopinput) 388 389 template.getElementById("applicationData")!.appendChild(loopLabel) 390 } else { 391 template.getElementById("applicationData")!.textContent = String.fromCharCode(...appData); 392 } 393 394 const addbtn = contextDependentAddButtonVisualizer(applicationBlock); 395 const removebtn = removeButtonVisualizer(applicationBlock); 396 template.getElementById("removeButton-template")!.appendChild(removebtn); 397 template.getElementById("addButton-template")!.appendChild(addbtn); 398 399 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 400 wrapdiv.dataset.blockid = applicationBlock.blockID.toString() 401 wrapdiv.dataset.renderblockid = applicationBlock.blockID.toString() //special purpose id for the main block sued to render this element 402 403 404 return template 405 } 406 407 function commentVisualizer(commentBlock: CommentExtension) { 408 const template = (document.getElementById("commentExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 409 const commentTextField = template.getElementById("comment-text-template")! as HTMLParagraphElement 410 commentTextField.textContent = commentBlock.getComment(); 411 commentTextField.addEventListener("input", () => { 412 commentBlock.setComment(commentTextField.textContent) 413 }) 414 415 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 416 wrapdiv.dataset.blockid = commentBlock.blockID.toString() 417 wrapdiv.dataset.renderblockid = commentBlock.blockID.toString() 418 419 const addButton = contextDependentAddButtonVisualizer(commentBlock) 420 template.getElementById("addButton-template")!.appendChild(addButton) 421 422 const deleteButton = removeButtonVisualizer(commentBlock) 423 template.getElementById("removeButton-template")!.appendChild(deleteButton) 424 425 426 return template 427 } 428 429 function headerVisualizer(headerBlock: Header) { 430 const template = (document.getElementById("header-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 431 const versionP = template.getElementById("gifVersion-template")! 432 versionP.textContent = String.fromCharCode(...headerBlock.data!.slice(3)); 433 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 434 wrapdiv.dataset.blockid = headerBlock.blockID.toString() 435 436 return template; 437 } 438 439 function imageDescriptorVisualizer(idBlock: ImageDescriptor) { 440 const template = (document.getElementById("imagedescriptor-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 441 442 { // Left Position 443 const leftpos = template.getElementById("id-leftpos-template") as HTMLInputElement 444 leftpos.valueAsNumber = idBlock.leftPosition() 445 leftpos.max = "65535" 446 leftpos.min = "0" 447 const updateLeftPos = function (this: HTMLInputElement, ev: Event) { 448 if (!isNaN(this.valueAsNumber)) { 449 idBlock.leftPosition(this.valueAsNumber) 450 } 451 } 452 leftpos.addEventListener("change", updateLeftPos) 453 } 454 455 { // Top Position 456 const topPos = template.getElementById("id-toppos-template") as HTMLInputElement 457 topPos.valueAsNumber = idBlock.topPosition() 458 topPos.max = "65535" 459 topPos.min = "0" 460 const updateLeftPos = function (this: HTMLInputElement, ev: Event) { 461 if (!isNaN(this.valueAsNumber)) { 462 idBlock.topPosition(this.valueAsNumber) 463 } 464 } 465 topPos.addEventListener("change", updateLeftPos) 466 } 467 468 { // Width 469 const width = template.getElementById("id-width-template") as HTMLInputElement 470 width.valueAsNumber = idBlock.width() 471 width.max = "65535" 472 width.min = "1" // in theory a gif image could have a 0 width but we don't allow it here 473 const updateLeftPos = function (this: HTMLInputElement, ev: Event) { 474 if (!isNaN(this.valueAsNumber)) { 475 const prevWidth = idBlock.width(); 476 const height = idBlock.height() 477 const diff = this.valueAsNumber - prevWidth 478 const tbid = gifView!.bundleImage(idBlock)!.tableBasedImageData 479 const indexstream = cachedImageDecompression(tbid) 480 if (diff < 0) { 481 for (let i = 1; i <= height; i++) { 482 indexstream.splice((i * prevWidth - ((i) * Math.abs(diff))), Math.abs(diff)); 483 } 484 485 } else if (diff > 0) { 486 const replace = [] 487 for (let j = 0; j < diff; j++) { 488 replace.push(0) 489 } 490 491 for (let i = 1; i <= height; i++) { 492 indexstream.splice((i * prevWidth + (i - 1) * diff), 0, ...replace); 493 } 494 } else { 495 return 496 } 497 498 const newtbiddata = compressor.compress(indexstream, tbid.LZWMinimumCodeSize()) 499 tbid.setImageData(newtbiddata); 500 //tbid.setDryImage(indexstream) 501 idBlock.width(this.valueAsNumber) 502 503 rerenderMainBlock(tbid.blockID) 504 } 505 } 506 width.addEventListener("change", updateLeftPos) 507 } 508 509 { // Height 510 const height = template.getElementById("id-height-template") as HTMLInputElement 511 height.valueAsNumber = idBlock.height() 512 height.max = "65535" 513 height.min = "1" 514 const updateLeftPos = function (this: HTMLInputElement, ev: Event) { 515 if (!isNaN(this.valueAsNumber)) { 516 const prevHeight = idBlock.height(); 517 const width = idBlock.width(); 518 const diff = this.valueAsNumber - prevHeight 519 const tbid = gifView!.bundleImage(idBlock)!.tableBasedImageData 520 const indexstream = cachedImageDecompression(tbid) 521 522 if (diff == 0) { 523 return; 524 } 525 526 if (diff < 0) { 527 indexstream.splice(indexstream.length - 1 + (diff * width)) 528 529 } else if (diff > 0) { 530 for (let j = 0; j < diff * width; j++) { 531 indexstream.push(0) 532 } 533 } 534 535 const newtbiddata = compressor.compress(indexstream, tbid.LZWMinimumCodeSize()) 536 tbid.setImageData(newtbiddata); 537 //tbid.setDryImage(indexstream) 538 idBlock.height(this.valueAsNumber) 539 540 rerenderMainBlock(tbid.blockID) 541 } 542 } 543 height.addEventListener("change", updateLeftPos) 544 } 545 546 { // LCT Flag 547 const lctflag = template.getElementById("id-lctflag-template") as HTMLInputElement 548 lctflag.checked = idBlock.lctFlag() 549 const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { 550 const bundled = gifView!.bundleImage(idBlock) 551 idBlock.lctFlag(this.checked) 552 553 if (this.checked) { //need to add a new local Color Table 554 const lctColorAmount = 1 << (idBlock.lctSize() + 1) 555 const newColortableColor = BlockAdder.makeRGBColor() 556 if (256 !== lctColorAmount) { 557 for (let i = 0; i < 256 - lctColorAmount; i++) { 558 newColortableColor.pop() 559 newColortableColor.pop() 560 newColortableColor.pop() 561 } 562 } 563 gifView!.addBlocks([new ColorTable({ colors: newColortableColor, type: BlockType.LocalColorTable })], gifView!.getBlockPosition(idBlock)) 564 } else { //bye bye lct 565 gifView!.removeBlocks([bundled!.colortable.blockID]); 566 } 567 568 rerenderMainBlock(bundled!.tableBasedImageData.blockID) 569 } 570 lctflag.addEventListener("change", updateLCTFlag) 571 } 572 573 { // Interlace Flag 574 const interlaceFlag = template.getElementById("id-interlaceflag-template") as HTMLInputElement 575 interlaceFlag.checked = idBlock.interlacedFlag() 576 const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { 577 idBlock.interlacedFlag(this.checked) 578 } 579 interlaceFlag.addEventListener("change", updateLCTFlag) 580 } 581 582 { // Sort Flag 583 const sortFlag = template.getElementById("id-sortflag-template") as HTMLInputElement 584 sortFlag.checked = idBlock.lctSortedFlag() 585 const updateLCTFlag = function (this: HTMLInputElement, ev: Event) { 586 idBlock.lctSortedFlag(this.checked) 587 } 588 sortFlag.addEventListener("change", updateLCTFlag) 589 } 590 591 { // LCT Size 592 const lctSize = template.getElementById("id-lctsize-template") as HTMLInputElement 593 lctSize.valueAsNumber = idBlock.lctSize() 594 lctSize.min = "0" 595 lctSize.max = "7" 596 const updateLCTSize = function (this: HTMLInputElement, ev: Event) { 597 if (!isNaN(this.valueAsNumber)) { 598 this.valueAsNumber = Math.min(Math.max(this.valueAsNumber, 0), 7) 599 idBlock.lctSize(this.valueAsNumber) 600 601 if (idBlock.lctFlag()) { 602 const newAmount = 1 << (this.valueAsNumber + 1) 603 const bundled = gifView!.bundleImage(idBlock)! 604 const colortable = bundled.colortable! 605 if (colortable.getColorAmount() < newAmount) { 606 const newcolor = BlockAdder.makeRGBColor().slice(3 * colortable.getColorAmount(), 3 * newAmount) 607 colortable.setColors((Array.from(colortable.data).concat(newcolor))) 608 609 } else if (colortable.getColorAmount() > newAmount) { 610 colortable.data = colortable.data.slice(0, newAmount * 3) 611 } 612 613 rerenderMainBlock(bundled!.tableBasedImageData.blockID) 614 } 615 } 616 } 617 618 lctSize.addEventListener("change", updateLCTSize) 619 } 620 621 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 622 wrapdiv.dataset.blockid = idBlock.blockID.toString() 623 624 return template; 625 } 626 627 function trailerVisualizer(trailerBlock: Trailer) { 628 const template = (document.getElementById("imagetrailer-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 629 630 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 631 wrapdiv.dataset.blockid = trailerBlock.blockID.toString() 632 wrapdiv.dataset.renderblockid = trailerBlock.blockID.toString() 633 634 return template; 635 } 636 637 function plainTextExtensionVisualizer(plainTextBlock: PlainTextExtension) { 638 const template = (document.getElementById("plainTextExtension-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 639 640 const leftpos = template.getElementById("pte-leftpos-template") as HTMLInputElement 641 const toppos = template.getElementById("pte-toppos-template") as HTMLInputElement 642 const gridwidth = template.getElementById("pte-gridwidth-template") as HTMLInputElement 643 const gridheight = template.getElementById("pte-gridheight-template") as HTMLInputElement 644 const cellwidth = template.getElementById("pte-cellwidth-template") as HTMLInputElement 645 const cellheight = template.getElementById("pte-cellheight-template") as HTMLInputElement 646 const foregroundindex = template.getElementById("pte-foreground-template") as HTMLInputElement 647 const backgroundindex = template.getElementById("pte-background-template") as HTMLInputElement 648 const plaintext = template.getElementById("pte-plaintext-template") as HTMLInputElement 649 650 //set initial values 651 leftpos.valueAsNumber = plainTextBlock.gridLeftPosition() 652 toppos.valueAsNumber = plainTextBlock.gridTopPosition() 653 gridwidth.valueAsNumber = plainTextBlock.gridWidth() 654 gridheight.valueAsNumber = plainTextBlock.gridHeight() 655 cellwidth.valueAsNumber = plainTextBlock.cellWidth() 656 cellheight.valueAsNumber = plainTextBlock.cellHeight() 657 foregroundindex.valueAsNumber = plainTextBlock.textForegorundColorIndex() 658 backgroundindex.valueAsNumber = plainTextBlock.textBackgroundColorIndex() 659 plaintext.textContent = plainTextBlock.getPlainTextData() 660 661 //event listeners on change 662 leftpos.addEventListener("change", () => { 663 plainTextBlock.gridLeftPosition(leftpos.valueAsNumber) 664 }) 665 toppos.addEventListener("change", () => { 666 plainTextBlock.gridTopPosition(toppos.valueAsNumber) 667 }) 668 gridwidth.addEventListener("change", () => { 669 plainTextBlock.gridWidth(gridwidth.valueAsNumber) 670 }) 671 gridheight.addEventListener("change", () => { 672 plainTextBlock.gridHeight(gridheight.valueAsNumber) 673 }) 674 cellwidth.addEventListener("change", () => { 675 plainTextBlock.cellWidth(cellwidth.valueAsNumber) 676 }) 677 cellheight.addEventListener("change", () => { 678 plainTextBlock.cellHeight(cellheight.valueAsNumber) 679 }) 680 foregroundindex.addEventListener("change", () => { 681 plainTextBlock.textForegorundColorIndex(foregroundindex.valueAsNumber) 682 }) 683 backgroundindex.addEventListener("change", () => { 684 plainTextBlock.textBackgroundColorIndex(backgroundindex.valueAsNumber) 685 }) 686 plaintext.addEventListener("input", () => { 687 plainTextBlock.setPlainTextData(plaintext.textContent) 688 }) 689 690 691 //set graphic control extension 692 const bundle = gifView!.bundlePlainText(plainTextBlock)! 693 if (bundle.graphicextension) { 694 template.getElementById("pte-gce-template")!.appendChild(graphicControlExtensionVisualizer(bundle.graphicextension)) 695 } 696 697 // set data so we can rerender the block if needed 698 const wrapdiv = template.querySelector(".canvas-element") as HTMLDivElement 699 wrapdiv.dataset.blockid = plainTextBlock.blockID.toString() 700 wrapdiv.dataset.renderblockid = plainTextBlock.blockID.toString() 701 702 //add and delete buttons 703 const addbtn = contextDependentAddButtonVisualizer(plainTextBlock) 704 const removebtn = removeButtonVisualizer(plainTextBlock) 705 template.getElementById("removeButton-template")?.appendChild(removebtn) 706 template.getElementById("addButton-template")?.appendChild(addbtn) 707 708 return template 709 } 710 711 /** 712 * Creates the dialog DOM element that allows a user to paint over a specified image. 713 * Drawing works via a secondary canvas layer and using the colortable indices. 714 * @param imageBundle the bundled information for the image which is to be used in the dialog 715 * @param initColorindex intial draw color index. default is 0 716 * @param initLineWidth initial draw thickness in pixel. default is 10px 717 * @returns The dialog object to be displayed as modal 718 */ 719 export function imageDrawDialogVisualizer(imageBundle: GifBlockImage, initColorindex = 0, initLineWidth = 3) { 720 //Not as efficient as things could be, but good enough 721 722 const template = (document.getElementById("imagedraw-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 723 724 const dryImage = Array.from(cachedImageDecompression(imageBundle!.tableBasedImageData)) //base symbols for the image which we can hydrate with the colortable 725 726 const imgwidth = imageBundle!.imagedescriptor.width(); 727 const imgheight = imageBundle!.imagedescriptor.height(); 728 const colortableWidthPerColor = 4; 729 const colortableWidth = colortableWidthPerColor * imageBundle!.colortable!.getColorAmount(); 730 const colortableHeight = 50; 731 732 const dialog = template.getElementById("draw-dialog")! as HTMLDialogElement 733 734 const closespan = template.getElementById("closespan-template")! as HTMLSpanElement 735 closespan.addEventListener("click", () => dialog.close()) 736 737 const colortablecanvas = template.getElementById("colortablecanvas-template")! as HTMLCanvasElement 738 colortablecanvas.width = colortableWidth 739 colortablecanvas.height = colortableHeight; 740 741 const imagecanvas = template.getElementById("imagelayer")! as HTMLCanvasElement 742 imagecanvas.width = imgwidth; 743 imagecanvas.height = imgheight; 744 745 const drawcanvas = template.getElementById("drawlayer")! as HTMLCanvasElement; //we will draw on this 746 drawcanvas.width = imgwidth; 747 drawcanvas.height = imgheight; 748 749 if (initColorindex >= imageBundle!.colortable!.getColorAmount()) { 750 initColorindex = 0; 751 } 752 753 const colorindexInput = template.getElementById("choosencolorindex-template")! as HTMLInputElement 754 colorindexInput.min = "0" 755 colorindexInput.valueAsNumber = initColorindex 756 colorindexInput.max = (imageBundle!.colortable!.getColorAmount() - 1).toString() 757 758 const drawmodeButton = template.getElementById("drawmodebutton-template")! as HTMLButtonElement //TODO 759 760 const colorpicker = template.getElementById("draw-colorpicker")! as HTMLInputElement 761 { //set initial values 762 colorpicker.dataset.colorIndex = initColorindex.toString(); // colortable index of the color that is going to be influenced by the picker 763 let r = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3].toString(16); 764 let g = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3 + 1].toString(16); 765 let b = imageBundle!.colortable!.data![colorindexInput.valueAsNumber * 3 + 2].toString(16); 766 colorpicker.value = `#${r.length == 1 ? "0" + r : r}${g.length == 1 ? "0" + g : g}${b.length == 1 ? "0" + b : b}` 767 drawcanvas.getContext("2d")!.strokeStyle = colorpicker.value 768 drawcanvas.getContext("2d")!.fillStyle = colorpicker.value 769 } 770 771 const updatePickerandInput = (colorindex: number) => { 772 colorpicker.dataset.colorIndex = colorindex.toString() 773 colorindexInput.valueAsNumber = colorindex; 774 const r = imageBundle!.colortable!.data![colorindex * 3].toString(16); 775 const g = imageBundle!.colortable!.data![colorindex * 3 + 1].toString(16); 776 const b = imageBundle!.colortable!.data![colorindex * 3 + 2].toString(16); 777 colorpicker.value = `#${r.length == 1 ? "0" + r : r}${g.length == 1 ? "0" + g : g}${b.length == 1 ? "0" + b : b}`; 778 drawcanvas.getContext("2d")!.strokeStyle = colorpicker.value; 779 drawcanvas.getContext("2d")!.fillStyle = colorpicker.value; 780 } 781 782 // sets the colorindex that the picker influences and changes its default value to the color selected on the colortable 783 function selectColorFromTableSelection(this: HTMLCanvasElement, ptr: PointerEvent) { 784 const index = Math.min(Math.floor((ptr.x - this.getBoundingClientRect().left) / colortableWidthPerColor), 255); 785 updatePickerandInput(index) 786 } 787 788 const selectColorFromInput = function (this: HTMLInputElement, e: Event) { 789 if (isNaN(this.valueAsNumber)) { 790 this.valueAsNumber = 0 791 } 792 this.valueAsNumber = Math.min(Math.max(parseInt(this.min), this.valueAsNumber), parseInt(this.max)) 793 updatePickerandInput(this.valueAsNumber) 794 } 795 796 // takes the drawn pixel from the draw canvas and updates the dry image indices with the currently selected colortable index 797 function transposeDrawingToImage() { 798 const drawctx = drawcanvas.getContext("2d")! 799 const drawdata = drawctx.getImageData(0, 0, drawcanvas.width, drawcanvas.height).data 800 801 for (let i = 0; i < drawcanvas.width * drawcanvas.height; i++) { 802 if (drawdata[i * 4 + 3] != 0) { 803 dryImage[i] = parseInt(colorpicker.dataset.colorIndex!); 804 } 805 } 806 drawctx.clearRect(0, 0, drawcanvas.width, drawcanvas.height) 807 renderImage() 808 } 809 810 //values to track during drawing 811 let drawing = false; 812 let lineWidth = initLineWidth; 813 let imageChanged = false; 814 let drawmode = true; // flag to switch between drawing and color selection 815 816 function changeColor(this: HTMLInputElement, ev: Event) { //called when the color picker has a change/input event. updates the colormap and the image 817 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(this.value) 818 let newColor = { r: 0, g: 0, b: 0 } 819 if (result) { 820 newColor = { 821 r: parseInt(result[1], 16), 822 g: parseInt(result[2], 16), 823 b: parseInt(result[3], 16) 824 } 825 } 826 const index = this.dataset.colorIndex ? parseInt(this.dataset.colorIndex) : 0 827 imageBundle!.colortable!.data![3 * index] = newColor.r; 828 imageBundle!.colortable!.data![3 * index + 1] = newColor.g; 829 imageBundle!.colortable!.data![3 * index + 2] = newColor.b; 830 renderColortable(); 831 renderImage(); 832 833 drawcanvas.getContext("2d")!.strokeStyle = this.value 834 drawcanvas.getContext("2d")!.fillStyle = colorpicker.value; 835 } 836 837 function renderColortable() { 838 const pixels = []; 839 const ctx = colortablecanvas.getContext("2d"); 840 const colors = imageBundle!.colortable!.data!; 841 for (let j = 0; j < colortableHeight; j++) { 842 for (let i = 0; i < imageBundle!.colortable!.getColorAmount(); i++) { 843 for (let k = 0; k < colortableWidthPerColor; k++) { 844 pixels.push(colors[i * 3]); 845 pixels.push(colors[i * 3 + 1]); 846 pixels.push(colors[i * 3 + 2]); 847 pixels.push(255); 848 } 849 } 850 } 851 ctx?.putImageData( 852 new ImageData(Uint8ClampedArray.from(pixels), colortableWidth, colortableHeight) 853 , 0, 0); 854 } 855 856 function renderImage() { 857 //hydrate image data with the colortable from memory 858 const picstream = indexStreamToPicture(dryImage, imageBundle!.colortable!.data!) 859 const ctx = imagecanvas.getContext("2d"); 860 const newimgdata = picStreamToImageData(picstream, imgwidth, imgheight)// new ImageData(Uint8ClampedArray.from(picstream), imgwidth, imgheight) 861 ctx?.putImageData(newimgdata, 0, 0); 862 } 863 864 //attach all listeners 865 colortablecanvas.addEventListener("click", selectColorFromTableSelection) 866 867 drawmodeButton.addEventListener("click", function(){ 868 drawmode = !drawmode 869 //change button text/image to indicate mode 870 drawmodeButton.classList.toggle("colorselect-button") 871 }) 872 873 // 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. 874 template.getElementById("resetbtn-template")!.addEventListener("click", () => { 875 const backupDryImage = cachedImageDecompression(imageBundle!.tableBasedImageData) 876 for (let index = 0; index < dryImage.length; index++) { 877 dryImage[index] = backupDryImage[index] 878 } 879 renderImage(); 880 }) 881 882 drawcanvas.addEventListener("mousedown", function (mouse: MouseEvent) { 883 if (drawmode) { 884 drawing = true; 885 imageChanged = true; 886 } else { 887 const y = Math.round(mouse.clientY) - Math.round(this.getBoundingClientRect().top) 888 const x = Math.round(mouse.clientX) - Math.round(this.getBoundingClientRect().left) 889 const index = Math.max(0, Math.min(y * imgwidth + x, dryImage.length - 1)) 890 updatePickerandInput(dryImage[index]) 891 } 892 893 }); 894 895 drawcanvas.addEventListener("mouseup", function(){ 896 if (drawing) { 897 drawing = false; 898 const drawctx = this.getContext("2d")! 899 drawctx.stroke(); 900 drawctx.beginPath(); 901 transposeDrawingToImage(); 902 } 903 }); 904 905 drawcanvas.addEventListener("mousemove", function(mouse: MouseEvent){ 906 if (drawing) { 907 const drawctx = this.getContext("2d")! 908 drawctx.lineWidth = lineWidth; 909 drawctx.lineCap = 'round'; 910 911 drawctx.lineTo(mouse.clientX - this.getBoundingClientRect().left, mouse.y - this.getBoundingClientRect().top); 912 drawctx.stroke(); 913 } 914 }); 915 916 drawcanvas.addEventListener("mouseleave", function (mouse: MouseEvent){ 917 if (drawing) { 918 const drawctx = this.getContext("2d")! 919 drawctx.lineTo(mouse.clientX - this.getBoundingClientRect().left, mouse.y - this.getBoundingClientRect().top); 920 drawctx.stroke(); 921 drawctx.beginPath(); 922 transposeDrawingToImage(); 923 } 924 }); 925 926 drawcanvas.addEventListener("mouseenter", function (mouse: MouseEvent){ 927 if (drawing && (mouse.buttons & 0b1) == 0) { 928 drawing = false; 929 const drawctx = this.getContext("2d")! 930 drawctx.stroke(); 931 drawctx.beginPath(); 932 } 933 }); 934 935 colorpicker.addEventListener("change", changeColor) 936 //We so not change the images colors on input cause that's better for performance on big pictures. 937 colorindexInput.addEventListener("change", selectColorFromInput) 938 939 dialog.addEventListener("close", () => { 940 // update image data based on the changes made 941 if (imageChanged) { 942 if (imageBundle!.colortable.getBitAmount() > imageBundle!.tableBasedImageData.LZWMinimumCodeSize()) { 943 const compressedData = compressor.compress(clampIndices(dryImage, (1 << imageBundle!.colortable.getBitAmount()) - 1), imageBundle!.colortable.getBitAmount()) 944 imageBundle!.tableBasedImageData.LZWMinimumCodeSize(imageBundle!.colortable.getBitAmount()) 945 imageBundle!.tableBasedImageData.setImageData(compressedData) 946 //imageBundle!.tableBasedImageData.setDryImage(dryImage) 947 } else { 948 const compressedData = compressor.compress(dryImage, imageBundle!.tableBasedImageData.LZWMinimumCodeSize()) 949 imageBundle!.tableBasedImageData.setImageData(compressedData) 950 //imageBundle!.tableBasedImageData.setDryImage(dryImage) 951 } 952 } 953 954 dialog.remove(); 955 rerenderMainBlock(imageBundle!.tableBasedImageData!.blockID); //we rerender the associcated and possibly changed blocks instead of everything 956 }) 957 958 const linethicknessInput = template.getElementById("linethickness-template")! as HTMLInputElement 959 linethicknessInput.valueAsNumber = lineWidth 960 linethicknessInput.addEventListener("change", () => { 961 lineWidth = linethicknessInput.valueAsNumber 962 }) 963 964 { // set the previous and next image buttons 965 const prevbutton = template.getElementById("prevImagebtn-template") as HTMLButtonElement 966 const nextbutton = template.getElementById("nextImagebtn-template") as HTMLButtonElement 967 968 const currimageIndex = gifView!.getBlockPositionFromID(imageBundle.tableBasedImageData.blockID) 969 const nextImage = gifView!.blocks.find((value, index) => value.blockType == BlockType.TableBasedImageData && index > currimageIndex) 970 if (nextImage) { 971 nextbutton.addEventListener("click", () => { 972 const newDialog = imageDrawDialogVisualizer(gifView!.bundleImage(nextImage)!, parseInt(colorpicker.dataset.colorIndex!), lineWidth) 973 document.body.appendChild(newDialog) 974 dialog.close() 975 newDialog.showModal() 976 }) 977 } else { 978 nextbutton.disabled = true 979 } 980 981 const prevImage = gifView!.blocks.findLast((value, index) => value.blockType == BlockType.TableBasedImageData && index < currimageIndex) 982 if (prevImage) { 983 prevbutton.addEventListener("click", () => { 984 const newDialog = imageDrawDialogVisualizer(gifView!.bundleImage(prevImage)!, parseInt(colorpicker.dataset.colorIndex!), lineWidth) 985 document.body.appendChild(newDialog) 986 dialog.close() 987 newDialog.showModal() 988 }) 989 } else { 990 prevbutton.disabled = true 991 } 992 993 } 994 renderColortable(); 995 renderImage(); 996 997 return dialog 998 } 999 1000 /** 1001 * Creates the dialog DOM element that allows a user to download the gif from memory. 1002 * @returns The dialog object to be displayed as modal 1003 */ 1004 export function downloadDialogVisualizer() { 1005 const template = (document.getElementById("download-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1006 const naming = template.getElementById("download-filename-template") as HTMLInputElement 1007 1008 if (typeof state.currFileName !== "undefined") { 1009 naming.value = state.currFileName 1010 } 1011 1012 const dialog = template.getElementById("download-dialog-template") as HTMLDialogElement 1013 dialog.addEventListener("close", () => { 1014 dialog.remove(); 1015 }) 1016 1017 const dwlButton = template.getElementById("download-button-template") as HTMLButtonElement 1018 dwlButton.addEventListener("click", () => { 1019 downloadCurrentGif(naming.value); 1020 dialog.close(); 1021 }) 1022 1023 const closespan = template.getElementById("closespan-template") as HTMLSpanElement 1024 closespan.addEventListener("click", () => dialog.close()) 1025 1026 return dialog 1027 } 1028 1029 /** 1030 * 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 1031 * @param contextBlock 1032 * @returns 1033 */ 1034 function contextDependentAddButtonVisualizer(contextBlock: LogicalScreenDescriptor | TableBasedImageData | CommentExtension | ApplicationExtension | PlainTextExtension) { 1035 const template = (document.getElementById("blockAdder-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1036 const addButton = template.getElementById("addbutton-template") as HTMLButtonElement 1037 const blockId = contextBlock.blockID 1038 addButton.dataset.id = blockId.toString() 1039 1040 { // Populate option handlers 1041 if (contextBlock.blockType !== BlockType.LogicalScreenDescriptor && contextBlock.blockType !== BlockType.ApplicationExtension) { 1042 template.getElementById("choice-duplicate")?.addEventListener("click", () => { 1043 BlockAdder.contextSensitiveDuplicateBlock(contextBlock) 1044 state.maxBlockCount++ //TODO there should be like a call to a function or a hook or smth. this is not good 1045 switchPage(0) 1046 }) 1047 } else { 1048 template.getElementById("choice-duplicate")!.remove(); 1049 } 1050 1051 template.getElementById("choice-image")?.addEventListener("click", () => { 1052 let newImageDialog; 1053 if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { 1054 newImageDialog = newImageDialogVisualizer(gifView!.globalColorTable) //we need to place the image after the gct 1055 } else { 1056 newImageDialog = newImageDialogVisualizer(contextBlock) 1057 } 1058 document.body.appendChild(newImageDialog) 1059 newImageDialog.showModal(); 1060 }) 1061 1062 template.getElementById("choice-uploadimage")?.addEventListener("click", () => { 1063 let newImageDialog; 1064 if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { 1065 newImageDialog = newImageUploadDialogVisualizer(gifView!.globalColorTable) //we need to place the image after the gct 1066 } else { 1067 newImageDialog = newImageUploadDialogVisualizer(contextBlock) 1068 } 1069 document.body.appendChild(newImageDialog) 1070 newImageDialog.showModal(); 1071 }) 1072 1073 template.getElementById("choice-comment")?.addEventListener("click", () => { 1074 const commentE = new CommentExtension() 1075 commentE.setComment("Edit me!") 1076 if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { 1077 BlockAdder.addCommentBlock(commentE, gifView!.getBlockPositionFromID(gifView!.globalColorTable.blockID)) 1078 } else { 1079 BlockAdder.addCommentBlock(commentE, gifView!.getBlockPositionFromID(contextBlock.blockID)) 1080 } 1081 state.maxBlockCount++ 1082 switchPage(0) 1083 }) 1084 1085 template.getElementById("choice-plainText")?.addEventListener("click", () => { 1086 //TODO open the plaintext dialog 1087 const newPlaintext = new PlainTextExtension({}); 1088 const gce = new GraphicControlExtension({}); 1089 if (contextBlock.blockType === BlockType.LogicalScreenDescriptor && gifView!.globalColorTable) { 1090 gifView!.addBlocks([gce, newPlaintext], gifView!.getBlockPositionFromID(gifView!.globalColorTable.blockID)) 1091 } else { 1092 gifView!.addBlocks([gce, newPlaintext], gifView!.getBlockPositionFromID(contextBlock.blockID)) 1093 } 1094 state.maxBlockCount++ 1095 switchPage(0) 1096 }) 1097 1098 if (contextBlock.blockType === BlockType.LogicalScreenDescriptor) { 1099 // According to some old sources, the loop application should occur directly after the global colortable 1100 template.getElementById("choice-application")!.addEventListener("click", () => { 1101 const newApplicationDialog = newApplicationDialogVisualizer(gifView!.globalColorTable ? gifView!.globalColorTable : contextBlock) 1102 document.body.appendChild(newApplicationDialog) 1103 newApplicationDialog.showModal() 1104 }) 1105 } else { 1106 template.getElementById("choice-application")!.remove() 1107 } 1108 1109 } 1110 1111 return template 1112 1113 } 1114 1115 /** 1116 * Creates an element that allows the deletion of a block or multiple blocks from the gif. 1117 * The removal is context dependent, such that the gif is still correct afterwards. 1118 */ 1119 function removeButtonVisualizer(block: GifBlock) { 1120 const template = (document.getElementById("blockDeleter-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1121 const deletebutton = template.getElementById("deletebutton-template")! as HTMLButtonElement 1122 1123 deletebutton.addEventListener("click", () => { 1124 BlockRemover.contextSensitiveRemoveBlock(block) 1125 //removeRenderedBlock(block.blockID) 1126 switchPage(0) 1127 UpdatePageFromTotal() 1128 }) 1129 1130 return template 1131 } 1132 1133 /** 1134 * Creates the dialog DOM element that allows a user to add a new image to their gif. 1135 */ 1136 function newImageDialogVisualizer(contextBlock: GifBlock): HTMLDialogElement { 1137 1138 const blockID = contextBlock.blockID 1139 1140 const template = (document.getElementById("newImage-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1141 const dialog = template.getElementById("newImage-dialog")! as HTMLDialogElement 1142 1143 const leftposInput = template.getElementById("id-leftpos-template")! as HTMLInputElement 1144 const topposInput = template.getElementById("id-toppos-template")! as HTMLInputElement 1145 const widthInput = template.getElementById("id-width-template")! as HTMLInputElement 1146 const heightInput = template.getElementById("id-height-template")! as HTMLInputElement 1147 const baseCheckbox = template.getElementById("descriptor-choice-template")! as HTMLInputElement 1148 baseCheckbox.addEventListener("change", () => { 1149 if (baseCheckbox.checked) { 1150 leftposInput.disabled = false 1151 leftposInput.valueAsNumber = 0 1152 topposInput.disabled = false 1153 topposInput.valueAsNumber = 0 1154 widthInput.disabled = false 1155 widthInput.valueAsNumber = 100 1156 heightInput.disabled = false 1157 heightInput.valueAsNumber = 100 1158 } else { 1159 leftposInput.disabled = true 1160 topposInput.disabled = true 1161 widthInput.disabled = true 1162 heightInput.disabled = true 1163 } 1164 }) 1165 1166 const colortableOptions = template.getElementById("color-values-template")! as HTMLDivElement 1167 const colortableCheckbox = template.getElementById("color-choice-template")! as HTMLInputElement 1168 1169 const colorbitsInput = template.getElementById("colorbits-template")! as HTMLInputElement 1170 const lctPresetSelect = template.getElementById("colortable-select-template")! as HTMLSelectElement 1171 1172 const gceOptions = template.getElementById("gce-values-template")! as HTMLDivElement 1173 const gceCheckbox = template.getElementById("gce-choice-template")! as HTMLInputElement 1174 1175 const disposalSelection = template.getElementById("disposalMethodSelection-template")! as HTMLSelectElement 1176 for (let key of Object.entries(DisposalMethod).filter((v) => isNaN(Number(v[1])))) { 1177 let option = document.createElement("option") 1178 option.textContent = key[1].toString() 1179 option.value = key[0].toString() 1180 disposalSelection.appendChild(option) 1181 } 1182 1183 const transparentFlagInput = template.getElementById("delay-template")! as HTMLInputElement 1184 const delayInput = template.getElementById("delay-template")! as HTMLInputElement 1185 const transparentIndexInput = template.getElementById("delay-template")! as HTMLInputElement 1186 1187 const addButton = template.getElementById("addImage-button-template")! as HTMLButtonElement 1188 addButton.addEventListener("click", () => { // Actually pool the inputs and create the image blocks before adding them 1189 let graphicalControl: GraphicControlExtension | undefined; 1190 let imagedescriptor: ImageDescriptor; 1191 let colortable: ColorTable | undefined; 1192 let tableBasedImageData: TableBasedImageData; 1193 1194 const idprops: ImageDescriptorProperties = { 1195 leftPosition: leftposInput.valueAsNumber, topPosition: topposInput.valueAsNumber, 1196 width: widthInput.valueAsNumber, height: heightInput.valueAsNumber 1197 } 1198 1199 if (colortableCheckbox.checked) { 1200 idprops.lctFlag = true; 1201 idprops.lctSize = colorbitsInput.valueAsNumber 1202 let colors: number[]; 1203 switch (lctPresetSelect.value) { 1204 case "rgb": 1205 colors = BlockAdder.makeRGBColor() 1206 break; 1207 case "grey": 1208 colors = BlockAdder.makeBWColor() 1209 break; 1210 case "funky": 1211 colors = BlockAdder.makeFunkyolor() 1212 break; 1213 default: 1214 colors = BlockAdder.makeRGBColor() 1215 break; 1216 } 1217 if (255 !== (1 << colorbitsInput.valueAsNumber + 1) - 1) { 1218 for (let i = 0; i < 255 - ((1 << colorbitsInput.valueAsNumber + 1) - 1); i++) { 1219 colors.pop(); 1220 colors.pop(); 1221 colors.pop(); 1222 } 1223 } 1224 colortable = new ColorTable({ type: BlockType.LocalColorTable, colors: colors }) 1225 } 1226 1227 imagedescriptor = new ImageDescriptor(idprops) 1228 1229 if (gceCheckbox.checked) { 1230 graphicalControl = new GraphicControlExtension({ 1231 delay: delayInput.valueAsNumber, transparencyFlag: transparentFlagInput.checked, 1232 transparencyIndex: transparentIndexInput.valueAsNumber, disposalMethod: parseInt(disposalSelection.value) 1233 }) 1234 } 1235 1236 const tbiData = compressor.compress(Array(widthInput.valueAsNumber * heightInput.valueAsNumber).fill(1), 8) 1237 tableBasedImageData = new TableBasedImageData({ lzwMinSize: 8, imageData: tbiData }); 1238 1239 const toAdd = [] 1240 graphicalControl ? toAdd.push(graphicalControl) : noop(); 1241 toAdd.push(imagedescriptor) 1242 colortable ? toAdd.push(colortable) : noop(); 1243 toAdd.push(tableBasedImageData) 1244 1245 gifView!.addBlocks(toAdd, gifView!.getBlockPositionFromID(blockID)) 1246 state.maxBlockCount++ 1247 dialog.close() 1248 switchPage(0) 1249 }); 1250 1251 const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement 1252 exitSpan!.addEventListener("click", () => dialog.close()) 1253 1254 dialog.addEventListener("close", () => { 1255 dialog.remove(); 1256 }) 1257 1258 return dialog 1259 } 1260 1261 /** 1262 * Creates the dialog DOM element that allows a user to upload an image into the gif. 1263 */ 1264 function newImageUploadDialogVisualizer(contextBlock: GifBlock): HTMLDialogElement { 1265 const blockID = contextBlock.blockID 1266 const template = (document.getElementById("newImageUpload-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1267 const dialog = template.getElementById("newImageUpload-dialog")! as HTMLDialogElement 1268 const coloramountInput = template.getElementById("coloramount-input-template")! as HTMLInputElement 1269 1270 const fileInput = template.getElementById("image-upload-template")! as HTMLInputElement 1271 fileInput.addEventListener("change", () => { 1272 handleImageOnUpload(fileInput, gifView?.getBlockPositionFromID(blockID)!, coloramountInput.valueAsNumber).then( 1273 () => dialog.close() 1274 ) 1275 1276 }) 1277 1278 const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement 1279 exitSpan!.addEventListener("click", () => dialog.close()) 1280 1281 dialog.addEventListener("close", () => { 1282 dialog.remove(); 1283 }) 1284 1285 return dialog 1286 } 1287 1288 /** 1289 * Creates the dialog DOM element that allows users to add Application extensions to the gif 1290 * For now only Netscape loops are supported 1291 */ 1292 function newApplicationDialogVisualizer(contextBlock: GifBlock) { 1293 const template = (document.getElementById("newApplication-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1294 1295 const dialog = template.getElementById("newApplication-dialog")! as HTMLDialogElement 1296 dialog.addEventListener("close", () => dialog.remove()) 1297 1298 const exitSpan = template.getElementById("closespan-dialog-template")! as HTMLSpanElement 1299 exitSpan!.addEventListener("click", () => dialog.close()) 1300 1301 const loops = template.getElementById("loopAmount")! as HTMLInputElement 1302 const addbtn = template.getElementById("addApplication-button-template")! as HTMLButtonElement 1303 addbtn.addEventListener("click", () => { 1304 const application = new NetscapeLoopApplication(); 1305 application.setLoops(loops.valueAsNumber) 1306 BlockAdder.addApplicationBlock(application, gifView!.getBlockPosition(contextBlock)) 1307 state.maxBlockCount++ 1308 dialog.close() 1309 switchPage(0) 1310 }) 1311 1312 return dialog 1313 } 1314 1315 /** 1316 * Creates a documentfragment with all the things needed to display and manipulate an image's related meta data 1317 */ 1318 function completeImageVisualizer(tbid: TableBasedImageData) { 1319 const template = (document.getElementById("bigImageDisplay-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1320 1321 const bundle = gifView!.bundleImage(tbid) 1322 const gceTemplate = bundle!.graphicextension ? graphicControlExtensionVisualizer(bundle!.graphicextension) : undefined 1323 const lctTemplate = bundle!.colortable.blockType === BlockType.LocalColorTable ? colormapVisualizer(bundle!.colortable) : undefined 1324 const idTemplate = imageDescriptorVisualizer(bundle!.imagedescriptor); 1325 const tbidTempale = imageVisualizer(bundle!) 1326 const addbtn = contextDependentAddButtonVisualizer(tbid) 1327 const delbtn = removeButtonVisualizer(tbid) 1328 1329 1330 template.getElementById("removeButton-template")?.appendChild(delbtn) 1331 template.getElementById("addButton-template")?.appendChild(addbtn) 1332 1333 gceTemplate ? template.getElementById("image-gce-elements")?.appendChild(gceTemplate) : noop(); 1334 template.getElementById("image-tbid")?.appendChild(tbidTempale) 1335 lctTemplate ? template.getElementById("image-lct")?.appendChild(lctTemplate) : noop(); 1336 template.getElementById("image-descriptor-elements")?.appendChild(idTemplate) 1337 1338 const mainwrapper = template.getElementById("imageDisplay-mainwrapper-template")! 1339 mainwrapper.dataset.renderblockid = bundle!.tableBasedImageData.blockID.toString(); 1340 1341 1342 return template 1343 } 1344 1345 /** 1346 * Creates a documentfragment with all the things needed to display and manipulate a gif's related meta data. 1347 * Version changes are currently not supported tho... 1348 */ 1349 function completeMetaVisualizer(headerBlock: Header) { 1350 const template = (document.getElementById("GIFInfoDisplay-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1351 const header = template.getElementById("GIFInfoDisplay-header-template")! 1352 const lsd = template.getElementById("GIFInfoDisplay-LSD-template")! 1353 const gct = template.getElementById("GIFInfoDisplay-gct-template")! 1354 1355 const addbutton = template.getElementById("addButton-template")! 1356 1357 header.appendChild(headerVisualizer(headerBlock)) 1358 lsd.appendChild(logicalScreenDescriptorVisualizer(gifView!.logicalScreen!)) 1359 gifView!.globalColorTable ? gct.appendChild(colormapVisualizer(gifView!.globalColorTable)) : noop 1360 1361 addbutton.appendChild(contextDependentAddButtonVisualizer(gifView!.logicalScreen!)) 1362 1363 template.getElementById("GIFInfoDisplay-mainwrapper-template")!.dataset.renderblockid = headerBlock.blockID.toString() 1364 1365 return template 1366 } 1367 1368 /** 1369 * Creates a dialog element to display the current gif in an img tag. 1370 */ 1371 export async function gifPreviewDialog() { 1372 const template = (document.getElementById("GifPreview-dialog-template")! as HTMLTemplateElement).content.cloneNode(true) as DocumentFragment 1373 1374 const dialog = template.getElementById("GifPreview-dialog")! as HTMLDialogElement 1375 dialog.addEventListener("close", () => { dialog.remove() }) 1376 1377 const closespan = template.getElementById("closespan-dialog-template")! 1378 closespan.addEventListener("click", () => dialog.close()) 1379 1380 const img = template.getElementById("gif-preview-image-template")! as HTMLImageElement 1381 const blob = await gifView!.exportToBlob() 1382 const reader = new FileReader(); 1383 reader.readAsDataURL(blob); 1384 reader.onloadend = function () { 1385 img.src = reader.result!.toString(); 1386 } 1387 return dialog 1388 } 1389 1390}