Showing
5 changed files
with
1751 additions
and
0 deletions
1 | +import GIF from "./lib/GIFEncoder"; | ||
2 | + | ||
3 | +class GifGenerator { | ||
4 | + constructor(canvas) { | ||
5 | + this.canvas = canvas; | ||
6 | + this.width = canvas.getWidth(); | ||
7 | + this.height = canvas.getHeight(); | ||
8 | + this.gif = new GIF(this.width, this.height); | ||
9 | + | ||
10 | + this.gif.writeHeader(); | ||
11 | + this.gif.setTransparent(null); | ||
12 | + this.gif.setRepeat(0); | ||
13 | + this.gif.setQuality(10); | ||
14 | + this.gif.setDither(false); | ||
15 | + this.gif.setGlobalPalette(false); | ||
16 | + } | ||
17 | + | ||
18 | + addFrame(delay = 0) { | ||
19 | + this.gif.setDelay(delay); | ||
20 | + this.gif.addFrame( | ||
21 | + this.canvas.getContext().getImageData(0, 0, this.width, this.height).data | ||
22 | + ); | ||
23 | + } | ||
24 | + | ||
25 | + render() { | ||
26 | + this.gif.finish(); | ||
27 | + const stream = this.gif.stream(); | ||
28 | + | ||
29 | + let bytes = []; | ||
30 | + stream.pages.map((page) => { | ||
31 | + bytes = bytes.concat([...page]); | ||
32 | + }); | ||
33 | + bytes = new Uint8Array(bytes); | ||
34 | + | ||
35 | + return new Blob([bytes], { type: "image/gif" }); | ||
36 | + } | ||
37 | +} | ||
38 | + | ||
39 | +window.GifGenerator = GifGenerator; | ... | ... |
gif-generator/src/lib/GIFEncoder.js
0 → 100644
1 | +/* | ||
2 | + GIFEncoder.js | ||
3 | + | ||
4 | + Authors | ||
5 | + Kevin Weiner (original Java version - kweiner@fmsware.com) | ||
6 | + Thibault Imbert (AS3 version - bytearray.org) | ||
7 | + Johan Nordberg (JS version - code@johan-nordberg.com) | ||
8 | +*/ | ||
9 | + | ||
10 | +var NeuQuant = require("./TypedNeuQuant.js"); | ||
11 | +var LZWEncoder = require("./LZWEncoder.js"); | ||
12 | + | ||
13 | +function ByteArray() { | ||
14 | + this.page = -1; | ||
15 | + this.pages = []; | ||
16 | + this.newPage(); | ||
17 | +} | ||
18 | + | ||
19 | +ByteArray.pageSize = 4096; | ||
20 | +ByteArray.charMap = {}; | ||
21 | + | ||
22 | +for (var i = 0; i < 256; i++) ByteArray.charMap[i] = String.fromCharCode(i); | ||
23 | + | ||
24 | +ByteArray.prototype.newPage = function () { | ||
25 | + this.pages[++this.page] = new Uint8Array(ByteArray.pageSize); | ||
26 | + this.cursor = 0; | ||
27 | +}; | ||
28 | + | ||
29 | +ByteArray.prototype.getData = function () { | ||
30 | + var rv = ""; | ||
31 | + for (var p = 0; p < this.pages.length; p++) { | ||
32 | + for (var i = 0; i < ByteArray.pageSize; i++) { | ||
33 | + rv += ByteArray.charMap[this.pages[p][i]]; | ||
34 | + } | ||
35 | + } | ||
36 | + return rv; | ||
37 | +}; | ||
38 | + | ||
39 | +ByteArray.prototype.writeByte = function (val) { | ||
40 | + if (this.cursor >= ByteArray.pageSize) this.newPage(); | ||
41 | + this.pages[this.page][this.cursor++] = val; | ||
42 | +}; | ||
43 | + | ||
44 | +ByteArray.prototype.writeUTFBytes = function (string) { | ||
45 | + for (var l = string.length, i = 0; i < l; i++) | ||
46 | + this.writeByte(string.charCodeAt(i)); | ||
47 | +}; | ||
48 | + | ||
49 | +ByteArray.prototype.writeBytes = function (array, offset, length) { | ||
50 | + for (var l = length || array.length, i = offset || 0; i < l; i++) | ||
51 | + this.writeByte(array[i]); | ||
52 | +}; | ||
53 | + | ||
54 | +function GIFEncoder(width, height) { | ||
55 | + // image size | ||
56 | + this.width = ~~width; | ||
57 | + this.height = ~~height; | ||
58 | + | ||
59 | + // transparent color if given | ||
60 | + this.transparent = null; | ||
61 | + | ||
62 | + // transparent index in color table | ||
63 | + this.transIndex = 0; | ||
64 | + | ||
65 | + // -1 = no repeat, 0 = forever. anything else is repeat count | ||
66 | + this.repeat = -1; | ||
67 | + | ||
68 | + // frame delay (hundredths) | ||
69 | + this.delay = 0; | ||
70 | + | ||
71 | + this.image = null; // current frame | ||
72 | + this.pixels = null; // BGR byte array from frame | ||
73 | + this.indexedPixels = null; // converted frame indexed to palette | ||
74 | + this.colorDepth = null; // number of bit planes | ||
75 | + this.colorTab = null; // RGB palette | ||
76 | + this.neuQuant = null; // NeuQuant instance that was used to generate this.colorTab. | ||
77 | + this.usedEntry = new Array(); // active palette entries | ||
78 | + this.palSize = 7; // color table size (bits-1) | ||
79 | + this.dispose = -1; // disposal code (-1 = use default) | ||
80 | + this.firstFrame = true; | ||
81 | + this.sample = 10; // default sample interval for quantizer | ||
82 | + this.dither = false; // default dithering | ||
83 | + this.globalPalette = false; | ||
84 | + | ||
85 | + this.out = new ByteArray(); | ||
86 | +} | ||
87 | + | ||
88 | +/* | ||
89 | + Sets the delay time between each frame, or changes it for subsequent frames | ||
90 | + (applies to last frame added) | ||
91 | +*/ | ||
92 | +GIFEncoder.prototype.setDelay = function (milliseconds) { | ||
93 | + this.delay = Math.round(milliseconds / 10); | ||
94 | +}; | ||
95 | + | ||
96 | +/* | ||
97 | + Sets frame rate in frames per second. | ||
98 | +*/ | ||
99 | +GIFEncoder.prototype.setFrameRate = function (fps) { | ||
100 | + this.delay = Math.round(100 / fps); | ||
101 | +}; | ||
102 | + | ||
103 | +/* | ||
104 | + Sets the GIF frame disposal code for the last added frame and any | ||
105 | + subsequent frames. | ||
106 | + | ||
107 | + Default is 0 if no transparent color has been set, otherwise 2. | ||
108 | +*/ | ||
109 | +GIFEncoder.prototype.setDispose = function (disposalCode) { | ||
110 | + if (disposalCode >= 0) this.dispose = disposalCode; | ||
111 | +}; | ||
112 | + | ||
113 | +/* | ||
114 | + Sets the number of times the set of GIF frames should be played. | ||
115 | + | ||
116 | + -1 = play once | ||
117 | + 0 = repeat indefinitely | ||
118 | + | ||
119 | + Default is -1 | ||
120 | + | ||
121 | + Must be invoked before the first image is added | ||
122 | +*/ | ||
123 | + | ||
124 | +GIFEncoder.prototype.setRepeat = function (repeat) { | ||
125 | + this.repeat = repeat; | ||
126 | +}; | ||
127 | + | ||
128 | +/* | ||
129 | + Sets the transparent color for the last added frame and any subsequent | ||
130 | + frames. Since all colors are subject to modification in the quantization | ||
131 | + process, the color in the final palette for each frame closest to the given | ||
132 | + color becomes the transparent color for that frame. May be set to null to | ||
133 | + indicate no transparent color. | ||
134 | +*/ | ||
135 | +GIFEncoder.prototype.setTransparent = function (color) { | ||
136 | + this.transparent = color; | ||
137 | +}; | ||
138 | + | ||
139 | +/* | ||
140 | + Adds next GIF frame. The frame is not written immediately, but is | ||
141 | + actually deferred until the next frame is received so that timing | ||
142 | + data can be inserted. Invoking finish() flushes all frames. | ||
143 | +*/ | ||
144 | +GIFEncoder.prototype.addFrame = function (imageData) { | ||
145 | + this.image = imageData; | ||
146 | + | ||
147 | + this.colorTab = | ||
148 | + this.globalPalette && this.globalPalette.slice ? this.globalPalette : null; | ||
149 | + | ||
150 | + this.getImagePixels(); // convert to correct format if necessary | ||
151 | + this.analyzePixels(); // build color table & map pixels | ||
152 | + | ||
153 | + if (this.globalPalette === true) this.globalPalette = this.colorTab; | ||
154 | + | ||
155 | + if (this.firstFrame) { | ||
156 | + this.writeLSD(); // logical screen descriptior | ||
157 | + this.writePalette(); // global color table | ||
158 | + if (this.repeat >= 0) { | ||
159 | + // use NS app extension to indicate reps | ||
160 | + this.writeNetscapeExt(); | ||
161 | + } | ||
162 | + } | ||
163 | + | ||
164 | + this.writeGraphicCtrlExt(); // write graphic control extension | ||
165 | + this.writeImageDesc(); // image descriptor | ||
166 | + if (!this.firstFrame && !this.globalPalette) this.writePalette(); // local color table | ||
167 | + this.writePixels(); // encode and write pixel data | ||
168 | + | ||
169 | + this.firstFrame = false; | ||
170 | +}; | ||
171 | + | ||
172 | +/* | ||
173 | + Adds final trailer to the GIF stream, if you don't call the finish method | ||
174 | + the GIF stream will not be valid. | ||
175 | +*/ | ||
176 | +GIFEncoder.prototype.finish = function () { | ||
177 | + this.out.writeByte(0x3b); // gif trailer | ||
178 | +}; | ||
179 | + | ||
180 | +/* | ||
181 | + Sets quality of color quantization (conversion of images to the maximum 256 | ||
182 | + colors allowed by the GIF specification). Lower values (minimum = 1) | ||
183 | + produce better colors, but slow processing significantly. 10 is the | ||
184 | + default, and produces good color mapping at reasonable speeds. Values | ||
185 | + greater than 20 do not yield significant improvements in speed. | ||
186 | +*/ | ||
187 | +GIFEncoder.prototype.setQuality = function (quality) { | ||
188 | + if (quality < 1) quality = 1; | ||
189 | + this.sample = quality; | ||
190 | +}; | ||
191 | + | ||
192 | +/* | ||
193 | + Sets dithering method. Available are: | ||
194 | + - FALSE no dithering | ||
195 | + - TRUE or FloydSteinberg | ||
196 | + - FalseFloydSteinberg | ||
197 | + - Stucki | ||
198 | + - Atkinson | ||
199 | + You can add '-serpentine' to use serpentine scanning | ||
200 | +*/ | ||
201 | +GIFEncoder.prototype.setDither = function (dither) { | ||
202 | + if (dither === true) dither = "FloydSteinberg"; | ||
203 | + this.dither = dither; | ||
204 | +}; | ||
205 | + | ||
206 | +/* | ||
207 | + Sets global palette for all frames. | ||
208 | + You can provide TRUE to create global palette from first picture. | ||
209 | + Or an array of r,g,b,r,g,b,... | ||
210 | +*/ | ||
211 | +GIFEncoder.prototype.setGlobalPalette = function (palette) { | ||
212 | + this.globalPalette = palette; | ||
213 | +}; | ||
214 | + | ||
215 | +/* | ||
216 | + Returns global palette used for all frames. | ||
217 | + If setGlobalPalette(true) was used, then this function will return | ||
218 | + calculated palette after the first frame is added. | ||
219 | +*/ | ||
220 | +GIFEncoder.prototype.getGlobalPalette = function () { | ||
221 | + return ( | ||
222 | + (this.globalPalette && | ||
223 | + this.globalPalette.slice && | ||
224 | + this.globalPalette.slice(0)) || | ||
225 | + this.globalPalette | ||
226 | + ); | ||
227 | +}; | ||
228 | + | ||
229 | +/* | ||
230 | + Writes GIF file header | ||
231 | +*/ | ||
232 | +GIFEncoder.prototype.writeHeader = function () { | ||
233 | + this.out.writeUTFBytes("GIF89a"); | ||
234 | +}; | ||
235 | + | ||
236 | +/* | ||
237 | + Analyzes current frame colors and creates color map. | ||
238 | +*/ | ||
239 | +GIFEncoder.prototype.analyzePixels = function () { | ||
240 | + if (!this.colorTab) { | ||
241 | + this.neuQuant = new NeuQuant(this.pixels, this.sample); | ||
242 | + this.neuQuant.buildColormap(); // create reduced palette | ||
243 | + this.colorTab = this.neuQuant.getColormap(); | ||
244 | + } | ||
245 | + | ||
246 | + // map image pixels to new palette | ||
247 | + if (this.dither) { | ||
248 | + this.ditherPixels( | ||
249 | + this.dither.replace("-serpentine", ""), | ||
250 | + this.dither.match(/-serpentine/) !== null | ||
251 | + ); | ||
252 | + } else { | ||
253 | + this.indexPixels(); | ||
254 | + } | ||
255 | + | ||
256 | + this.pixels = null; | ||
257 | + this.colorDepth = 8; | ||
258 | + this.palSize = 7; | ||
259 | + | ||
260 | + // get closest match to transparent color if specified | ||
261 | + if (this.transparent !== null) { | ||
262 | + this.transIndex = this.findClosest(this.transparent, true); | ||
263 | + } | ||
264 | +}; | ||
265 | + | ||
266 | +/* | ||
267 | + Index pixels, without dithering | ||
268 | +*/ | ||
269 | +GIFEncoder.prototype.indexPixels = function (imgq) { | ||
270 | + var nPix = this.pixels.length / 3; | ||
271 | + this.indexedPixels = new Uint8Array(nPix); | ||
272 | + var k = 0; | ||
273 | + for (var j = 0; j < nPix; j++) { | ||
274 | + var index = this.findClosestRGB( | ||
275 | + this.pixels[k++] & 0xff, | ||
276 | + this.pixels[k++] & 0xff, | ||
277 | + this.pixels[k++] & 0xff | ||
278 | + ); | ||
279 | + this.usedEntry[index] = true; | ||
280 | + this.indexedPixels[j] = index; | ||
281 | + } | ||
282 | +}; | ||
283 | + | ||
284 | +/* | ||
285 | + Taken from http://jsbin.com/iXofIji/2/edit by PAEz | ||
286 | +*/ | ||
287 | +GIFEncoder.prototype.ditherPixels = function (kernel, serpentine) { | ||
288 | + var kernels = { | ||
289 | + FalseFloydSteinberg: [ | ||
290 | + [3 / 8, 1, 0], | ||
291 | + [3 / 8, 0, 1], | ||
292 | + [2 / 8, 1, 1], | ||
293 | + ], | ||
294 | + FloydSteinberg: [ | ||
295 | + [7 / 16, 1, 0], | ||
296 | + [3 / 16, -1, 1], | ||
297 | + [5 / 16, 0, 1], | ||
298 | + [1 / 16, 1, 1], | ||
299 | + ], | ||
300 | + Stucki: [ | ||
301 | + [8 / 42, 1, 0], | ||
302 | + [4 / 42, 2, 0], | ||
303 | + [2 / 42, -2, 1], | ||
304 | + [4 / 42, -1, 1], | ||
305 | + [8 / 42, 0, 1], | ||
306 | + [4 / 42, 1, 1], | ||
307 | + [2 / 42, 2, 1], | ||
308 | + [1 / 42, -2, 2], | ||
309 | + [2 / 42, -1, 2], | ||
310 | + [4 / 42, 0, 2], | ||
311 | + [2 / 42, 1, 2], | ||
312 | + [1 / 42, 2, 2], | ||
313 | + ], | ||
314 | + Atkinson: [ | ||
315 | + [1 / 8, 1, 0], | ||
316 | + [1 / 8, 2, 0], | ||
317 | + [1 / 8, -1, 1], | ||
318 | + [1 / 8, 0, 1], | ||
319 | + [1 / 8, 1, 1], | ||
320 | + [1 / 8, 0, 2], | ||
321 | + ], | ||
322 | + }; | ||
323 | + | ||
324 | + if (!kernel || !kernels[kernel]) { | ||
325 | + throw "Unknown dithering kernel: " + kernel; | ||
326 | + } | ||
327 | + | ||
328 | + var ds = kernels[kernel]; | ||
329 | + var index = 0, | ||
330 | + height = this.height, | ||
331 | + width = this.width, | ||
332 | + data = this.pixels; | ||
333 | + var direction = serpentine ? -1 : 1; | ||
334 | + | ||
335 | + this.indexedPixels = new Uint8Array(this.pixels.length / 3); | ||
336 | + | ||
337 | + for (var y = 0; y < height; y++) { | ||
338 | + if (serpentine) direction = direction * -1; | ||
339 | + | ||
340 | + for ( | ||
341 | + var x = direction == 1 ? 0 : width - 1, xend = direction == 1 ? width : 0; | ||
342 | + x !== xend; | ||
343 | + x += direction | ||
344 | + ) { | ||
345 | + index = y * width + x; | ||
346 | + // Get original colour | ||
347 | + var idx = index * 3; | ||
348 | + var r1 = data[idx]; | ||
349 | + var g1 = data[idx + 1]; | ||
350 | + var b1 = data[idx + 2]; | ||
351 | + | ||
352 | + // Get converted colour | ||
353 | + idx = this.findClosestRGB(r1, g1, b1); | ||
354 | + this.usedEntry[idx] = true; | ||
355 | + this.indexedPixels[index] = idx; | ||
356 | + idx *= 3; | ||
357 | + var r2 = this.colorTab[idx]; | ||
358 | + var g2 = this.colorTab[idx + 1]; | ||
359 | + var b2 = this.colorTab[idx + 2]; | ||
360 | + | ||
361 | + var er = r1 - r2; | ||
362 | + var eg = g1 - g2; | ||
363 | + var eb = b1 - b2; | ||
364 | + | ||
365 | + for ( | ||
366 | + var i = direction == 1 ? 0 : ds.length - 1, | ||
367 | + end = direction == 1 ? ds.length : 0; | ||
368 | + i !== end; | ||
369 | + i += direction | ||
370 | + ) { | ||
371 | + var x1 = ds[i][1]; // *direction; // Should this by timesd by direction?..to make the kernel go in the opposite direction....got no idea.... | ||
372 | + var y1 = ds[i][2]; | ||
373 | + if (x1 + x >= 0 && x1 + x < width && y1 + y >= 0 && y1 + y < height) { | ||
374 | + var d = ds[i][0]; | ||
375 | + idx = index + x1 + y1 * width; | ||
376 | + idx *= 3; | ||
377 | + | ||
378 | + data[idx] = Math.max(0, Math.min(255, data[idx] + er * d)); | ||
379 | + data[idx + 1] = Math.max(0, Math.min(255, data[idx + 1] + eg * d)); | ||
380 | + data[idx + 2] = Math.max(0, Math.min(255, data[idx + 2] + eb * d)); | ||
381 | + } | ||
382 | + } | ||
383 | + } | ||
384 | + } | ||
385 | +}; | ||
386 | + | ||
387 | +/* | ||
388 | + Returns index of palette color closest to c | ||
389 | +*/ | ||
390 | +GIFEncoder.prototype.findClosest = function (c, used) { | ||
391 | + return this.findClosestRGB( | ||
392 | + (c & 0xff0000) >> 16, | ||
393 | + (c & 0x00ff00) >> 8, | ||
394 | + c & 0x0000ff, | ||
395 | + used | ||
396 | + ); | ||
397 | +}; | ||
398 | + | ||
399 | +GIFEncoder.prototype.findClosestRGB = function (r, g, b, used) { | ||
400 | + if (this.colorTab === null) return -1; | ||
401 | + | ||
402 | + if (this.neuQuant && !used) { | ||
403 | + return this.neuQuant.lookupRGB(r, g, b); | ||
404 | + } | ||
405 | + | ||
406 | + var c = b | (g << 8) | (r << 16); | ||
407 | + | ||
408 | + var minpos = 0; | ||
409 | + var dmin = 256 * 256 * 256; | ||
410 | + var len = this.colorTab.length; | ||
411 | + | ||
412 | + for (var i = 0, index = 0; i < len; index++) { | ||
413 | + var dr = r - (this.colorTab[i++] & 0xff); | ||
414 | + var dg = g - (this.colorTab[i++] & 0xff); | ||
415 | + var db = b - (this.colorTab[i++] & 0xff); | ||
416 | + var d = dr * dr + dg * dg + db * db; | ||
417 | + if ((!used || this.usedEntry[index]) && d < dmin) { | ||
418 | + dmin = d; | ||
419 | + minpos = index; | ||
420 | + } | ||
421 | + } | ||
422 | + | ||
423 | + return minpos; | ||
424 | +}; | ||
425 | + | ||
426 | +/* | ||
427 | + Extracts image pixels into byte array pixels | ||
428 | + (removes alphachannel from canvas imagedata) | ||
429 | +*/ | ||
430 | +GIFEncoder.prototype.getImagePixels = function () { | ||
431 | + var w = this.width; | ||
432 | + var h = this.height; | ||
433 | + this.pixels = new Uint8Array(w * h * 3); | ||
434 | + | ||
435 | + var data = this.image; | ||
436 | + var srcPos = 0; | ||
437 | + var count = 0; | ||
438 | + | ||
439 | + for (var i = 0; i < h; i++) { | ||
440 | + for (var j = 0; j < w; j++) { | ||
441 | + this.pixels[count++] = data[srcPos++]; | ||
442 | + this.pixels[count++] = data[srcPos++]; | ||
443 | + this.pixels[count++] = data[srcPos++]; | ||
444 | + srcPos++; | ||
445 | + } | ||
446 | + } | ||
447 | +}; | ||
448 | + | ||
449 | +/* | ||
450 | + Writes Graphic Control Extension | ||
451 | +*/ | ||
452 | +GIFEncoder.prototype.writeGraphicCtrlExt = function () { | ||
453 | + this.out.writeByte(0x21); // extension introducer | ||
454 | + this.out.writeByte(0xf9); // GCE label | ||
455 | + this.out.writeByte(4); // data block size | ||
456 | + | ||
457 | + var transp, disp; | ||
458 | + if (this.transparent === null) { | ||
459 | + transp = 0; | ||
460 | + disp = 0; // dispose = no action | ||
461 | + } else { | ||
462 | + transp = 1; | ||
463 | + disp = 2; // force clear if using transparent color | ||
464 | + } | ||
465 | + | ||
466 | + if (this.dispose >= 0) { | ||
467 | + disp = this.dispose & 7; // user override | ||
468 | + } | ||
469 | + disp <<= 2; | ||
470 | + | ||
471 | + // packed fields | ||
472 | + this.out.writeByte( | ||
473 | + 0 | // 1:3 reserved | ||
474 | + disp | // 4:6 disposal | ||
475 | + 0 | // 7 user input - 0 = none | ||
476 | + transp // 8 transparency flag | ||
477 | + ); | ||
478 | + | ||
479 | + this.writeShort(this.delay); // delay x 1/100 sec | ||
480 | + this.out.writeByte(this.transIndex); // transparent color index | ||
481 | + this.out.writeByte(0); // block terminator | ||
482 | +}; | ||
483 | + | ||
484 | +/* | ||
485 | + Writes Image Descriptor | ||
486 | +*/ | ||
487 | +GIFEncoder.prototype.writeImageDesc = function () { | ||
488 | + this.out.writeByte(0x2c); // image separator | ||
489 | + this.writeShort(0); // image position x,y = 0,0 | ||
490 | + this.writeShort(0); | ||
491 | + this.writeShort(this.width); // image size | ||
492 | + this.writeShort(this.height); | ||
493 | + | ||
494 | + // packed fields | ||
495 | + if (this.firstFrame || this.globalPalette) { | ||
496 | + // no LCT - GCT is used for first (or only) frame | ||
497 | + this.out.writeByte(0); | ||
498 | + } else { | ||
499 | + // specify normal LCT | ||
500 | + this.out.writeByte( | ||
501 | + 0x80 | // 1 local color table 1=yes | ||
502 | + 0 | // 2 interlace - 0=no | ||
503 | + 0 | // 3 sorted - 0=no | ||
504 | + 0 | // 4-5 reserved | ||
505 | + this.palSize // 6-8 size of color table | ||
506 | + ); | ||
507 | + } | ||
508 | +}; | ||
509 | + | ||
510 | +/* | ||
511 | + Writes Logical Screen Descriptor | ||
512 | +*/ | ||
513 | +GIFEncoder.prototype.writeLSD = function () { | ||
514 | + // logical screen size | ||
515 | + this.writeShort(this.width); | ||
516 | + this.writeShort(this.height); | ||
517 | + | ||
518 | + // packed fields | ||
519 | + this.out.writeByte( | ||
520 | + 0x80 | // 1 : global color table flag = 1 (gct used) | ||
521 | + 0x70 | // 2-4 : color resolution = 7 | ||
522 | + 0x00 | // 5 : gct sort flag = 0 | ||
523 | + this.palSize // 6-8 : gct size | ||
524 | + ); | ||
525 | + | ||
526 | + this.out.writeByte(0); // background color index | ||
527 | + this.out.writeByte(0); // pixel aspect ratio - assume 1:1 | ||
528 | +}; | ||
529 | + | ||
530 | +/* | ||
531 | + Writes Netscape application extension to define repeat count. | ||
532 | +*/ | ||
533 | +GIFEncoder.prototype.writeNetscapeExt = function () { | ||
534 | + this.out.writeByte(0x21); // extension introducer | ||
535 | + this.out.writeByte(0xff); // app extension label | ||
536 | + this.out.writeByte(11); // block size | ||
537 | + this.out.writeUTFBytes("NETSCAPE2.0"); // app id + auth code | ||
538 | + this.out.writeByte(3); // sub-block size | ||
539 | + this.out.writeByte(1); // loop sub-block id | ||
540 | + this.writeShort(this.repeat); // loop count (extra iterations, 0=repeat forever) | ||
541 | + this.out.writeByte(0); // block terminator | ||
542 | +}; | ||
543 | + | ||
544 | +/* | ||
545 | + Writes color table | ||
546 | +*/ | ||
547 | +GIFEncoder.prototype.writePalette = function () { | ||
548 | + this.out.writeBytes(this.colorTab); | ||
549 | + var n = 3 * 256 - this.colorTab.length; | ||
550 | + for (var i = 0; i < n; i++) this.out.writeByte(0); | ||
551 | +}; | ||
552 | + | ||
553 | +GIFEncoder.prototype.writeShort = function (pValue) { | ||
554 | + this.out.writeByte(pValue & 0xff); | ||
555 | + this.out.writeByte((pValue >> 8) & 0xff); | ||
556 | +}; | ||
557 | + | ||
558 | +/* | ||
559 | + Encodes and writes pixel data | ||
560 | +*/ | ||
561 | +GIFEncoder.prototype.writePixels = function () { | ||
562 | + var enc = new LZWEncoder( | ||
563 | + this.width, | ||
564 | + this.height, | ||
565 | + this.indexedPixels, | ||
566 | + this.colorDepth | ||
567 | + ); | ||
568 | + enc.encode(this.out); | ||
569 | +}; | ||
570 | + | ||
571 | +/* | ||
572 | + Retrieves the GIF stream | ||
573 | +*/ | ||
574 | +GIFEncoder.prototype.stream = function () { | ||
575 | + return this.out; | ||
576 | +}; | ||
577 | + | ||
578 | +module.exports = GIFEncoder; |
gif-generator/src/lib/LZWEncoder.js
0 → 100644
1 | +/* | ||
2 | + LZWEncoder.js | ||
3 | + | ||
4 | + Authors | ||
5 | + Kevin Weiner (original Java version - kweiner@fmsware.com) | ||
6 | + Thibault Imbert (AS3 version - bytearray.org) | ||
7 | + Johan Nordberg (JS version - code@johan-nordberg.com) | ||
8 | + | ||
9 | + Acknowledgements | ||
10 | + GIFCOMPR.C - GIF Image compression routines | ||
11 | + Lempel-Ziv compression based on 'compress'. GIF modifications by | ||
12 | + David Rowley (mgardi@watdcsu.waterloo.edu) | ||
13 | + GIF Image compression - modified 'compress' | ||
14 | + Based on: compress.c - File compression ala IEEE Computer, June 1984. | ||
15 | + By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) | ||
16 | + Jim McKie (decvax!mcvax!jim) | ||
17 | + Steve Davies (decvax!vax135!petsd!peora!srd) | ||
18 | + Ken Turkowski (decvax!decwrl!turtlevax!ken) | ||
19 | + James A. Woods (decvax!ihnp4!ames!jaw) | ||
20 | + Joe Orost (decvax!vax135!petsd!joe) | ||
21 | +*/ | ||
22 | + | ||
23 | +var EOF = -1; | ||
24 | +var BITS = 12; | ||
25 | +var HSIZE = 5003; // 80% occupancy | ||
26 | +var masks = [ | ||
27 | + 0x0000, | ||
28 | + 0x0001, | ||
29 | + 0x0003, | ||
30 | + 0x0007, | ||
31 | + 0x000f, | ||
32 | + 0x001f, | ||
33 | + 0x003f, | ||
34 | + 0x007f, | ||
35 | + 0x00ff, | ||
36 | + 0x01ff, | ||
37 | + 0x03ff, | ||
38 | + 0x07ff, | ||
39 | + 0x0fff, | ||
40 | + 0x1fff, | ||
41 | + 0x3fff, | ||
42 | + 0x7fff, | ||
43 | + 0xffff, | ||
44 | +]; | ||
45 | + | ||
46 | +function LZWEncoder(width, height, pixels, colorDepth) { | ||
47 | + var initCodeSize = Math.max(2, colorDepth); | ||
48 | + | ||
49 | + var accum = new Uint8Array(256); | ||
50 | + var htab = new Int32Array(HSIZE); | ||
51 | + var codetab = new Int32Array(HSIZE); | ||
52 | + | ||
53 | + var cur_accum, | ||
54 | + cur_bits = 0; | ||
55 | + var a_count; | ||
56 | + var free_ent = 0; // first unused entry | ||
57 | + var maxcode; | ||
58 | + | ||
59 | + // block compression parameters -- after all codes are used up, | ||
60 | + // and compression rate changes, start over. | ||
61 | + var clear_flg = false; | ||
62 | + | ||
63 | + // Algorithm: use open addressing double hashing (no chaining) on the | ||
64 | + // prefix code / next character combination. We do a variant of Knuth's | ||
65 | + // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime | ||
66 | + // secondary probe. Here, the modular division first probe is gives way | ||
67 | + // to a faster exclusive-or manipulation. Also do block compression with | ||
68 | + // an adaptive reset, whereby the code table is cleared when the compression | ||
69 | + // ratio decreases, but after the table fills. The variable-length output | ||
70 | + // codes are re-sized at this point, and a special CLEAR code is generated | ||
71 | + // for the decompressor. Late addition: construct the table according to | ||
72 | + // file size for noticeable speed improvement on small files. Please direct | ||
73 | + // questions about this implementation to ames!jaw. | ||
74 | + var g_init_bits, ClearCode, EOFCode; | ||
75 | + | ||
76 | + // Add a character to the end of the current packet, and if it is 254 | ||
77 | + // characters, flush the packet to disk. | ||
78 | + function char_out(c, outs) { | ||
79 | + accum[a_count++] = c; | ||
80 | + if (a_count >= 254) flush_char(outs); | ||
81 | + } | ||
82 | + | ||
83 | + // Clear out the hash table | ||
84 | + // table clear for block compress | ||
85 | + function cl_block(outs) { | ||
86 | + cl_hash(HSIZE); | ||
87 | + free_ent = ClearCode + 2; | ||
88 | + clear_flg = true; | ||
89 | + output(ClearCode, outs); | ||
90 | + } | ||
91 | + | ||
92 | + // Reset code table | ||
93 | + function cl_hash(hsize) { | ||
94 | + for (var i = 0; i < hsize; ++i) htab[i] = -1; | ||
95 | + } | ||
96 | + | ||
97 | + function compress(init_bits, outs) { | ||
98 | + var fcode, c, i, ent, disp, hsize_reg, hshift; | ||
99 | + | ||
100 | + // Set up the globals: g_init_bits - initial number of bits | ||
101 | + g_init_bits = init_bits; | ||
102 | + | ||
103 | + // Set up the necessary values | ||
104 | + clear_flg = false; | ||
105 | + n_bits = g_init_bits; | ||
106 | + maxcode = MAXCODE(n_bits); | ||
107 | + | ||
108 | + ClearCode = 1 << (init_bits - 1); | ||
109 | + EOFCode = ClearCode + 1; | ||
110 | + free_ent = ClearCode + 2; | ||
111 | + | ||
112 | + a_count = 0; // clear packet | ||
113 | + | ||
114 | + ent = nextPixel(); | ||
115 | + | ||
116 | + hshift = 0; | ||
117 | + for (fcode = HSIZE; fcode < 65536; fcode *= 2) ++hshift; | ||
118 | + hshift = 8 - hshift; // set hash code range bound | ||
119 | + hsize_reg = HSIZE; | ||
120 | + cl_hash(hsize_reg); // clear hash table | ||
121 | + | ||
122 | + output(ClearCode, outs); | ||
123 | + | ||
124 | + outer_loop: while ((c = nextPixel()) != EOF) { | ||
125 | + fcode = (c << BITS) + ent; | ||
126 | + i = (c << hshift) ^ ent; // xor hashing | ||
127 | + if (htab[i] === fcode) { | ||
128 | + ent = codetab[i]; | ||
129 | + continue; | ||
130 | + } else if (htab[i] >= 0) { | ||
131 | + // non-empty slot | ||
132 | + disp = hsize_reg - i; // secondary hash (after G. Knott) | ||
133 | + if (i === 0) disp = 1; | ||
134 | + do { | ||
135 | + if ((i -= disp) < 0) i += hsize_reg; | ||
136 | + if (htab[i] === fcode) { | ||
137 | + ent = codetab[i]; | ||
138 | + continue outer_loop; | ||
139 | + } | ||
140 | + } while (htab[i] >= 0); | ||
141 | + } | ||
142 | + output(ent, outs); | ||
143 | + ent = c; | ||
144 | + if (free_ent < 1 << BITS) { | ||
145 | + codetab[i] = free_ent++; // code -> hashtable | ||
146 | + htab[i] = fcode; | ||
147 | + } else { | ||
148 | + cl_block(outs); | ||
149 | + } | ||
150 | + } | ||
151 | + | ||
152 | + // Put out the final code. | ||
153 | + output(ent, outs); | ||
154 | + output(EOFCode, outs); | ||
155 | + } | ||
156 | + | ||
157 | + function encode(outs) { | ||
158 | + outs.writeByte(initCodeSize); // write "initial code size" byte | ||
159 | + remaining = width * height; // reset navigation variables | ||
160 | + curPixel = 0; | ||
161 | + compress(initCodeSize + 1, outs); // compress and write the pixel data | ||
162 | + outs.writeByte(0); // write block terminator | ||
163 | + } | ||
164 | + | ||
165 | + // Flush the packet to disk, and reset the accumulator | ||
166 | + function flush_char(outs) { | ||
167 | + if (a_count > 0) { | ||
168 | + outs.writeByte(a_count); | ||
169 | + outs.writeBytes(accum, 0, a_count); | ||
170 | + a_count = 0; | ||
171 | + } | ||
172 | + } | ||
173 | + | ||
174 | + function MAXCODE(n_bits) { | ||
175 | + return (1 << n_bits) - 1; | ||
176 | + } | ||
177 | + | ||
178 | + // Return the next pixel from the image | ||
179 | + function nextPixel() { | ||
180 | + if (remaining === 0) return EOF; | ||
181 | + --remaining; | ||
182 | + var pix = pixels[curPixel++]; | ||
183 | + return pix & 0xff; | ||
184 | + } | ||
185 | + | ||
186 | + function output(code, outs) { | ||
187 | + cur_accum &= masks[cur_bits]; | ||
188 | + | ||
189 | + if (cur_bits > 0) cur_accum |= code << cur_bits; | ||
190 | + else cur_accum = code; | ||
191 | + | ||
192 | + cur_bits += n_bits; | ||
193 | + | ||
194 | + while (cur_bits >= 8) { | ||
195 | + char_out(cur_accum & 0xff, outs); | ||
196 | + cur_accum >>= 8; | ||
197 | + cur_bits -= 8; | ||
198 | + } | ||
199 | + | ||
200 | + // If the next entry is going to be too big for the code size, | ||
201 | + // then increase it, if possible. | ||
202 | + if (free_ent > maxcode || clear_flg) { | ||
203 | + if (clear_flg) { | ||
204 | + maxcode = MAXCODE((n_bits = g_init_bits)); | ||
205 | + clear_flg = false; | ||
206 | + } else { | ||
207 | + ++n_bits; | ||
208 | + if (n_bits == BITS) maxcode = 1 << BITS; | ||
209 | + else maxcode = MAXCODE(n_bits); | ||
210 | + } | ||
211 | + } | ||
212 | + | ||
213 | + if (code == EOFCode) { | ||
214 | + // At EOF, write the rest of the buffer. | ||
215 | + while (cur_bits > 0) { | ||
216 | + char_out(cur_accum & 0xff, outs); | ||
217 | + cur_accum >>= 8; | ||
218 | + cur_bits -= 8; | ||
219 | + } | ||
220 | + flush_char(outs); | ||
221 | + } | ||
222 | + } | ||
223 | + | ||
224 | + this.encode = encode; | ||
225 | +} | ||
226 | + | ||
227 | +module.exports = LZWEncoder; |
gif-generator/src/lib/NeuQuant.js
0 → 100644
1 | +/* NeuQuant Neural-Net Quantization Algorithm | ||
2 | + * ------------------------------------------ | ||
3 | + * | ||
4 | + * Copyright (c) 1994 Anthony Dekker | ||
5 | + * | ||
6 | + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. | ||
7 | + * See "Kohonen neural networks for optimal colour quantization" | ||
8 | + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. | ||
9 | + * for a discussion of the algorithm. | ||
10 | + * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML | ||
11 | + * | ||
12 | + * Any party obtaining a copy of these files from the author, directly or | ||
13 | + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, | ||
14 | + * world-wide, paid up, royalty-free, nonexclusive right and license to deal | ||
15 | + * in this software and documentation files (the "Software"), including without | ||
16 | + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
17 | + * and/or sell copies of the Software, and to permit persons who receive | ||
18 | + * copies from any such party to do so, with the only requirement being | ||
19 | + * that this copyright notice remain intact. | ||
20 | + * | ||
21 | + * (JavaScript port 2012 by Johan Nordberg) | ||
22 | + */ | ||
23 | + | ||
24 | +function toInt(v) { | ||
25 | + return ~~v; | ||
26 | +} | ||
27 | + | ||
28 | +var ncycles = 100; // number of learning cycles | ||
29 | +var netsize = 256; // number of colors used | ||
30 | +var maxnetpos = netsize - 1; | ||
31 | + | ||
32 | +// defs for freq and bias | ||
33 | +var netbiasshift = 4; // bias for colour values | ||
34 | +var intbiasshift = 16; // bias for fractions | ||
35 | +var intbias = 1 << intbiasshift; | ||
36 | +var gammashift = 10; | ||
37 | +var gamma = 1 << gammashift; | ||
38 | +var betashift = 10; | ||
39 | +var beta = intbias >> betashift; /* beta = 1/1024 */ | ||
40 | +var betagamma = intbias << (gammashift - betashift); | ||
41 | + | ||
42 | +// defs for decreasing radius factor | ||
43 | +var initrad = netsize >> 3; // for 256 cols, radius starts | ||
44 | +var radiusbiasshift = 6; // at 32.0 biased by 6 bits | ||
45 | +var radiusbias = 1 << radiusbiasshift; | ||
46 | +var initradius = initrad * radiusbias; //and decreases by a | ||
47 | +var radiusdec = 30; // factor of 1/30 each cycle | ||
48 | + | ||
49 | +// defs for decreasing alpha factor | ||
50 | +var alphabiasshift = 10; // alpha starts at 1.0 | ||
51 | +var initalpha = 1 << alphabiasshift; | ||
52 | +var alphadec; // biased by 10 bits | ||
53 | + | ||
54 | +/* radbias and alpharadbias used for radpower calculation */ | ||
55 | +var radbiasshift = 8; | ||
56 | +var radbias = 1 << radbiasshift; | ||
57 | +var alpharadbshift = alphabiasshift + radbiasshift; | ||
58 | +var alpharadbias = 1 << alpharadbshift; | ||
59 | + | ||
60 | +// four primes near 500 - assume no image has a length so large that it is | ||
61 | +// divisible by all four primes | ||
62 | +var prime1 = 499; | ||
63 | +var prime2 = 491; | ||
64 | +var prime3 = 487; | ||
65 | +var prime4 = 503; | ||
66 | +var minpicturebytes = 3 * prime4; | ||
67 | + | ||
68 | +/* | ||
69 | + Constructor: NeuQuant | ||
70 | + | ||
71 | + Arguments: | ||
72 | + | ||
73 | + pixels - array of pixels in RGB format | ||
74 | + samplefac - sampling factor 1 to 30 where lower is better quality | ||
75 | + | ||
76 | + > | ||
77 | + > pixels = [r, g, b, r, g, b, r, g, b, ..] | ||
78 | + > | ||
79 | +*/ | ||
80 | +function NeuQuant(pixels, samplefac) { | ||
81 | + var network; // int[netsize][4] | ||
82 | + var netindex; // for network lookup - really 256 | ||
83 | + | ||
84 | + // bias and freq arrays for learning | ||
85 | + var bias; | ||
86 | + var freq; | ||
87 | + var radpower; | ||
88 | + | ||
89 | + /* | ||
90 | + Private Method: init | ||
91 | + | ||
92 | + sets up arrays | ||
93 | + */ | ||
94 | + function init() { | ||
95 | + network = []; | ||
96 | + netindex = []; | ||
97 | + bias = []; | ||
98 | + freq = []; | ||
99 | + radpower = []; | ||
100 | + | ||
101 | + var i, v; | ||
102 | + for (i = 0; i < netsize; i++) { | ||
103 | + v = (i << (netbiasshift + 8)) / netsize; | ||
104 | + network[i] = [v, v, v]; | ||
105 | + freq[i] = intbias / netsize; | ||
106 | + bias[i] = 0; | ||
107 | + } | ||
108 | + } | ||
109 | + | ||
110 | + /* | ||
111 | + Private Method: unbiasnet | ||
112 | + | ||
113 | + unbiases network to give byte values 0..255 and record position i to prepare for sort | ||
114 | + */ | ||
115 | + function unbiasnet() { | ||
116 | + for (var i = 0; i < netsize; i++) { | ||
117 | + network[i][0] >>= netbiasshift; | ||
118 | + network[i][1] >>= netbiasshift; | ||
119 | + network[i][2] >>= netbiasshift; | ||
120 | + network[i][3] = i; // record color number | ||
121 | + } | ||
122 | + } | ||
123 | + | ||
124 | + /* | ||
125 | + Private Method: altersingle | ||
126 | + | ||
127 | + moves neuron *i* towards biased (b,g,r) by factor *alpha* | ||
128 | + */ | ||
129 | + function altersingle(alpha, i, b, g, r) { | ||
130 | + network[i][0] -= (alpha * (network[i][0] - b)) / initalpha; | ||
131 | + network[i][1] -= (alpha * (network[i][1] - g)) / initalpha; | ||
132 | + network[i][2] -= (alpha * (network[i][2] - r)) / initalpha; | ||
133 | + } | ||
134 | + | ||
135 | + /* | ||
136 | + Private Method: alterneigh | ||
137 | + | ||
138 | + moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha* | ||
139 | + */ | ||
140 | + function alterneigh(radius, i, b, g, r) { | ||
141 | + var lo = Math.abs(i - radius); | ||
142 | + var hi = Math.min(i + radius, netsize); | ||
143 | + | ||
144 | + var j = i + 1; | ||
145 | + var k = i - 1; | ||
146 | + var m = 1; | ||
147 | + | ||
148 | + var p, a; | ||
149 | + while (j < hi || k > lo) { | ||
150 | + a = radpower[m++]; | ||
151 | + | ||
152 | + if (j < hi) { | ||
153 | + p = network[j++]; | ||
154 | + p[0] -= (a * (p[0] - b)) / alpharadbias; | ||
155 | + p[1] -= (a * (p[1] - g)) / alpharadbias; | ||
156 | + p[2] -= (a * (p[2] - r)) / alpharadbias; | ||
157 | + } | ||
158 | + | ||
159 | + if (k > lo) { | ||
160 | + p = network[k--]; | ||
161 | + p[0] -= (a * (p[0] - b)) / alpharadbias; | ||
162 | + p[1] -= (a * (p[1] - g)) / alpharadbias; | ||
163 | + p[2] -= (a * (p[2] - r)) / alpharadbias; | ||
164 | + } | ||
165 | + } | ||
166 | + } | ||
167 | + | ||
168 | + /* | ||
169 | + Private Method: contest | ||
170 | + | ||
171 | + searches for biased BGR values | ||
172 | + */ | ||
173 | + function contest(b, g, r) { | ||
174 | + /* | ||
175 | + finds closest neuron (min dist) and updates freq | ||
176 | + finds best neuron (min dist-bias) and returns position | ||
177 | + for frequently chosen neurons, freq[i] is high and bias[i] is negative | ||
178 | + bias[i] = gamma * ((1 / netsize) - freq[i]) | ||
179 | + */ | ||
180 | + | ||
181 | + var bestd = ~(1 << 31); | ||
182 | + var bestbiasd = bestd; | ||
183 | + var bestpos = -1; | ||
184 | + var bestbiaspos = bestpos; | ||
185 | + | ||
186 | + var i, n, dist, biasdist, betafreq; | ||
187 | + for (i = 0; i < netsize; i++) { | ||
188 | + n = network[i]; | ||
189 | + | ||
190 | + dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r); | ||
191 | + if (dist < bestd) { | ||
192 | + bestd = dist; | ||
193 | + bestpos = i; | ||
194 | + } | ||
195 | + | ||
196 | + biasdist = dist - (bias[i] >> (intbiasshift - netbiasshift)); | ||
197 | + if (biasdist < bestbiasd) { | ||
198 | + bestbiasd = biasdist; | ||
199 | + bestbiaspos = i; | ||
200 | + } | ||
201 | + | ||
202 | + betafreq = freq[i] >> betashift; | ||
203 | + freq[i] -= betafreq; | ||
204 | + bias[i] += betafreq << gammashift; | ||
205 | + } | ||
206 | + | ||
207 | + freq[bestpos] += beta; | ||
208 | + bias[bestpos] -= betagamma; | ||
209 | + | ||
210 | + return bestbiaspos; | ||
211 | + } | ||
212 | + | ||
213 | + /* | ||
214 | + Private Method: inxbuild | ||
215 | + | ||
216 | + sorts network and builds netindex[0..255] | ||
217 | + */ | ||
218 | + function inxbuild() { | ||
219 | + var i, | ||
220 | + j, | ||
221 | + p, | ||
222 | + q, | ||
223 | + smallpos, | ||
224 | + smallval, | ||
225 | + previouscol = 0, | ||
226 | + startpos = 0; | ||
227 | + for (i = 0; i < netsize; i++) { | ||
228 | + p = network[i]; | ||
229 | + smallpos = i; | ||
230 | + smallval = p[1]; // index on g | ||
231 | + // find smallest in i..netsize-1 | ||
232 | + for (j = i + 1; j < netsize; j++) { | ||
233 | + q = network[j]; | ||
234 | + if (q[1] < smallval) { | ||
235 | + // index on g | ||
236 | + smallpos = j; | ||
237 | + smallval = q[1]; // index on g | ||
238 | + } | ||
239 | + } | ||
240 | + q = network[smallpos]; | ||
241 | + // swap p (i) and q (smallpos) entries | ||
242 | + if (i != smallpos) { | ||
243 | + j = q[0]; | ||
244 | + q[0] = p[0]; | ||
245 | + p[0] = j; | ||
246 | + j = q[1]; | ||
247 | + q[1] = p[1]; | ||
248 | + p[1] = j; | ||
249 | + j = q[2]; | ||
250 | + q[2] = p[2]; | ||
251 | + p[2] = j; | ||
252 | + j = q[3]; | ||
253 | + q[3] = p[3]; | ||
254 | + p[3] = j; | ||
255 | + } | ||
256 | + // smallval entry is now in position i | ||
257 | + | ||
258 | + if (smallval != previouscol) { | ||
259 | + netindex[previouscol] = (startpos + i) >> 1; | ||
260 | + for (j = previouscol + 1; j < smallval; j++) netindex[j] = i; | ||
261 | + previouscol = smallval; | ||
262 | + startpos = i; | ||
263 | + } | ||
264 | + } | ||
265 | + netindex[previouscol] = (startpos + maxnetpos) >> 1; | ||
266 | + for (j = previouscol + 1; j < 256; j++) netindex[j] = maxnetpos; // really 256 | ||
267 | + } | ||
268 | + | ||
269 | + /* | ||
270 | + Private Method: inxsearch | ||
271 | + | ||
272 | + searches for BGR values 0..255 and returns a color index | ||
273 | + */ | ||
274 | + function inxsearch(b, g, r) { | ||
275 | + var a, p, dist; | ||
276 | + | ||
277 | + var bestd = 1000; // biggest possible dist is 256*3 | ||
278 | + var best = -1; | ||
279 | + | ||
280 | + var i = netindex[g]; // index on g | ||
281 | + var j = i - 1; // start at netindex[g] and work outwards | ||
282 | + | ||
283 | + while (i < netsize || j >= 0) { | ||
284 | + if (i < netsize) { | ||
285 | + p = network[i]; | ||
286 | + dist = p[1] - g; // inx key | ||
287 | + if (dist >= bestd) i = netsize; | ||
288 | + // stop iter | ||
289 | + else { | ||
290 | + i++; | ||
291 | + if (dist < 0) dist = -dist; | ||
292 | + a = p[0] - b; | ||
293 | + if (a < 0) a = -a; | ||
294 | + dist += a; | ||
295 | + if (dist < bestd) { | ||
296 | + a = p[2] - r; | ||
297 | + if (a < 0) a = -a; | ||
298 | + dist += a; | ||
299 | + if (dist < bestd) { | ||
300 | + bestd = dist; | ||
301 | + best = p[3]; | ||
302 | + } | ||
303 | + } | ||
304 | + } | ||
305 | + } | ||
306 | + if (j >= 0) { | ||
307 | + p = network[j]; | ||
308 | + dist = g - p[1]; // inx key - reverse dif | ||
309 | + if (dist >= bestd) j = -1; | ||
310 | + // stop iter | ||
311 | + else { | ||
312 | + j--; | ||
313 | + if (dist < 0) dist = -dist; | ||
314 | + a = p[0] - b; | ||
315 | + if (a < 0) a = -a; | ||
316 | + dist += a; | ||
317 | + if (dist < bestd) { | ||
318 | + a = p[2] - r; | ||
319 | + if (a < 0) a = -a; | ||
320 | + dist += a; | ||
321 | + if (dist < bestd) { | ||
322 | + bestd = dist; | ||
323 | + best = p[3]; | ||
324 | + } | ||
325 | + } | ||
326 | + } | ||
327 | + } | ||
328 | + } | ||
329 | + | ||
330 | + return best; | ||
331 | + } | ||
332 | + | ||
333 | + /* | ||
334 | + Private Method: learn | ||
335 | + | ||
336 | + "Main Learning Loop" | ||
337 | + */ | ||
338 | + function learn() { | ||
339 | + var i; | ||
340 | + | ||
341 | + var lengthcount = pixels.length; | ||
342 | + var alphadec = toInt(30 + (samplefac - 1) / 3); | ||
343 | + var samplepixels = toInt(lengthcount / (3 * samplefac)); | ||
344 | + var delta = toInt(samplepixels / ncycles); | ||
345 | + var alpha = initalpha; | ||
346 | + var radius = initradius; | ||
347 | + | ||
348 | + var rad = radius >> radiusbiasshift; | ||
349 | + | ||
350 | + if (rad <= 1) rad = 0; | ||
351 | + for (i = 0; i < rad; i++) | ||
352 | + radpower[i] = toInt( | ||
353 | + alpha * (((rad * rad - i * i) * radbias) / (rad * rad)) | ||
354 | + ); | ||
355 | + | ||
356 | + var step; | ||
357 | + if (lengthcount < minpicturebytes) { | ||
358 | + samplefac = 1; | ||
359 | + step = 3; | ||
360 | + } else if (lengthcount % prime1 !== 0) { | ||
361 | + step = 3 * prime1; | ||
362 | + } else if (lengthcount % prime2 !== 0) { | ||
363 | + step = 3 * prime2; | ||
364 | + } else if (lengthcount % prime3 !== 0) { | ||
365 | + step = 3 * prime3; | ||
366 | + } else { | ||
367 | + step = 3 * prime4; | ||
368 | + } | ||
369 | + | ||
370 | + var b, g, r, j; | ||
371 | + var pix = 0; // current pixel | ||
372 | + | ||
373 | + i = 0; | ||
374 | + while (i < samplepixels) { | ||
375 | + b = (pixels[pix] & 0xff) << netbiasshift; | ||
376 | + g = (pixels[pix + 1] & 0xff) << netbiasshift; | ||
377 | + r = (pixels[pix + 2] & 0xff) << netbiasshift; | ||
378 | + | ||
379 | + j = contest(b, g, r); | ||
380 | + | ||
381 | + altersingle(alpha, j, b, g, r); | ||
382 | + if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours | ||
383 | + | ||
384 | + pix += step; | ||
385 | + if (pix >= lengthcount) pix -= lengthcount; | ||
386 | + | ||
387 | + i++; | ||
388 | + | ||
389 | + if (delta === 0) delta = 1; | ||
390 | + if (i % delta === 0) { | ||
391 | + alpha -= alpha / alphadec; | ||
392 | + radius -= radius / radiusdec; | ||
393 | + rad = radius >> radiusbiasshift; | ||
394 | + | ||
395 | + if (rad <= 1) rad = 0; | ||
396 | + for (j = 0; j < rad; j++) | ||
397 | + radpower[j] = toInt( | ||
398 | + alpha * (((rad * rad - j * j) * radbias) / (rad * rad)) | ||
399 | + ); | ||
400 | + } | ||
401 | + } | ||
402 | + } | ||
403 | + | ||
404 | + /* | ||
405 | + Method: buildColormap | ||
406 | + | ||
407 | + 1. initializes network | ||
408 | + 2. trains it | ||
409 | + 3. removes misconceptions | ||
410 | + 4. builds colorindex | ||
411 | + */ | ||
412 | + function buildColormap() { | ||
413 | + init(); | ||
414 | + learn(); | ||
415 | + unbiasnet(); | ||
416 | + inxbuild(); | ||
417 | + } | ||
418 | + this.buildColormap = buildColormap; | ||
419 | + | ||
420 | + /* | ||
421 | + Method: getColormap | ||
422 | + | ||
423 | + builds colormap from the index | ||
424 | + | ||
425 | + returns array in the format: | ||
426 | + | ||
427 | + > | ||
428 | + > [r, g, b, r, g, b, r, g, b, ..] | ||
429 | + > | ||
430 | + */ | ||
431 | + function getColormap() { | ||
432 | + var map = []; | ||
433 | + var index = []; | ||
434 | + | ||
435 | + for (var i = 0; i < netsize; i++) index[network[i][3]] = i; | ||
436 | + | ||
437 | + var k = 0; | ||
438 | + for (var l = 0; l < netsize; l++) { | ||
439 | + var j = index[l]; | ||
440 | + map[k++] = network[j][0]; | ||
441 | + map[k++] = network[j][1]; | ||
442 | + map[k++] = network[j][2]; | ||
443 | + } | ||
444 | + return map; | ||
445 | + } | ||
446 | + this.getColormap = getColormap; | ||
447 | + | ||
448 | + /* | ||
449 | + Method: lookupRGB | ||
450 | + | ||
451 | + looks for the closest *r*, *g*, *b* color in the map and | ||
452 | + returns its index | ||
453 | + */ | ||
454 | + this.lookupRGB = inxsearch; | ||
455 | +} | ||
456 | + | ||
457 | +module.exports = NeuQuant; |
gif-generator/src/lib/TypedNeuQuant.js
0 → 100644
1 | +/* NeuQuant Neural-Net Quantization Algorithm | ||
2 | + * ------------------------------------------ | ||
3 | + * | ||
4 | + * Copyright (c) 1994 Anthony Dekker | ||
5 | + * | ||
6 | + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. | ||
7 | + * See "Kohonen neural networks for optimal colour quantization" | ||
8 | + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. | ||
9 | + * for a discussion of the algorithm. | ||
10 | + * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML | ||
11 | + * | ||
12 | + * Any party obtaining a copy of these files from the author, directly or | ||
13 | + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, | ||
14 | + * world-wide, paid up, royalty-free, nonexclusive right and license to deal | ||
15 | + * in this software and documentation files (the "Software"), including without | ||
16 | + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
17 | + * and/or sell copies of the Software, and to permit persons who receive | ||
18 | + * copies from any such party to do so, with the only requirement being | ||
19 | + * that this copyright notice remain intact. | ||
20 | + * | ||
21 | + * (JavaScript port 2012 by Johan Nordberg) | ||
22 | + */ | ||
23 | + | ||
24 | +var ncycles = 100; // number of learning cycles | ||
25 | +var netsize = 256; // number of colors used | ||
26 | +var maxnetpos = netsize - 1; | ||
27 | + | ||
28 | +// defs for freq and bias | ||
29 | +var netbiasshift = 4; // bias for colour values | ||
30 | +var intbiasshift = 16; // bias for fractions | ||
31 | +var intbias = 1 << intbiasshift; | ||
32 | +var gammashift = 10; | ||
33 | +var gamma = 1 << gammashift; | ||
34 | +var betashift = 10; | ||
35 | +var beta = intbias >> betashift; /* beta = 1/1024 */ | ||
36 | +var betagamma = intbias << (gammashift - betashift); | ||
37 | + | ||
38 | +// defs for decreasing radius factor | ||
39 | +var initrad = netsize >> 3; // for 256 cols, radius starts | ||
40 | +var radiusbiasshift = 6; // at 32.0 biased by 6 bits | ||
41 | +var radiusbias = 1 << radiusbiasshift; | ||
42 | +var initradius = initrad * radiusbias; //and decreases by a | ||
43 | +var radiusdec = 30; // factor of 1/30 each cycle | ||
44 | + | ||
45 | +// defs for decreasing alpha factor | ||
46 | +var alphabiasshift = 10; // alpha starts at 1.0 | ||
47 | +var initalpha = 1 << alphabiasshift; | ||
48 | +var alphadec; // biased by 10 bits | ||
49 | + | ||
50 | +/* radbias and alpharadbias used for radpower calculation */ | ||
51 | +var radbiasshift = 8; | ||
52 | +var radbias = 1 << radbiasshift; | ||
53 | +var alpharadbshift = alphabiasshift + radbiasshift; | ||
54 | +var alpharadbias = 1 << alpharadbshift; | ||
55 | + | ||
56 | +// four primes near 500 - assume no image has a length so large that it is | ||
57 | +// divisible by all four primes | ||
58 | +var prime1 = 499; | ||
59 | +var prime2 = 491; | ||
60 | +var prime3 = 487; | ||
61 | +var prime4 = 503; | ||
62 | +var minpicturebytes = 3 * prime4; | ||
63 | + | ||
64 | +/* | ||
65 | + Constructor: NeuQuant | ||
66 | + | ||
67 | + Arguments: | ||
68 | + | ||
69 | + pixels - array of pixels in RGB format | ||
70 | + samplefac - sampling factor 1 to 30 where lower is better quality | ||
71 | + | ||
72 | + > | ||
73 | + > pixels = [r, g, b, r, g, b, r, g, b, ..] | ||
74 | + > | ||
75 | +*/ | ||
76 | +function NeuQuant(pixels, samplefac) { | ||
77 | + var network; // int[netsize][4] | ||
78 | + var netindex; // for network lookup - really 256 | ||
79 | + | ||
80 | + // bias and freq arrays for learning | ||
81 | + var bias; | ||
82 | + var freq; | ||
83 | + var radpower; | ||
84 | + | ||
85 | + /* | ||
86 | + Private Method: init | ||
87 | + | ||
88 | + sets up arrays | ||
89 | + */ | ||
90 | + function init() { | ||
91 | + network = []; | ||
92 | + netindex = new Int32Array(256); | ||
93 | + bias = new Int32Array(netsize); | ||
94 | + freq = new Int32Array(netsize); | ||
95 | + radpower = new Int32Array(netsize >> 3); | ||
96 | + | ||
97 | + var i, v; | ||
98 | + for (i = 0; i < netsize; i++) { | ||
99 | + v = (i << (netbiasshift + 8)) / netsize; | ||
100 | + network[i] = new Float64Array([v, v, v, 0]); | ||
101 | + //network[i] = [v, v, v, 0] | ||
102 | + freq[i] = intbias / netsize; | ||
103 | + bias[i] = 0; | ||
104 | + } | ||
105 | + } | ||
106 | + | ||
107 | + /* | ||
108 | + Private Method: unbiasnet | ||
109 | + | ||
110 | + unbiases network to give byte values 0..255 and record position i to prepare for sort | ||
111 | + */ | ||
112 | + function unbiasnet() { | ||
113 | + for (var i = 0; i < netsize; i++) { | ||
114 | + network[i][0] >>= netbiasshift; | ||
115 | + network[i][1] >>= netbiasshift; | ||
116 | + network[i][2] >>= netbiasshift; | ||
117 | + network[i][3] = i; // record color number | ||
118 | + } | ||
119 | + } | ||
120 | + | ||
121 | + /* | ||
122 | + Private Method: altersingle | ||
123 | + | ||
124 | + moves neuron *i* towards biased (b,g,r) by factor *alpha* | ||
125 | + */ | ||
126 | + function altersingle(alpha, i, b, g, r) { | ||
127 | + network[i][0] -= (alpha * (network[i][0] - b)) / initalpha; | ||
128 | + network[i][1] -= (alpha * (network[i][1] - g)) / initalpha; | ||
129 | + network[i][2] -= (alpha * (network[i][2] - r)) / initalpha; | ||
130 | + } | ||
131 | + | ||
132 | + /* | ||
133 | + Private Method: alterneigh | ||
134 | + | ||
135 | + moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha* | ||
136 | + */ | ||
137 | + function alterneigh(radius, i, b, g, r) { | ||
138 | + var lo = Math.abs(i - radius); | ||
139 | + var hi = Math.min(i + radius, netsize); | ||
140 | + | ||
141 | + var j = i + 1; | ||
142 | + var k = i - 1; | ||
143 | + var m = 1; | ||
144 | + | ||
145 | + var p, a; | ||
146 | + while (j < hi || k > lo) { | ||
147 | + a = radpower[m++]; | ||
148 | + | ||
149 | + if (j < hi) { | ||
150 | + p = network[j++]; | ||
151 | + p[0] -= (a * (p[0] - b)) / alpharadbias; | ||
152 | + p[1] -= (a * (p[1] - g)) / alpharadbias; | ||
153 | + p[2] -= (a * (p[2] - r)) / alpharadbias; | ||
154 | + } | ||
155 | + | ||
156 | + if (k > lo) { | ||
157 | + p = network[k--]; | ||
158 | + p[0] -= (a * (p[0] - b)) / alpharadbias; | ||
159 | + p[1] -= (a * (p[1] - g)) / alpharadbias; | ||
160 | + p[2] -= (a * (p[2] - r)) / alpharadbias; | ||
161 | + } | ||
162 | + } | ||
163 | + } | ||
164 | + | ||
165 | + /* | ||
166 | + Private Method: contest | ||
167 | + | ||
168 | + searches for biased BGR values | ||
169 | + */ | ||
170 | + function contest(b, g, r) { | ||
171 | + /* | ||
172 | + finds closest neuron (min dist) and updates freq | ||
173 | + finds best neuron (min dist-bias) and returns position | ||
174 | + for frequently chosen neurons, freq[i] is high and bias[i] is negative | ||
175 | + bias[i] = gamma * ((1 / netsize) - freq[i]) | ||
176 | + */ | ||
177 | + | ||
178 | + var bestd = ~(1 << 31); | ||
179 | + var bestbiasd = bestd; | ||
180 | + var bestpos = -1; | ||
181 | + var bestbiaspos = bestpos; | ||
182 | + | ||
183 | + var i, n, dist, biasdist, betafreq; | ||
184 | + for (i = 0; i < netsize; i++) { | ||
185 | + n = network[i]; | ||
186 | + | ||
187 | + dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r); | ||
188 | + if (dist < bestd) { | ||
189 | + bestd = dist; | ||
190 | + bestpos = i; | ||
191 | + } | ||
192 | + | ||
193 | + biasdist = dist - (bias[i] >> (intbiasshift - netbiasshift)); | ||
194 | + if (biasdist < bestbiasd) { | ||
195 | + bestbiasd = biasdist; | ||
196 | + bestbiaspos = i; | ||
197 | + } | ||
198 | + | ||
199 | + betafreq = freq[i] >> betashift; | ||
200 | + freq[i] -= betafreq; | ||
201 | + bias[i] += betafreq << gammashift; | ||
202 | + } | ||
203 | + | ||
204 | + freq[bestpos] += beta; | ||
205 | + bias[bestpos] -= betagamma; | ||
206 | + | ||
207 | + return bestbiaspos; | ||
208 | + } | ||
209 | + | ||
210 | + /* | ||
211 | + Private Method: inxbuild | ||
212 | + | ||
213 | + sorts network and builds netindex[0..255] | ||
214 | + */ | ||
215 | + function inxbuild() { | ||
216 | + var i, | ||
217 | + j, | ||
218 | + p, | ||
219 | + q, | ||
220 | + smallpos, | ||
221 | + smallval, | ||
222 | + previouscol = 0, | ||
223 | + startpos = 0; | ||
224 | + for (i = 0; i < netsize; i++) { | ||
225 | + p = network[i]; | ||
226 | + smallpos = i; | ||
227 | + smallval = p[1]; // index on g | ||
228 | + // find smallest in i..netsize-1 | ||
229 | + for (j = i + 1; j < netsize; j++) { | ||
230 | + q = network[j]; | ||
231 | + if (q[1] < smallval) { | ||
232 | + // index on g | ||
233 | + smallpos = j; | ||
234 | + smallval = q[1]; // index on g | ||
235 | + } | ||
236 | + } | ||
237 | + q = network[smallpos]; | ||
238 | + // swap p (i) and q (smallpos) entries | ||
239 | + if (i != smallpos) { | ||
240 | + j = q[0]; | ||
241 | + q[0] = p[0]; | ||
242 | + p[0] = j; | ||
243 | + j = q[1]; | ||
244 | + q[1] = p[1]; | ||
245 | + p[1] = j; | ||
246 | + j = q[2]; | ||
247 | + q[2] = p[2]; | ||
248 | + p[2] = j; | ||
249 | + j = q[3]; | ||
250 | + q[3] = p[3]; | ||
251 | + p[3] = j; | ||
252 | + } | ||
253 | + // smallval entry is now in position i | ||
254 | + | ||
255 | + if (smallval != previouscol) { | ||
256 | + netindex[previouscol] = (startpos + i) >> 1; | ||
257 | + for (j = previouscol + 1; j < smallval; j++) netindex[j] = i; | ||
258 | + previouscol = smallval; | ||
259 | + startpos = i; | ||
260 | + } | ||
261 | + } | ||
262 | + netindex[previouscol] = (startpos + maxnetpos) >> 1; | ||
263 | + for (j = previouscol + 1; j < 256; j++) netindex[j] = maxnetpos; // really 256 | ||
264 | + } | ||
265 | + | ||
266 | + /* | ||
267 | + Private Method: inxsearch | ||
268 | + | ||
269 | + searches for BGR values 0..255 and returns a color index | ||
270 | + */ | ||
271 | + function inxsearch(b, g, r) { | ||
272 | + var a, p, dist; | ||
273 | + | ||
274 | + var bestd = 1000; // biggest possible dist is 256*3 | ||
275 | + var best = -1; | ||
276 | + | ||
277 | + var i = netindex[g]; // index on g | ||
278 | + var j = i - 1; // start at netindex[g] and work outwards | ||
279 | + | ||
280 | + while (i < netsize || j >= 0) { | ||
281 | + if (i < netsize) { | ||
282 | + p = network[i]; | ||
283 | + dist = p[1] - g; // inx key | ||
284 | + if (dist >= bestd) i = netsize; | ||
285 | + // stop iter | ||
286 | + else { | ||
287 | + i++; | ||
288 | + if (dist < 0) dist = -dist; | ||
289 | + a = p[0] - b; | ||
290 | + if (a < 0) a = -a; | ||
291 | + dist += a; | ||
292 | + if (dist < bestd) { | ||
293 | + a = p[2] - r; | ||
294 | + if (a < 0) a = -a; | ||
295 | + dist += a; | ||
296 | + if (dist < bestd) { | ||
297 | + bestd = dist; | ||
298 | + best = p[3]; | ||
299 | + } | ||
300 | + } | ||
301 | + } | ||
302 | + } | ||
303 | + if (j >= 0) { | ||
304 | + p = network[j]; | ||
305 | + dist = g - p[1]; // inx key - reverse dif | ||
306 | + if (dist >= bestd) j = -1; | ||
307 | + // stop iter | ||
308 | + else { | ||
309 | + j--; | ||
310 | + if (dist < 0) dist = -dist; | ||
311 | + a = p[0] - b; | ||
312 | + if (a < 0) a = -a; | ||
313 | + dist += a; | ||
314 | + if (dist < bestd) { | ||
315 | + a = p[2] - r; | ||
316 | + if (a < 0) a = -a; | ||
317 | + dist += a; | ||
318 | + if (dist < bestd) { | ||
319 | + bestd = dist; | ||
320 | + best = p[3]; | ||
321 | + } | ||
322 | + } | ||
323 | + } | ||
324 | + } | ||
325 | + } | ||
326 | + | ||
327 | + return best; | ||
328 | + } | ||
329 | + | ||
330 | + /* | ||
331 | + Private Method: learn | ||
332 | + | ||
333 | + "Main Learning Loop" | ||
334 | + */ | ||
335 | + function learn() { | ||
336 | + var i; | ||
337 | + | ||
338 | + var lengthcount = pixels.length; | ||
339 | + var alphadec = 30 + (samplefac - 1) / 3; | ||
340 | + var samplepixels = lengthcount / (3 * samplefac); | ||
341 | + var delta = ~~(samplepixels / ncycles); | ||
342 | + var alpha = initalpha; | ||
343 | + var radius = initradius; | ||
344 | + | ||
345 | + var rad = radius >> radiusbiasshift; | ||
346 | + | ||
347 | + if (rad <= 1) rad = 0; | ||
348 | + for (i = 0; i < rad; i++) | ||
349 | + radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); | ||
350 | + | ||
351 | + var step; | ||
352 | + if (lengthcount < minpicturebytes) { | ||
353 | + samplefac = 1; | ||
354 | + step = 3; | ||
355 | + } else if (lengthcount % prime1 !== 0) { | ||
356 | + step = 3 * prime1; | ||
357 | + } else if (lengthcount % prime2 !== 0) { | ||
358 | + step = 3 * prime2; | ||
359 | + } else if (lengthcount % prime3 !== 0) { | ||
360 | + step = 3 * prime3; | ||
361 | + } else { | ||
362 | + step = 3 * prime4; | ||
363 | + } | ||
364 | + | ||
365 | + var b, g, r, j; | ||
366 | + var pix = 0; // current pixel | ||
367 | + | ||
368 | + i = 0; | ||
369 | + while (i < samplepixels) { | ||
370 | + b = (pixels[pix] & 0xff) << netbiasshift; | ||
371 | + g = (pixels[pix + 1] & 0xff) << netbiasshift; | ||
372 | + r = (pixels[pix + 2] & 0xff) << netbiasshift; | ||
373 | + | ||
374 | + j = contest(b, g, r); | ||
375 | + | ||
376 | + altersingle(alpha, j, b, g, r); | ||
377 | + if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours | ||
378 | + | ||
379 | + pix += step; | ||
380 | + if (pix >= lengthcount) pix -= lengthcount; | ||
381 | + | ||
382 | + i++; | ||
383 | + | ||
384 | + if (delta === 0) delta = 1; | ||
385 | + if (i % delta === 0) { | ||
386 | + alpha -= alpha / alphadec; | ||
387 | + radius -= radius / radiusdec; | ||
388 | + rad = radius >> radiusbiasshift; | ||
389 | + | ||
390 | + if (rad <= 1) rad = 0; | ||
391 | + for (j = 0; j < rad; j++) | ||
392 | + radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); | ||
393 | + } | ||
394 | + } | ||
395 | + } | ||
396 | + | ||
397 | + /* | ||
398 | + Method: buildColormap | ||
399 | + | ||
400 | + 1. initializes network | ||
401 | + 2. trains it | ||
402 | + 3. removes misconceptions | ||
403 | + 4. builds colorindex | ||
404 | + */ | ||
405 | + function buildColormap() { | ||
406 | + init(); | ||
407 | + learn(); | ||
408 | + unbiasnet(); | ||
409 | + inxbuild(); | ||
410 | + } | ||
411 | + this.buildColormap = buildColormap; | ||
412 | + | ||
413 | + /* | ||
414 | + Method: getColormap | ||
415 | + | ||
416 | + builds colormap from the index | ||
417 | + | ||
418 | + returns array in the format: | ||
419 | + | ||
420 | + > | ||
421 | + > [r, g, b, r, g, b, r, g, b, ..] | ||
422 | + > | ||
423 | + */ | ||
424 | + function getColormap() { | ||
425 | + var map = []; | ||
426 | + var index = []; | ||
427 | + | ||
428 | + for (var i = 0; i < netsize; i++) index[network[i][3]] = i; | ||
429 | + | ||
430 | + var k = 0; | ||
431 | + for (var l = 0; l < netsize; l++) { | ||
432 | + var j = index[l]; | ||
433 | + map[k++] = network[j][0]; | ||
434 | + map[k++] = network[j][1]; | ||
435 | + map[k++] = network[j][2]; | ||
436 | + } | ||
437 | + return map; | ||
438 | + } | ||
439 | + this.getColormap = getColormap; | ||
440 | + | ||
441 | + /* | ||
442 | + Method: lookupRGB | ||
443 | + | ||
444 | + looks for the closest *r*, *g*, *b* color in the map and | ||
445 | + returns its index | ||
446 | + */ | ||
447 | + this.lookupRGB = inxsearch; | ||
448 | +} | ||
449 | + | ||
450 | +module.exports = NeuQuant; |
-
Please register or login to post a comment