Code for a in-browser gif editor project
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}