@scratch/scratch-render 11.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/SVGSkin.js ADDED
@@ -0,0 +1,239 @@
1
+ const twgl = require('twgl.js');
2
+
3
+ const Skin = require('./Skin');
4
+ const {loadSvgString, serializeSvgToString} = require('@scratch/scratch-svg-renderer');
5
+ const ShaderManager = require('./ShaderManager');
6
+
7
+ const MAX_TEXTURE_DIMENSION = 2048;
8
+
9
+ /**
10
+ * All scaled renderings of the SVG are stored in an array. The 1.0 scale of
11
+ * the SVG is stored at the 8th index. The smallest possible 1 / 256 scale
12
+ * rendering is stored at the 0th index.
13
+ * @const {number}
14
+ */
15
+ const INDEX_OFFSET = 8;
16
+
17
+ class SVGSkin extends Skin {
18
+ /**
19
+ * Create a new SVG skin.
20
+ * @param {!int} id - The ID for this Skin.
21
+ * @param {!RenderWebGL} renderer - The renderer which will use this skin.
22
+ * @constructor
23
+ * @extends Skin
24
+ */
25
+ constructor (id, renderer) {
26
+ super(id);
27
+
28
+ /** @type {RenderWebGL} */
29
+ this._renderer = renderer;
30
+
31
+ /** @type {HTMLImageElement} */
32
+ this._svgImage = document.createElement('img');
33
+
34
+ /** @type {boolean} */
35
+ this._svgImageLoaded = false;
36
+
37
+ /** @type {Array<number>} */
38
+ this._size = [0, 0];
39
+
40
+ /** @type {HTMLCanvasElement} */
41
+ this._canvas = document.createElement('canvas');
42
+
43
+ /** @type {CanvasRenderingContext2D} */
44
+ this._context = this._canvas.getContext('2d');
45
+
46
+ /** @type {Array<WebGLTexture>} */
47
+ this._scaledMIPs = [];
48
+
49
+ /** @type {number} */
50
+ this._largestMIPScale = 0;
51
+
52
+ /**
53
+ * Ratio of the size of the SVG and the max size of the WebGL texture
54
+ * @type {Number}
55
+ */
56
+ this._maxTextureScale = 1;
57
+ }
58
+
59
+ /**
60
+ * Dispose of this object. Do not use it after calling this method.
61
+ */
62
+ dispose () {
63
+ this.resetMIPs();
64
+ super.dispose();
65
+ }
66
+
67
+ /**
68
+ * @return {Array<number>} the natural size, in Scratch units, of this skin.
69
+ */
70
+ get size () {
71
+ return [this._size[0], this._size[1]];
72
+ }
73
+
74
+ useNearest (scale, drawable) {
75
+ // If the effect bits for mosaic, pixelate, whirl, or fisheye are set, use linear
76
+ if ((drawable.enabledEffects & (
77
+ ShaderManager.EFFECT_INFO.fisheye.mask |
78
+ ShaderManager.EFFECT_INFO.whirl.mask |
79
+ ShaderManager.EFFECT_INFO.pixelate.mask |
80
+ ShaderManager.EFFECT_INFO.mosaic.mask
81
+ )) !== 0) {
82
+ return false;
83
+ }
84
+
85
+ // We can't use nearest neighbor unless we are a multiple of 90 rotation
86
+ if (drawable._direction % 90 !== 0) {
87
+ return false;
88
+ }
89
+
90
+ // Because SVG skins' bounding boxes are currently not pixel-aligned, the idea here is to hide blurriness
91
+ // by using nearest-neighbor scaling if one screen-space pixel is "close enough" to one texture pixel.
92
+ // If the scale of the skin is very close to 100 (0.99999 variance is okay I guess)
93
+ // TODO: Make this check more precise. We should use nearest if there's less than one pixel's difference
94
+ // between the screen-space and texture-space sizes of the skin. Mipmaps make this harder because there are
95
+ // multiple textures (and hence multiple texture spaces) and we need to know which one to choose.
96
+ if (Math.abs(scale[0]) > 99 && Math.abs(scale[0]) < 101 &&
97
+ Math.abs(scale[1]) > 99 && Math.abs(scale[1]) < 101) {
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * Create a MIP for a given scale.
105
+ * @param {number} scale - The relative size of the MIP
106
+ * @return {SVGMIP} An object that handles creating and updating SVG textures.
107
+ */
108
+ createMIP (scale) {
109
+ const [width, height] = this._size;
110
+ this._canvas.width = width * scale;
111
+ this._canvas.height = height * scale;
112
+ if (
113
+ this._canvas.width <= 0 ||
114
+ this._canvas.height <= 0 ||
115
+ // Even if the canvas at the current scale has a nonzero size, the image's dimensions are floored
116
+ // pre-scaling; e.g. if an image has a width of 0.4 and is being rendered at 3x scale, the canvas will have
117
+ // a width of 1, but the image's width will be rounded down to 0 on some browsers (Firefox) prior to being
118
+ // drawn at that scale, resulting in an IndexSizeError if we attempt to draw it.
119
+ this._svgImage.naturalWidth <= 0 ||
120
+ this._svgImage.naturalHeight <= 0
121
+ ) return super.getTexture();
122
+ this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
123
+ this._context.setTransform(scale, 0, 0, scale, 0, 0);
124
+ this._context.drawImage(this._svgImage, 0, 0);
125
+
126
+ // Pull out the ImageData from the canvas. ImageData speeds up
127
+ // updating Silhouette and is better handled by more browsers in
128
+ // regards to memory.
129
+ const textureData = this._context.getImageData(0, 0, this._canvas.width, this._canvas.height);
130
+
131
+ const textureOptions = {
132
+ auto: false,
133
+ wrap: this._renderer.gl.CLAMP_TO_EDGE,
134
+ src: textureData,
135
+ premultiplyAlpha: true
136
+ };
137
+
138
+ const mip = twgl.createTexture(this._renderer.gl, textureOptions);
139
+
140
+ // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up.
141
+ if (this._largestMIPScale < scale) {
142
+ this._silhouette.update(textureData);
143
+ this._largestMIPScale = scale;
144
+ }
145
+
146
+ return mip;
147
+ }
148
+
149
+ updateSilhouette (scale = [100, 100]) {
150
+ // Ensure a silhouette exists.
151
+ this.getTexture(scale);
152
+ }
153
+
154
+ /**
155
+ * @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
156
+ * @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
157
+ */
158
+ getTexture (scale) {
159
+ // The texture only ever gets uniform scale. Take the larger of the two axes.
160
+ const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
161
+ const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale);
162
+
163
+ // Math.ceil(Math.log2(scale)) means we use the "1x" texture at (0.5, 1] scale,
164
+ // the "2x" texture at (1, 2] scale, the "4x" texture at (2, 4] scale, etc.
165
+ // This means that one texture pixel will always be between 0.5x and 1x the size of one rendered pixel,
166
+ // but never bigger than one rendered pixel--this prevents blurriness from blowing up the texture too much.
167
+ const mipLevel = Math.max(Math.ceil(Math.log2(requestedScale)) + INDEX_OFFSET, 0);
168
+ // Can't use bitwise stuff here because we need to handle negative exponents
169
+ const mipScale = Math.pow(2, mipLevel - INDEX_OFFSET);
170
+
171
+ if (this._svgImageLoaded && !this._scaledMIPs[mipLevel]) {
172
+ this._scaledMIPs[mipLevel] = this.createMIP(mipScale);
173
+ }
174
+
175
+ return this._scaledMIPs[mipLevel] || super.getTexture();
176
+ }
177
+
178
+ /**
179
+ * Do a hard reset of the existing MIPs by deleting them.
180
+ */
181
+ resetMIPs () {
182
+ this._scaledMIPs.forEach(oldMIP => this._renderer.gl.deleteTexture(oldMIP));
183
+ this._scaledMIPs.length = 0;
184
+ this._largestMIPScale = 0;
185
+ }
186
+
187
+ /**
188
+ * Set the contents of this skin to a snapshot of the provided SVG data.
189
+ * @param {string} svgData - new SVG to use.
190
+ * @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
191
+ * calculated from the bounding box
192
+ * @fires Skin.event:WasAltered
193
+ */
194
+ setSVG (svgData, rotationCenter) {
195
+ const svgTag = loadSvgString(svgData);
196
+ const svgText = serializeSvgToString(svgTag, true /* shouldInjectFonts */);
197
+ this._svgImageLoaded = false;
198
+
199
+ const {x, y, width, height} = svgTag.viewBox.baseVal;
200
+ // While we're setting the size before the image is loaded, this doesn't cause the skin to appear with the wrong
201
+ // size for a few frames while the new image is loading, because we don't emit the `WasAltered` event, telling
202
+ // drawables using this skin to update, until the image is loaded.
203
+ // We need to do this because the VM reads the skin's `size` directly after calling `setSVG`.
204
+ // TODO: return a Promise so that the VM can read the skin's `size` after the image is loaded.
205
+ this._size[0] = width;
206
+ this._size[1] = height;
207
+
208
+ // If there is another load already in progress, replace the old onload to effectively cancel the old load
209
+ this._svgImage.onload = () => {
210
+ if (width === 0 || height === 0) {
211
+ super.setEmptyImageData();
212
+ return;
213
+ }
214
+
215
+ const maxDimension = Math.ceil(Math.max(width, height));
216
+ let testScale = 2;
217
+ for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
218
+ this._maxTextureScale = testScale;
219
+ }
220
+
221
+ this.resetMIPs();
222
+
223
+ if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
224
+ // Compensate for viewbox offset.
225
+ // See https://github.com/LLK/scratch-render/pull/90.
226
+ this._rotationCenter[0] = rotationCenter[0] - x;
227
+ this._rotationCenter[1] = rotationCenter[1] - y;
228
+
229
+ this._svgImageLoaded = true;
230
+
231
+ this.emit(Skin.Events.WasAltered);
232
+ };
233
+
234
+ this._svgImage.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
235
+ }
236
+
237
+ }
238
+
239
+ module.exports = SVGSkin;
@@ -0,0 +1,187 @@
1
+ const twgl = require('twgl.js');
2
+
3
+
4
+ class ShaderManager {
5
+ /**
6
+ * @param {WebGLRenderingContext} gl WebGL rendering context to create shaders for
7
+ * @constructor
8
+ */
9
+ constructor (gl) {
10
+ this._gl = gl;
11
+
12
+ /**
13
+ * The cache of all shaders compiled so far, filled on demand.
14
+ * @type {Object<ShaderManager.DRAW_MODE, Array<ProgramInfo>>}
15
+ * @private
16
+ */
17
+ this._shaderCache = {};
18
+ for (const modeName in ShaderManager.DRAW_MODE) {
19
+ if (Object.prototype.hasOwnProperty.call(ShaderManager.DRAW_MODE, modeName)) {
20
+ this._shaderCache[modeName] = [];
21
+ }
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Fetch the shader for a particular set of active effects.
27
+ * Build the shader if necessary.
28
+ * @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
29
+ * @param {int} effectBits Bitmask representing the enabled effects.
30
+ * @returns {ProgramInfo} The shader's program info.
31
+ */
32
+ getShader (drawMode, effectBits) {
33
+ const cache = this._shaderCache[drawMode];
34
+ if (drawMode === ShaderManager.DRAW_MODE.silhouette) {
35
+ // Silhouette mode isn't affected by these effects.
36
+ effectBits &= ~(ShaderManager.EFFECT_INFO.color.mask | ShaderManager.EFFECT_INFO.brightness.mask);
37
+ }
38
+ let shader = cache[effectBits];
39
+ if (!shader) {
40
+ shader = cache[effectBits] = this._buildShader(drawMode, effectBits);
41
+ }
42
+ return shader;
43
+ }
44
+
45
+ /**
46
+ * Build the shader for a particular set of active effects.
47
+ * @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
48
+ * @param {int} effectBits Bitmask representing the enabled effects.
49
+ * @returns {ProgramInfo} The new shader's program info.
50
+ * @private
51
+ */
52
+ _buildShader (drawMode, effectBits) {
53
+ const numEffects = ShaderManager.EFFECTS.length;
54
+
55
+ const defines = [
56
+ `#define DRAW_MODE_${drawMode}`
57
+ ];
58
+ for (let index = 0; index < numEffects; ++index) {
59
+ if ((effectBits & (1 << index)) !== 0) {
60
+ defines.push(`#define ENABLE_${ShaderManager.EFFECTS[index]}`);
61
+ }
62
+ }
63
+
64
+ const definesText = `${defines.join('\n')}\n`;
65
+
66
+ /* eslint-disable global-require */
67
+ const vsFullText = definesText + require('raw-loader!./shaders/sprite.vert');
68
+ const fsFullText = definesText + require('raw-loader!./shaders/sprite.frag');
69
+ /* eslint-enable global-require */
70
+
71
+ return twgl.createProgramInfo(this._gl, [vsFullText, fsFullText]);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @typedef {object} ShaderManager.Effect
77
+ * @prop {int} mask - The bit in 'effectBits' representing the effect.
78
+ * @prop {function} converter - A conversion function which takes a Scratch value (generally in the range
79
+ * 0..100 or -100..100) and maps it to a value useful to the shader. This
80
+ * mapping may not be reversible.
81
+ * @prop {boolean} shapeChanges - Whether the effect could change the drawn shape.
82
+ */
83
+
84
+ /**
85
+ * Mapping of each effect name to info about that effect.
86
+ * @enum {ShaderManager.Effect}
87
+ */
88
+ ShaderManager.EFFECT_INFO = {
89
+ /** Color effect */
90
+ color: {
91
+ uniformName: 'u_color',
92
+ mask: 1 << 0,
93
+ converter: x => (x / 200) % 1,
94
+ shapeChanges: false
95
+ },
96
+ /** Fisheye effect */
97
+ fisheye: {
98
+ uniformName: 'u_fisheye',
99
+ mask: 1 << 1,
100
+ converter: x => Math.max(0, (x + 100) / 100),
101
+ shapeChanges: true
102
+ },
103
+ /** Whirl effect */
104
+ whirl: {
105
+ uniformName: 'u_whirl',
106
+ mask: 1 << 2,
107
+ converter: x => -x * Math.PI / 180,
108
+ shapeChanges: true
109
+ },
110
+ /** Pixelate effect */
111
+ pixelate: {
112
+ uniformName: 'u_pixelate',
113
+ mask: 1 << 3,
114
+ converter: x => Math.abs(x) / 10,
115
+ shapeChanges: true
116
+ },
117
+ /** Mosaic effect */
118
+ mosaic: {
119
+ uniformName: 'u_mosaic',
120
+ mask: 1 << 4,
121
+ converter: x => {
122
+ x = Math.round((Math.abs(x) + 10) / 10);
123
+ /** @todo cap by Math.min(srcWidth, srcHeight) */
124
+ return Math.max(1, Math.min(x, 512));
125
+ },
126
+ shapeChanges: true
127
+ },
128
+ /** Brightness effect */
129
+ brightness: {
130
+ uniformName: 'u_brightness',
131
+ mask: 1 << 5,
132
+ converter: x => Math.max(-100, Math.min(x, 100)) / 100,
133
+ shapeChanges: false
134
+ },
135
+ /** Ghost effect */
136
+ ghost: {
137
+ uniformName: 'u_ghost',
138
+ mask: 1 << 6,
139
+ converter: x => 1 - (Math.max(0, Math.min(x, 100)) / 100),
140
+ shapeChanges: false
141
+ }
142
+ };
143
+
144
+ /**
145
+ * The name of each supported effect.
146
+ * @type {Array}
147
+ */
148
+ ShaderManager.EFFECTS = Object.keys(ShaderManager.EFFECT_INFO);
149
+
150
+ /**
151
+ * The available draw modes.
152
+ * @readonly
153
+ * @enum {string}
154
+ */
155
+ ShaderManager.DRAW_MODE = {
156
+ /**
157
+ * Draw normally. Its output will use premultiplied alpha.
158
+ */
159
+ default: 'default',
160
+
161
+ /**
162
+ * Draw with non-premultiplied alpha. Useful for reading pixels from GL into an ImageData object.
163
+ */
164
+ straightAlpha: 'straightAlpha',
165
+
166
+ /**
167
+ * Draw a silhouette using a solid color.
168
+ */
169
+ silhouette: 'silhouette',
170
+
171
+ /**
172
+ * Draw only the parts of the drawable which match a particular color.
173
+ */
174
+ colorMask: 'colorMask',
175
+
176
+ /**
177
+ * Draw a line with caps.
178
+ */
179
+ line: 'line',
180
+
181
+ /**
182
+ * Draw the background in a certain color. Must sometimes be used instead of gl.clear.
183
+ */
184
+ background: 'background'
185
+ };
186
+
187
+ module.exports = ShaderManager;
@@ -0,0 +1,257 @@
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;