@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.
@@ -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;