UNPKG

9.28 kBJavaScriptView Raw
1/**
2 * @fileoverview
3 * A representation of a Skin's silhouette that can test if a point on the skin
4 * renders a pixel where it is drawn.
5 */
6
7/**
8 * <canvas> element used to update Silhouette data from skin bitmap data.
9 * @type {CanvasElement}
10 */
11let __SilhouetteUpdateCanvas;
12
13// Optimized Math.min and Math.max for integers;
14// taken from https://web.archive.org/web/20190716181049/http://guihaire.com/code/?p=549
15const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31));
16const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31));
17
18/**
19 * Internal helper function (in hopes that compiler can inline). Get a pixel
20 * from silhouette data, or 0 if outside it's bounds.
21 * @private
22 * @param {Silhouette} silhouette - has data width and height
23 * @param {number} x - x
24 * @param {number} y - y
25 * @return {number} Alpha value for x/y position
26 */
27const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
28 // 0 if outside bounds, otherwise read from data.
29 if (x >= width || y >= height || x < 0 || y < 0) {
30 return 0;
31 }
32 return data[(((y * width) + x) * 4) + 3];
33};
34
35/**
36 * Memory buffers for doing 4 corner sampling for linear interpolation
37 */
38const __cornerWork = [
39 new Uint8ClampedArray(4),
40 new Uint8ClampedArray(4),
41 new Uint8ClampedArray(4),
42 new Uint8ClampedArray(4)
43];
44
45/**
46 * Get the color from a given silhouette at an x/y local texture position.
47 * Multiply color values by alpha for proper blending.
48 * @param {Silhouette} $0 The silhouette to sample.
49 * @param {number} x X position of texture [0, width).
50 * @param {number} y Y position of texture [0, height).
51 * @param {Uint8ClampedArray} dst A color 4b space.
52 * @return {Uint8ClampedArray} The dst vector.
53 */
54const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
55 // Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
56 // (See github.com/LLK/scratch-render/blob/954cfff02b08069a082cbedd415c1fecd9b1e4fb/src/BitmapSkin.js#L88)
57 x = intMax(0, intMin(x, width - 1));
58 y = intMax(0, intMin(y, height - 1));
59
60 // 0 if outside bounds, otherwise read from data.
61 if (x >= width || y >= height || x < 0 || y < 0) {
62 return dst.fill(0);
63 }
64 const offset = ((y * width) + x) * 4;
65 // premultiply alpha
66 const alpha = data[offset + 3] / 255;
67 dst[0] = data[offset] * alpha;
68 dst[1] = data[offset + 1] * alpha;
69 dst[2] = data[offset + 2] * alpha;
70 dst[3] = data[offset + 3];
71 return dst;
72};
73
74/**
75 * Get the color from a given silhouette at an x/y local texture position.
76 * Do not multiply color values by alpha, as it has already been done.
77 * @param {Silhouette} $0 The silhouette to sample.
78 * @param {number} x X position of texture [0, width).
79 * @param {number} y Y position of texture [0, height).
80 * @param {Uint8ClampedArray} dst A color 4b space.
81 * @return {Uint8ClampedArray} The dst vector.
82 */
83const getPremultipliedColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
84 // Clamp coords to edge, matching GL_CLAMP_TO_EDGE.
85 x = intMax(0, intMin(x, width - 1));
86 y = intMax(0, intMin(y, height - 1));
87
88 const offset = ((y * width) + x) * 4;
89 dst[0] = data[offset];
90 dst[1] = data[offset + 1];
91 dst[2] = data[offset + 2];
92 dst[3] = data[offset + 3];
93 return dst;
94};
95
96class Silhouette {
97 constructor () {
98 /**
99 * The width of the data representing the current skin data.
100 * @type {number}
101 */
102 this._width = 0;
103
104 /**
105 * The height of the data representing the current skin date.
106 * @type {number}
107 */
108 this._height = 0;
109
110 /**
111 * The data representing a skin's silhouette shape.
112 * @type {Uint8ClampedArray}
113 */
114 this._colorData = null;
115
116 // By default, silhouettes are assumed not to contain premultiplied image data,
117 // so when we get a color, we want to multiply it by its alpha channel.
118 // Point `_getColor` to the version of the function that multiplies.
119 this._getColor = getColor4b;
120
121 this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
122 }
123
124 /**
125 * Update this silhouette with the bitmapData for a skin.
126 * @param {ImageData|HTMLCanvasElement|HTMLImageElement} bitmapData An image, canvas or other element that the skin
127 * @param {boolean} isPremultiplied True if the source bitmap data comes premultiplied (e.g. from readPixels).
128 * rendering can be queried from.
129 */
130 update (bitmapData, isPremultiplied = false) {
131 let imageData;
132 if (bitmapData instanceof ImageData) {
133 // If handed ImageData directly, use it directly.
134 imageData = bitmapData;
135 this._width = bitmapData.width;
136 this._height = bitmapData.height;
137 } else {
138 // Draw about anything else to our update canvas and poll image data
139 // from that.
140 const canvas = Silhouette._updateCanvas();
141 const width = this._width = canvas.width = bitmapData.width;
142 const height = this._height = canvas.height = bitmapData.height;
143 const ctx = canvas.getContext('2d');
144
145 if (!(width && height)) {
146 return;
147 }
148 ctx.clearRect(0, 0, width, height);
149 ctx.drawImage(bitmapData, 0, 0, width, height);
150 imageData = ctx.getImageData(0, 0, width, height);
151 }
152
153 if (isPremultiplied) {
154 this._getColor = getPremultipliedColor4b;
155 } else {
156 this._getColor = getColor4b;
157 }
158
159 this._colorData = imageData.data;
160 // delete our custom overriden "uninitalized" color functions
161 // let the prototype work for itself
162 delete this.colorAtNearest;
163 delete this.colorAtLinear;
164 }
165
166 /**
167 * Sample a color from the silhouette at a given local position using
168 * "nearest neighbor"
169 * @param {twgl.v3} vec [x,y] texture space (0-1)
170 * @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
171 * @returns {Uint8ClampedArray} dst
172 */
173 colorAtNearest (vec, dst) {
174 return this._getColor(
175 this,
176 Math.floor(vec[0] * (this._width - 1)),
177 Math.floor(vec[1] * (this._height - 1)),
178 dst
179 );
180 }
181
182 /**
183 * Sample a color from the silhouette at a given local position using
184 * "linear interpolation"
185 * @param {twgl.v3} vec [x,y] texture space (0-1)
186 * @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
187 * @returns {Uint8ClampedArray} dst
188 */
189 colorAtLinear (vec, dst) {
190 const x = vec[0] * (this._width - 1);
191 const y = vec[1] * (this._height - 1);
192
193 const x1D = x % 1;
194 const y1D = y % 1;
195 const x0D = 1 - x1D;
196 const y0D = 1 - y1D;
197
198 const xFloor = Math.floor(x);
199 const yFloor = Math.floor(y);
200
201 const x0y0 = this._getColor(this, xFloor, yFloor, __cornerWork[0]);
202 const x1y0 = this._getColor(this, xFloor + 1, yFloor, __cornerWork[1]);
203 const x0y1 = this._getColor(this, xFloor, yFloor + 1, __cornerWork[2]);
204 const x1y1 = this._getColor(this, xFloor + 1, yFloor + 1, __cornerWork[3]);
205
206 dst[0] = (x0y0[0] * x0D * y0D) + (x0y1[0] * x0D * y1D) + (x1y0[0] * x1D * y0D) + (x1y1[0] * x1D * y1D);
207 dst[1] = (x0y0[1] * x0D * y0D) + (x0y1[1] * x0D * y1D) + (x1y0[1] * x1D * y0D) + (x1y1[1] * x1D * y1D);
208 dst[2] = (x0y0[2] * x0D * y0D) + (x0y1[2] * x0D * y1D) + (x1y0[2] * x1D * y0D) + (x1y1[2] * x1D * y1D);
209 dst[3] = (x0y0[3] * x0D * y0D) + (x0y1[3] * x0D * y1D) + (x1y0[3] * x1D * y0D) + (x1y1[3] * x1D * y1D);
210
211 return dst;
212 }
213
214 /**
215 * Test if texture coordinate touches the silhouette using nearest neighbor.
216 * @param {twgl.v3} vec A texture coordinate.
217 * @return {boolean} If the nearest pixel has an alpha value.
218 */
219 isTouchingNearest (vec) {
220 if (!this._colorData) return;
221 return getPoint(
222 this,
223 Math.floor(vec[0] * (this._width - 1)),
224 Math.floor(vec[1] * (this._height - 1))
225 ) > 0;
226 }
227
228 /**
229 * Test to see if any of the 4 pixels used in the linear interpolate touch
230 * the silhouette.
231 * @param {twgl.v3} vec A texture coordinate.
232 * @return {boolean} Any of the pixels have some alpha.
233 */
234 isTouchingLinear (vec) {
235 if (!this._colorData) return;
236 const x = Math.floor(vec[0] * (this._width - 1));
237 const y = Math.floor(vec[1] * (this._height - 1));
238 return getPoint(this, x, y) > 0 ||
239 getPoint(this, x + 1, y) > 0 ||
240 getPoint(this, x, y + 1) > 0 ||
241 getPoint(this, x + 1, y + 1) > 0;
242 }
243
244 /**
245 * Get the canvas element reused by Silhouettes to update their data with.
246 * @private
247 * @return {CanvasElement} A canvas to draw bitmap data to.
248 */
249 static _updateCanvas () {
250 if (typeof __SilhouetteUpdateCanvas === 'undefined') {
251 __SilhouetteUpdateCanvas = document.createElement('canvas');
252 }
253 return __SilhouetteUpdateCanvas;
254 }
255}
256
257module.exports = Silhouette;