@cjser/sindresorhus__gifkit 0.1.0-cjser.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjser/index.cjs +2168 -0
- package/index.d.ts +838 -0
- package/index.js +118 -0
- package/license +9 -0
- package/package.json +83 -0
- package/readme.md +294 -0
- package/source/byte-stream.js +141 -0
- package/source/constants.js +36 -0
- package/source/decode.js +453 -0
- package/source/encode.js +536 -0
- package/source/loop-count.js +72 -0
- package/source/lzw.js +322 -0
- package/source/quantize.js +185 -0
- package/source/render.js +470 -0
- package/source/validate.js +485 -0
package/source/render.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultMaximumPixelCount,
|
|
3
|
+
defaultMaximumRenderPixelCount,
|
|
4
|
+
defaultMaximumBlockCount,
|
|
5
|
+
isLittleEndian,
|
|
6
|
+
} from './constants.js';
|
|
7
|
+
import {
|
|
8
|
+
requireByte,
|
|
9
|
+
requireNonZeroUnsigned16,
|
|
10
|
+
requireBlockObject,
|
|
11
|
+
requireCountWithinLimit,
|
|
12
|
+
requirePixelCountWithinLimit,
|
|
13
|
+
requirePixelCountValueWithinLimit,
|
|
14
|
+
normalizeColorTable,
|
|
15
|
+
normalizeIndexedPixels,
|
|
16
|
+
normalizeImageGeometry,
|
|
17
|
+
normalizeGraphicControlExtension,
|
|
18
|
+
} from './validate.js';
|
|
19
|
+
import {rejectRenamedLoopCount, normalizePlayCount, renderTotalPlayCount} from './loop-count.js';
|
|
20
|
+
|
|
21
|
+
export function renderGIFFrameSequence(gif, options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
repeat,
|
|
24
|
+
signal,
|
|
25
|
+
...renderOptions
|
|
26
|
+
} = normalizeFrameSequenceOptions(options);
|
|
27
|
+
|
|
28
|
+
if (signal?.aborted) {
|
|
29
|
+
return emptyGIFFrameSequence();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const renderState = createGIFRenderState(gif, renderOptions);
|
|
33
|
+
return renderGIFFrameSequenceFromState(renderState, {repeat, signal});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderGIFFrames(gif, options = {}) {
|
|
37
|
+
const renderState = createGIFRenderState(gif, normalizeRenderOptions(options));
|
|
38
|
+
const renderedFrames = [];
|
|
39
|
+
let renderedPixelCount = 0;
|
|
40
|
+
const beforeRenderFrame = () => {
|
|
41
|
+
renderedPixelCount += renderState.logicalScreenPixelCount;
|
|
42
|
+
requirePixelCountValueWithinLimit(renderedPixelCount, defaultMaximumRenderPixelCount, 'rendered frame pixels');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const frame of renderGIFFrameSequenceFromState(renderState, {beforeRenderFrame})) {
|
|
46
|
+
renderedFrames.push(toRenderedFrame(frame));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
width: renderState.logicalScreenWidth,
|
|
51
|
+
height: renderState.logicalScreenHeight,
|
|
52
|
+
...(renderState.playCount !== undefined && {playCount: renderState.playCount}),
|
|
53
|
+
frames: renderedFrames,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createGIFRenderState(gif, renderOptions) {
|
|
58
|
+
const {background, strict} = renderOptions;
|
|
59
|
+
|
|
60
|
+
if (!gif || typeof gif !== 'object') {
|
|
61
|
+
throw new TypeError('Expected a decoded GIF object');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const logicalScreenWidth = requireNonZeroUnsigned16(gif.width, 'width');
|
|
65
|
+
const logicalScreenHeight = requireNonZeroUnsigned16(gif.height, 'height');
|
|
66
|
+
const logicalScreenPixelCount = requirePixelCountWithinLimit(logicalScreenWidth, logicalScreenHeight, defaultMaximumRenderPixelCount, 'logical screen');
|
|
67
|
+
const globalColorTable = normalizeColorTable(gif.globalColorTable, 'globalColorTable');
|
|
68
|
+
const backgroundColorIndex = requireByte(gif.backgroundColorIndex ?? 0, 'backgroundColorIndex');
|
|
69
|
+
rejectRenamedLoopCount(gif);
|
|
70
|
+
|
|
71
|
+
const playCount = gif.playCount === undefined
|
|
72
|
+
? undefined
|
|
73
|
+
: normalizePlayCount(gif.playCount, 'playCount');
|
|
74
|
+
|
|
75
|
+
let blocks;
|
|
76
|
+
if (gif.blocks === undefined) {
|
|
77
|
+
blocks = [];
|
|
78
|
+
} else if (Array.isArray(gif.blocks)) {
|
|
79
|
+
blocks = gif.blocks;
|
|
80
|
+
} else {
|
|
81
|
+
throw new TypeError('blocks must be an array');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
requireCountWithinLimit(blocks.length, defaultMaximumBlockCount, 'render block count');
|
|
85
|
+
const canvasPixels = new Uint8ClampedArray(logicalScreenWidth * logicalScreenHeight * 4);
|
|
86
|
+
|
|
87
|
+
const renderContext = {
|
|
88
|
+
logicalScreenWidth,
|
|
89
|
+
logicalScreenHeight,
|
|
90
|
+
globalColorTable,
|
|
91
|
+
backgroundColorIndex,
|
|
92
|
+
background,
|
|
93
|
+
strict,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
fillCanvasBackground(canvasPixels, renderContext);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
logicalScreenWidth,
|
|
100
|
+
logicalScreenHeight,
|
|
101
|
+
logicalScreenPixelCount,
|
|
102
|
+
playCount,
|
|
103
|
+
blocks,
|
|
104
|
+
canvasPixels,
|
|
105
|
+
renderContext,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function * renderGIFFrameSequenceFromState(renderState, {repeat = false, signal, beforeRenderFrame} = {}) {
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const totalLoops = repeat
|
|
115
|
+
? renderTotalPlayCount(renderState.playCount)
|
|
116
|
+
: 1;
|
|
117
|
+
const initialCanvasPixels = totalLoops > 1
|
|
118
|
+
? new Uint8ClampedArray(renderState.canvasPixels)
|
|
119
|
+
: undefined;
|
|
120
|
+
let loopIndex = 0;
|
|
121
|
+
|
|
122
|
+
while (loopIndex < totalLoops) {
|
|
123
|
+
if (loopIndex > 0) {
|
|
124
|
+
renderState.canvasPixels.set(initialCanvasPixels);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let frameIndex = 0;
|
|
128
|
+
let blockIndex = 0;
|
|
129
|
+
const blockCount = renderState.blocks.length;
|
|
130
|
+
let isRenderedFrameInLoop = false;
|
|
131
|
+
|
|
132
|
+
while (blockIndex < blockCount) {
|
|
133
|
+
if (signal?.aborted) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const block = renderState.blocks[blockIndex];
|
|
138
|
+
blockIndex += 1;
|
|
139
|
+
requireBlockObject(block);
|
|
140
|
+
|
|
141
|
+
switch (block.type) {
|
|
142
|
+
case 'image': {
|
|
143
|
+
isRenderedFrameInLoop = true;
|
|
144
|
+
beforeRenderFrame?.();
|
|
145
|
+
|
|
146
|
+
yield {
|
|
147
|
+
...renderImageBlockToFrame(renderState.canvasPixels, block, renderState.renderContext),
|
|
148
|
+
index: frameIndex,
|
|
149
|
+
loopIndex,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
frameIndex += 1;
|
|
153
|
+
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case 'commentExtension':
|
|
158
|
+
case 'applicationExtension':
|
|
159
|
+
case 'unknownExtension': {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
default: {
|
|
164
|
+
throw new Error(`Unsupported block type ${JSON.stringify(block.type)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!isRenderedFrameInLoop) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
loopIndex += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function * emptyGIFFrameSequence() {}
|
|
178
|
+
|
|
179
|
+
function toRenderedFrame(frame) {
|
|
180
|
+
return {
|
|
181
|
+
left: frame.left,
|
|
182
|
+
top: frame.top,
|
|
183
|
+
width: frame.width,
|
|
184
|
+
height: frame.height,
|
|
185
|
+
delay: frame.delay,
|
|
186
|
+
disposalMethod: frame.disposalMethod,
|
|
187
|
+
pixels: frame.pixels,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeRenderOptions(options) {
|
|
192
|
+
const {
|
|
193
|
+
background = 'gif',
|
|
194
|
+
strict = true,
|
|
195
|
+
} = options;
|
|
196
|
+
|
|
197
|
+
if (background !== 'transparent' && background !== 'gif') {
|
|
198
|
+
throw new TypeError(`background must be 'transparent' or 'gif', got ${JSON.stringify(background)}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (typeof strict !== 'boolean') {
|
|
202
|
+
throw new TypeError(`strict must be a boolean, got ${JSON.stringify(strict)}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
background,
|
|
207
|
+
strict,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeFrameSequenceOptions(options) {
|
|
212
|
+
const renderOptions = normalizeRenderOptions(options);
|
|
213
|
+
|
|
214
|
+
const {
|
|
215
|
+
repeat = true,
|
|
216
|
+
signal,
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
if (typeof repeat !== 'boolean') {
|
|
220
|
+
throw new TypeError(`repeat must be a boolean, got ${JSON.stringify(repeat)}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (signal !== undefined && typeof signal?.aborted !== 'boolean') {
|
|
224
|
+
throw new TypeError('signal must be an AbortSignal');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
...renderOptions,
|
|
229
|
+
repeat,
|
|
230
|
+
signal,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function fillCanvasBackground(canvasPixels, {globalColorTable, backgroundColorIndex, background}) {
|
|
235
|
+
const backgroundTriplet = getColorTriplet(globalColorTable, backgroundColorIndex);
|
|
236
|
+
const isUseTransparentBackground = background === 'transparent' || backgroundTriplet === undefined;
|
|
237
|
+
const red = isUseTransparentBackground ? 0 : backgroundTriplet[0];
|
|
238
|
+
const green = isUseTransparentBackground ? 0 : backgroundTriplet[1];
|
|
239
|
+
const blue = isUseTransparentBackground ? 0 : backgroundTriplet[2];
|
|
240
|
+
const alpha = isUseTransparentBackground ? 0 : 255;
|
|
241
|
+
|
|
242
|
+
for (let offset = 0; offset < canvasPixels.length; offset += 4) {
|
|
243
|
+
canvasPixels[offset] = red;
|
|
244
|
+
canvasPixels[offset + 1] = green;
|
|
245
|
+
canvasPixels[offset + 2] = blue;
|
|
246
|
+
canvasPixels[offset + 3] = alpha;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderImageBlockToFrame(canvasPixels, imageBlock, renderContext) {
|
|
251
|
+
const geometry = normalizeImageGeometry(imageBlock, {
|
|
252
|
+
logicalScreenWidth: renderContext.logicalScreenWidth,
|
|
253
|
+
logicalScreenHeight: renderContext.logicalScreenHeight,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const indexedPixels = normalizeIndexedPixels(imageBlock.pixels, geometry.width * geometry.height, 'image.pixels');
|
|
257
|
+
|
|
258
|
+
if (indexedPixels === undefined) {
|
|
259
|
+
throw new Error('An image block requires pixels to render');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const colorTable = normalizeColorTable(imageBlock.colorTable, 'colorTable');
|
|
263
|
+
const activeColorTable = colorTable ?? renderContext.globalColorTable;
|
|
264
|
+
if (activeColorTable === undefined) {
|
|
265
|
+
throw new Error('Cannot render an image block without an active color table');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const graphicControlExtension = normalizeGraphicControlExtension(imageBlock.graphicControlExtension);
|
|
269
|
+
requirePixelCountWithinLimit(geometry.width, geometry.height, defaultMaximumPixelCount, 'image block');
|
|
270
|
+
const transparentColorIndex = graphicControlExtension?.transparentColorIndex;
|
|
271
|
+
const disposalMethod = graphicControlExtension?.disposalMethod ?? 'unspecified';
|
|
272
|
+
const normalizedImageBlock = {
|
|
273
|
+
...imageBlock,
|
|
274
|
+
...geometry,
|
|
275
|
+
indexedPixels,
|
|
276
|
+
};
|
|
277
|
+
const activeColorTableUint32 = isLittleEndian ? createColorTableUint32(activeColorTable) : undefined;
|
|
278
|
+
|
|
279
|
+
if (
|
|
280
|
+
renderContext.strict
|
|
281
|
+
&& transparentColorIndex !== undefined
|
|
282
|
+
&& transparentColorIndex >= activeColorTable.length / 3
|
|
283
|
+
) {
|
|
284
|
+
throw new Error(`Transparent color index ${transparentColorIndex} exceeds the active color table`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const snapshotBeforeRendering = disposalMethod === 'restorePrevious' ? new Uint8ClampedArray(canvasPixels) : undefined;
|
|
288
|
+
|
|
289
|
+
drawIndexedImageOntoCanvas(canvasPixels, normalizedImageBlock, {
|
|
290
|
+
...renderContext,
|
|
291
|
+
activeColorTable,
|
|
292
|
+
activeColorTableUint32,
|
|
293
|
+
transparentColorIndex,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const renderedFrame = {
|
|
297
|
+
left: geometry.left,
|
|
298
|
+
top: geometry.top,
|
|
299
|
+
width: geometry.width,
|
|
300
|
+
height: geometry.height,
|
|
301
|
+
delay: (graphicControlExtension?.delayInHundredthsOfASecond ?? 0) / 100,
|
|
302
|
+
disposalMethod,
|
|
303
|
+
pixels: new Uint8ClampedArray(canvasPixels),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (disposalMethod === 'restoreBackground') {
|
|
307
|
+
restoreAreaToBackground(canvasPixels, normalizedImageBlock, renderContext);
|
|
308
|
+
} else if (snapshotBeforeRendering !== undefined) {
|
|
309
|
+
canvasPixels.set(snapshotBeforeRendering);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return renderedFrame;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function drawIndexedImageOntoCanvas(canvasPixels, imageBlock, {activeColorTable, activeColorTableUint32, transparentColorIndex, logicalScreenWidth, strict}) {
|
|
316
|
+
if (activeColorTableUint32 !== undefined) {
|
|
317
|
+
drawIndexedImageOntoCanvasUint32(canvasPixels, imageBlock, {
|
|
318
|
+
activeColorTableUint32,
|
|
319
|
+
transparentColorIndex,
|
|
320
|
+
logicalScreenWidth,
|
|
321
|
+
strict,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const {
|
|
327
|
+
left = 0,
|
|
328
|
+
top = 0,
|
|
329
|
+
width,
|
|
330
|
+
height,
|
|
331
|
+
indexedPixels,
|
|
332
|
+
} = imageBlock;
|
|
333
|
+
for (let row = 0; row < height; row += 1) {
|
|
334
|
+
let sourceOffset = row * width;
|
|
335
|
+
let destinationOffset = (((top + row) * logicalScreenWidth) + left) * 4;
|
|
336
|
+
|
|
337
|
+
for (let column = 0; column < width; column += 1) {
|
|
338
|
+
const pixelIndex = indexedPixels[sourceOffset];
|
|
339
|
+
|
|
340
|
+
sourceOffset += 1;
|
|
341
|
+
|
|
342
|
+
if (transparentColorIndex !== undefined && pixelIndex === transparentColorIndex) {
|
|
343
|
+
destinationOffset += 4;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const colorOffset = pixelIndex * 3;
|
|
348
|
+
if (colorOffset + 2 >= activeColorTable.length) {
|
|
349
|
+
if (strict) {
|
|
350
|
+
throw new Error(`Pixel index ${pixelIndex} is outside the active color table`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
destinationOffset += 4;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
canvasPixels[destinationOffset] = activeColorTable[colorOffset];
|
|
358
|
+
canvasPixels[destinationOffset + 1] = activeColorTable[colorOffset + 1];
|
|
359
|
+
canvasPixels[destinationOffset + 2] = activeColorTable[colorOffset + 2];
|
|
360
|
+
canvasPixels[destinationOffset + 3] = 255;
|
|
361
|
+
destinationOffset += 4;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function drawIndexedImageOntoCanvasUint32(canvasPixels, imageBlock, {activeColorTableUint32, transparentColorIndex, logicalScreenWidth, strict}) {
|
|
367
|
+
const {
|
|
368
|
+
left = 0,
|
|
369
|
+
top = 0,
|
|
370
|
+
width,
|
|
371
|
+
height,
|
|
372
|
+
indexedPixels,
|
|
373
|
+
} = imageBlock;
|
|
374
|
+
const canvasPixelsUint32 = new Uint32Array(canvasPixels.buffer, canvasPixels.byteOffset, canvasPixels.byteLength / Uint32Array.BYTES_PER_ELEMENT);
|
|
375
|
+
for (let row = 0; row < height; row += 1) {
|
|
376
|
+
let sourceOffset = row * width;
|
|
377
|
+
let destinationOffset = ((top + row) * logicalScreenWidth) + left;
|
|
378
|
+
|
|
379
|
+
for (let column = 0; column < width; column += 1) {
|
|
380
|
+
const pixelIndex = indexedPixels[sourceOffset];
|
|
381
|
+
|
|
382
|
+
sourceOffset += 1;
|
|
383
|
+
|
|
384
|
+
if (transparentColorIndex !== undefined && pixelIndex === transparentColorIndex) {
|
|
385
|
+
destinationOffset += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const color = activeColorTableUint32[pixelIndex];
|
|
390
|
+
if (color === undefined) {
|
|
391
|
+
if (strict) {
|
|
392
|
+
throw new Error(`Pixel index ${pixelIndex} is outside the active color table`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
destinationOffset += 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
canvasPixelsUint32[destinationOffset] = color;
|
|
400
|
+
destinationOffset += 1;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function createColorTableUint32(colorTable) {
|
|
406
|
+
const colorTableUint32 = new Uint32Array(colorTable.length / 3);
|
|
407
|
+
|
|
408
|
+
for (let colorIndex = 0; colorIndex < colorTableUint32.length; colorIndex += 1) {
|
|
409
|
+
const colorOffset = colorIndex * 3;
|
|
410
|
+
|
|
411
|
+
colorTableUint32[colorIndex] = 0xFF_00_00_00
|
|
412
|
+
| (colorTable[colorOffset + 2] << 16)
|
|
413
|
+
| (colorTable[colorOffset + 1] << 8)
|
|
414
|
+
| colorTable[colorOffset];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return colorTableUint32;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function restoreAreaToBackground(canvasPixels, imageBlock, {logicalScreenWidth, logicalScreenHeight, globalColorTable, backgroundColorIndex, background}) {
|
|
421
|
+
const {
|
|
422
|
+
left = 0,
|
|
423
|
+
top = 0,
|
|
424
|
+
width,
|
|
425
|
+
height,
|
|
426
|
+
} = imageBlock;
|
|
427
|
+
const backgroundTriplet = getColorTriplet(globalColorTable, backgroundColorIndex);
|
|
428
|
+
const isUseTransparentBackground = background === 'transparent' || backgroundTriplet === undefined;
|
|
429
|
+
const red = isUseTransparentBackground ? 0 : backgroundTriplet[0];
|
|
430
|
+
const green = isUseTransparentBackground ? 0 : backgroundTriplet[1];
|
|
431
|
+
const blue = isUseTransparentBackground ? 0 : backgroundTriplet[2];
|
|
432
|
+
const alpha = isUseTransparentBackground ? 0 : 255;
|
|
433
|
+
|
|
434
|
+
for (let row = 0; row < height; row += 1) {
|
|
435
|
+
const destinationRow = top + row;
|
|
436
|
+
if (destinationRow < 0 || destinationRow >= logicalScreenHeight) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (let column = 0; column < width; column += 1) {
|
|
441
|
+
const destinationColumn = left + column;
|
|
442
|
+
if (destinationColumn < 0 || destinationColumn >= logicalScreenWidth) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const destinationOffset = ((destinationRow * logicalScreenWidth) + destinationColumn) * 4;
|
|
447
|
+
canvasPixels[destinationOffset] = red;
|
|
448
|
+
canvasPixels[destinationOffset + 1] = green;
|
|
449
|
+
canvasPixels[destinationOffset + 2] = blue;
|
|
450
|
+
canvasPixels[destinationOffset + 3] = alpha;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function getColorTriplet(colorTable, paletteIndex) {
|
|
456
|
+
if (colorTable === undefined) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const colorOffset = paletteIndex * 3;
|
|
461
|
+
if (colorOffset + 2 >= colorTable.length) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return [
|
|
466
|
+
colorTable[colorOffset],
|
|
467
|
+
colorTable[colorOffset + 1],
|
|
468
|
+
colorTable[colorOffset + 2],
|
|
469
|
+
];
|
|
470
|
+
}
|