@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,734 @@
1
+ const twgl = require('twgl.js');
2
+
3
+ const Rectangle = require('./Rectangle');
4
+ const RenderConstants = require('./RenderConstants');
5
+ const ShaderManager = require('./ShaderManager');
6
+ const Skin = require('./Skin');
7
+ const EffectTransform = require('./EffectTransform');
8
+ const log = require('./util/log');
9
+
10
+ /**
11
+ * An internal workspace for calculating texture locations from world vectors
12
+ * this is REUSED for memory conservation reasons
13
+ * @type {twgl.v3}
14
+ */
15
+ const __isTouchingPosition = twgl.v3.create();
16
+ const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6;
17
+
18
+ /**
19
+ * Convert a scratch space location into a texture space float. Uses the
20
+ * internal __isTouchingPosition as a return value, so this should be copied
21
+ * if you ever need to get two local positions and store both. Requires that
22
+ * the drawable inverseMatrix is up to date.
23
+ *
24
+ * @param {Drawable} drawable The drawable to get the inverse matrix and uniforms from
25
+ * @param {twgl.v3} vec [x,y] scratch space vector
26
+ * @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix
27
+ */
28
+ const getLocalPosition = (drawable, vec) => {
29
+ // Transfrom from world coordinates to Drawable coordinates.
30
+ const localPosition = __isTouchingPosition;
31
+ const v0 = vec[0];
32
+ const v1 = vec[1];
33
+ const m = drawable._inverseMatrix;
34
+ // var v2 = v[2];
35
+ const d = (v0 * m[3]) + (v1 * m[7]) + m[15];
36
+ // The RenderWebGL quad flips the texture's X axis. So rendered bottom
37
+ // left is 1, 0 and the top right is 0, 1. Flip the X axis so
38
+ // localPosition matches that transformation.
39
+ localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d);
40
+ localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5;
41
+ // Fix floating point issues near 0. Filed https://github.com/LLK/scratch-render/issues/688 that
42
+ // they're happening in the first place.
43
+ // TODO: Check if this can be removed after render pull 479 is merged
44
+ if (Math.abs(localPosition[0]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[0] = 0;
45
+ if (Math.abs(localPosition[1]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[1] = 0;
46
+ // Apply texture effect transform if the localPosition is within the drawable's space,
47
+ // and any effects are currently active.
48
+ if (drawable.enabledEffects !== 0 &&
49
+ (localPosition[0] >= 0 && localPosition[0] < 1) &&
50
+ (localPosition[1] >= 0 && localPosition[1] < 1)) {
51
+
52
+ EffectTransform.transformPoint(drawable, localPosition, localPosition);
53
+ }
54
+ return localPosition;
55
+ };
56
+
57
+ class Drawable {
58
+ /**
59
+ * An object which can be drawn by the renderer.
60
+ * @todo double-buffer all rendering state (position, skin, effects, etc.)
61
+ * @param {!int} id - This Drawable's unique ID.
62
+ * @constructor
63
+ */
64
+ constructor (id) {
65
+ /** @type {!int} */
66
+ this._id = id;
67
+
68
+ /**
69
+ * The uniforms to be used by the vertex and pixel shaders.
70
+ * Some of these are used by other parts of the renderer as well.
71
+ * @type {Object.<string,*>}
72
+ * @private
73
+ */
74
+ this._uniforms = {
75
+ /**
76
+ * The model matrix, to concat with projection at draw time.
77
+ * @type {module:twgl/m4.Mat4}
78
+ */
79
+ u_modelMatrix: twgl.m4.identity(),
80
+
81
+ /**
82
+ * The color to use in the silhouette draw mode.
83
+ * @type {Array<number>}
84
+ */
85
+ u_silhouetteColor: Drawable.color4fFromID(this._id)
86
+ };
87
+
88
+ // Effect values are uniforms too
89
+ const numEffects = ShaderManager.EFFECTS.length;
90
+ for (let index = 0; index < numEffects; ++index) {
91
+ const effectName = ShaderManager.EFFECTS[index];
92
+ const effectInfo = ShaderManager.EFFECT_INFO[effectName];
93
+ const converter = effectInfo.converter;
94
+ this._uniforms[effectInfo.uniformName] = converter(0);
95
+ }
96
+
97
+ this._position = twgl.v3.create(0, 0);
98
+ this._scale = twgl.v3.create(100, 100);
99
+ this._direction = 90;
100
+ this._transformDirty = true;
101
+ this._rotationMatrix = twgl.m4.identity();
102
+ this._rotationTransformDirty = true;
103
+ this._rotationAdjusted = twgl.v3.create();
104
+ this._rotationCenterDirty = true;
105
+ this._skinScale = twgl.v3.create(0, 0, 0);
106
+ this._skinScaleDirty = true;
107
+ this._inverseMatrix = twgl.m4.identity();
108
+ this._inverseTransformDirty = true;
109
+ this._visible = true;
110
+
111
+ /** A bitmask identifying which effects are currently in use.
112
+ * @readonly
113
+ * @type {int} */
114
+ this.enabledEffects = 0;
115
+
116
+ /** @todo move convex hull functionality, maybe bounds functionality overall, to Skin classes */
117
+ this._convexHullPoints = null;
118
+ this._convexHullDirty = true;
119
+
120
+ // The precise bounding box will be from the transformed convex hull points,
121
+ // so initialize the array of transformed hull points in setConvexHullPoints.
122
+ // Initializing it once per convex hull recalculation avoids unnecessary creation of twgl.v3 objects.
123
+ this._transformedHullPoints = null;
124
+ this._transformedHullDirty = true;
125
+
126
+ this._skinWasAltered = this._skinWasAltered.bind(this);
127
+
128
+ this.isTouching = this._isTouchingNever;
129
+ }
130
+
131
+ /**
132
+ * Dispose of this Drawable. Do not use it after calling this method.
133
+ */
134
+ dispose () {
135
+ // Use the setter: disconnect events
136
+ this.skin = null;
137
+ }
138
+
139
+ /**
140
+ * Mark this Drawable's transform as dirty.
141
+ * It will be recalculated next time it's needed.
142
+ */
143
+ setTransformDirty () {
144
+ this._transformDirty = true;
145
+ this._inverseTransformDirty = true;
146
+ this._transformedHullDirty = true;
147
+ }
148
+
149
+ /**
150
+ * @returns {number} The ID for this Drawable.
151
+ */
152
+ get id () {
153
+ return this._id;
154
+ }
155
+
156
+ /**
157
+ * @returns {Skin} the current skin for this Drawable.
158
+ */
159
+ get skin () {
160
+ return this._skin;
161
+ }
162
+
163
+ /**
164
+ * @param {Skin} newSkin - A new Skin for this Drawable.
165
+ */
166
+ set skin (newSkin) {
167
+ if (this._skin !== newSkin) {
168
+ if (this._skin) {
169
+ this._skin.removeListener(Skin.Events.WasAltered, this._skinWasAltered);
170
+ }
171
+ this._skin = newSkin;
172
+ if (this._skin) {
173
+ this._skin.addListener(Skin.Events.WasAltered, this._skinWasAltered);
174
+ }
175
+ this._skinWasAltered();
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @returns {Array<number>} the current scaling percentages applied to this Drawable. [100,100] is normal size.
181
+ */
182
+ get scale () {
183
+ return [this._scale[0], this._scale[1]];
184
+ }
185
+
186
+ /**
187
+ * @returns {object.<string, *>} the shader uniforms to be used when rendering this Drawable.
188
+ */
189
+ getUniforms () {
190
+ if (this._transformDirty) {
191
+ this._calculateTransform();
192
+ }
193
+ return this._uniforms;
194
+ }
195
+
196
+ /**
197
+ * @returns {boolean} whether this Drawable is visible.
198
+ */
199
+ getVisible () {
200
+ return this._visible;
201
+ }
202
+
203
+ /**
204
+ * Update the position if it is different. Marks the transform as dirty.
205
+ * @param {Array.<number>} position A new position.
206
+ */
207
+ updatePosition (position) {
208
+ if (this._position[0] !== position[0] ||
209
+ this._position[1] !== position[1]) {
210
+ this._position[0] = Math.round(position[0]);
211
+ this._position[1] = Math.round(position[1]);
212
+ this.setTransformDirty();
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Update the direction if it is different. Marks the transform as dirty.
218
+ * @param {number} direction A new direction.
219
+ */
220
+ updateDirection (direction) {
221
+ if (this._direction !== direction) {
222
+ this._direction = direction;
223
+ this._rotationTransformDirty = true;
224
+ this.setTransformDirty();
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Update the scale if it is different. Marks the transform as dirty.
230
+ * @param {Array.<number>} scale A new scale.
231
+ */
232
+ updateScale (scale) {
233
+ if (this._scale[0] !== scale[0] ||
234
+ this._scale[1] !== scale[1]) {
235
+ this._scale[0] = scale[0];
236
+ this._scale[1] = scale[1];
237
+ this._rotationCenterDirty = true;
238
+ this._skinScaleDirty = true;
239
+ this.setTransformDirty();
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Update visibility if it is different. Marks the convex hull as dirty.
245
+ * @param {boolean} visible A new visibility state.
246
+ */
247
+ updateVisible (visible) {
248
+ if (this._visible !== visible) {
249
+ this._visible = visible;
250
+ this.setConvexHullDirty();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Update an effect. Marks the convex hull as dirty if the effect changes shape.
256
+ * @param {string} effectName The name of the effect.
257
+ * @param {number} rawValue A new effect value.
258
+ */
259
+ updateEffect (effectName, rawValue) {
260
+ const effectInfo = ShaderManager.EFFECT_INFO[effectName];
261
+ if (rawValue) {
262
+ this.enabledEffects |= effectInfo.mask;
263
+ } else {
264
+ this.enabledEffects &= ~effectInfo.mask;
265
+ }
266
+ const converter = effectInfo.converter;
267
+ this._uniforms[effectInfo.uniformName] = converter(rawValue);
268
+ if (effectInfo.shapeChanges) {
269
+ this.setConvexHullDirty();
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Update the position, direction, scale, or effect properties of this Drawable.
275
+ * @deprecated Use specific update* methods instead.
276
+ * @param {object.<string,*>} properties The new property values to set.
277
+ */
278
+ updateProperties (properties) {
279
+ if ('position' in properties) {
280
+ this.updatePosition(properties.position);
281
+ }
282
+ if ('direction' in properties) {
283
+ this.updateDirection(properties.direction);
284
+ }
285
+ if ('scale' in properties) {
286
+ this.updateScale(properties.scale);
287
+ }
288
+ if ('visible' in properties) {
289
+ this.updateVisible(properties.visible);
290
+ }
291
+ const numEffects = ShaderManager.EFFECTS.length;
292
+ for (let index = 0; index < numEffects; ++index) {
293
+ const effectName = ShaderManager.EFFECTS[index];
294
+ if (effectName in properties) {
295
+ this.updateEffect(effectName, properties[effectName]);
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Calculate the transform to use when rendering this Drawable.
302
+ * @private
303
+ */
304
+ _calculateTransform () {
305
+ if (this._rotationTransformDirty) {
306
+ const rotation = (270 - this._direction) * Math.PI / 180;
307
+
308
+ // Calling rotationZ sets the destination matrix to a rotation
309
+ // around the Z axis setting matrix components 0, 1, 4 and 5 with
310
+ // cosine and sine values of the rotation.
311
+ // twgl.m4.rotationZ(rotation, this._rotationMatrix);
312
+
313
+ // twgl assumes the last value set to the matrix was anything.
314
+ // Drawable knows, it was another rotationZ matrix, so we can skip
315
+ // assigning the values that will never change.
316
+ const c = Math.cos(rotation);
317
+ const s = Math.sin(rotation);
318
+ this._rotationMatrix[0] = c;
319
+ this._rotationMatrix[1] = s;
320
+ // this._rotationMatrix[2] = 0;
321
+ // this._rotationMatrix[3] = 0;
322
+ this._rotationMatrix[4] = -s;
323
+ this._rotationMatrix[5] = c;
324
+ // this._rotationMatrix[6] = 0;
325
+ // this._rotationMatrix[7] = 0;
326
+ // this._rotationMatrix[8] = 0;
327
+ // this._rotationMatrix[9] = 0;
328
+ // this._rotationMatrix[10] = 1;
329
+ // this._rotationMatrix[11] = 0;
330
+ // this._rotationMatrix[12] = 0;
331
+ // this._rotationMatrix[13] = 0;
332
+ // this._rotationMatrix[14] = 0;
333
+ // this._rotationMatrix[15] = 1;
334
+
335
+ this._rotationTransformDirty = false;
336
+ }
337
+
338
+ // Adjust rotation center relative to the skin.
339
+ if (this._rotationCenterDirty && this.skin !== null) {
340
+ // twgl version of the following in function work.
341
+ // let rotationAdjusted = twgl.v3.subtract(
342
+ // this.skin.rotationCenter,
343
+ // twgl.v3.divScalar(this.skin.size, 2, this._rotationAdjusted),
344
+ // this._rotationAdjusted
345
+ // );
346
+ // rotationAdjusted = twgl.v3.multiply(
347
+ // rotationAdjusted, this._scale, rotationAdjusted
348
+ // );
349
+ // rotationAdjusted = twgl.v3.divScalar(
350
+ // rotationAdjusted, 100, rotationAdjusted
351
+ // );
352
+ // rotationAdjusted[1] *= -1; // Y flipped to Scratch coordinate.
353
+ // rotationAdjusted[2] = 0; // Z coordinate is 0.
354
+
355
+ // Locally assign rotationCenter and skinSize to keep from having
356
+ // the Skin getter properties called twice while locally assigning
357
+ // their components for readability.
358
+ const rotationCenter = this.skin.rotationCenter;
359
+ const skinSize = this.skin.size;
360
+ const center0 = rotationCenter[0];
361
+ const center1 = rotationCenter[1];
362
+ const skinSize0 = skinSize[0];
363
+ const skinSize1 = skinSize[1];
364
+ const scale0 = this._scale[0];
365
+ const scale1 = this._scale[1];
366
+
367
+ const rotationAdjusted = this._rotationAdjusted;
368
+ rotationAdjusted[0] = (center0 - (skinSize0 / 2)) * scale0 / 100;
369
+ rotationAdjusted[1] = ((center1 - (skinSize1 / 2)) * scale1 / 100) * -1;
370
+ // rotationAdjusted[2] = 0;
371
+
372
+ this._rotationCenterDirty = false;
373
+ }
374
+
375
+ if (this._skinScaleDirty && this.skin !== null) {
376
+ // twgl version of the following in function work.
377
+ // const scaledSize = twgl.v3.divScalar(
378
+ // twgl.v3.multiply(this.skin.size, this._scale),
379
+ // 100
380
+ // );
381
+ // // was NaN because the vectors have only 2 components.
382
+ // scaledSize[2] = 0;
383
+
384
+ // Locally assign skinSize to keep from having the Skin getter
385
+ // properties called twice.
386
+ const skinSize = this.skin.size;
387
+ const scaledSize = this._skinScale;
388
+ scaledSize[0] = skinSize[0] * this._scale[0] / 100;
389
+ scaledSize[1] = skinSize[1] * this._scale[1] / 100;
390
+ // scaledSize[2] = 0;
391
+
392
+ this._skinScaleDirty = false;
393
+ }
394
+
395
+ const modelMatrix = this._uniforms.u_modelMatrix;
396
+
397
+ // twgl version of the following in function work.
398
+ // twgl.m4.identity(modelMatrix);
399
+ // twgl.m4.translate(modelMatrix, this._position, modelMatrix);
400
+ // twgl.m4.multiply(modelMatrix, this._rotationMatrix, modelMatrix);
401
+ // twgl.m4.translate(modelMatrix, this._rotationAdjusted, modelMatrix);
402
+ // twgl.m4.scale(modelMatrix, scaledSize, modelMatrix);
403
+
404
+ // Drawable configures a 3D matrix for drawing in WebGL, but most values
405
+ // will never be set because the inputs are on the X and Y position axis
406
+ // and the Z rotation axis. Drawable can bring the work inside
407
+ // _calculateTransform and greatly reduce the ammount of math and array
408
+ // assignments needed.
409
+
410
+ const scale0 = this._skinScale[0];
411
+ const scale1 = this._skinScale[1];
412
+ const rotation00 = this._rotationMatrix[0];
413
+ const rotation01 = this._rotationMatrix[1];
414
+ const rotation10 = this._rotationMatrix[4];
415
+ const rotation11 = this._rotationMatrix[5];
416
+ const adjusted0 = this._rotationAdjusted[0];
417
+ const adjusted1 = this._rotationAdjusted[1];
418
+ const position0 = this._position[0];
419
+ const position1 = this._position[1];
420
+
421
+ // Commented assignments show what the values are when the matrix was
422
+ // instantiated. Those values will never change so they do not need to
423
+ // be reassigned.
424
+ modelMatrix[0] = scale0 * rotation00;
425
+ modelMatrix[1] = scale0 * rotation01;
426
+ // modelMatrix[2] = 0;
427
+ // modelMatrix[3] = 0;
428
+ modelMatrix[4] = scale1 * rotation10;
429
+ modelMatrix[5] = scale1 * rotation11;
430
+ // modelMatrix[6] = 0;
431
+ // modelMatrix[7] = 0;
432
+ // modelMatrix[8] = 0;
433
+ // modelMatrix[9] = 0;
434
+ // modelMatrix[10] = 1;
435
+ // modelMatrix[11] = 0;
436
+ modelMatrix[12] = (rotation00 * adjusted0) + (rotation10 * adjusted1) + position0;
437
+ modelMatrix[13] = (rotation01 * adjusted0) + (rotation11 * adjusted1) + position1;
438
+ // modelMatrix[14] = 0;
439
+ // modelMatrix[15] = 1;
440
+
441
+ this._transformDirty = false;
442
+ }
443
+
444
+ /**
445
+ * Whether the Drawable needs convex hull points provided by the renderer.
446
+ * @return {boolean} True when no convex hull known, or it's dirty.
447
+ */
448
+ needsConvexHullPoints () {
449
+ return !this._convexHullPoints || this._convexHullDirty || this._convexHullPoints.length === 0;
450
+ }
451
+
452
+ /**
453
+ * Set the convex hull to be dirty.
454
+ * Do this whenever the Drawable's shape has possibly changed.
455
+ */
456
+ setConvexHullDirty () {
457
+ this._convexHullDirty = true;
458
+ }
459
+
460
+ /**
461
+ * Set the convex hull points for the Drawable.
462
+ * @param {Array<Array<number>>} points Convex hull points, as [[x, y], ...]
463
+ */
464
+ setConvexHullPoints (points) {
465
+ this._convexHullPoints = points;
466
+ this._convexHullDirty = false;
467
+
468
+ // Re-create the "transformed hull points" array.
469
+ // We only do this when the hull points change to avoid unnecessary allocations and GC.
470
+ this._transformedHullPoints = [];
471
+ for (let i = 0; i < points.length; i++) {
472
+ this._transformedHullPoints.push(twgl.v3.create());
473
+ }
474
+ this._transformedHullDirty = true;
475
+ }
476
+
477
+ /**
478
+ * @function
479
+ * @name isTouching
480
+ * Check if the world position touches the skin.
481
+ * The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date.
482
+ * @see updateCPURenderAttributes
483
+ * @param {twgl.v3} vec World coordinate vector.
484
+ * @return {boolean} True if the world position touches the skin.
485
+ */
486
+
487
+ // `updateCPURenderAttributes` sets this Drawable instance's `isTouching` method
488
+ // to one of the following three functions:
489
+ // If this drawable has no skin, set it to `_isTouchingNever`.
490
+ // Otherwise, if this drawable uses nearest-neighbor scaling at its current scale, set it to `_isTouchingNearest`.
491
+ // Otherwise, set it to `_isTouchingLinear`.
492
+ // This allows several checks to be moved from the `isTouching` function to `updateCPURenderAttributes`.
493
+
494
+ // eslint-disable-next-line no-unused-vars
495
+ _isTouchingNever (vec) {
496
+ return false;
497
+ }
498
+
499
+ _isTouchingNearest (vec) {
500
+ return this.skin.isTouchingNearest(getLocalPosition(this, vec));
501
+ }
502
+
503
+ _isTouchingLinear (vec) {
504
+ return this.skin.isTouchingLinear(getLocalPosition(this, vec));
505
+ }
506
+
507
+ /**
508
+ * Get the precise bounds for a Drawable.
509
+ * This function applies the transform matrix to the known convex hull,
510
+ * and then finds the minimum box along the axes.
511
+ * Before calling this, ensure the renderer has updated convex hull points.
512
+ * @param {?Rectangle} result optional destination for bounds calculation
513
+ * @return {!Rectangle} Bounds for a tight box around the Drawable.
514
+ */
515
+ getBounds (result) {
516
+ if (this.needsConvexHullPoints()) {
517
+ throw new Error('Needs updated convex hull points before bounds calculation.');
518
+ }
519
+ if (this._transformDirty) {
520
+ this._calculateTransform();
521
+ }
522
+ const transformedHullPoints = this._getTransformedHullPoints();
523
+ // Search through transformed points to generate box on axes.
524
+ result = result || new Rectangle();
525
+ result.initFromPointsAABB(transformedHullPoints);
526
+ return result;
527
+ }
528
+
529
+ /**
530
+ * Get the precise bounds for the upper 8px slice of the Drawable.
531
+ * Used for calculating where to position a text bubble.
532
+ * Before calling this, ensure the renderer has updated convex hull points.
533
+ * @param {?Rectangle} result optional destination for bounds calculation
534
+ * @return {!Rectangle} Bounds for a tight box around a slice of the Drawable.
535
+ */
536
+ getBoundsForBubble (result) {
537
+ if (this.needsConvexHullPoints()) {
538
+ throw new Error('Needs updated convex hull points before bubble bounds calculation.');
539
+ }
540
+ if (this._transformDirty) {
541
+ this._calculateTransform();
542
+ }
543
+ const slice = 8; // px, how tall the top slice to measure should be.
544
+ const transformedHullPoints = this._getTransformedHullPoints();
545
+ const maxY = Math.max.apply(null, transformedHullPoints.map(p => p[1]));
546
+ const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice);
547
+ // Search through filtered points to generate box on axes.
548
+ result = result || new Rectangle();
549
+ result.initFromPointsAABB(filteredHullPoints);
550
+ return result;
551
+ }
552
+
553
+ /**
554
+ * Get the rough axis-aligned bounding box for the Drawable.
555
+ * Calculated by transforming the skin's bounds.
556
+ * Note that this is less precise than the box returned by `getBounds`,
557
+ * which is tightly snapped to account for a Drawable's transparent regions.
558
+ * `getAABB` returns a much less accurate bounding box, but will be much
559
+ * faster to calculate so may be desired for quick checks/optimizations.
560
+ * @param {?Rectangle} result optional destination for bounds calculation
561
+ * @return {!Rectangle} Rough axis-aligned bounding box for Drawable.
562
+ */
563
+ getAABB (result) {
564
+ if (this._transformDirty) {
565
+ this._calculateTransform();
566
+ }
567
+ const tm = this._uniforms.u_modelMatrix;
568
+ result = result || new Rectangle();
569
+ result.initFromModelMatrix(tm);
570
+ return result;
571
+ }
572
+
573
+ /**
574
+ * Return the best Drawable bounds possible without performing graphics queries.
575
+ * I.e., returns the tight bounding box when the convex hull points are already
576
+ * known, but otherwise return the rough AABB of the Drawable.
577
+ * @param {?Rectangle} result optional destination for bounds calculation
578
+ * @return {!Rectangle} Bounds for the Drawable.
579
+ */
580
+ getFastBounds (result) {
581
+ if (!this.needsConvexHullPoints()) {
582
+ return this.getBounds(result);
583
+ }
584
+ return this.getAABB(result);
585
+ }
586
+
587
+ /**
588
+ * Transform all the convex hull points by the current Drawable's
589
+ * transform. This allows us to skip recalculating the convex hull
590
+ * for many Drawable updates, including translation, rotation, scaling.
591
+ * @return {!Array.<!Array.number>} Array of glPoints which are Array<x, y>
592
+ * @private
593
+ */
594
+ _getTransformedHullPoints () {
595
+ if (!this._transformedHullDirty) {
596
+ return this._transformedHullPoints;
597
+ }
598
+
599
+ const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1);
600
+ const skinSize = this.skin.size;
601
+ const halfXPixel = 1 / skinSize[0] / 2;
602
+ const halfYPixel = 1 / skinSize[1] / 2;
603
+ const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection);
604
+ for (let i = 0; i < this._convexHullPoints.length; i++) {
605
+ const point = this._convexHullPoints[i];
606
+ const dstPoint = this._transformedHullPoints[i];
607
+
608
+ dstPoint[0] = 0.5 + (-point[0] / skinSize[0]) - halfXPixel;
609
+ dstPoint[1] = (point[1] / skinSize[1]) - 0.5 + halfYPixel;
610
+ twgl.m4.transformPoint(tm, dstPoint, dstPoint);
611
+ }
612
+
613
+ this._transformedHullDirty = false;
614
+
615
+ return this._transformedHullPoints;
616
+ }
617
+
618
+ /**
619
+ * Update the transform matrix and calculate it's inverse for collision
620
+ * and local texture position purposes.
621
+ */
622
+ updateMatrix () {
623
+ if (this._transformDirty) {
624
+ this._calculateTransform();
625
+ }
626
+ // Get the inverse of the model matrix or update it.
627
+ if (this._inverseTransformDirty) {
628
+ const inverse = this._inverseMatrix;
629
+ twgl.m4.copy(this._uniforms.u_modelMatrix, inverse);
630
+ // The normal matrix uses a z scaling of 0 causing model[10] to be
631
+ // 0. Getting a 4x4 inverse is impossible without a scaling in x, y,
632
+ // and z.
633
+ inverse[10] = 1;
634
+ twgl.m4.inverse(inverse, inverse);
635
+ this._inverseTransformDirty = false;
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Update everything necessary to render this drawable on the CPU.
641
+ */
642
+ updateCPURenderAttributes () {
643
+ this.updateMatrix();
644
+ // CPU rendering always occurs at the "native" size, so no need to scale up this._scale
645
+ if (this.skin) {
646
+ this.skin.updateSilhouette(this._scale);
647
+
648
+ if (this.skin.useNearest(this._scale, this)) {
649
+ this.isTouching = this._isTouchingNearest;
650
+ } else {
651
+ this.isTouching = this._isTouchingLinear;
652
+ }
653
+ } else {
654
+ log.warn(`Could not find skin for drawable with id: ${this._id}`);
655
+
656
+ this.isTouching = this._isTouchingNever;
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Respond to an internal change in the current Skin.
662
+ * @private
663
+ */
664
+ _skinWasAltered () {
665
+ this._rotationCenterDirty = true;
666
+ this._skinScaleDirty = true;
667
+ this.setConvexHullDirty();
668
+ this.setTransformDirty();
669
+ }
670
+
671
+ /**
672
+ * Calculate a color to represent the given ID number. At least one component of
673
+ * the resulting color will be non-zero if the ID is not RenderConstants.ID_NONE.
674
+ * @param {int} id The ID to convert.
675
+ * @returns {Array<number>} An array of [r,g,b,a], each component in the range [0,1].
676
+ */
677
+ static color4fFromID (id) {
678
+ id -= RenderConstants.ID_NONE;
679
+ const r = ((id >> 0) & 255) / 255.0;
680
+ const g = ((id >> 8) & 255) / 255.0;
681
+ const b = ((id >> 16) & 255) / 255.0;
682
+ return [r, g, b, 1.0];
683
+ }
684
+
685
+ /**
686
+ * Calculate the ID number represented by the given color. If all components of
687
+ * the color are zero, the result will be RenderConstants.ID_NONE; otherwise the result
688
+ * will be a valid ID.
689
+ * @param {int} r The red value of the color, in the range [0,255].
690
+ * @param {int} g The green value of the color, in the range [0,255].
691
+ * @param {int} b The blue value of the color, in the range [0,255].
692
+ * @returns {int} The ID represented by that color.
693
+ */
694
+ static color3bToID (r, g, b) {
695
+ let id;
696
+ id = (r & 255) << 0;
697
+ id |= (g & 255) << 8;
698
+ id |= (b & 255) << 16;
699
+ return id + RenderConstants.ID_NONE;
700
+ }
701
+
702
+ /**
703
+ * Sample a color from a drawable's texture.
704
+ * The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date.
705
+ * @see updateCPURenderAttributes
706
+ * @param {twgl.v3} vec The scratch space [x,y] vector
707
+ * @param {Drawable} drawable The drawable to sample the texture from
708
+ * @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point.
709
+ * @param {number} [effectMask] A bitmask for which effects to use. Optional.
710
+ * @returns {Uint8ClampedArray} The dst object filled with the color4b
711
+ */
712
+ static sampleColor4b (vec, drawable, dst, effectMask) {
713
+ const localPosition = getLocalPosition(drawable, vec);
714
+ if (localPosition[0] < 0 || localPosition[1] < 0 ||
715
+ localPosition[0] > 1 || localPosition[1] > 1) {
716
+ dst[0] = 0;
717
+ dst[1] = 0;
718
+ dst[2] = 0;
719
+ dst[3] = 0;
720
+ return dst;
721
+ }
722
+
723
+ const textColor =
724
+ // commenting out to only use nearest for now
725
+ // drawable.skin.useNearest(drawable._scale, drawable) ?
726
+ drawable.skin._silhouette.colorAtNearest(localPosition, dst);
727
+ // : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
728
+
729
+ if (drawable.enabledEffects === 0) return textColor;
730
+ return EffectTransform.transformColor(drawable, textColor, effectMask);
731
+ }
732
+ }
733
+
734
+ module.exports = Drawable;