@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/.nvmrc +1 -0
- package/CHANGELOG.md +2281 -0
- package/LICENSE +12 -0
- package/README.md +87 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/Rectangle-AABB-Matrix.md +192 -0
- package/package.json +84 -0
- package/release.config.js +10 -0
- package/src/BitmapSkin.js +120 -0
- package/src/Drawable.js +734 -0
- package/src/EffectTransform.js +197 -0
- package/src/PenSkin.js +350 -0
- package/src/Rectangle.js +196 -0
- package/src/RenderConstants.js +34 -0
- package/src/RenderWebGL.js +2029 -0
- package/src/SVGSkin.js +239 -0
- package/src/ShaderManager.js +187 -0
- package/src/Silhouette.js +257 -0
- package/src/Skin.js +235 -0
- package/src/TextBubbleSkin.js +284 -0
- package/src/index.js +7 -0
- package/src/playground/getMousePosition.js +37 -0
- package/src/playground/index.html +41 -0
- package/src/playground/playground.js +202 -0
- package/src/playground/queryPlayground.html +73 -0
- package/src/playground/queryPlayground.js +196 -0
- package/src/playground/style.css +11 -0
- package/src/shaders/sprite.frag +249 -0
- package/src/shaders/sprite.vert +75 -0
- package/src/util/canvas-measurement-provider.js +41 -0
- package/src/util/color-conversions.js +97 -0
- package/src/util/log.js +4 -0
- package/src/util/text-wrapper.js +112 -0
|
@@ -0,0 +1,2029 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
|
|
3
|
+
const hull = require('hull.js');
|
|
4
|
+
const twgl = require('twgl.js');
|
|
5
|
+
|
|
6
|
+
const BitmapSkin = require('./BitmapSkin');
|
|
7
|
+
const Drawable = require('./Drawable');
|
|
8
|
+
const Rectangle = require('./Rectangle');
|
|
9
|
+
const PenSkin = require('./PenSkin');
|
|
10
|
+
const RenderConstants = require('./RenderConstants');
|
|
11
|
+
const ShaderManager = require('./ShaderManager');
|
|
12
|
+
const SVGSkin = require('./SVGSkin');
|
|
13
|
+
const TextBubbleSkin = require('./TextBubbleSkin');
|
|
14
|
+
const EffectTransform = require('./EffectTransform');
|
|
15
|
+
const log = require('./util/log');
|
|
16
|
+
|
|
17
|
+
const __isTouchingDrawablesPoint = twgl.v3.create();
|
|
18
|
+
const __candidatesBounds = new Rectangle();
|
|
19
|
+
const __fenceBounds = new Rectangle();
|
|
20
|
+
const __touchingColor = new Uint8ClampedArray(4);
|
|
21
|
+
const __blendColor = new Uint8ClampedArray(4);
|
|
22
|
+
|
|
23
|
+
// More pixels than this and we give up to the GPU and take the cost of readPixels
|
|
24
|
+
// Width * Height * Number of drawables at location
|
|
25
|
+
const __cpuTouchingColorPixelCount = 4e4;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @callback RenderWebGL#idFilterFunc
|
|
29
|
+
* @param {int} drawableID The ID to filter.
|
|
30
|
+
* @return {bool} True if the ID passes the filter, otherwise false.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maximum touch size for a picking check.
|
|
35
|
+
* @todo Figure out a reasonable max size. Maybe this should be configurable?
|
|
36
|
+
* @type {Array<int>}
|
|
37
|
+
* @memberof RenderWebGL
|
|
38
|
+
*/
|
|
39
|
+
const MAX_TOUCH_SIZE = [3, 3];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Passed to the uniforms for mask in touching color
|
|
43
|
+
*/
|
|
44
|
+
const MASK_TOUCHING_COLOR_TOLERANCE = 2;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maximum number of pixels in either dimension of "extracted drawable" data
|
|
48
|
+
* @type {int}
|
|
49
|
+
*/
|
|
50
|
+
const MAX_EXTRACTED_DRAWABLE_DIMENSION = 2048;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Determines if the mask color is "close enough" (only test the 6 top bits for
|
|
54
|
+
* each color). These bit masks are what scratch 2 used to use, so we do the same.
|
|
55
|
+
* @param {Uint8Array} a A color3b or color4b value.
|
|
56
|
+
* @param {Uint8Array} b A color3b or color4b value.
|
|
57
|
+
* @returns {boolean} If the colors match within the parameters.
|
|
58
|
+
*/
|
|
59
|
+
const maskMatches = (a, b) => (
|
|
60
|
+
// has some non-alpha component to test against
|
|
61
|
+
a[3] > 0 &&
|
|
62
|
+
(a[0] & 0b11111100) === (b[0] & 0b11111100) &&
|
|
63
|
+
(a[1] & 0b11111100) === (b[1] & 0b11111100) &&
|
|
64
|
+
(a[2] & 0b11111100) === (b[2] & 0b11111100)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determines if the given color is "close enough" (only test the 5 top bits for
|
|
69
|
+
* red and green, 4 bits for blue). These bit masks are what scratch 2 used to use,
|
|
70
|
+
* so we do the same.
|
|
71
|
+
* @param {Uint8Array} a A color3b or color4b value.
|
|
72
|
+
* @param {Uint8Array} b A color3b or color4b value / or a larger array when used with offsets
|
|
73
|
+
* @param {number} offset An offset into the `b` array, which lets you use a larger array to test
|
|
74
|
+
* multiple values at the same time.
|
|
75
|
+
* @returns {boolean} If the colors match within the parameters.
|
|
76
|
+
*/
|
|
77
|
+
const colorMatches = (a, b, offset) => (
|
|
78
|
+
(a[0] & 0b11111000) === (b[offset + 0] & 0b11111000) &&
|
|
79
|
+
(a[1] & 0b11111000) === (b[offset + 1] & 0b11111000) &&
|
|
80
|
+
(a[2] & 0b11110000) === (b[offset + 2] & 0b11110000)
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Sprite Fencing - The number of pixels a sprite is required to leave remaining
|
|
85
|
+
* onscreen around the edge of the staging area.
|
|
86
|
+
* @type {number}
|
|
87
|
+
*/
|
|
88
|
+
const FENCE_WIDTH = 15;
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RenderWebGL extends EventEmitter {
|
|
92
|
+
/**
|
|
93
|
+
* Check if this environment appears to support this renderer before attempting to create an instance.
|
|
94
|
+
* Catching an exception from the constructor is also a valid way to test for (lack of) support.
|
|
95
|
+
* @param {canvas} [optCanvas] - An optional canvas to use for the test. Otherwise a temporary canvas will be used.
|
|
96
|
+
* @returns {boolean} - True if this environment appears to support this renderer, false otherwise.
|
|
97
|
+
*/
|
|
98
|
+
static isSupported (optCanvas) {
|
|
99
|
+
try {
|
|
100
|
+
// Create the context the same way that the constructor will: attributes may make the difference.
|
|
101
|
+
return !!RenderWebGL._getContext(optCanvas || document.createElement('canvas'));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ask TWGL to create a rendering context with the attributes used by this renderer.
|
|
109
|
+
* @param {canvas} canvas - attach the context to this canvas.
|
|
110
|
+
* @returns {WebGLRenderingContext} - a TWGL rendering context (backed by either WebGL 1.0 or 2.0).
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
static _getContext (canvas) {
|
|
114
|
+
const contextAttribs = {alpha: false, stencil: true, antialias: false};
|
|
115
|
+
// getWebGLContext = try WebGL 1.0 only
|
|
116
|
+
// getContext = try WebGL 2.0 and if that doesn't work, try WebGL 1.0
|
|
117
|
+
// getWebGLContext || getContext = try WebGL 1.0 and if that doesn't work, try WebGL 2.0
|
|
118
|
+
return twgl.getWebGLContext(canvas, contextAttribs) ||
|
|
119
|
+
twgl.getContext(canvas, contextAttribs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create a renderer for drawing Scratch sprites to a canvas using WebGL.
|
|
124
|
+
* Coordinates will default to Scratch 2.0 values if unspecified.
|
|
125
|
+
* The stage's "native" size will be calculated from the these coordinates.
|
|
126
|
+
* For example, the defaults result in a native size of 480x360.
|
|
127
|
+
* Queries such as "touching color?" will always execute at the native size.
|
|
128
|
+
* @see RenderWebGL#setStageSize
|
|
129
|
+
* @see RenderWebGL#resize
|
|
130
|
+
* @param {canvas} canvas The canvas to draw onto.
|
|
131
|
+
* @param {int} [xLeft=-240] The x-coordinate of the left edge.
|
|
132
|
+
* @param {int} [xRight=240] The x-coordinate of the right edge.
|
|
133
|
+
* @param {int} [yBottom=-180] The y-coordinate of the bottom edge.
|
|
134
|
+
* @param {int} [yTop=180] The y-coordinate of the top edge.
|
|
135
|
+
* @constructor
|
|
136
|
+
* @listens RenderWebGL#event:NativeSizeChanged
|
|
137
|
+
*/
|
|
138
|
+
constructor (canvas, xLeft, xRight, yBottom, yTop) {
|
|
139
|
+
super();
|
|
140
|
+
|
|
141
|
+
/** @type {WebGLRenderingContext} */
|
|
142
|
+
const gl = this._gl = RenderWebGL._getContext(canvas);
|
|
143
|
+
if (!gl) {
|
|
144
|
+
throw new Error('Could not get WebGL context: this browser or environment may not support WebGL.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @type {RenderWebGL.UseGpuModes} */
|
|
148
|
+
this._useGpuMode = RenderWebGL.UseGpuModes.Automatic;
|
|
149
|
+
|
|
150
|
+
/** @type {Drawable[]} */
|
|
151
|
+
this._allDrawables = [];
|
|
152
|
+
|
|
153
|
+
/** @type {Skin[]} */
|
|
154
|
+
this._allSkins = [];
|
|
155
|
+
|
|
156
|
+
/** @type {Array<int>} */
|
|
157
|
+
this._drawList = [];
|
|
158
|
+
|
|
159
|
+
// A list of layer group names in the order they should appear
|
|
160
|
+
// from furthest back to furthest in front.
|
|
161
|
+
/** @type {Array<String>} */
|
|
162
|
+
this._groupOrdering = [];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @typedef LayerGroup
|
|
166
|
+
* @property {int} groupIndex The relative position of this layer group in the group ordering
|
|
167
|
+
* @property {int} drawListOffset The absolute position of this layer group in the draw list
|
|
168
|
+
* This number gets updated as drawables get added to or deleted from the draw list.
|
|
169
|
+
*/
|
|
170
|
+
|
|
171
|
+
// Map of group name to layer group
|
|
172
|
+
/** @type {Object.<string, LayerGroup>} */
|
|
173
|
+
this._layerGroups = {};
|
|
174
|
+
|
|
175
|
+
/** @type {int} */
|
|
176
|
+
this._nextDrawableId = RenderConstants.ID_NONE + 1;
|
|
177
|
+
|
|
178
|
+
/** @type {int} */
|
|
179
|
+
this._nextSkinId = RenderConstants.ID_NONE + 1;
|
|
180
|
+
|
|
181
|
+
/** @type {module:twgl/m4.Mat4} */
|
|
182
|
+
this._projection = twgl.m4.identity();
|
|
183
|
+
|
|
184
|
+
/** @type {ShaderManager} */
|
|
185
|
+
this._shaderManager = new ShaderManager(gl);
|
|
186
|
+
|
|
187
|
+
/** @type {HTMLCanvasElement} */
|
|
188
|
+
this._tempCanvas = document.createElement('canvas');
|
|
189
|
+
|
|
190
|
+
/** @type {any} */
|
|
191
|
+
this._regionId = null;
|
|
192
|
+
|
|
193
|
+
/** @type {function} */
|
|
194
|
+
this._exitRegion = null;
|
|
195
|
+
|
|
196
|
+
/** @type {object} */
|
|
197
|
+
this._backgroundDrawRegionId = {
|
|
198
|
+
enter: () => this._enterDrawBackground(),
|
|
199
|
+
exit: () => this._exitDrawBackground()
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/** @type {Array.<snapshotCallback>} */
|
|
203
|
+
this._snapshotCallbacks = [];
|
|
204
|
+
|
|
205
|
+
/** @type {Array<number>} */
|
|
206
|
+
// Don't set this directly-- use setBackgroundColor so it stays in sync with _backgroundColor3b
|
|
207
|
+
this._backgroundColor4f = [0, 0, 0, 1];
|
|
208
|
+
|
|
209
|
+
/** @type {Uint8ClampedArray} */
|
|
210
|
+
// Don't set this directly-- use setBackgroundColor so it stays in sync with _backgroundColor4f
|
|
211
|
+
this._backgroundColor3b = new Uint8ClampedArray(3);
|
|
212
|
+
|
|
213
|
+
this._createGeometry();
|
|
214
|
+
|
|
215
|
+
this.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
|
|
216
|
+
|
|
217
|
+
this.setBackgroundColor(1, 1, 1);
|
|
218
|
+
this.setStageSize(xLeft || -240, xRight || 240, yBottom || -180, yTop || 180);
|
|
219
|
+
this.resize(this._nativeSize[0], this._nativeSize[1]);
|
|
220
|
+
|
|
221
|
+
gl.disable(gl.DEPTH_TEST);
|
|
222
|
+
/** @todo disable when no partial transparency? */
|
|
223
|
+
gl.enable(gl.BLEND);
|
|
224
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @returns {WebGLRenderingContext} the WebGL rendering context associated with this renderer.
|
|
229
|
+
*/
|
|
230
|
+
get gl () {
|
|
231
|
+
return this._gl;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @returns {HTMLCanvasElement} the canvas of the WebGL rendering context associated with this renderer.
|
|
236
|
+
*/
|
|
237
|
+
get canvas () {
|
|
238
|
+
return this._gl && this._gl.canvas;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Set the physical size of the stage in device-independent pixels.
|
|
243
|
+
* This will be multiplied by the device's pixel ratio on high-DPI displays.
|
|
244
|
+
* @param {int} pixelsWide The desired width in device-independent pixels.
|
|
245
|
+
* @param {int} pixelsTall The desired height in device-independent pixels.
|
|
246
|
+
*/
|
|
247
|
+
resize (pixelsWide, pixelsTall) {
|
|
248
|
+
const {canvas} = this._gl;
|
|
249
|
+
const pixelRatio = window.devicePixelRatio || 1;
|
|
250
|
+
const newWidth = pixelsWide * pixelRatio;
|
|
251
|
+
const newHeight = pixelsTall * pixelRatio;
|
|
252
|
+
|
|
253
|
+
// Certain operations, such as moving the color picker, call `resize` once per frame, even though the canvas
|
|
254
|
+
// size doesn't change. To avoid unnecessary canvas updates, check that we *really* need to resize the canvas.
|
|
255
|
+
if (canvas.width !== newWidth || canvas.height !== newHeight) {
|
|
256
|
+
canvas.width = newWidth;
|
|
257
|
+
canvas.height = newHeight;
|
|
258
|
+
// Resizing the canvas causes it to be cleared, so redraw it.
|
|
259
|
+
this.draw();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Set the background color for the stage. The stage will be cleared with this
|
|
266
|
+
* color each frame.
|
|
267
|
+
* @param {number} red The red component for the background.
|
|
268
|
+
* @param {number} green The green component for the background.
|
|
269
|
+
* @param {number} blue The blue component for the background.
|
|
270
|
+
*/
|
|
271
|
+
setBackgroundColor (red, green, blue) {
|
|
272
|
+
this._backgroundColor4f[0] = red;
|
|
273
|
+
this._backgroundColor4f[1] = green;
|
|
274
|
+
this._backgroundColor4f[2] = blue;
|
|
275
|
+
|
|
276
|
+
this._backgroundColor3b[0] = red * 255;
|
|
277
|
+
this._backgroundColor3b[1] = green * 255;
|
|
278
|
+
this._backgroundColor3b[2] = blue * 255;
|
|
279
|
+
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tell the renderer to draw various debug information to the provided canvas
|
|
284
|
+
* during certain operations.
|
|
285
|
+
* @param {canvas} canvas The canvas to use for debug output.
|
|
286
|
+
*/
|
|
287
|
+
setDebugCanvas (canvas) {
|
|
288
|
+
this._debugCanvas = canvas;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Control the use of the GPU or CPU paths in `isTouchingColor`.
|
|
293
|
+
* @param {RenderWebGL.UseGpuModes} useGpuMode - automatically decide, force CPU, or force GPU.
|
|
294
|
+
*/
|
|
295
|
+
setUseGpuMode (useGpuMode) {
|
|
296
|
+
this._useGpuMode = useGpuMode;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Set logical size of the stage in Scratch units.
|
|
301
|
+
* @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240.
|
|
302
|
+
* @param {int} xRight The right edge's x-coordinate. Scratch 2 uses 240.
|
|
303
|
+
* @param {int} yBottom The bottom edge's y-coordinate. Scratch 2 uses -180.
|
|
304
|
+
* @param {int} yTop The top edge's y-coordinate. Scratch 2 uses 180.
|
|
305
|
+
*/
|
|
306
|
+
setStageSize (xLeft, xRight, yBottom, yTop) {
|
|
307
|
+
this._xLeft = xLeft;
|
|
308
|
+
this._xRight = xRight;
|
|
309
|
+
this._yBottom = yBottom;
|
|
310
|
+
this._yTop = yTop;
|
|
311
|
+
|
|
312
|
+
// swap yBottom & yTop to fit Scratch convention of +y=up
|
|
313
|
+
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
|
|
314
|
+
|
|
315
|
+
this._setNativeSize(Math.abs(xRight - xLeft), Math.abs(yBottom - yTop));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @return {Array<int>} the "native" size of the stage, which is used for pen, query renders, etc.
|
|
320
|
+
*/
|
|
321
|
+
getNativeSize () {
|
|
322
|
+
return [this._nativeSize[0], this._nativeSize[1]];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Set the "native" size of the stage, which is used for pen, query renders, etc.
|
|
327
|
+
* @param {int} width - the new width to set.
|
|
328
|
+
* @param {int} height - the new height to set.
|
|
329
|
+
* @private
|
|
330
|
+
* @fires RenderWebGL#event:NativeSizeChanged
|
|
331
|
+
*/
|
|
332
|
+
_setNativeSize (width, height) {
|
|
333
|
+
this._nativeSize = [width, height];
|
|
334
|
+
this.emit(RenderConstants.Events.NativeSizeChanged, {newSize: this._nativeSize});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Create a new bitmap skin from a snapshot of the provided bitmap data.
|
|
339
|
+
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
|
|
340
|
+
* @param {!int} [costumeResolution=1] - The resolution to use for this bitmap.
|
|
341
|
+
* @param {?Array<number>} [rotationCenter] Optional: rotation center of the skin. If not supplied, the center of
|
|
342
|
+
* the skin will be used.
|
|
343
|
+
* @returns {!int} the ID for the new skin.
|
|
344
|
+
*/
|
|
345
|
+
createBitmapSkin (bitmapData, costumeResolution, rotationCenter) {
|
|
346
|
+
const skinId = this._nextSkinId++;
|
|
347
|
+
const newSkin = new BitmapSkin(skinId, this);
|
|
348
|
+
newSkin.setBitmap(bitmapData, costumeResolution, rotationCenter);
|
|
349
|
+
this._allSkins[skinId] = newSkin;
|
|
350
|
+
return skinId;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a new SVG skin.
|
|
355
|
+
* @param {!string} svgData - new SVG to use.
|
|
356
|
+
* @param {?Array<number>} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the
|
|
357
|
+
* skin will be used
|
|
358
|
+
* @returns {!int} the ID for the new skin.
|
|
359
|
+
*/
|
|
360
|
+
createSVGSkin (svgData, rotationCenter) {
|
|
361
|
+
const skinId = this._nextSkinId++;
|
|
362
|
+
const newSkin = new SVGSkin(skinId, this);
|
|
363
|
+
newSkin.setSVG(svgData, rotationCenter);
|
|
364
|
+
this._allSkins[skinId] = newSkin;
|
|
365
|
+
return skinId;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Create a new PenSkin - a skin which implements a Scratch pen layer.
|
|
370
|
+
* @returns {!int} the ID for the new skin.
|
|
371
|
+
*/
|
|
372
|
+
createPenSkin () {
|
|
373
|
+
const skinId = this._nextSkinId++;
|
|
374
|
+
const newSkin = new PenSkin(skinId, this);
|
|
375
|
+
this._allSkins[skinId] = newSkin;
|
|
376
|
+
return skinId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Create a new SVG skin using the text bubble svg creator. The rotation center
|
|
381
|
+
* is always placed at the top left.
|
|
382
|
+
* @param {!string} type - either "say" or "think".
|
|
383
|
+
* @param {!string} text - the text for the bubble.
|
|
384
|
+
* @param {!boolean} pointsLeft - which side the bubble is pointing.
|
|
385
|
+
* @returns {!int} the ID for the new skin.
|
|
386
|
+
*/
|
|
387
|
+
createTextSkin (type, text, pointsLeft) {
|
|
388
|
+
const skinId = this._nextSkinId++;
|
|
389
|
+
const newSkin = new TextBubbleSkin(skinId, this);
|
|
390
|
+
newSkin.setTextBubble(type, text, pointsLeft);
|
|
391
|
+
this._allSkins[skinId] = newSkin;
|
|
392
|
+
return skinId;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Update an existing SVG skin, or create an SVG skin if the previous skin was not SVG.
|
|
397
|
+
* @param {!int} skinId the ID for the skin to change.
|
|
398
|
+
* @param {!string} svgData - new SVG to use.
|
|
399
|
+
* @param {?Array<number>} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the
|
|
400
|
+
* skin will be used
|
|
401
|
+
*/
|
|
402
|
+
updateSVGSkin (skinId, svgData, rotationCenter) {
|
|
403
|
+
if (this._allSkins[skinId] instanceof SVGSkin) {
|
|
404
|
+
this._allSkins[skinId].setSVG(svgData, rotationCenter);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const newSkin = new SVGSkin(skinId, this);
|
|
409
|
+
newSkin.setSVG(svgData, rotationCenter);
|
|
410
|
+
this._reskin(skinId, newSkin);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Update an existing bitmap skin, or create a bitmap skin if the previous skin was not bitmap.
|
|
415
|
+
* @param {!int} skinId the ID for the skin to change.
|
|
416
|
+
* @param {!ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imgData - new contents for this skin.
|
|
417
|
+
* @param {!number} bitmapResolution - the resolution scale for a bitmap costume.
|
|
418
|
+
* @param {?Array<number>} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the
|
|
419
|
+
* skin will be used
|
|
420
|
+
*/
|
|
421
|
+
updateBitmapSkin (skinId, imgData, bitmapResolution, rotationCenter) {
|
|
422
|
+
if (this._allSkins[skinId] instanceof BitmapSkin) {
|
|
423
|
+
this._allSkins[skinId].setBitmap(imgData, bitmapResolution, rotationCenter);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const newSkin = new BitmapSkin(skinId, this);
|
|
428
|
+
newSkin.setBitmap(imgData, bitmapResolution, rotationCenter);
|
|
429
|
+
this._reskin(skinId, newSkin);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_reskin (skinId, newSkin) {
|
|
433
|
+
const oldSkin = this._allSkins[skinId];
|
|
434
|
+
this._allSkins[skinId] = newSkin;
|
|
435
|
+
|
|
436
|
+
// Tell drawables to update
|
|
437
|
+
for (const drawable of this._allDrawables) {
|
|
438
|
+
if (drawable && drawable.skin === oldSkin) {
|
|
439
|
+
drawable.skin = newSkin;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
oldSkin.dispose();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Update a skin using the text bubble svg creator.
|
|
447
|
+
* @param {!int} skinId the ID for the skin to change.
|
|
448
|
+
* @param {!string} type - either "say" or "think".
|
|
449
|
+
* @param {!string} text - the text for the bubble.
|
|
450
|
+
* @param {!boolean} pointsLeft - which side the bubble is pointing.
|
|
451
|
+
*/
|
|
452
|
+
updateTextSkin (skinId, type, text, pointsLeft) {
|
|
453
|
+
if (this._allSkins[skinId] instanceof TextBubbleSkin) {
|
|
454
|
+
this._allSkins[skinId].setTextBubble(type, text, pointsLeft);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const newSkin = new TextBubbleSkin(skinId, this);
|
|
459
|
+
newSkin.setTextBubble(type, text, pointsLeft);
|
|
460
|
+
this._reskin(skinId, newSkin);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Destroy an existing skin. Do not use the skin or its ID after calling this.
|
|
466
|
+
* @param {!int} skinId - The ID of the skin to destroy.
|
|
467
|
+
*/
|
|
468
|
+
destroySkin (skinId) {
|
|
469
|
+
const oldSkin = this._allSkins[skinId];
|
|
470
|
+
oldSkin.dispose();
|
|
471
|
+
delete this._allSkins[skinId];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create a new Drawable and add it to the scene.
|
|
476
|
+
* @param {string} group Layer group to add the drawable to
|
|
477
|
+
* @returns {int} The ID of the new Drawable.
|
|
478
|
+
*/
|
|
479
|
+
createDrawable (group) {
|
|
480
|
+
if (!group || !Object.prototype.hasOwnProperty.call(this._layerGroups, group)) {
|
|
481
|
+
log.warn('Cannot create a drawable without a known layer group');
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const drawableID = this._nextDrawableId++;
|
|
485
|
+
const drawable = new Drawable(drawableID);
|
|
486
|
+
this._allDrawables[drawableID] = drawable;
|
|
487
|
+
this._addToDrawList(drawableID, group);
|
|
488
|
+
|
|
489
|
+
drawable.skin = null;
|
|
490
|
+
|
|
491
|
+
return drawableID;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Set the layer group ordering for the renderer.
|
|
496
|
+
* @param {Array<string>} groupOrdering The ordered array of layer group
|
|
497
|
+
* names
|
|
498
|
+
*/
|
|
499
|
+
setLayerGroupOrdering (groupOrdering) {
|
|
500
|
+
this._groupOrdering = groupOrdering;
|
|
501
|
+
for (let i = 0; i < this._groupOrdering.length; i++) {
|
|
502
|
+
this._layerGroups[this._groupOrdering[i]] = {
|
|
503
|
+
groupIndex: i,
|
|
504
|
+
drawListOffset: 0
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_addToDrawList (drawableID, group) {
|
|
510
|
+
const currentLayerGroup = this._layerGroups[group];
|
|
511
|
+
const currentGroupOrderingIndex = currentLayerGroup.groupIndex;
|
|
512
|
+
|
|
513
|
+
const drawListOffset = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
|
514
|
+
this._drawList.splice(drawListOffset, 0, drawableID);
|
|
515
|
+
|
|
516
|
+
this._updateOffsets('add', currentGroupOrderingIndex);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_updateOffsets (updateType, currentGroupOrderingIndex) {
|
|
520
|
+
for (let i = currentGroupOrderingIndex + 1; i < this._groupOrdering.length; i++) {
|
|
521
|
+
const laterGroupName = this._groupOrdering[i];
|
|
522
|
+
if (updateType === 'add') {
|
|
523
|
+
this._layerGroups[laterGroupName].drawListOffset++;
|
|
524
|
+
} else if (updateType === 'delete'){
|
|
525
|
+
this._layerGroups[laterGroupName].drawListOffset--;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
get _visibleDrawList () {
|
|
531
|
+
return this._drawList.filter(id => this._allDrawables[id]._visible);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Given a layer group, return the index where it ends (non-inclusive),
|
|
535
|
+
// e.g. the returned index does not have a drawable from this layer group in it)
|
|
536
|
+
_endIndexForKnownLayerGroup (layerGroup) {
|
|
537
|
+
const groupIndex = layerGroup.groupIndex;
|
|
538
|
+
if (groupIndex === this._groupOrdering.length - 1) {
|
|
539
|
+
return this._drawList.length;
|
|
540
|
+
}
|
|
541
|
+
return this._layerGroups[this._groupOrdering[groupIndex + 1]].drawListOffset;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Destroy a Drawable, removing it from the scene.
|
|
546
|
+
* @param {int} drawableID The ID of the Drawable to remove.
|
|
547
|
+
* @param {string} group Group name that the drawable belongs to
|
|
548
|
+
*/
|
|
549
|
+
destroyDrawable (drawableID, group) {
|
|
550
|
+
if (!group || !Object.prototype.hasOwnProperty.call(this._layerGroups, group)) {
|
|
551
|
+
log.warn('Cannot destroy drawable without known layer group.');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const drawable = this._allDrawables[drawableID];
|
|
555
|
+
drawable.dispose();
|
|
556
|
+
delete this._allDrawables[drawableID];
|
|
557
|
+
|
|
558
|
+
const currentLayerGroup = this._layerGroups[group];
|
|
559
|
+
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
|
560
|
+
|
|
561
|
+
let index = currentLayerGroup.drawListOffset;
|
|
562
|
+
while (index < endIndex) {
|
|
563
|
+
if (this._drawList[index] === drawableID) {
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
index++;
|
|
567
|
+
}
|
|
568
|
+
if (index < endIndex) {
|
|
569
|
+
this._drawList.splice(index, 1);
|
|
570
|
+
this._updateOffsets('delete', currentLayerGroup.groupIndex);
|
|
571
|
+
} else {
|
|
572
|
+
log.warn('Could not destroy drawable that could not be found in layer group.');
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Returns the position of the given drawableID in the draw list. This is
|
|
579
|
+
* the absolute position irrespective of layer group.
|
|
580
|
+
* @param {number} drawableID The drawable ID to find.
|
|
581
|
+
* @return {number} The postion of the given drawable ID.
|
|
582
|
+
*/
|
|
583
|
+
getDrawableOrder (drawableID) {
|
|
584
|
+
return this._drawList.indexOf(drawableID);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Set a drawable's order in the drawable list (effectively, z/layer).
|
|
589
|
+
* Can be used to move drawables to absolute positions in the list,
|
|
590
|
+
* or relative to their current positions.
|
|
591
|
+
* "go back N layers": setDrawableOrder(id, -N, true, 1); (assuming stage at 0).
|
|
592
|
+
* "go to back": setDrawableOrder(id, 1); (assuming stage at 0).
|
|
593
|
+
* "go to front": setDrawableOrder(id, Infinity);
|
|
594
|
+
* @param {int} drawableID ID of Drawable to reorder.
|
|
595
|
+
* @param {number} order New absolute order or relative order adjusment.
|
|
596
|
+
* @param {string=} group Name of layer group drawable belongs to.
|
|
597
|
+
* Reordering will not take place if drawable cannot be found within the bounds
|
|
598
|
+
* of the layer group.
|
|
599
|
+
* @param {boolean=} optIsRelative If set, `order` refers to a relative change.
|
|
600
|
+
* @param {number=} optMin If set, order constrained to be at least `optMin`.
|
|
601
|
+
* @return {?number} New order if changed, or null.
|
|
602
|
+
*/
|
|
603
|
+
setDrawableOrder (drawableID, order, group, optIsRelative, optMin) {
|
|
604
|
+
if (!group || !Object.prototype.hasOwnProperty.call(this._layerGroups, group)) {
|
|
605
|
+
log.warn('Cannot set the order of a drawable without a known layer group.');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const currentLayerGroup = this._layerGroups[group];
|
|
610
|
+
const startIndex = currentLayerGroup.drawListOffset;
|
|
611
|
+
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
|
612
|
+
|
|
613
|
+
let oldIndex = startIndex;
|
|
614
|
+
while (oldIndex < endIndex) {
|
|
615
|
+
if (this._drawList[oldIndex] === drawableID) {
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
oldIndex++;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (oldIndex < endIndex) {
|
|
622
|
+
// Remove drawable from the list.
|
|
623
|
+
if (order === 0) {
|
|
624
|
+
return oldIndex;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const _ = this._drawList.splice(oldIndex, 1)[0];
|
|
628
|
+
// Determine new index.
|
|
629
|
+
let newIndex = order;
|
|
630
|
+
if (optIsRelative) {
|
|
631
|
+
newIndex += oldIndex;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const possibleMin = (optMin || 0) + startIndex;
|
|
635
|
+
const min = (possibleMin >= startIndex && possibleMin < endIndex) ? possibleMin : startIndex;
|
|
636
|
+
newIndex = Math.max(newIndex, min);
|
|
637
|
+
|
|
638
|
+
newIndex = Math.min(newIndex, endIndex);
|
|
639
|
+
|
|
640
|
+
// Insert at new index.
|
|
641
|
+
this._drawList.splice(newIndex, 0, drawableID);
|
|
642
|
+
return newIndex;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Draw all current drawables and present the frame on the canvas.
|
|
650
|
+
*/
|
|
651
|
+
draw () {
|
|
652
|
+
this._doExitDrawRegion();
|
|
653
|
+
|
|
654
|
+
const gl = this._gl;
|
|
655
|
+
|
|
656
|
+
twgl.bindFramebufferInfo(gl, null);
|
|
657
|
+
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
|
658
|
+
gl.clearColor(...this._backgroundColor4f);
|
|
659
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
660
|
+
|
|
661
|
+
this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, this._projection, {
|
|
662
|
+
framebufferWidth: gl.canvas.width,
|
|
663
|
+
framebufferHeight: gl.canvas.height
|
|
664
|
+
});
|
|
665
|
+
if (this._snapshotCallbacks.length > 0) {
|
|
666
|
+
const snapshot = gl.canvas.toDataURL();
|
|
667
|
+
this._snapshotCallbacks.forEach(cb => cb(snapshot));
|
|
668
|
+
this._snapshotCallbacks = [];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Get the precise bounds for a Drawable.
|
|
674
|
+
* @param {int} drawableID ID of Drawable to get bounds for.
|
|
675
|
+
* @return {object} Bounds for a tight box around the Drawable.
|
|
676
|
+
*/
|
|
677
|
+
getBounds (drawableID) {
|
|
678
|
+
const drawable = this._allDrawables[drawableID];
|
|
679
|
+
// Tell the Drawable about its updated convex hull, if necessary.
|
|
680
|
+
if (drawable.needsConvexHullPoints()) {
|
|
681
|
+
const points = this._getConvexHullPointsForDrawable(drawableID);
|
|
682
|
+
drawable.setConvexHullPoints(points);
|
|
683
|
+
}
|
|
684
|
+
const bounds = drawable.getFastBounds();
|
|
685
|
+
// In debug mode, draw the bounds.
|
|
686
|
+
if (this._debugCanvas) {
|
|
687
|
+
const gl = this._gl;
|
|
688
|
+
this._debugCanvas.width = gl.canvas.width;
|
|
689
|
+
this._debugCanvas.height = gl.canvas.height;
|
|
690
|
+
const context = this._debugCanvas.getContext('2d');
|
|
691
|
+
context.drawImage(gl.canvas, 0, 0);
|
|
692
|
+
context.strokeStyle = '#FF0000';
|
|
693
|
+
const pr = window.devicePixelRatio;
|
|
694
|
+
context.strokeRect(
|
|
695
|
+
pr * (bounds.left + (this._nativeSize[0] / 2)),
|
|
696
|
+
pr * (-bounds.top + (this._nativeSize[1] / 2)),
|
|
697
|
+
pr * (bounds.right - bounds.left),
|
|
698
|
+
pr * (-bounds.bottom + bounds.top)
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return bounds;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Get the precise bounds for a Drawable around the top slice.
|
|
706
|
+
* Used for positioning speech bubbles more closely to the sprite.
|
|
707
|
+
* @param {int} drawableID ID of Drawable to get bubble bounds for.
|
|
708
|
+
* @return {object} Bounds for a tight box around the Drawable top slice.
|
|
709
|
+
*/
|
|
710
|
+
getBoundsForBubble (drawableID) {
|
|
711
|
+
const drawable = this._allDrawables[drawableID];
|
|
712
|
+
// Tell the Drawable about its updated convex hull, if necessary.
|
|
713
|
+
if (drawable.needsConvexHullPoints()) {
|
|
714
|
+
const points = this._getConvexHullPointsForDrawable(drawableID);
|
|
715
|
+
drawable.setConvexHullPoints(points);
|
|
716
|
+
}
|
|
717
|
+
const bounds = drawable.getBoundsForBubble();
|
|
718
|
+
// In debug mode, draw the bounds.
|
|
719
|
+
if (this._debugCanvas) {
|
|
720
|
+
const gl = this._gl;
|
|
721
|
+
this._debugCanvas.width = gl.canvas.width;
|
|
722
|
+
this._debugCanvas.height = gl.canvas.height;
|
|
723
|
+
const context = this._debugCanvas.getContext('2d');
|
|
724
|
+
context.drawImage(gl.canvas, 0, 0);
|
|
725
|
+
context.strokeStyle = '#FF0000';
|
|
726
|
+
const pr = window.devicePixelRatio;
|
|
727
|
+
context.strokeRect(
|
|
728
|
+
pr * (bounds.left + (this._nativeSize[0] / 2)),
|
|
729
|
+
pr * (-bounds.top + (this._nativeSize[1] / 2)),
|
|
730
|
+
pr * (bounds.right - bounds.left),
|
|
731
|
+
pr * (-bounds.bottom + bounds.top)
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return bounds;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Get the current skin (costume) size of a Drawable.
|
|
739
|
+
* @param {int} drawableID The ID of the Drawable to measure.
|
|
740
|
+
* @return {Array<number>} Skin size, width and height.
|
|
741
|
+
*/
|
|
742
|
+
getCurrentSkinSize (drawableID) {
|
|
743
|
+
const drawable = this._allDrawables[drawableID];
|
|
744
|
+
return this.getSkinSize(drawable.skin.id);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Get the size of a skin by ID.
|
|
749
|
+
* @param {int} skinID The ID of the Skin to measure.
|
|
750
|
+
* @return {Array<number>} Skin size, width and height.
|
|
751
|
+
*/
|
|
752
|
+
getSkinSize (skinID) {
|
|
753
|
+
const skin = this._allSkins[skinID];
|
|
754
|
+
return skin.size;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Get the rotation center of a skin by ID.
|
|
759
|
+
* @param {int} skinID The ID of the Skin
|
|
760
|
+
* @return {Array<number>} The rotationCenterX and rotationCenterY
|
|
761
|
+
*/
|
|
762
|
+
getSkinRotationCenter (skinID) {
|
|
763
|
+
const skin = this._allSkins[skinID];
|
|
764
|
+
return skin.calculateRotationCenter();
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Check if a particular Drawable is touching a particular color.
|
|
769
|
+
* Unlike touching drawable, if the "tester" is invisble, we will still test.
|
|
770
|
+
* @param {int} drawableID The ID of the Drawable to check.
|
|
771
|
+
* @param {Array<int>} color3b Test if the Drawable is touching this color.
|
|
772
|
+
* @param {Array<int>} [mask3b] Optionally mask the check to this part of Drawable.
|
|
773
|
+
* @returns {boolean} True iff the Drawable is touching the color.
|
|
774
|
+
*/
|
|
775
|
+
isTouchingColor (drawableID, color3b, mask3b) {
|
|
776
|
+
const candidates = this._candidatesTouching(drawableID, this._visibleDrawList);
|
|
777
|
+
|
|
778
|
+
let bounds;
|
|
779
|
+
if (colorMatches(color3b, this._backgroundColor3b, 0)) {
|
|
780
|
+
// If the color we're checking for is the background color, don't confine the check to
|
|
781
|
+
// candidate drawables' bounds--since the background spans the entire stage, we must check
|
|
782
|
+
// everything that lies inside the drawable.
|
|
783
|
+
bounds = this._touchingBounds(drawableID);
|
|
784
|
+
// e.g. empty costume, or off the stage
|
|
785
|
+
if (bounds === null) return false;
|
|
786
|
+
} else if (candidates.length === 0) {
|
|
787
|
+
// If not checking for the background color, we can return early if there are no candidate drawables.
|
|
788
|
+
return false;
|
|
789
|
+
} else {
|
|
790
|
+
bounds = this._candidatesBounds(candidates);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const maxPixelsForCPU = this._getMaxPixelsForCPU();
|
|
794
|
+
|
|
795
|
+
const debugCanvasContext = this._debugCanvas && this._debugCanvas.getContext('2d');
|
|
796
|
+
if (debugCanvasContext) {
|
|
797
|
+
this._debugCanvas.width = bounds.width;
|
|
798
|
+
this._debugCanvas.height = bounds.height;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// if there are just too many pixels to CPU render efficiently, we need to let readPixels happen
|
|
802
|
+
if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) {
|
|
803
|
+
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const drawable = this._allDrawables[drawableID];
|
|
807
|
+
const point = __isTouchingDrawablesPoint;
|
|
808
|
+
const color = __touchingColor;
|
|
809
|
+
const hasMask = Boolean(mask3b);
|
|
810
|
+
|
|
811
|
+
drawable.updateCPURenderAttributes();
|
|
812
|
+
|
|
813
|
+
// Masked drawable ignores ghost effect
|
|
814
|
+
const effectMask = ~ShaderManager.EFFECT_INFO.ghost.mask;
|
|
815
|
+
|
|
816
|
+
// Scratch Space - +y is top
|
|
817
|
+
for (let y = bounds.bottom; y <= bounds.top; y++) {
|
|
818
|
+
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
|
|
819
|
+
return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom);
|
|
820
|
+
}
|
|
821
|
+
for (let x = bounds.left; x <= bounds.right; x++) {
|
|
822
|
+
point[1] = y;
|
|
823
|
+
point[0] = x;
|
|
824
|
+
// if we use a mask, check our sample color...
|
|
825
|
+
if (hasMask ?
|
|
826
|
+
maskMatches(Drawable.sampleColor4b(point, drawable, color, effectMask), mask3b) :
|
|
827
|
+
drawable.isTouching(point)) {
|
|
828
|
+
RenderWebGL.sampleColor3b(point, candidates, color);
|
|
829
|
+
if (debugCanvasContext) {
|
|
830
|
+
debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
|
|
831
|
+
debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1);
|
|
832
|
+
}
|
|
833
|
+
// ...and the target color is drawn at this pixel
|
|
834
|
+
if (colorMatches(color, color3b, 0)) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
_getMaxPixelsForCPU () {
|
|
844
|
+
switch (this._useGpuMode) {
|
|
845
|
+
case RenderWebGL.UseGpuModes.ForceCPU:
|
|
846
|
+
return Infinity;
|
|
847
|
+
case RenderWebGL.UseGpuModes.ForceGPU:
|
|
848
|
+
return 0;
|
|
849
|
+
case RenderWebGL.UseGpuModes.Automatic:
|
|
850
|
+
default:
|
|
851
|
+
return __cpuTouchingColorPixelCount;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
_enterDrawBackground () {
|
|
856
|
+
const gl = this.gl;
|
|
857
|
+
const currentShader = this._shaderManager.getShader(ShaderManager.DRAW_MODE.background, 0);
|
|
858
|
+
gl.disable(gl.BLEND);
|
|
859
|
+
gl.useProgram(currentShader.program);
|
|
860
|
+
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
_exitDrawBackground () {
|
|
864
|
+
const gl = this.gl;
|
|
865
|
+
gl.enable(gl.BLEND);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
_isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) {
|
|
869
|
+
this._doExitDrawRegion();
|
|
870
|
+
|
|
871
|
+
const gl = this._gl;
|
|
872
|
+
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
|
873
|
+
|
|
874
|
+
// Limit size of viewport to the bounds around the target Drawable,
|
|
875
|
+
// and create the projection matrix for the draw.
|
|
876
|
+
gl.viewport(0, 0, bounds.width, bounds.height);
|
|
877
|
+
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
|
|
878
|
+
|
|
879
|
+
// Clear the query buffer to fully transparent. This will be the color of pixels that fail the stencil test.
|
|
880
|
+
gl.clearColor(0, 0, 0, 0);
|
|
881
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
|
882
|
+
|
|
883
|
+
let extraUniforms;
|
|
884
|
+
if (mask3b) {
|
|
885
|
+
extraUniforms = {
|
|
886
|
+
u_colorMask: [mask3b[0] / 255, mask3b[1] / 255, mask3b[2] / 255],
|
|
887
|
+
u_colorMaskTolerance: MASK_TOUCHING_COLOR_TOLERANCE / 255
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
// Using the stencil buffer, mask out the drawing to either the drawable's alpha channel
|
|
893
|
+
// or pixels of the drawable which match the mask color, depending on whether a mask color is given.
|
|
894
|
+
// Masked-out pixels will not be checked.
|
|
895
|
+
gl.enable(gl.STENCIL_TEST);
|
|
896
|
+
gl.stencilFunc(gl.ALWAYS, 1, 1);
|
|
897
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
|
|
898
|
+
gl.colorMask(false, false, false, false);
|
|
899
|
+
this._drawThese(
|
|
900
|
+
[drawableID],
|
|
901
|
+
mask3b ?
|
|
902
|
+
ShaderManager.DRAW_MODE.colorMask :
|
|
903
|
+
ShaderManager.DRAW_MODE.silhouette,
|
|
904
|
+
projection,
|
|
905
|
+
{
|
|
906
|
+
extraUniforms,
|
|
907
|
+
ignoreVisibility: true, // Touching color ignores sprite visibility,
|
|
908
|
+
effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
gl.stencilFunc(gl.EQUAL, 1, 1);
|
|
912
|
+
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
|
|
913
|
+
gl.colorMask(true, true, true, true);
|
|
914
|
+
|
|
915
|
+
// Draw the background as a quad. Drawing a background with gl.clear will not mask to the stenciled area.
|
|
916
|
+
this.enterDrawRegion(this._backgroundDrawRegionId);
|
|
917
|
+
|
|
918
|
+
const uniforms = {
|
|
919
|
+
u_backgroundColor: this._backgroundColor4f
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const currentShader = this._shaderManager.getShader(ShaderManager.DRAW_MODE.background, 0);
|
|
923
|
+
twgl.setUniforms(currentShader, uniforms);
|
|
924
|
+
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
|
|
925
|
+
|
|
926
|
+
// Draw the candidate drawables on top of the background.
|
|
927
|
+
this._drawThese(candidateIDs, ShaderManager.DRAW_MODE.default, projection,
|
|
928
|
+
{idFilterFunc: testID => testID !== drawableID}
|
|
929
|
+
);
|
|
930
|
+
} finally {
|
|
931
|
+
gl.colorMask(true, true, true, true);
|
|
932
|
+
gl.disable(gl.STENCIL_TEST);
|
|
933
|
+
this._doExitDrawRegion();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
_isTouchingColorGpuFin (bounds, color3b, stop) {
|
|
938
|
+
const gl = this._gl;
|
|
939
|
+
const pixels = new Uint8Array(Math.floor(bounds.width * (bounds.height - stop) * 4));
|
|
940
|
+
gl.readPixels(0, 0, bounds.width, (bounds.height - stop), gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
941
|
+
|
|
942
|
+
if (this._debugCanvas) {
|
|
943
|
+
this._debugCanvas.width = bounds.width;
|
|
944
|
+
this._debugCanvas.height = bounds.height;
|
|
945
|
+
const context = this._debugCanvas.getContext('2d');
|
|
946
|
+
const imageData = context.getImageData(0, 0, bounds.width, bounds.height - stop);
|
|
947
|
+
imageData.data.set(pixels);
|
|
948
|
+
context.putImageData(imageData, 0, 0);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) {
|
|
952
|
+
// Transparent pixels are masked (either by the drawable's alpha channel or color mask).
|
|
953
|
+
if (pixels[pixelBase + 3] !== 0 && colorMatches(color3b, pixels, pixelBase)) {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Check if a particular Drawable is touching any in a set of Drawables.
|
|
963
|
+
* @param {int} drawableID The ID of the Drawable to check.
|
|
964
|
+
* @param {?Array<int>} candidateIDs The Drawable IDs to check, otherwise all visible drawables in the renderer
|
|
965
|
+
* @returns {boolean} True if the Drawable is touching one of candidateIDs.
|
|
966
|
+
*/
|
|
967
|
+
isTouchingDrawables (drawableID, candidateIDs = this._drawList) {
|
|
968
|
+
const candidates = this._candidatesTouching(drawableID,
|
|
969
|
+
// even if passed an invisible drawable, we will NEVER touch it!
|
|
970
|
+
candidateIDs.filter(id => this._allDrawables[id]._visible));
|
|
971
|
+
// if we are invisble we don't touch anything.
|
|
972
|
+
if (candidates.length === 0 || !this._allDrawables[drawableID]._visible) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Get the union of all the candidates intersections.
|
|
977
|
+
const bounds = this._candidatesBounds(candidates);
|
|
978
|
+
|
|
979
|
+
const drawable = this._allDrawables[drawableID];
|
|
980
|
+
const point = __isTouchingDrawablesPoint;
|
|
981
|
+
|
|
982
|
+
drawable.updateCPURenderAttributes();
|
|
983
|
+
|
|
984
|
+
// This is an EXTREMELY brute force collision detector, but it is
|
|
985
|
+
// still faster than asking the GPU to give us the pixels.
|
|
986
|
+
for (let x = bounds.left; x <= bounds.right; x++) {
|
|
987
|
+
// Scratch Space - +y is top
|
|
988
|
+
point[0] = x;
|
|
989
|
+
for (let y = bounds.bottom; y <= bounds.top; y++) {
|
|
990
|
+
point[1] = y;
|
|
991
|
+
if (drawable.isTouching(point)) {
|
|
992
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
993
|
+
if (candidates[index].drawable.isTouching(point)) {
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Convert a client based x/y position on the canvas to a Scratch 3 world space
|
|
1006
|
+
* Rectangle. This creates recangles with a radius to cover selecting multiple
|
|
1007
|
+
* scratch pixels with touch / small render areas.
|
|
1008
|
+
*
|
|
1009
|
+
* @param {int} centerX The client x coordinate of the picking location.
|
|
1010
|
+
* @param {int} centerY The client y coordinate of the picking location.
|
|
1011
|
+
* @param {int} [width] The client width of the touch event (optional).
|
|
1012
|
+
* @param {int} [height] The client width of the touch event (optional).
|
|
1013
|
+
* @returns {Rectangle} Scratch world space rectangle, iterate bottom <= top,
|
|
1014
|
+
* left <= right.
|
|
1015
|
+
*/
|
|
1016
|
+
clientSpaceToScratchBounds (centerX, centerY, width = 1, height = 1) {
|
|
1017
|
+
const gl = this._gl;
|
|
1018
|
+
|
|
1019
|
+
const clientToScratchX = this._nativeSize[0] / gl.canvas.clientWidth;
|
|
1020
|
+
const clientToScratchY = this._nativeSize[1] / gl.canvas.clientHeight;
|
|
1021
|
+
|
|
1022
|
+
width *= clientToScratchX;
|
|
1023
|
+
height *= clientToScratchY;
|
|
1024
|
+
|
|
1025
|
+
width = Math.max(1, Math.min(Math.round(width), MAX_TOUCH_SIZE[0]));
|
|
1026
|
+
height = Math.max(1, Math.min(Math.round(height), MAX_TOUCH_SIZE[1]));
|
|
1027
|
+
const x = (centerX * clientToScratchX) - ((width - 1) / 2);
|
|
1028
|
+
// + because scratch y is inverted
|
|
1029
|
+
const y = (centerY * clientToScratchY) + ((height - 1) / 2);
|
|
1030
|
+
|
|
1031
|
+
const xOfs = (width % 2) ? 0 : -0.5;
|
|
1032
|
+
// y is offset +0.5
|
|
1033
|
+
const yOfs = (height % 2) ? 0 : -0.5;
|
|
1034
|
+
|
|
1035
|
+
const bounds = new Rectangle();
|
|
1036
|
+
bounds.initFromBounds(Math.floor(this._xLeft + x + xOfs), Math.floor(this._xLeft + x + xOfs + width - 1),
|
|
1037
|
+
Math.ceil(this._yTop - y + yOfs), Math.ceil(this._yTop - y + yOfs + height - 1));
|
|
1038
|
+
return bounds;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* Determine if the drawable is touching a client based x/y. Helper method for sensing
|
|
1043
|
+
* touching mouse-pointer. Ignores visibility.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {int} drawableID The ID of the drawable to check.
|
|
1046
|
+
* @param {int} centerX The client x coordinate of the picking location.
|
|
1047
|
+
* @param {int} centerY The client y coordinate of the picking location.
|
|
1048
|
+
* @param {int} [touchWidth] The client width of the touch event (optional).
|
|
1049
|
+
* @param {int} [touchHeight] The client height of the touch event (optional).
|
|
1050
|
+
* @returns {boolean} If the drawable has any pixels that would draw in the touch area
|
|
1051
|
+
*/
|
|
1052
|
+
drawableTouching (drawableID, centerX, centerY, touchWidth, touchHeight) {
|
|
1053
|
+
const drawable = this._allDrawables[drawableID];
|
|
1054
|
+
if (!drawable) {
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
const bounds = this.clientSpaceToScratchBounds(centerX, centerY, touchWidth, touchHeight);
|
|
1058
|
+
const worldPos = twgl.v3.create();
|
|
1059
|
+
|
|
1060
|
+
drawable.updateCPURenderAttributes();
|
|
1061
|
+
|
|
1062
|
+
for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) {
|
|
1063
|
+
for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) {
|
|
1064
|
+
if (drawable.isTouching(worldPos)) {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Detect which sprite, if any, is at the given location.
|
|
1074
|
+
* This function will pick all drawables that are visible, unless specific
|
|
1075
|
+
* candidate drawable IDs are provided. Used for determining what is clicked
|
|
1076
|
+
* or dragged. Will not select hidden / ghosted sprites.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {int} centerX The client x coordinate of the picking location.
|
|
1079
|
+
* @param {int} centerY The client y coordinate of the picking location.
|
|
1080
|
+
* @param {int} [touchWidth] The client width of the touch event (optional).
|
|
1081
|
+
* @param {int} [touchHeight] The client height of the touch event (optional).
|
|
1082
|
+
* @param {Array<int>} [candidateIDs] The Drawable IDs to pick from, otherwise all visible drawables.
|
|
1083
|
+
* @returns {int} The ID of the topmost Drawable under the picking location, or
|
|
1084
|
+
* RenderConstants.ID_NONE if there is no Drawable at that location.
|
|
1085
|
+
*/
|
|
1086
|
+
pick (centerX, centerY, touchWidth, touchHeight, candidateIDs) {
|
|
1087
|
+
const bounds = this.clientSpaceToScratchBounds(centerX, centerY, touchWidth, touchHeight);
|
|
1088
|
+
if (bounds.left === -Infinity || bounds.bottom === -Infinity) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
candidateIDs = (candidateIDs || this._drawList).filter(id => {
|
|
1093
|
+
const drawable = this._allDrawables[id];
|
|
1094
|
+
// default pick list ignores visible and ghosted sprites.
|
|
1095
|
+
if (drawable.getVisible() && drawable.getUniforms().u_ghost !== 0) {
|
|
1096
|
+
const drawableBounds = drawable.getFastBounds();
|
|
1097
|
+
const inRange = bounds.intersects(drawableBounds);
|
|
1098
|
+
if (!inRange) return false;
|
|
1099
|
+
|
|
1100
|
+
drawable.updateCPURenderAttributes();
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
return false;
|
|
1104
|
+
});
|
|
1105
|
+
if (candidateIDs.length === 0) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const hits = [];
|
|
1110
|
+
const worldPos = twgl.v3.create(0, 0, 0);
|
|
1111
|
+
// Iterate over the scratch pixels and check if any candidate can be
|
|
1112
|
+
// touched at that point.
|
|
1113
|
+
for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) {
|
|
1114
|
+
for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) {
|
|
1115
|
+
|
|
1116
|
+
// Check candidates in the reverse order they would have been
|
|
1117
|
+
// drawn. This will determine what candiate's silhouette pixel
|
|
1118
|
+
// would have been drawn at the point.
|
|
1119
|
+
for (let d = candidateIDs.length - 1; d >= 0; d--) {
|
|
1120
|
+
const id = candidateIDs[d];
|
|
1121
|
+
const drawable = this._allDrawables[id];
|
|
1122
|
+
if (drawable.isTouching(worldPos)) {
|
|
1123
|
+
hits[id] = (hits[id] || 0) + 1;
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Bias toward selecting anything over nothing
|
|
1131
|
+
hits[RenderConstants.ID_NONE] = 0;
|
|
1132
|
+
|
|
1133
|
+
let hit = RenderConstants.ID_NONE;
|
|
1134
|
+
for (const hitID in hits) {
|
|
1135
|
+
if (Object.prototype.hasOwnProperty.call(hits, hitID) && (hits[hitID] > hits[hit])) {
|
|
1136
|
+
hit = hitID;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return Number(hit);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* @typedef DrawableExtraction
|
|
1145
|
+
* @property {ImageData} data Raw pixel data for the drawable
|
|
1146
|
+
* @property {number} x The x coordinate of the drawable's bounding box's top-left corner, in 'CSS pixels'
|
|
1147
|
+
* @property {number} y The y coordinate of the drawable's bounding box's top-left corner, in 'CSS pixels'
|
|
1148
|
+
* @property {number} width The drawable's bounding box width, in 'CSS pixels'
|
|
1149
|
+
* @property {number} height The drawable's bounding box height, in 'CSS pixels'
|
|
1150
|
+
*/
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Return a drawable's pixel data and bounds in screen space.
|
|
1154
|
+
* @param {int} drawableID The ID of the drawable to get pixel data for
|
|
1155
|
+
* @return {DrawableExtraction} Data about the picked drawable
|
|
1156
|
+
*/
|
|
1157
|
+
extractDrawableScreenSpace (drawableID) {
|
|
1158
|
+
const drawable = this._allDrawables[drawableID];
|
|
1159
|
+
if (!drawable) throw new Error(`Could not extract drawable with ID ${drawableID}; it does not exist`);
|
|
1160
|
+
|
|
1161
|
+
this._doExitDrawRegion();
|
|
1162
|
+
|
|
1163
|
+
const nativeCenterX = this._nativeSize[0] * 0.5;
|
|
1164
|
+
const nativeCenterY = this._nativeSize[1] * 0.5;
|
|
1165
|
+
|
|
1166
|
+
const scratchBounds = drawable.getFastBounds();
|
|
1167
|
+
|
|
1168
|
+
const canvas = this.canvas;
|
|
1169
|
+
// Ratio of the screen-space scale of the stage's canvas to the "native size" of the stage
|
|
1170
|
+
const scaleFactor = canvas.width / this._nativeSize[0];
|
|
1171
|
+
|
|
1172
|
+
// Bounds of the extracted drawable, in "canvas pixel space"
|
|
1173
|
+
// (origin is 0, 0, destination is the canvas width, height).
|
|
1174
|
+
const canvasSpaceBounds = new Rectangle();
|
|
1175
|
+
canvasSpaceBounds.initFromBounds(
|
|
1176
|
+
(scratchBounds.left + nativeCenterX) * scaleFactor,
|
|
1177
|
+
(scratchBounds.right + nativeCenterX) * scaleFactor,
|
|
1178
|
+
// in "canvas space", +y is down, but Rectangle methods assume bottom < top, so swap them
|
|
1179
|
+
(nativeCenterY - scratchBounds.top) * scaleFactor,
|
|
1180
|
+
(nativeCenterY - scratchBounds.bottom) * scaleFactor
|
|
1181
|
+
);
|
|
1182
|
+
canvasSpaceBounds.snapToInt();
|
|
1183
|
+
|
|
1184
|
+
// undo the transformation to transform the bounds, snapped to "canvas-pixel space", back to "Scratch space"
|
|
1185
|
+
// We have to transform -> snap -> invert transform so that the "Scratch-space" bounds are snapped in
|
|
1186
|
+
// "canvas-pixel space".
|
|
1187
|
+
scratchBounds.initFromBounds(
|
|
1188
|
+
(canvasSpaceBounds.left / scaleFactor) - nativeCenterX,
|
|
1189
|
+
(canvasSpaceBounds.right / scaleFactor) - nativeCenterX,
|
|
1190
|
+
nativeCenterY - (canvasSpaceBounds.top / scaleFactor),
|
|
1191
|
+
nativeCenterY - (canvasSpaceBounds.bottom / scaleFactor)
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
const gl = this._gl;
|
|
1195
|
+
|
|
1196
|
+
// Set a reasonable max limit width and height for the bufferInfo bounds
|
|
1197
|
+
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
1198
|
+
const clampedWidth = Math.min(MAX_EXTRACTED_DRAWABLE_DIMENSION, canvasSpaceBounds.width, maxTextureSize);
|
|
1199
|
+
const clampedHeight = Math.min(MAX_EXTRACTED_DRAWABLE_DIMENSION, canvasSpaceBounds.height, maxTextureSize);
|
|
1200
|
+
|
|
1201
|
+
// Make a new bufferInfo since this._queryBufferInfo is limited to 480x360
|
|
1202
|
+
const bufferInfo = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], clampedWidth, clampedHeight);
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
twgl.bindFramebufferInfo(gl, bufferInfo);
|
|
1206
|
+
|
|
1207
|
+
// Limit size of viewport to the bounds around the target Drawable,
|
|
1208
|
+
// and create the projection matrix for the draw.
|
|
1209
|
+
gl.viewport(0, 0, clampedWidth, clampedHeight);
|
|
1210
|
+
const projection = twgl.m4.ortho(
|
|
1211
|
+
scratchBounds.left,
|
|
1212
|
+
scratchBounds.right,
|
|
1213
|
+
scratchBounds.top,
|
|
1214
|
+
scratchBounds.bottom,
|
|
1215
|
+
-1, 1
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
gl.clearColor(0, 0, 0, 0);
|
|
1219
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1220
|
+
this._drawThese([drawableID], ShaderManager.DRAW_MODE.straightAlpha, projection,
|
|
1221
|
+
{
|
|
1222
|
+
// Don't apply the ghost effect. TODO: is this an intentional design decision?
|
|
1223
|
+
effectMask: ~ShaderManager.EFFECT_INFO.ghost.mask,
|
|
1224
|
+
// We're doing this in screen-space, so the framebuffer dimensions should be those of the canvas in
|
|
1225
|
+
// screen-space. This is used to ensure SVG skins are rendered at the proper resolution.
|
|
1226
|
+
framebufferWidth: canvas.width,
|
|
1227
|
+
framebufferHeight: canvas.height
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
const data = new Uint8Array(Math.floor(clampedWidth * clampedHeight * 4));
|
|
1231
|
+
gl.readPixels(0, 0, clampedWidth, clampedHeight, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
1232
|
+
// readPixels can only read into a Uint8Array, but ImageData has to take a Uint8ClampedArray.
|
|
1233
|
+
// We can share the same underlying buffer between them to avoid having to copy any data.
|
|
1234
|
+
const imageData = new ImageData(new Uint8ClampedArray(data.buffer), clampedWidth, clampedHeight);
|
|
1235
|
+
|
|
1236
|
+
// On high-DPI devices, the canvas' width (in canvas pixels) will be larger than its width in CSS pixels.
|
|
1237
|
+
// We want to return the CSS-space bounds,
|
|
1238
|
+
// so take into account the ratio between the canvas' pixel dimensions and its layout dimensions.
|
|
1239
|
+
// This is usually the same as 1 / window.devicePixelRatio, but if e.g. you zoom your browser window without
|
|
1240
|
+
// the canvas resizing, then it'll differ.
|
|
1241
|
+
const ratio = canvas.getBoundingClientRect().width / canvas.width;
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
imageData,
|
|
1245
|
+
x: canvasSpaceBounds.left * ratio,
|
|
1246
|
+
y: canvasSpaceBounds.bottom * ratio,
|
|
1247
|
+
width: canvasSpaceBounds.width * ratio,
|
|
1248
|
+
height: canvasSpaceBounds.height * ratio
|
|
1249
|
+
};
|
|
1250
|
+
} finally {
|
|
1251
|
+
gl.deleteFramebuffer(bufferInfo.framebuffer);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* @typedef ColorExtraction
|
|
1257
|
+
* @property {Uint8Array} data Raw pixel data for the drawable
|
|
1258
|
+
* @property {int} width Drawable bounding box width
|
|
1259
|
+
* @property {int} height Drawable bounding box height
|
|
1260
|
+
* @property {object} color Color object with RGBA properties at picked location
|
|
1261
|
+
*/
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Return drawable pixel data and color at a given position
|
|
1265
|
+
* @param {int} x The client x coordinate of the picking location.
|
|
1266
|
+
* @param {int} y The client y coordinate of the picking location.
|
|
1267
|
+
* @param {int} radius The client radius to extract pixels with.
|
|
1268
|
+
* @return {?ColorExtraction} Data about the picked color
|
|
1269
|
+
*/
|
|
1270
|
+
extractColor (x, y, radius) {
|
|
1271
|
+
this._doExitDrawRegion();
|
|
1272
|
+
|
|
1273
|
+
const scratchX = Math.round(this._nativeSize[0] * ((x / this._gl.canvas.clientWidth) - 0.5));
|
|
1274
|
+
const scratchY = Math.round(-this._nativeSize[1] * ((y / this._gl.canvas.clientHeight) - 0.5));
|
|
1275
|
+
|
|
1276
|
+
const gl = this._gl;
|
|
1277
|
+
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
|
1278
|
+
|
|
1279
|
+
const bounds = new Rectangle();
|
|
1280
|
+
bounds.initFromBounds(scratchX - radius, scratchX + radius, scratchY - radius, scratchY + radius);
|
|
1281
|
+
|
|
1282
|
+
const pickX = scratchX - bounds.left;
|
|
1283
|
+
const pickY = bounds.top - scratchY;
|
|
1284
|
+
|
|
1285
|
+
gl.viewport(0, 0, bounds.width, bounds.height);
|
|
1286
|
+
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
|
|
1287
|
+
|
|
1288
|
+
gl.clearColor(...this._backgroundColor4f);
|
|
1289
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
1290
|
+
this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, projection);
|
|
1291
|
+
|
|
1292
|
+
const data = new Uint8Array(Math.floor(bounds.width * bounds.height * 4));
|
|
1293
|
+
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
|
|
1294
|
+
|
|
1295
|
+
const pixelBase = Math.floor(4 * ((pickY * bounds.width) + pickX));
|
|
1296
|
+
const color = {
|
|
1297
|
+
r: data[pixelBase],
|
|
1298
|
+
g: data[pixelBase + 1],
|
|
1299
|
+
b: data[pixelBase + 2],
|
|
1300
|
+
a: data[pixelBase + 3]
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
if (this._debugCanvas) {
|
|
1304
|
+
this._debugCanvas.width = bounds.width;
|
|
1305
|
+
this._debugCanvas.height = bounds.height;
|
|
1306
|
+
const ctx = this._debugCanvas.getContext('2d');
|
|
1307
|
+
const imageData = ctx.createImageData(bounds.width, bounds.height);
|
|
1308
|
+
imageData.data.set(data);
|
|
1309
|
+
ctx.putImageData(imageData, 0, 0);
|
|
1310
|
+
ctx.strokeStyle = 'black';
|
|
1311
|
+
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
|
|
1312
|
+
ctx.rect(pickX - 4, pickY - 4, 8, 8);
|
|
1313
|
+
ctx.fill();
|
|
1314
|
+
ctx.stroke();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return {
|
|
1318
|
+
data: data,
|
|
1319
|
+
width: bounds.width,
|
|
1320
|
+
height: bounds.height,
|
|
1321
|
+
color: color
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Get the candidate bounding box for a touching query.
|
|
1327
|
+
* @param {int} drawableID ID for drawable of query.
|
|
1328
|
+
* @return {?Rectangle} Rectangle bounds for touching query, or null.
|
|
1329
|
+
*/
|
|
1330
|
+
_touchingBounds (drawableID) {
|
|
1331
|
+
const drawable = this._allDrawables[drawableID];
|
|
1332
|
+
|
|
1333
|
+
/** @todo remove this once URL-based skin setting is removed. */
|
|
1334
|
+
if (!drawable.skin || !drawable.skin.getTexture([100, 100])) return null;
|
|
1335
|
+
|
|
1336
|
+
const bounds = drawable.getFastBounds();
|
|
1337
|
+
|
|
1338
|
+
// Limit queries to the stage size.
|
|
1339
|
+
bounds.clamp(this._xLeft, this._xRight, this._yBottom, this._yTop);
|
|
1340
|
+
|
|
1341
|
+
// Use integer coordinates for queries - weird things happen
|
|
1342
|
+
// when you provide float width/heights to gl.viewport and projection.
|
|
1343
|
+
bounds.snapToInt();
|
|
1344
|
+
|
|
1345
|
+
if (bounds.width === 0 || bounds.height === 0) {
|
|
1346
|
+
// No space to query.
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
return bounds;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Filter a list of candidates for a touching query into only those that
|
|
1354
|
+
* could possibly intersect the given bounds.
|
|
1355
|
+
* @param {int} drawableID - ID for drawable of query.
|
|
1356
|
+
* @param {Array<int>} candidateIDs - Candidates for touching query.
|
|
1357
|
+
* @return {?Array< {id, drawable, intersection} >} Filtered candidates with useful data.
|
|
1358
|
+
*/
|
|
1359
|
+
_candidatesTouching (drawableID, candidateIDs) {
|
|
1360
|
+
const bounds = this._touchingBounds(drawableID);
|
|
1361
|
+
const result = [];
|
|
1362
|
+
if (bounds === null) {
|
|
1363
|
+
return result;
|
|
1364
|
+
}
|
|
1365
|
+
// iterate through the drawables list BACKWARDS - we want the top most item to be the first we check
|
|
1366
|
+
for (let index = candidateIDs.length - 1; index >= 0; index--) {
|
|
1367
|
+
const id = candidateIDs[index];
|
|
1368
|
+
if (id !== drawableID) {
|
|
1369
|
+
const drawable = this._allDrawables[id];
|
|
1370
|
+
// Text bubbles aren't considered in "touching" queries
|
|
1371
|
+
if (drawable.skin instanceof TextBubbleSkin) continue;
|
|
1372
|
+
if (drawable.skin && drawable._visible) {
|
|
1373
|
+
// Update the CPU position data
|
|
1374
|
+
drawable.updateCPURenderAttributes();
|
|
1375
|
+
const candidateBounds = drawable.getFastBounds();
|
|
1376
|
+
|
|
1377
|
+
// Push bounds out to integers. If a drawable extends out into half a pixel, that half-pixel still
|
|
1378
|
+
// needs to be tested. Plus, in some areas we construct another rectangle from the union of these,
|
|
1379
|
+
// and iterate over its pixels (width * height). Turns out that doesn't work so well when the
|
|
1380
|
+
// width/height aren't integers.
|
|
1381
|
+
candidateBounds.snapToInt();
|
|
1382
|
+
|
|
1383
|
+
if (bounds.intersects(candidateBounds)) {
|
|
1384
|
+
result.push({
|
|
1385
|
+
id,
|
|
1386
|
+
drawable,
|
|
1387
|
+
intersection: Rectangle.intersect(bounds, candidateBounds)
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Helper to get the union bounds from a set of candidates returned from the above method
|
|
1398
|
+
* @private
|
|
1399
|
+
* @param {Array<object>} candidates info from _candidatesTouching
|
|
1400
|
+
* @return {Rectangle} the outer bounding box union
|
|
1401
|
+
*/
|
|
1402
|
+
_candidatesBounds (candidates) {
|
|
1403
|
+
return candidates.reduce((memo, {intersection}) => {
|
|
1404
|
+
if (!memo) {
|
|
1405
|
+
return intersection;
|
|
1406
|
+
}
|
|
1407
|
+
// store the union of the two rectangles in our static rectangle instance
|
|
1408
|
+
return Rectangle.union(memo, intersection, __candidatesBounds);
|
|
1409
|
+
}, null);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Update a drawable's skin.
|
|
1414
|
+
* @param {number} drawableID The drawable's id.
|
|
1415
|
+
* @param {number} skinId The skin to update to.
|
|
1416
|
+
*/
|
|
1417
|
+
updateDrawableSkinId (drawableID, skinId) {
|
|
1418
|
+
const drawable = this._allDrawables[drawableID];
|
|
1419
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1420
|
+
if (!drawable) return;
|
|
1421
|
+
drawable.skin = this._allSkins[skinId];
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Update a drawable's position.
|
|
1426
|
+
* @param {number} drawableID The drawable's id.
|
|
1427
|
+
* @param {Array.<number>} position The new position.
|
|
1428
|
+
*/
|
|
1429
|
+
updateDrawablePosition (drawableID, position) {
|
|
1430
|
+
const drawable = this._allDrawables[drawableID];
|
|
1431
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1432
|
+
if (!drawable) return;
|
|
1433
|
+
drawable.updatePosition(position);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Update a drawable's direction.
|
|
1438
|
+
* @param {number} drawableID The drawable's id.
|
|
1439
|
+
* @param {number} direction A new direction.
|
|
1440
|
+
*/
|
|
1441
|
+
updateDrawableDirection (drawableID, direction) {
|
|
1442
|
+
const drawable = this._allDrawables[drawableID];
|
|
1443
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1444
|
+
if (!drawable) return;
|
|
1445
|
+
drawable.updateDirection(direction);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Update a drawable's scale.
|
|
1450
|
+
* @param {number} drawableID The drawable's id.
|
|
1451
|
+
* @param {Array.<number>} scale A new scale.
|
|
1452
|
+
*/
|
|
1453
|
+
updateDrawableScale (drawableID, scale) {
|
|
1454
|
+
const drawable = this._allDrawables[drawableID];
|
|
1455
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1456
|
+
if (!drawable) return;
|
|
1457
|
+
drawable.updateScale(scale);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Update a drawable's direction and scale together.
|
|
1462
|
+
* @param {number} drawableID The drawable's id.
|
|
1463
|
+
* @param {number} direction A new direction.
|
|
1464
|
+
* @param {Array.<number>} scale A new scale.
|
|
1465
|
+
*/
|
|
1466
|
+
updateDrawableDirectionScale (drawableID, direction, scale) {
|
|
1467
|
+
const drawable = this._allDrawables[drawableID];
|
|
1468
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1469
|
+
if (!drawable) return;
|
|
1470
|
+
drawable.updateDirection(direction);
|
|
1471
|
+
drawable.updateScale(scale);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Update a drawable's visibility.
|
|
1476
|
+
* @param {number} drawableID The drawable's id.
|
|
1477
|
+
* @param {boolean} visible Will the drawable be visible?
|
|
1478
|
+
*/
|
|
1479
|
+
updateDrawableVisible (drawableID, visible) {
|
|
1480
|
+
const drawable = this._allDrawables[drawableID];
|
|
1481
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1482
|
+
if (!drawable) return;
|
|
1483
|
+
drawable.updateVisible(visible);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Update a drawable's visual effect.
|
|
1488
|
+
* @param {number} drawableID The drawable's id.
|
|
1489
|
+
* @param {string} effectName The effect to change.
|
|
1490
|
+
* @param {number} value A new effect value.
|
|
1491
|
+
*/
|
|
1492
|
+
updateDrawableEffect (drawableID, effectName, value) {
|
|
1493
|
+
const drawable = this._allDrawables[drawableID];
|
|
1494
|
+
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
|
1495
|
+
if (!drawable) return;
|
|
1496
|
+
drawable.updateEffect(effectName, value);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* Update the position, direction, scale, or effect properties of this Drawable.
|
|
1501
|
+
* @deprecated Use specific updateDrawable* methods instead.
|
|
1502
|
+
* @param {int} drawableID The ID of the Drawable to update.
|
|
1503
|
+
* @param {object.<string,*>} properties The new property values to set.
|
|
1504
|
+
*/
|
|
1505
|
+
updateDrawableProperties (drawableID, properties) {
|
|
1506
|
+
const drawable = this._allDrawables[drawableID];
|
|
1507
|
+
if (!drawable) {
|
|
1508
|
+
/**
|
|
1509
|
+
* @todo(https://github.com/LLK/scratch-vm/issues/2288) fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
|
1510
|
+
* Right now this happens so much on some projects that a warning or exception here can hang the browser.
|
|
1511
|
+
*/
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if ('skinId' in properties) {
|
|
1515
|
+
this.updateDrawableSkinId(drawableID, properties.skinId);
|
|
1516
|
+
}
|
|
1517
|
+
drawable.updateProperties(properties);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Update the position object's x & y members to keep the drawable fenced in view.
|
|
1522
|
+
* @param {int} drawableID - The ID of the Drawable to update.
|
|
1523
|
+
* @param {Array.<number, number>} position to be fenced - An array of type [x, y]
|
|
1524
|
+
* @return {Array.<number, number>} The fenced position as an array [x, y]
|
|
1525
|
+
*/
|
|
1526
|
+
getFencedPositionOfDrawable (drawableID, position) {
|
|
1527
|
+
let x = position[0];
|
|
1528
|
+
let y = position[1];
|
|
1529
|
+
|
|
1530
|
+
const drawable = this._allDrawables[drawableID];
|
|
1531
|
+
if (!drawable) {
|
|
1532
|
+
// @todo(https://github.com/LLK/scratch-vm/issues/2288) fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
|
1533
|
+
// Right now this happens so much on some projects that a warning or exception here can hang the browser.
|
|
1534
|
+
return [x, y];
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const dx = x - drawable._position[0];
|
|
1538
|
+
const dy = y - drawable._position[1];
|
|
1539
|
+
const aabb = drawable._skin.getFenceBounds(drawable, __fenceBounds);
|
|
1540
|
+
const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2);
|
|
1541
|
+
|
|
1542
|
+
const sx = this._xRight - Math.min(FENCE_WIDTH, inset);
|
|
1543
|
+
if (aabb.right + dx < -sx) {
|
|
1544
|
+
x = Math.ceil(drawable._position[0] - (sx + aabb.right));
|
|
1545
|
+
} else if (aabb.left + dx > sx) {
|
|
1546
|
+
x = Math.floor(drawable._position[0] + (sx - aabb.left));
|
|
1547
|
+
}
|
|
1548
|
+
const sy = this._yTop - Math.min(FENCE_WIDTH, inset);
|
|
1549
|
+
if (aabb.top + dy < -sy) {
|
|
1550
|
+
y = Math.ceil(drawable._position[1] - (sy + aabb.top));
|
|
1551
|
+
} else if (aabb.bottom + dy > sy) {
|
|
1552
|
+
y = Math.floor(drawable._position[1] + (sy - aabb.bottom));
|
|
1553
|
+
}
|
|
1554
|
+
return [x, y];
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Clear a pen layer.
|
|
1559
|
+
* @param {int} penSkinID - the unique ID of a Pen Skin.
|
|
1560
|
+
*/
|
|
1561
|
+
penClear (penSkinID) {
|
|
1562
|
+
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
|
|
1563
|
+
skin.clear();
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Draw a point on a pen layer.
|
|
1568
|
+
* @param {int} penSkinID - the unique ID of a Pen Skin.
|
|
1569
|
+
* @param {PenAttributes} penAttributes - how the point should be drawn.
|
|
1570
|
+
* @param {number} x - the X coordinate of the point to draw.
|
|
1571
|
+
* @param {number} y - the Y coordinate of the point to draw.
|
|
1572
|
+
*/
|
|
1573
|
+
penPoint (penSkinID, penAttributes, x, y) {
|
|
1574
|
+
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
|
|
1575
|
+
skin.drawPoint(penAttributes, x, y);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Draw a line on a pen layer.
|
|
1580
|
+
* @param {int} penSkinID - the unique ID of a Pen Skin.
|
|
1581
|
+
* @param {PenAttributes} penAttributes - how the line should be drawn.
|
|
1582
|
+
* @param {number} x0 - the X coordinate of the beginning of the line.
|
|
1583
|
+
* @param {number} y0 - the Y coordinate of the beginning of the line.
|
|
1584
|
+
* @param {number} x1 - the X coordinate of the end of the line.
|
|
1585
|
+
* @param {number} y1 - the Y coordinate of the end of the line.
|
|
1586
|
+
*/
|
|
1587
|
+
penLine (penSkinID, penAttributes, x0, y0, x1, y1) {
|
|
1588
|
+
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
|
|
1589
|
+
skin.drawLine(penAttributes, x0, y0, x1, y1);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Stamp a Drawable onto a pen layer.
|
|
1594
|
+
* @param {int} penSkinID - the unique ID of a Pen Skin.
|
|
1595
|
+
* @param {int} stampID - the unique ID of the Drawable to use as the stamp.
|
|
1596
|
+
*/
|
|
1597
|
+
penStamp (penSkinID, stampID) {
|
|
1598
|
+
const stampDrawable = this._allDrawables[stampID];
|
|
1599
|
+
if (!stampDrawable) {
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const bounds = this._touchingBounds(stampID);
|
|
1604
|
+
if (!bounds) {
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
this._doExitDrawRegion();
|
|
1609
|
+
|
|
1610
|
+
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
|
|
1611
|
+
|
|
1612
|
+
const gl = this._gl;
|
|
1613
|
+
twgl.bindFramebufferInfo(gl, skin._framebuffer);
|
|
1614
|
+
|
|
1615
|
+
// Limit size of viewport to the bounds around the stamp Drawable and create the projection matrix for the draw.
|
|
1616
|
+
gl.viewport(
|
|
1617
|
+
(this._nativeSize[0] * 0.5) + bounds.left,
|
|
1618
|
+
(this._nativeSize[1] * 0.5) - bounds.top,
|
|
1619
|
+
bounds.width,
|
|
1620
|
+
bounds.height
|
|
1621
|
+
);
|
|
1622
|
+
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
|
|
1623
|
+
|
|
1624
|
+
// Draw the stamped sprite onto the PenSkin's framebuffer.
|
|
1625
|
+
this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection, {ignoreVisibility: true});
|
|
1626
|
+
skin._silhouetteDirty = true;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/* ******
|
|
1630
|
+
* Truly internal functions: these support the functions above.
|
|
1631
|
+
********/
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Build geometry (vertex and index) buffers.
|
|
1635
|
+
* @private
|
|
1636
|
+
*/
|
|
1637
|
+
_createGeometry () {
|
|
1638
|
+
const quad = {
|
|
1639
|
+
a_position: {
|
|
1640
|
+
numComponents: 2,
|
|
1641
|
+
data: [
|
|
1642
|
+
-0.5, -0.5,
|
|
1643
|
+
0.5, -0.5,
|
|
1644
|
+
-0.5, 0.5,
|
|
1645
|
+
-0.5, 0.5,
|
|
1646
|
+
0.5, -0.5,
|
|
1647
|
+
0.5, 0.5
|
|
1648
|
+
]
|
|
1649
|
+
},
|
|
1650
|
+
a_texCoord: {
|
|
1651
|
+
numComponents: 2,
|
|
1652
|
+
data: [
|
|
1653
|
+
1, 0,
|
|
1654
|
+
0, 0,
|
|
1655
|
+
1, 1,
|
|
1656
|
+
1, 1,
|
|
1657
|
+
0, 0,
|
|
1658
|
+
0, 1
|
|
1659
|
+
]
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* Respond to a change in the "native" rendering size. The native size is used by buffers which are fixed in size
|
|
1667
|
+
* regardless of the size of the main render target. This includes the buffers used for queries such as picking and
|
|
1668
|
+
* color-touching. The fixed size allows (more) consistent behavior across devices and presentation modes.
|
|
1669
|
+
* @param {object} event - The change event.
|
|
1670
|
+
* @private
|
|
1671
|
+
*/
|
|
1672
|
+
onNativeSizeChanged (event) {
|
|
1673
|
+
const [width, height] = event.newSize;
|
|
1674
|
+
|
|
1675
|
+
const gl = this._gl;
|
|
1676
|
+
const attachments = [
|
|
1677
|
+
{format: gl.RGBA},
|
|
1678
|
+
{format: gl.DEPTH_STENCIL}
|
|
1679
|
+
];
|
|
1680
|
+
|
|
1681
|
+
if (!this._pickBufferInfo) {
|
|
1682
|
+
this._pickBufferInfo = twgl.createFramebufferInfo(gl, attachments, MAX_TOUCH_SIZE[0], MAX_TOUCH_SIZE[1]);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/** @todo should we create this on demand to save memory? */
|
|
1686
|
+
// A 480x360 32-bpp buffer is 675 KiB.
|
|
1687
|
+
if (this._queryBufferInfo) {
|
|
1688
|
+
twgl.resizeFramebufferInfo(gl, this._queryBufferInfo, attachments, width, height);
|
|
1689
|
+
} else {
|
|
1690
|
+
this._queryBufferInfo = twgl.createFramebufferInfo(gl, attachments, width, height);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* Enter a draw region.
|
|
1696
|
+
*
|
|
1697
|
+
* A draw region is where multiple draw operations are performed with the
|
|
1698
|
+
* same GL state. WebGL performs poorly when it changes state like blend
|
|
1699
|
+
* mode. Marking a collection of state values as a "region" the renderer
|
|
1700
|
+
* can skip superfluous extra state calls when it is already in that
|
|
1701
|
+
* region. Since one region may be entered from within another a exit
|
|
1702
|
+
* handle can also be registered that is called when a new region is about
|
|
1703
|
+
* to be entered to restore a common inbetween state.
|
|
1704
|
+
*
|
|
1705
|
+
* @param {any} regionId - id of the region to enter
|
|
1706
|
+
* @param {function} enter - handle to call when first entering a region
|
|
1707
|
+
* @param {function} exit - handle to call when leaving a region
|
|
1708
|
+
*/
|
|
1709
|
+
enterDrawRegion (regionId, enter = regionId.enter, exit = regionId.exit) {
|
|
1710
|
+
if (this._regionId !== regionId) {
|
|
1711
|
+
this._doExitDrawRegion();
|
|
1712
|
+
this._regionId = regionId;
|
|
1713
|
+
enter();
|
|
1714
|
+
this._exitRegion = exit;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Forcefully exit the current region returning to a common inbetween GL
|
|
1720
|
+
* state.
|
|
1721
|
+
*/
|
|
1722
|
+
_doExitDrawRegion () {
|
|
1723
|
+
if (this._exitRegion !== null) {
|
|
1724
|
+
this._exitRegion();
|
|
1725
|
+
}
|
|
1726
|
+
this._exitRegion = null;
|
|
1727
|
+
this._regionId = null;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/**
|
|
1731
|
+
* Draw a set of Drawables, by drawable ID
|
|
1732
|
+
* @param {Array<int>} drawables The Drawable IDs to draw, possibly this._drawList.
|
|
1733
|
+
* @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
|
|
1734
|
+
* @param {module:twgl/m4.Mat4} projection The projection matrix to use.
|
|
1735
|
+
* @param {object} [opts] Options for drawing
|
|
1736
|
+
* @param {idFilterFunc} opts.filter An optional filter function.
|
|
1737
|
+
* @param {object.<string,*>} opts.extraUniforms Extra uniforms for the shaders.
|
|
1738
|
+
* @param {int} opts.effectMask Bitmask for effects to allow
|
|
1739
|
+
* @param {boolean} opts.ignoreVisibility Draw all, despite visibility (e.g. stamping, touching color)
|
|
1740
|
+
* @param {int} opts.framebufferWidth The width of the framebuffer being drawn onto. Defaults to "native" width
|
|
1741
|
+
* @param {int} opts.framebufferHeight The height of the framebuffer being drawn onto. Defaults to "native" height
|
|
1742
|
+
* @private
|
|
1743
|
+
*/
|
|
1744
|
+
_drawThese (drawables, drawMode, projection, opts = {}) {
|
|
1745
|
+
|
|
1746
|
+
const gl = this._gl;
|
|
1747
|
+
let currentShader = null;
|
|
1748
|
+
|
|
1749
|
+
const framebufferSpaceScaleDiffers = (
|
|
1750
|
+
'framebufferWidth' in opts && 'framebufferHeight' in opts &&
|
|
1751
|
+
opts.framebufferWidth !== this._nativeSize[0] && opts.framebufferHeight !== this._nativeSize[1]
|
|
1752
|
+
);
|
|
1753
|
+
|
|
1754
|
+
const numDrawables = drawables.length;
|
|
1755
|
+
for (let drawableIndex = 0; drawableIndex < numDrawables; ++drawableIndex) {
|
|
1756
|
+
const drawableID = drawables[drawableIndex];
|
|
1757
|
+
|
|
1758
|
+
// If we have a filter, check whether the ID fails
|
|
1759
|
+
if (opts.filter && !opts.filter(drawableID)) continue;
|
|
1760
|
+
|
|
1761
|
+
const drawable = this._allDrawables[drawableID];
|
|
1762
|
+
/** @todo check if drawable is inside the viewport before anything else */
|
|
1763
|
+
|
|
1764
|
+
// Hidden drawables (e.g., by a "hide" block) are not drawn unless
|
|
1765
|
+
// the ignoreVisibility flag is used (e.g. for stamping or touchingColor).
|
|
1766
|
+
if (!drawable.getVisible() && !opts.ignoreVisibility) continue;
|
|
1767
|
+
|
|
1768
|
+
// drawableScale is the "framebuffer-pixel-space" scale of the drawable, as percentages of the drawable's
|
|
1769
|
+
// "native size" (so 100 = same as skin's "native size", 200 = twice "native size").
|
|
1770
|
+
// If the framebuffer dimensions are the same as the stage's "native" size, there's no need to calculate it.
|
|
1771
|
+
const drawableScale = framebufferSpaceScaleDiffers ? [
|
|
1772
|
+
drawable.scale[0] * opts.framebufferWidth / this._nativeSize[0],
|
|
1773
|
+
drawable.scale[1] * opts.framebufferHeight / this._nativeSize[1]
|
|
1774
|
+
] : drawable.scale;
|
|
1775
|
+
|
|
1776
|
+
// If the skin or texture isn't ready yet, skip it.
|
|
1777
|
+
if (!drawable.skin || !drawable.skin.getTexture(drawableScale)) continue;
|
|
1778
|
+
|
|
1779
|
+
const uniforms = {};
|
|
1780
|
+
|
|
1781
|
+
let effectBits = drawable.enabledEffects;
|
|
1782
|
+
effectBits &= Object.prototype.hasOwnProperty.call(opts, 'effectMask') ? opts.effectMask : effectBits;
|
|
1783
|
+
const newShader = this._shaderManager.getShader(drawMode, effectBits);
|
|
1784
|
+
|
|
1785
|
+
// Manually perform region check. Do not create functions inside a
|
|
1786
|
+
// loop.
|
|
1787
|
+
if (this._regionId !== newShader) {
|
|
1788
|
+
this._doExitDrawRegion();
|
|
1789
|
+
this._regionId = newShader;
|
|
1790
|
+
|
|
1791
|
+
currentShader = newShader;
|
|
1792
|
+
gl.useProgram(currentShader.program);
|
|
1793
|
+
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
|
|
1794
|
+
Object.assign(uniforms, {
|
|
1795
|
+
u_projectionMatrix: projection
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
Object.assign(uniforms,
|
|
1800
|
+
drawable.skin.getUniforms(drawableScale),
|
|
1801
|
+
drawable.getUniforms());
|
|
1802
|
+
|
|
1803
|
+
// Apply extra uniforms after the Drawable's, to allow overwriting.
|
|
1804
|
+
if (opts.extraUniforms) {
|
|
1805
|
+
Object.assign(uniforms, opts.extraUniforms);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
if (uniforms.u_skin) {
|
|
1809
|
+
twgl.setTextureParameters(
|
|
1810
|
+
gl, uniforms.u_skin, {
|
|
1811
|
+
minMag: drawable.skin.useNearest(drawableScale, drawable) ? gl.NEAREST : gl.LINEAR
|
|
1812
|
+
}
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
twgl.setUniforms(currentShader, uniforms);
|
|
1817
|
+
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
this._regionId = null;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Get the convex hull points for a particular Drawable.
|
|
1825
|
+
* To do this, calculate it based on the drawable's Silhouette.
|
|
1826
|
+
* @param {int} drawableID The Drawable IDs calculate convex hull for.
|
|
1827
|
+
* @return {Array<Array<number>>} points Convex hull points, as [[x, y], ...]
|
|
1828
|
+
*/
|
|
1829
|
+
_getConvexHullPointsForDrawable (drawableID) {
|
|
1830
|
+
const drawable = this._allDrawables[drawableID];
|
|
1831
|
+
|
|
1832
|
+
const [width, height] = drawable.skin.size;
|
|
1833
|
+
// No points in the hull if invisible or size is 0.
|
|
1834
|
+
if (!drawable.getVisible() || width === 0 || height === 0) {
|
|
1835
|
+
return [];
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
drawable.updateCPURenderAttributes();
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Return the determinant of two vectors, the vector from A to B and the vector from A to C.
|
|
1842
|
+
*
|
|
1843
|
+
* The determinant is useful in this case to know if AC is counter-clockwise from AB.
|
|
1844
|
+
* A positive value means that AC is counter-clockwise from AB. A negative value means AC is clockwise from AB.
|
|
1845
|
+
*
|
|
1846
|
+
* @param {Float32Array} A A 2d vector in space.
|
|
1847
|
+
* @param {Float32Array} B A 2d vector in space.
|
|
1848
|
+
* @param {Float32Array} C A 2d vector in space.
|
|
1849
|
+
* @return {number} Greater than 0 if counter clockwise, less than if clockwise, 0 if all points are on a line.
|
|
1850
|
+
*/
|
|
1851
|
+
const determinant = function (A, B, C) {
|
|
1852
|
+
// AB = B - A
|
|
1853
|
+
// AC = C - A
|
|
1854
|
+
// det (AB BC) = AB0 * AC1 - AB1 * AC0
|
|
1855
|
+
return (((B[0] - A[0]) * (C[1] - A[1])) - ((B[1] - A[1]) * (C[0] - A[0])));
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
// This algorithm for calculating the convex hull somewhat resembles the monotone chain algorithm.
|
|
1859
|
+
// The main difference is that instead of sorting the points by x-coordinate, and y-coordinate in case of ties,
|
|
1860
|
+
// it goes through them by y-coordinate in the outer loop and x-coordinate in the inner loop.
|
|
1861
|
+
// This gives us "left" and "right" hulls, whereas the monotone chain algorithm gives "top" and "bottom" hulls.
|
|
1862
|
+
// Adapted from https://github.com/LLK/scratch-flash/blob/dcbeeb59d44c3be911545dfe54d46a32404f8e69/src/scratch/ScratchCostume.as#L369-L413
|
|
1863
|
+
|
|
1864
|
+
const leftHull = [];
|
|
1865
|
+
const rightHull = [];
|
|
1866
|
+
|
|
1867
|
+
// While convex hull algorithms usually push and pop values from the list of hull points,
|
|
1868
|
+
// here, we keep indices for the "last" point in each array. Any points past these indices are ignored.
|
|
1869
|
+
// This is functionally equivalent to pushing and popping from a "stack" of hull points.
|
|
1870
|
+
let leftEndPointIndex = -1;
|
|
1871
|
+
let rightEndPointIndex = -1;
|
|
1872
|
+
|
|
1873
|
+
const _pixelPos = twgl.v3.create();
|
|
1874
|
+
const _effectPos = twgl.v3.create();
|
|
1875
|
+
|
|
1876
|
+
let currentPoint;
|
|
1877
|
+
|
|
1878
|
+
// *Not* Scratch Space-- +y is bottom
|
|
1879
|
+
// Loop over all rows of pixels, starting at the top
|
|
1880
|
+
for (let y = 0; y < height; y++) {
|
|
1881
|
+
_pixelPos[1] = y / height;
|
|
1882
|
+
|
|
1883
|
+
// We start at the leftmost point, then go rightwards until we hit an opaque pixel
|
|
1884
|
+
let x = 0;
|
|
1885
|
+
for (; x < width; x++) {
|
|
1886
|
+
_pixelPos[0] = x / width;
|
|
1887
|
+
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
|
|
1888
|
+
if (drawable.skin.isTouchingLinear(_effectPos)) {
|
|
1889
|
+
currentPoint = [x, y];
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// If we managed to loop all the way through, there are no opaque pixels on this row. Go to the next one
|
|
1895
|
+
if (x >= width) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// Because leftEndPointIndex is initialized to -1, this is skipped for the first two rows.
|
|
1900
|
+
// It runs only when there are enough points in the left hull to make at least one line.
|
|
1901
|
+
// If appending the current point to the left hull makes a counter-clockwise turn,
|
|
1902
|
+
// we want to append the current point. Otherwise, we decrement the index of the "last" hull point until the
|
|
1903
|
+
// current point makes a counter-clockwise turn.
|
|
1904
|
+
// This decrementing has the same effect as popping from the point list, but is hopefully faster.
|
|
1905
|
+
while (leftEndPointIndex > 0) {
|
|
1906
|
+
if (determinant(leftHull[leftEndPointIndex], leftHull[leftEndPointIndex - 1], currentPoint) > 0) {
|
|
1907
|
+
break;
|
|
1908
|
+
} else {
|
|
1909
|
+
// leftHull.pop();
|
|
1910
|
+
--leftEndPointIndex;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// This has the same effect as pushing to the point list.
|
|
1915
|
+
// This "list head pointer" coding style leaves excess points dangling at the end of the list,
|
|
1916
|
+
// but that doesn't matter; we simply won't copy them over to the final hull.
|
|
1917
|
+
|
|
1918
|
+
// leftHull.push(currentPoint);
|
|
1919
|
+
leftHull[++leftEndPointIndex] = currentPoint;
|
|
1920
|
+
|
|
1921
|
+
// Now we repeat the process for the right side, looking leftwards for a pixel.
|
|
1922
|
+
for (x = width - 1; x >= 0; x--) {
|
|
1923
|
+
_pixelPos[0] = x / width;
|
|
1924
|
+
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
|
|
1925
|
+
if (drawable.skin.isTouchingLinear(_effectPos)) {
|
|
1926
|
+
currentPoint = [x, y];
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Because we're coming at this from the right, it goes clockwise this time.
|
|
1932
|
+
while (rightEndPointIndex > 0) {
|
|
1933
|
+
if (determinant(rightHull[rightEndPointIndex], rightHull[rightEndPointIndex - 1], currentPoint) < 0) {
|
|
1934
|
+
break;
|
|
1935
|
+
} else {
|
|
1936
|
+
--rightEndPointIndex;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
rightHull[++rightEndPointIndex] = currentPoint;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// Start off "hullPoints" with the left hull points.
|
|
1944
|
+
const hullPoints = leftHull;
|
|
1945
|
+
// This is where we get rid of those dangling extra points.
|
|
1946
|
+
hullPoints.length = leftEndPointIndex + 1;
|
|
1947
|
+
// Add points from the right side in reverse order so all points are ordered clockwise.
|
|
1948
|
+
for (let j = rightEndPointIndex; j >= 0; --j) {
|
|
1949
|
+
hullPoints.push(rightHull[j]);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Simplify boundary points using hull.js.
|
|
1953
|
+
// TODO: Remove this; this algorithm already generates convex hulls.
|
|
1954
|
+
return hull(hullPoints, Infinity);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Sample a "final" color from an array of drawables at a given scratch space.
|
|
1959
|
+
* Will blend any alpha values with the drawables "below" it.
|
|
1960
|
+
* @param {twgl.v3} vec Scratch Vector Space to sample
|
|
1961
|
+
* @param {Array<Drawables>} drawables A list of drawables with the "top most"
|
|
1962
|
+
* drawable at index 0
|
|
1963
|
+
* @param {Uint8ClampedArray} dst The color3b space to store the answer in.
|
|
1964
|
+
* @return {Uint8ClampedArray} The dst vector with everything blended down.
|
|
1965
|
+
*/
|
|
1966
|
+
static sampleColor3b (vec, drawables, dst) {
|
|
1967
|
+
dst = dst || new Uint8ClampedArray(3);
|
|
1968
|
+
dst.fill(0);
|
|
1969
|
+
let blendAlpha = 1;
|
|
1970
|
+
for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) {
|
|
1971
|
+
/*
|
|
1972
|
+
if (left > vec[0] || right < vec[0] ||
|
|
1973
|
+
bottom > vec[1] || top < vec[0]) {
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
*/
|
|
1977
|
+
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
|
|
1978
|
+
// Equivalent to gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
|
1979
|
+
dst[0] += __blendColor[0] * blendAlpha;
|
|
1980
|
+
dst[1] += __blendColor[1] * blendAlpha;
|
|
1981
|
+
dst[2] += __blendColor[2] * blendAlpha;
|
|
1982
|
+
blendAlpha *= (1 - (__blendColor[3] / 255));
|
|
1983
|
+
}
|
|
1984
|
+
// Backdrop could be transparent, so we need to go to the "clear color" of the
|
|
1985
|
+
// draw scene (white) as a fallback if everything was alpha
|
|
1986
|
+
dst[0] += blendAlpha * 255;
|
|
1987
|
+
dst[1] += blendAlpha * 255;
|
|
1988
|
+
dst[2] += blendAlpha * 255;
|
|
1989
|
+
return dst;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* @callback RenderWebGL#snapshotCallback
|
|
1994
|
+
* @param {string} dataURI Data URI of the snapshot of the renderer
|
|
1995
|
+
*/
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* @param {snapshotCallback} callback Function called in the next frame with the snapshot data
|
|
1999
|
+
*/
|
|
2000
|
+
requestSnapshot (callback) {
|
|
2001
|
+
this._snapshotCallbacks.push(callback);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// :3
|
|
2006
|
+
RenderWebGL.prototype.canHazPixels = RenderWebGL.prototype.extractDrawableScreenSpace;
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Values for setUseGPU()
|
|
2010
|
+
* @enum {string}
|
|
2011
|
+
*/
|
|
2012
|
+
RenderWebGL.UseGpuModes = {
|
|
2013
|
+
/**
|
|
2014
|
+
* Heuristically decide whether to use the GPU path, the CPU path, or a dynamic mixture of the two.
|
|
2015
|
+
*/
|
|
2016
|
+
Automatic: 'Automatic',
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Always use the GPU path.
|
|
2020
|
+
*/
|
|
2021
|
+
ForceGPU: 'ForceGPU',
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* Always use the CPU path.
|
|
2025
|
+
*/
|
|
2026
|
+
ForceCPU: 'ForceCPU'
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
module.exports = RenderWebGL;
|