littlejsengine 1.18.8 → 1.18.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FAQ.md +197 -15
- package/README.md +30 -11
- package/dist/littlejs.d.ts +166 -74
- package/dist/littlejs.esm.js +610 -234
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +597 -229
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +590 -220
- package/package.json +4 -1
- package/plugins/box2d.js +17 -6
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +22 -3
- package/plugins/pathFinder.js +40 -9
- package/plugins/pluginExport.js +4 -3
- package/plugins/postProcess.js +8 -1
- package/plugins/tweenSystem.js +33 -20
- package/plugins/uiSystem.js +21 -3
- package/src/engine.js +16 -4
- package/src/engineAudio.js +30 -15
- package/src/engineBuild.mjs +1 -1
- package/src/engineDebug.js +7 -9
- package/src/engineDraw.js +144 -33
- package/src/engineExport.js +9 -2
- package/src/engineInput.js +17 -12
- package/src/engineLogo.js +1 -1
- package/src/engineMath.js +21 -7
- package/src/engineObject.js +19 -11
- package/src/engineParticles.js +26 -27
- package/src/engineSettings.js +3 -2
- package/src/engineTileLayer.js +62 -43
- package/src/engineUtilities.js +67 -11
- package/src/engineWebGL.js +37 -7
package/src/engineObject.js
CHANGED
|
@@ -71,7 +71,7 @@ class EngineObject
|
|
|
71
71
|
this.color = color.copy();
|
|
72
72
|
/** @property {Color} - Additive color to apply when rendered */
|
|
73
73
|
this.additiveColor = undefined;
|
|
74
|
-
/** @property {boolean} - Should
|
|
74
|
+
/** @property {boolean} - Should the rendered tile flip along the y axis. Affects rendering and the local→world transform of attached children (a mirrored parent flips its children's localPos.x and localAngle). Does not affect this object's own physics, collision, or localToWorld/worldToLocal. */
|
|
75
75
|
this.mirror = false;
|
|
76
76
|
/** @property {boolean} - Has object been destroyed? */
|
|
77
77
|
this.destroyed = false;
|
|
@@ -157,6 +157,9 @@ class EngineObject
|
|
|
157
157
|
// child objects do not have physics
|
|
158
158
|
ASSERT(!this.parent);
|
|
159
159
|
|
|
160
|
+
// bail if a collision callback destroyed us mid-frame
|
|
161
|
+
if (this.destroyed) return;
|
|
162
|
+
|
|
160
163
|
if (this.clampSpeed)
|
|
161
164
|
{
|
|
162
165
|
// limit max speed to prevent missing collisions
|
|
@@ -212,6 +215,8 @@ class EngineObject
|
|
|
212
215
|
|
|
213
216
|
// notify objects of collision and check if should be resolved
|
|
214
217
|
const collide1 = this.collideWithObject(o);
|
|
218
|
+
// callback may have destroyed us; stop resolving against more objects
|
|
219
|
+
if (this.destroyed) return;
|
|
215
220
|
const collide2 = o.collideWithObject(this);
|
|
216
221
|
if (!collide1 || !collide2) continue;
|
|
217
222
|
|
|
@@ -307,13 +312,17 @@ class EngineObject
|
|
|
307
312
|
const restitution = max(this.restitution, hitLayer.restitution);
|
|
308
313
|
if (isBlockedX)
|
|
309
314
|
{
|
|
310
|
-
// try to
|
|
315
|
+
// try to step over a 1-tile bump (direction follows gravity sign
|
|
316
|
+
// so inverted gravity steps down off a ceiling bump instead of up;
|
|
317
|
+
// zero gravity defaults to the normal-gravity step-up direction)
|
|
311
318
|
const epsilon = 1e-3;
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
319
|
+
const maxMove = .1;
|
|
320
|
+
const gravitySign = gravity.y > 0 ? -1 : 1;
|
|
321
|
+
const y = gravitySign > 0 ?
|
|
322
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon :
|
|
323
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon;
|
|
324
|
+
const delta = abs(y - this.pos.y);
|
|
325
|
+
if (delta < maxMove)
|
|
317
326
|
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
318
327
|
{
|
|
319
328
|
this.pos.y = y;
|
|
@@ -453,6 +462,8 @@ class EngineObject
|
|
|
453
462
|
* @return {EngineObject} The child object added */
|
|
454
463
|
addChild(child, localPos=vec2(), localAngle=0)
|
|
455
464
|
{
|
|
465
|
+
ASSERT(!this.destroyed, 'cannot add child to destroyed object');
|
|
466
|
+
if (this.destroyed) return child;
|
|
456
467
|
ASSERT(!child.parent && !this.children.includes(child));
|
|
457
468
|
ASSERT(child instanceof EngineObject, 'child must be an EngineObject');
|
|
458
469
|
ASSERT(child !== this, 'cannot add self as child');
|
|
@@ -469,10 +480,7 @@ class EngineObject
|
|
|
469
480
|
removeChild(child)
|
|
470
481
|
{
|
|
471
482
|
ASSERT(child.parent === this && this.children.includes(child));
|
|
472
|
-
|
|
473
|
-
const index = this.children.indexOf(child);
|
|
474
|
-
ASSERT(index >= 0, 'child not found in children array');
|
|
475
|
-
index >= 0 && this.children.splice(index, 1);
|
|
483
|
+
this.children.splice(this.children.indexOf(child), 1);
|
|
476
484
|
child.parent = undefined;
|
|
477
485
|
}
|
|
478
486
|
|
package/src/engineParticles.js
CHANGED
|
@@ -68,7 +68,7 @@ class ParticleEmitter extends EngineObject
|
|
|
68
68
|
* @param {number} [angleDamping] - How much to dampen particle angular speed
|
|
69
69
|
* @param {number} [gravityScale] - How much gravity effect particles
|
|
70
70
|
* @param {number} [particleConeAngle] - Cone for start particle angle
|
|
71
|
-
* @param {number} [fadeRate] -
|
|
71
|
+
* @param {number} [fadeRate] - Fraction of life spent fading: half at fade-in (start), half at fade-out (end). e.g. .2 = 10% fade-in, 80% full opacity, 10% fade-out
|
|
72
72
|
* @param {number} [randomness] - Apply extra randomness percent
|
|
73
73
|
* @param {boolean} [collideTiles] - Do particles collide against tiles
|
|
74
74
|
* @param {boolean} [additive] - Should particles use additive blend
|
|
@@ -152,7 +152,7 @@ class ParticleEmitter extends EngineObject
|
|
|
152
152
|
this.gravityScale = gravityScale;
|
|
153
153
|
/** @property {number} - Cone for start particle angle */
|
|
154
154
|
this.particleConeAngle = particleConeAngle;
|
|
155
|
-
/** @property {number} -
|
|
155
|
+
/** @property {number} - Fraction of life spent fading, split half at start and half at end (e.g. .2 = 10% fade-in + 10% fade-out) */
|
|
156
156
|
this.fadeRate = fadeRate;
|
|
157
157
|
/** @property {number} - Apply extra randomness percent */
|
|
158
158
|
this.randomness = randomness;
|
|
@@ -430,33 +430,32 @@ class Particle
|
|
|
430
430
|
const hitLayer = tileCollisionTest(this.pos);
|
|
431
431
|
if (!testCollision(oldPos))
|
|
432
432
|
{
|
|
433
|
-
|
|
433
|
+
// testCollision already invoked collideCallback with the
|
|
434
|
+
// correct (this, data, pos) args; no need to re-check here.
|
|
435
|
+
// test which side we bounced off (or both if a corner)
|
|
436
|
+
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
|
|
437
|
+
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
|
|
438
|
+
const hitRestitution = max(restitution, hitLayer.restitution);
|
|
439
|
+
const hitFriction = max(friction, hitLayer.friction);
|
|
440
|
+
if (isBlockedX)
|
|
434
441
|
{
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const hitFriction = max(friction, hitLayer.friction);
|
|
440
|
-
if (isBlockedX)
|
|
441
|
-
{
|
|
442
|
-
// move to previous X position and bounce
|
|
443
|
-
this.pos.x = oldPos.x;
|
|
444
|
-
this.velocity.x *= -hitRestitution;
|
|
445
|
-
this.velocity.y *= hitFriction;
|
|
446
|
-
}
|
|
447
|
-
if (isBlockedY || !isBlockedX)
|
|
448
|
-
{
|
|
449
|
-
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
450
|
-
if (wasFalling)
|
|
451
|
-
this.groundObject = hitLayer;
|
|
452
|
-
|
|
453
|
-
// move to previous Y position and bounce
|
|
454
|
-
this.pos.y = oldPos.y;
|
|
455
|
-
this.velocity.y *= -hitRestitution;
|
|
456
|
-
this.velocity.x *= hitFriction;
|
|
457
|
-
}
|
|
458
|
-
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
442
|
+
// move to previous X position and bounce
|
|
443
|
+
this.pos.x = oldPos.x;
|
|
444
|
+
this.velocity.x *= -hitRestitution;
|
|
445
|
+
this.velocity.y *= hitFriction;
|
|
459
446
|
}
|
|
447
|
+
if (isBlockedY || !isBlockedX)
|
|
448
|
+
{
|
|
449
|
+
const wasFalling = this.velocity.y < 0 && gravity.y < 0 || this.velocity.y > 0 && gravity.y > 0;
|
|
450
|
+
if (wasFalling)
|
|
451
|
+
this.groundObject = hitLayer;
|
|
452
|
+
|
|
453
|
+
// move to previous Y position and bounce
|
|
454
|
+
this.pos.y = oldPos.y;
|
|
455
|
+
this.velocity.y *= -hitRestitution;
|
|
456
|
+
this.velocity.x *= hitFriction;
|
|
457
|
+
}
|
|
458
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
460
459
|
}
|
|
461
460
|
}
|
|
462
461
|
}
|
package/src/engineSettings.js
CHANGED
|
@@ -51,7 +51,7 @@ let canvasColorTiles = true;
|
|
|
51
51
|
|
|
52
52
|
/** Color to clear the canvas to before render, does not clear if alpha is 0
|
|
53
53
|
* @type {Color}
|
|
54
|
-
* @memberof
|
|
54
|
+
* @memberof Settings */
|
|
55
55
|
let canvasClearColor = CLEAR_BLACK;
|
|
56
56
|
|
|
57
57
|
/** The max size of the canvas, centered if window is larger
|
|
@@ -251,7 +251,8 @@ let touchInputEnable = true;
|
|
|
251
251
|
let touchGamepadEnable = false;
|
|
252
252
|
|
|
253
253
|
/** True if touch gamepad should have start button in the center
|
|
254
|
-
* - Prevents activating
|
|
254
|
+
* - Prevents activating within 2*touchGamepadSize of the virtual stick or face buttons
|
|
255
|
+
* (one radius for the visible control + one radius of buffer beyond its edge)
|
|
255
256
|
* - When the game is paused, any touch will press the button
|
|
256
257
|
* - Set size to enable the center button
|
|
257
258
|
* @type {number}
|
package/src/engineTileLayer.js
CHANGED
|
@@ -74,14 +74,29 @@ function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
|
74
74
|
* @memberof TileLayers */
|
|
75
75
|
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
76
76
|
{
|
|
77
|
+
// check every layer and keep the closest hit so a far hit in an
|
|
78
|
+
// earlier-registered layer doesn't shadow a closer hit in a later one
|
|
79
|
+
let closestHit, closestDistSq, closestNormal;
|
|
80
|
+
const scratchNormal = normal && vec2();
|
|
77
81
|
for (const layer of tileCollisionLayers)
|
|
78
82
|
{
|
|
79
83
|
if (!solidOnly || layer.isSolid)
|
|
80
84
|
{
|
|
81
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject,
|
|
82
|
-
if (hitPos)
|
|
85
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
|
|
86
|
+
if (hitPos)
|
|
87
|
+
{
|
|
88
|
+
const d = posStart.distanceSquared(hitPos);
|
|
89
|
+
if (closestHit === undefined || d < closestDistSq)
|
|
90
|
+
{
|
|
91
|
+
closestHit = hitPos;
|
|
92
|
+
closestDistSq = d;
|
|
93
|
+
if (normal) closestNormal = scratchNormal.copy();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
}
|
|
98
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
99
|
+
return closestHit;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -255,38 +270,6 @@ class CanvasLayer extends EngineObject
|
|
|
255
270
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
/** Draw a tile onto the layer canvas in world space
|
|
259
|
-
* @param {Vector2} pos
|
|
260
|
-
* @param {Vector2} [size=vec2(1)]
|
|
261
|
-
* @param {TileInfo} [tileInfo]
|
|
262
|
-
* @param {Color} [color=WHITE]
|
|
263
|
-
* @param {number} [angle]
|
|
264
|
-
* @param {boolean} [mirror] */
|
|
265
|
-
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
266
|
-
{
|
|
267
|
-
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
268
|
-
size = size.multiply(this.tileInfo.size);
|
|
269
|
-
pos.y = this.canvas.height - pos.y;
|
|
270
|
-
|
|
271
|
-
// draw the tile onto the layer canvas
|
|
272
|
-
const oldMainCanvasSize = mainCanvasSize;
|
|
273
|
-
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
274
|
-
const useWebGL = this.hasWebGL();
|
|
275
|
-
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
276
|
-
const drawContext = useWebGL ? undefined : this.context;
|
|
277
|
-
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
278
|
-
useWebGL && glSetRenderTarget();
|
|
279
|
-
mainCanvasSize = oldMainCanvasSize;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** Draw a rectangle onto the layer canvas in world space
|
|
283
|
-
* @param {Vector2} pos
|
|
284
|
-
* @param {Vector2} [size=vec2(1)]
|
|
285
|
-
* @param {Color} [color=WHITE]
|
|
286
|
-
* @param {number} [angle] */
|
|
287
|
-
drawRect(pos, size, color, angle)
|
|
288
|
-
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
289
|
-
|
|
290
273
|
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
291
274
|
updateWebGL()
|
|
292
275
|
{ this.textureInfo.createWebGLTexture(); }
|
|
@@ -342,6 +325,8 @@ class TileLayer extends CanvasLayer
|
|
|
342
325
|
this.redrawTileData = ()=> {};
|
|
343
326
|
this.drawLayerTile = ()=> {};
|
|
344
327
|
this.drawLayerRect = ()=> {};
|
|
328
|
+
this.drawTile = ()=> {};
|
|
329
|
+
this.drawRect = ()=> {};
|
|
345
330
|
this.clearLayerRect = ()=> {};
|
|
346
331
|
return;
|
|
347
332
|
}
|
|
@@ -369,7 +354,7 @@ class TileLayer extends CanvasLayer
|
|
|
369
354
|
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
370
355
|
|
|
371
356
|
if (!layerPos.arrayCheck(this.size)) return;
|
|
372
|
-
this.data[(layerPos.y|0)*this.size.x+layerPos.x|0] = data;
|
|
357
|
+
this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
|
|
373
358
|
|
|
374
359
|
if (!redraw) return;
|
|
375
360
|
const isRedraw = drawContext === this.context;
|
|
@@ -384,11 +369,11 @@ class TileLayer extends CanvasLayer
|
|
|
384
369
|
|
|
385
370
|
/** Get data at a given position in the array
|
|
386
371
|
* @param {Vector2} layerPos - Local position in array
|
|
387
|
-
* @return {TileLayerData} */
|
|
372
|
+
* @return {TileLayerData|undefined} */
|
|
388
373
|
getData(layerPos)
|
|
389
|
-
{
|
|
374
|
+
{
|
|
390
375
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
391
|
-
return layerPos.arrayCheck(this.size)
|
|
376
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
392
377
|
}
|
|
393
378
|
|
|
394
379
|
// Update the tile layer, refresh texture if needed
|
|
@@ -496,7 +481,7 @@ class TileLayer extends CanvasLayer
|
|
|
496
481
|
|
|
497
482
|
// draw the tile if it has layer data
|
|
498
483
|
const d = this.getData(layerPos);
|
|
499
|
-
if (!d.tile) return;
|
|
484
|
+
if (!d || !d.tile) return;
|
|
500
485
|
|
|
501
486
|
const tileInfo = this.tileInfo && this.tileInfo.tile(d.tile);
|
|
502
487
|
this.drawLayerTile(drawPos, drawSize, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
@@ -542,6 +527,38 @@ class TileLayer extends CanvasLayer
|
|
|
542
527
|
drawLayerRect(pos, size, color, angle=0)
|
|
543
528
|
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
544
529
|
|
|
530
|
+
/** Draw a tile onto the layer canvas in world space
|
|
531
|
+
* @param {Vector2} pos
|
|
532
|
+
* @param {Vector2} [size=vec2(1)]
|
|
533
|
+
* @param {TileInfo} [tileInfo]
|
|
534
|
+
* @param {Color} [color=WHITE]
|
|
535
|
+
* @param {number} [angle]
|
|
536
|
+
* @param {boolean} [mirror] */
|
|
537
|
+
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
538
|
+
{
|
|
539
|
+
pos = pos.subtract(this.pos).multiply(this.tileInfo.size);
|
|
540
|
+
size = size.multiply(this.tileInfo.size);
|
|
541
|
+
pos.y = this.canvas.height - pos.y;
|
|
542
|
+
|
|
543
|
+
// draw the tile onto the layer canvas
|
|
544
|
+
const oldMainCanvasSize = mainCanvasSize;
|
|
545
|
+
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
546
|
+
const useWebGL = this.hasWebGL();
|
|
547
|
+
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
548
|
+
const drawContext = useWebGL ? undefined : this.context;
|
|
549
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
550
|
+
useWebGL && glSetRenderTarget();
|
|
551
|
+
mainCanvasSize = oldMainCanvasSize;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Draw a rectangle onto the layer canvas in world space
|
|
555
|
+
* @param {Vector2} pos
|
|
556
|
+
* @param {Vector2} [size=vec2(1)]
|
|
557
|
+
* @param {Color} [color=WHITE]
|
|
558
|
+
* @param {number} [angle] */
|
|
559
|
+
drawRect(pos, size, color, angle)
|
|
560
|
+
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
561
|
+
|
|
545
562
|
/** Clear a rectangle in layer space
|
|
546
563
|
* @param {Vector2} pos - position in pixel coordinates
|
|
547
564
|
* @param {Vector2} size
|
|
@@ -620,7 +637,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
620
637
|
setCollisionData(layerPos, data=1)
|
|
621
638
|
{
|
|
622
639
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
623
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
640
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
624
641
|
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
625
642
|
}
|
|
626
643
|
|
|
@@ -635,7 +652,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
635
652
|
getCollisionData(layerPos)
|
|
636
653
|
{
|
|
637
654
|
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
638
|
-
const i = (layerPos.y|0)*this.size.x + layerPos.x|0;
|
|
655
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
639
656
|
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
640
657
|
}
|
|
641
658
|
|
|
@@ -660,8 +677,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
660
677
|
const posY = pos.y - this.pos.y;
|
|
661
678
|
const minX = max(posX - size.x/2|0, 0);
|
|
662
679
|
const minY = max(posY - size.y/2|0, 0);
|
|
663
|
-
|
|
664
|
-
|
|
680
|
+
// ensure at least one cell is visited even when size is 0 and pos
|
|
681
|
+
// lands exactly on an integer boundary (documented point-test mode)
|
|
682
|
+
const maxX = min(max(posX + size.x/2, minX + 1), this.size.x);
|
|
683
|
+
const maxY = min(max(posY + size.y/2, minY + 1), this.size.y);
|
|
665
684
|
const hitPos = new Vector2;
|
|
666
685
|
for (let y = minY; y < maxY; ++y)
|
|
667
686
|
for (let x = minX; x < maxX; ++x)
|
package/src/engineUtilities.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - File saving (text, canvas, data URLs)
|
|
7
7
|
* - Native share dialog support
|
|
8
8
|
* - Local storage save data management
|
|
9
|
+
* - Gradient noise (1D and 2D)
|
|
9
10
|
* @namespace Utilities
|
|
10
11
|
*/
|
|
11
12
|
|
|
@@ -72,9 +73,15 @@ class Timer
|
|
|
72
73
|
* @return {number} */
|
|
73
74
|
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
74
75
|
|
|
75
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
76
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
77
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
76
78
|
* @return {number} */
|
|
77
|
-
getPercent()
|
|
79
|
+
getPercent()
|
|
80
|
+
{
|
|
81
|
+
if (!this.isSet()) return 0;
|
|
82
|
+
if (!this.setTime) return 1;
|
|
83
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
/** Get the time this timer was set to, returns 0 if not set
|
|
80
87
|
* @return {number} */
|
|
@@ -101,9 +108,9 @@ class Timer
|
|
|
101
108
|
* @memberof Utilities */
|
|
102
109
|
function formatTime(t)
|
|
103
110
|
{
|
|
104
|
-
const
|
|
111
|
+
const signStr = t < 0 ? '-' : '';
|
|
105
112
|
t = abs(t)|0;
|
|
106
|
-
return
|
|
113
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
@@ -189,15 +196,20 @@ function shareURL(title, url, callback)
|
|
|
189
196
|
function readSaveData(saveName, defaultSaveData)
|
|
190
197
|
{
|
|
191
198
|
ASSERT(isStringLike(saveName), 'loadData requires saveName string');
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
199
|
+
|
|
200
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
201
|
+
// iframes) and corrupt JSON in stored data
|
|
195
202
|
let loadedData = {};
|
|
196
|
-
|
|
203
|
+
try
|
|
197
204
|
{
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
const data = localStorage[saveName];
|
|
206
|
+
if (data)
|
|
207
|
+
{
|
|
208
|
+
try { loadedData = JSON.parse(data); }
|
|
209
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
210
|
+
}
|
|
200
211
|
}
|
|
212
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
201
213
|
return { ...defaultSaveData, ...loadedData };
|
|
202
214
|
}
|
|
203
215
|
|
|
@@ -208,5 +220,49 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
208
220
|
function writeSaveData(saveName, saveData)
|
|
209
221
|
{
|
|
210
222
|
ASSERT(isStringLike(saveName), 'saveData requires saveName string');
|
|
211
|
-
localStorage
|
|
223
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
224
|
+
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
225
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
229
|
+
|
|
230
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
231
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
232
|
+
function noiseHash(i)
|
|
233
|
+
{
|
|
234
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
235
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
236
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
237
|
+
h ^= h >>> 16;
|
|
238
|
+
return (h >>> 0) / 2**32;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 1D gradient noise — returns a smooth value in [0, 1] for any real x.
|
|
242
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
243
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
244
|
+
* @param {number} x
|
|
245
|
+
* @return {number}
|
|
246
|
+
* @memberof Utilities */
|
|
247
|
+
function noise1D(x)
|
|
248
|
+
{
|
|
249
|
+
const i = floor(x);
|
|
250
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 2D gradient noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
254
|
+
* @param {number} x
|
|
255
|
+
* @param {number} y
|
|
256
|
+
* @return {number}
|
|
257
|
+
* @memberof Utilities */
|
|
258
|
+
function noise2D(x, y)
|
|
259
|
+
{
|
|
260
|
+
const ix = floor(x), iy = floor(y);
|
|
261
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
262
|
+
// large prime decorrelates neighboring rows
|
|
263
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
264
|
+
return lerp(
|
|
265
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
266
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
267
|
+
fy);
|
|
212
268
|
}
|
package/src/engineWebGL.js
CHANGED
|
@@ -88,6 +88,10 @@ function glInit(rootElement)
|
|
|
88
88
|
for (const info of glTextureInfos)
|
|
89
89
|
info.glTexture = undefined;
|
|
90
90
|
glActiveTexture = undefined;
|
|
91
|
+
// drop any partially-filled batch so the next glFlush doesn't
|
|
92
|
+
// upload stale glBatchCount against fresh empty buffers on restore
|
|
93
|
+
glBatchCount = 0;
|
|
94
|
+
glPolyMode = false;
|
|
91
95
|
pluginList.forEach(plugin=>plugin.glContextLost?.());
|
|
92
96
|
});
|
|
93
97
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
@@ -446,6 +450,10 @@ function glSetTextureData(texture, image)
|
|
|
446
450
|
glContext.bindTexture(glContext.TEXTURE_2D, texture);
|
|
447
451
|
glContext.texImage2D(glContext.TEXTURE_2D, 0, glContext.RGBA, glContext.RGBA, glContext.UNSIGNED_BYTE, image);
|
|
448
452
|
|
|
453
|
+
// keep mipmaps in sync with new level 0 data (same condition as glCreateTexture)
|
|
454
|
+
if (!tilesPixelated && isPowerOfTwo(image.width) && isPowerOfTwo(image.height))
|
|
455
|
+
glContext.generateMipmap(glContext.TEXTURE_2D);
|
|
456
|
+
|
|
449
457
|
// rebind active texture
|
|
450
458
|
glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
|
|
451
459
|
}
|
|
@@ -491,7 +499,7 @@ function glFlush()
|
|
|
491
499
|
{
|
|
492
500
|
if (glEnable && glContext && glBatchCount)
|
|
493
501
|
{
|
|
494
|
-
// set
|
|
502
|
+
// set blend mode
|
|
495
503
|
const destBlend = glBatchAdditive ? glContext.ONE : glContext.ONE_MINUS_SRC_ALPHA;
|
|
496
504
|
glContext.blendFuncSeparate(glContext.SRC_ALPHA, destBlend, glContext.ONE, destBlend);
|
|
497
505
|
glContext.enable(glContext.BLEND);
|
|
@@ -505,7 +513,8 @@ function glFlush()
|
|
|
505
513
|
glContext.drawArrays(glContext.TRIANGLE_STRIP, 0, glBatchCount);
|
|
506
514
|
else
|
|
507
515
|
glContext.drawArraysInstanced(glContext.TRIANGLE_STRIP, 0, 4, glBatchCount);
|
|
508
|
-
drawCount
|
|
516
|
+
++drawCount;
|
|
517
|
+
primitiveCount += glBatchCount;
|
|
509
518
|
glBatchCount = 0;
|
|
510
519
|
}
|
|
511
520
|
glBatchAdditive = glAdditive;
|
|
@@ -566,6 +575,22 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
566
575
|
glPositionData[offset++] = angle;
|
|
567
576
|
}
|
|
568
577
|
|
|
578
|
+
/** Add an untextured rect to the gl draw list
|
|
579
|
+
* Zeroes the uvs and rgba so the texture contribution multiplies to 0,
|
|
580
|
+
* then carries the real color in the additive slot. Works regardless of
|
|
581
|
+
* which texture is currently bound.
|
|
582
|
+
* @param {number} x
|
|
583
|
+
* @param {number} y
|
|
584
|
+
* @param {number} sizeX
|
|
585
|
+
* @param {number} sizeY
|
|
586
|
+
* @param {number} angle
|
|
587
|
+
* @param {number} rgba - color as 32-bit integer
|
|
588
|
+
* @memberof WebGL */
|
|
589
|
+
function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
590
|
+
{
|
|
591
|
+
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
592
|
+
}
|
|
593
|
+
|
|
569
594
|
/** Transform and add a polygon to the gl draw list
|
|
570
595
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
571
596
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -579,13 +604,13 @@ function glDraw(x, y, sizeX, sizeY, angle=0, uv0X=0, uv0Y=0, uv1X=1, uv1Y=1, rgb
|
|
|
579
604
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
580
605
|
{
|
|
581
606
|
const pointsOut = [];
|
|
607
|
+
const sa = sin(-angle);
|
|
608
|
+
const ca = cos(-angle);
|
|
582
609
|
for (const p of points)
|
|
583
610
|
{
|
|
584
611
|
// transform the point
|
|
585
612
|
const px = p.x*sx;
|
|
586
613
|
const py = p.y*sy;
|
|
587
|
-
const sa = sin(-angle);
|
|
588
|
-
const ca = cos(-angle);
|
|
589
614
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
590
615
|
}
|
|
591
616
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -617,11 +642,13 @@ function glDrawPoints(points, rgba)
|
|
|
617
642
|
{
|
|
618
643
|
if (!glEnable || points.length < 3)
|
|
619
644
|
return; // needs at least 3 points to have area
|
|
620
|
-
|
|
645
|
+
|
|
621
646
|
// flush if there is not enough room or if different blend mode
|
|
622
647
|
const vertCount = points.length + 2;
|
|
623
648
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
624
649
|
glFlush();
|
|
650
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
651
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
625
652
|
glSetPolyMode();
|
|
626
653
|
|
|
627
654
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -645,11 +672,13 @@ function glDrawColoredPoints(points, pointColors)
|
|
|
645
672
|
{
|
|
646
673
|
if (!glEnable || points.length < 3)
|
|
647
674
|
return; // needs at least 3 points to have area
|
|
648
|
-
|
|
675
|
+
|
|
649
676
|
// flush if there is not enough room or if different blend mode
|
|
650
677
|
const vertCount = points.length + 2;
|
|
651
678
|
if (glBatchCount+vertCount >= gl_MAX_POLY_VERTEXES || glBatchAdditive !== glAdditive)
|
|
652
679
|
glFlush();
|
|
680
|
+
ASSERT(vertCount < gl_MAX_POLY_VERTEXES, 'poly exceeds max batch size');
|
|
681
|
+
if (vertCount >= gl_MAX_POLY_VERTEXES) return; // release-build safety net
|
|
653
682
|
glSetPolyMode();
|
|
654
683
|
|
|
655
684
|
// setup triangle strip with degenerate verts at start and end
|
|
@@ -719,7 +748,8 @@ function glMakeOutline(points, width, wrap=true)
|
|
|
719
748
|
const strip = [];
|
|
720
749
|
const n = points.length;
|
|
721
750
|
const e = 1e-6;
|
|
722
|
-
|
|
751
|
+
// miter ratio cap (dimensionless, matches SVG/Canvas2D convention)
|
|
752
|
+
const miterLimit = 10;
|
|
723
753
|
for (let i = 0; i < n; i++)
|
|
724
754
|
{
|
|
725
755
|
// for each vertex, calculate normal based on adjacent edges
|