forked from mattdesl/imagebuffer
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
543 lines (489 loc) · 17.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
function isLittleEndian() {
//could use a more robust check here ....
var a = new ArrayBuffer(4);
var b = new Uint8Array(a);
var c = new Uint32Array(a);
b[0] = 0xa1;
b[1] = 0xb2;
b[2] = 0xc3;
b[3] = 0xd4;
if(c[0] == 0xd4c3b2a1)
return true;
if(c[0] == 0xa1b2c3d4)
return false;
else {
//Could not determine endianness
return null;
}
}
//test to see if ImageData uses
//CanvasPixelArray or Uint8ClampedArray
function isUint8ClampedImageData() {
if (typeof Uint8ClampedArray === "undefined")
return false;
var elem = document.createElement('canvas');
var ctx = elem.getContext('2d');
if (!ctx) //bail out early..
return false;
var image = ctx.createImageData(1, 1);
return image.data instanceof Uint8ClampedArray;
}
//null means 'could not detect endianness'
var LITTLE_ENDIAN = isLittleEndian();
//determine our capabilities
var SUPPORTS_32BIT =
typeof ArrayBuffer !== "undefined"
&& typeof Uint8ClampedArray !== "undefined"
&& typeof Int32Array !== "undefined"
&& LITTLE_ENDIAN !== null
&& isUint8ClampedImageData();
/**
* An ImageBuffer is a simple array of pixels that make up an image.
* Int32Array is used for better performance if supported, otherwise
* simple 8-bit manipulation is used as a fallback.
*
* To use this class; construct a new ImageBuffer with the specified dimensions, and modify
* its pixels with either setPixel/getPixel or setPixelAt/getPixelAt
* methods. Then, you can use the buffer.apply(imageData) to apply the changes to
* a shared ImageData object.
*
* You can also cache the image for later use by calling createImage(). Note that
* this is an expensive operation which should be used wisely.
*
* If you pass an ImageData object as the first parameter to the constructor, instead
* of width and height, any changes to the pixels array should be reflected immediately
* on the given ImageData object. In such a case, apply() has no effect.
*
* @class ImageBuffer
* @constructor
* @param {Number} width the width of the image
* @param {Number} height the height of the image
*/
var ImageBuffer = function(width, height) {
this.imageData = null;
if (typeof width !== "number") { //first argument is non-numerical.. must be ImageData
this.imageData = width;
width = this.imageData.width;
height = this.imageData.height;
}
this.width = width;
this.height = height;
this.pixels = null;
this.direct = false;
this.uint8 = null;
//If an ImageData is provided, we will try to manipulate its array directly.
if (this.imageData) {
this.direct = true;
//we can do direct manipulation
if (SUPPORTS_32BIT) {
this.uint8 = this.imageData.data;
this.pixels = new Int32Array(this.uint8.buffer);
}
//CanvasPixelArray + 8bit data... :(
else {
this.pixels = this.uint8 = this.imageData.data;
}
} else {
//use a separate buffer
if (SUPPORTS_32BIT) {
this.uint8 = new Uint8ClampedArray(width * height * ImageBuffer.NUM_COMPONENTS);
this.pixels = new Int32Array(this.uint8.buffer);
}
//assume no typed array support, use a simple array..
else {
this.pixels = this.uint8 = new Array(width * height * ImageBuffer.NUM_COMPONENTS);
}
}
};
ImageBuffer.prototype.constructor = ImageBuffer;
/**
* This is a utility function to set the color at the specified X and Y
* position (from top left).
*
* @method setPixelAt
* @param {Number} x the x position to modify
* @param {Number} y the y position to modify
* @param {Number} r the red byte, 0-255
* @param {Number} g the green byte, 0-255
* @param {Number} b the blue byte, 0-255
* @param {Number} a the alpha byte, 0-255
*/
ImageBuffer.prototype.setPixelAt = function(x, y, r, g, b, a) {
var i = ~~(x + (y * this.width));
this.setPixel(i, r, g, b, a);
};
/**
* This is a utility function to get the color at the specified X and Y
* position (from top left). You can specify a color object to reduce allocations.
*
* @method getPixelAt
* @param {Number} x the x position to modify
* @param {Number} y the y position to modify
* @param {Number} out the color object with `r, g, b, a` properties, or null
* @return {Object} a color representing the pixel at that location
*/
ImageBuffer.prototype.getPixelAt = function(x, y, out) {
var i = ~~(x + (y * this.width));
return this.getPixel(i, out);
};
/**
* Creates a new Image object from this ImageBuffer. You can pass
* a context to re-use, otherwise this method will create a new canvas
* and get its 2d context. This method uses toDataURL to generate
* a new Image.
*
* Note that this is not supported on older 2.x Android devices.
*
* @method createImage
* @param {CanvasRenderingContext} context the canvas 2D rendering context
* @return {Image} a new Image object with the data URI of your ImageBuffer
*/
ImageBuffer.prototype.createImage = function(context) {
var canvas;
if (!context) { //creates a new canvas element
canvas = document.createElement("canvas");
context = canvas.getContext("2d");
} else {
canvas = context.canvas; //context's back-reference
}
if (typeof canvas.toDataURL !== "function")
throw new Error("Canvas.toDataURL is not supported");
canvas.width = this.width;
canvas.height = this.height;
var imageData = this.imageData;
//if we need to first apply the image.... do so here:
if (!this.direct || !this.imageData) {
imageData = context.createImageData(this.width, this.height);
this.apply(imageData);
}
//put the data onto the context
context.clearRect(0, 0, this.width, this.height);
context.putImageData(imageData, 0, 0);
//create a new image object
var img = new Image();
img.src = canvas.toDataURL.apply(canvas, Array.prototype.slice.call(arguments, 1));
//we can only hope the GC will get rid of these quickly !
imageData = null;
context = null;
canvas = null;
return img;
};
/**
* Applies this buffer's pixels to an ImageData object. If
* the supplied ImageData is strictly equal to this buffer's
* ImageData, and we are modifying pixels directly, then this call does
* nothing.
*
* You can provide another ImageBuffer object, which essentially copies
* this buffer's pixels to the specified ImageBuffer. If the specified ImageBuffer
* is "directly" modifying its own ImageData's pixels, then it should be updated
* immediately.
*
* @method apply
* @param {ImageData|ImageBuffer} imageData the image data or ImageBuffer
*/
ImageBuffer.prototype.apply = function(imageData) {
if (this.imageData === imageData && this.direct) {
return;
}
if (SUPPORTS_32BIT) {
//update the other ImageBuffer with this buffer's pixels
if (imageData instanceof ImageBuffer) {
imageData.pixels.set(this.pixels);
}
//it must be an ImageData object.. update that
else {
imageData.data.set(this.uint8);
}
}
//No support for typed arrays..
//can't assume that set(otherArray) works :(
else {
var data = imageData instanceof ImageBuffer
? imageData.pixels : imageData.data;
if (!data)
throw new Error("imageData must be an ImageBuffer or Canvas ImageData object");
var pixels = this.pixels;
if (data.length !== pixels.length)
throw new Error("the image data for apply() must have the same dimensions");
//straight copy
for (var i=0; i<pixels.length; i++) {
data[i] = pixels[i];
}
}
};
ImageBuffer.NUM_COMPONENTS = 4;
/**
* Will be `true` if this context supports 32bit pixel
* maipulation using array buffer views.
*
* @attribute SUPPORTS_32BIT
* @readOnly
* @static
* @final
* @type {Boolean}
*/
ImageBuffer.SUPPORTS_32BIT = SUPPORTS_32BIT;
/**
* Will be `true` if little endianness was detected,
* or `false` if big endian was detected. If we could
* not detect the endianness (e.g. typed arrays not
* available, spec not implemented correctly), then
* this value will be null.
*
* @attribute LITTLE_ENDIAN
* @readOnly
* @static
* @final
* @type {Boolean|null}
*/
ImageBuffer.LITTLE_ENDIAN = LITTLE_ENDIAN;
/**
* Sets the pixel at the given index of the ImageBuffer's "data" array,
* which might be a Int32Array (modern browsers) or CanvasPixelArray (fallback),
* depending on the context's capabilities. Also takes endianness into account.
*
* @method setPixel
* @param {Int32Array|CanvasPixelArray} pixels the pixels data from ImageBuffer
* @param {Number} index the offset in the data to manipulate
* @param {Number} r the red byte, 0-255
* @param {Number} g the green byte, 0-255
* @param {Number} b the blue byte, 0-255
* @param {Number} a the alpha byte, 0-255
*/
/**
* This is a convenience method to multiply all of the
* pixels in inputBuffer with the specified (r, g, b, a) color,
* and place the result into outputBuffer. It's assumed that
* both buffers have the same size.
*
* @method multiply
* @static
* @param {ImageBuffer} inputBuffer the input image data
* @param {ImageBuffer} inputBuffer the output image data
* @param {Number} r the red byte, 0-255
* @param {Number} g the green byte, 0-255
* @param {Number} b the blue byte, 0-255
* @param {Number} a the alpha byte, 0-255
*/
if (SUPPORTS_32BIT) {
if (LITTLE_ENDIAN) {
ImageBuffer.prototype.setPixel = function(index, r, g, b, a) {
this.pixels[index] = (a << 24) | (b << 16) | (g << 8) | r;
};
ImageBuffer.multiply = function(inputBuffer, outputBuffer, r, g, b, a) {
var rgba = (a << 24) | (b << 16) | (g << 8) | r;
var input = inputBuffer.pixels,
output = outputBuffer.pixels,
len = input.length,
a1, a2, b1, b2, g1, g2, r1, r2,
val;
for (var i=0; i<len; i++) {
val1 = input[i];
a1 = ((val1 & 0xff000000) >>> 24);
a2 = ((rgba & 0xff000000) >>> 24);
b1 = ((val1 & 0x00ff0000) >>> 16);
b2 = ((rgba & 0x00ff0000) >>> 16);
g1 = ((val1 & 0x0000ff00) >>> 8);
g2 = ((rgba & 0x0000ff00) >>> 8);
r1 = ((val1 & 0x000000ff));
r2 = ((rgba & 0x000000ff));
r = r1 * r2 / 255;
g = g1 * g2 / 255;
b = b1 * b2 / 255;
a = a1 * a2 / 255;
output[i] = (a << 24) | (b << 16) | (g << 8) | r;
}
};
} else {
ImageBuffer.prototype.setPixel = function(index, r, g, b, a) {
this.pixels[index] = (r << 24) | (g << 16) | (b << 8) | a;
};
///TOOD: optimize with something like this:
///rgba = ((rgba & 0xFF000000) * (rgba2 >> 24)) | (((rgba & 0x00FF0000) * ((rgba2 >> 16) & 0xFF))) | (((rgba) & 0x0000FF00) * ((rgba2 >> 8) & 0xFF)) | ((rgba & 0x000000FF) * (rgba2 & 0xFF));
ImageBuffer.multiply = function(inputBuffer, outputBuffer, r, g, b, a) {
var rgba = (r << 24) | (g << 16) | (b << 8) | a;
var input = inputBuffer.pixels,
output = outputBuffer.pixels,
len = input.length,
a1, a2, b1, b2, g1, g2, r1, r2,
val1;
for (var i=0; i<len; i++) {
val1 = input[i];
r1 = ((val1 & 0xff000000) >>> 24);
r2 = ((rgba & 0xff000000) >>> 24);
g1 = ((val1 & 0x00ff0000) >>> 16);
g2 = ((rgba & 0x00ff0000) >>> 16);
b1 = ((val1 & 0x0000ff00) >>> 8);
b2 = ((rgba & 0x0000ff00) >>> 8);
a1 = ((val1 & 0x000000ff));
a2 = ((rgba & 0x000000ff));
r = r1 * r2 / 255;
g = g1 * g2 / 255;
b = b1 * b2 / 255;
a = a1 * a2 / 255;
output[i] = (r << 24) | (g << 16) | (b << 8) | a;
}
};
}
} else {
ImageBuffer.prototype.setPixel = function(index, r, g, b, a) {
var pixels = this.pixels;
index *= 4;
pixels[index] = r;
pixels[++index] = g;
pixels[++index] = b;
pixels[++index] = a;
};
ImageBuffer.multiply = function(inputBuffer, outputBuffer, r, g, b, a) {
var input = inputBuffer.pixels,
output = outputBuffer.pixels,
len = input.length;
for (var i=0; i<len; i+=4) {
output[i] = input[i] * r / 255;
output[i+1] = input[i+1] * g / 255;
output[i+2] = input[i+2] * b / 255;
output[i+3] = input[i+3] * a / 255;
}
};
}
/**
* Gets the pixel at the given index of the ImageBuffer's "data" array,
* which might be a Int32Array (modern browsers) or CanvasPixelArray (fallback),
* depending on the context's capabilities. Also takes endianness into account.
*
* The returned value is an object containing the color components as bytes (0-255)
* in `r, g, b, a`. If `out` is specified, it will use that instead to reduce object creation.
*
* @method getPixel
* @param {Int32Array|CanvasPixelArray} pixels the pixels data from ImageBuffer
* @param {Number} index the offset in the data to grab the color
* @param {Number} out the color object with `r, g, b, a` properties, or null
* @return {Object} a color representing the pixel at that location
*/
ImageBuffer.prototype.getPixel = function(index, out) {
var pixels = this.uint8;
index *= 4;
if (!out)
out = {r:0, g:0, b:0, a:0};
out.r = pixels[index];
out.g = pixels[++index];
out.b = pixels[++index];
out.a = pixels[++index];
return out;
};
/**
* Packs the r, g, b, a components into a single integer, for use with
* Int32Array. If LITTLE_ENDIAN, then ABGR order is used. Otherwise,
* RGBA order is used.
*
* @method packPixel
* @static
* @param {Number} r the red byte, 0-255
* @param {Number} g the green byte, 0-255
* @param {Number} b the blue byte, 0-255
* @param {Number} a the alpha byte, 0-255
* @return {Number} the packed color
*/
/**
* Unpacks the r, g, b, a components into the specified color object, or a new
* object, for use with Int32Array. If LITTLE_ENDIAN, then ABGR order is used when
* unpacking, otherwise, RGBA order is used. The resulting color object has the
* `r, g, b, a` properties which are unrelated to endianness.
*
* Note that the integer is assumed to be packed in the correct endianness. On little-endian
* the format is 0xAABBGGRR and on big-endian the format is 0xRRGGBBAA. If you want a
* endian-independent method, use fromRGBA(rgba) and toRGBA(r, g, b, a).
*
* @method unpackPixel
* @static
* @param {Number} rgba the integer, packed in endian order by packPixel
* @param {Number} out the color object with `r, g, b, a` properties, or null
* @return {Object} a color representing the pixel at that location
*/
if (LITTLE_ENDIAN) {
ImageBuffer.packPixel = function(r, g, b, a) {
return (a << 24) | (b << 16) | (g << 8) | r;
};
ImageBuffer.unpackPixel = function(rgba, out) {
if (!out)
out = {r:0, g:0, b:0, a:0};
out.a = ((rgba & 0xff000000) >>> 24);
out.b = ((rgba & 0x00ff0000) >>> 16);
out.g = ((rgba & 0x0000ff00) >>> 8);
out.r = ((rgba & 0x000000ff));
return out;
};
} else {
ImageBuffer.packPixel = function(r, g, b, a) {
return (r << 24) | (g << 16) | (b << 8) | a;
};
ImageBuffer.unpackPixel = function(rgba, out) {
if (!out)
out = {r:0, g:0, b:0, a:0};
out.r = ((rgba & 0xff000000) >>> 24);
out.g = ((rgba & 0x00ff0000) >>> 16);
out.b = ((rgba & 0x0000ff00) >>> 8);
out.a = ((rgba & 0x000000ff));
return out;
};
}
/**
* A utility to convert an integer in 0xRRGGBBAA format to a color object.
* This does not rely on endianness.
*
* @method fromRGBA
* @static
* @param {Number} rgba an RGBA hex
* @param {Object} out the object to use, optional
* @return {Object} a color object
*/
ImageBuffer.fromRGBA = function(rgba, out) {
if (!out)
out = {r:0, g:0, b:0, a:0};
out.r = ((rgba & 0xff000000) >>> 24);
out.g = ((rgba & 0x00ff0000) >>> 16);
out.b = ((rgba & 0x0000ff00) >>> 8);
out.a = ((rgba & 0x000000ff));
return out;
};
/**
* A utility to convert RGBA components to a 32 bit integer
* in RRGGBBAA format.
*
* @method toRGBA
* @static
* @param {Number} r the r color component (0 - 255)
* @param {Number} g the g color component (0 - 255)
* @param {Number} b the b color component (0 - 255)
* @param {Number} a the a color component (0 - 255)
* @return {Number} a RGBA-packed 32 bit integer
*/
ImageBuffer.toRGBA = function(r, g, b, a) {
return (r << 24) | (g << 16) | (b << 8) | a;
};
/**
* A utility function to create a lightweight 'color'
* object with the default components. Any components
* that are not specified will default to zero.
*
* This is useful when you want to use a shared color
* object for the getPixel and getPixelAt methods.
*
* @method createColor
* @static
* @param {Number} r the r color component (0 - 255)
* @param {Number} g the g color component (0 - 255)
* @param {Number} b the b color component (0 - 255)
* @param {Number} a the a color component (0 - 255)
* @return {Object} the resulting color object, with r, g, b, a properties
*/
ImageBuffer.createColor = function(r, g, b, a) {
return {
r: r||0,
g: g||0,
b: b||0,
a: a||0
};
};
module.exports = ImageBuffer;