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 | */
|
11 | let __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
|
15 | const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31));
|
16 | const 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 | */
|
27 | const 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 | */
|
38 | const __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 | */
|
54 | const 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 | */
|
83 | const 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 |
|
96 | class 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 |
|
257 | module.exports = Silhouette;
|