littlejsengine 1.24.1 → 1.24.3

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +78 -1
  2. package/EDITOR.md +2 -0
  3. package/FAQ.md +13 -2
  4. package/README.md +1 -1
  5. package/REFERENCE.md +22 -7
  6. package/dist/littlejs.d.ts +204 -118
  7. package/dist/littlejs.esm.js +2479 -1983
  8. package/dist/littlejs.esm.min.js +3 -3
  9. package/dist/littlejs.js +2476 -1983
  10. package/dist/littlejs.min.js +2 -2
  11. package/dist/littlejs.release.js +2434 -1990
  12. package/package.json +1 -1
  13. package/plugins/box2d.js +9 -5
  14. package/plugins/gltf.js +67 -9
  15. package/plugins/lightSystem.js +1 -1
  16. package/plugins/math3d.js +1014 -1013
  17. package/plugins/medalSystem.js +7 -11
  18. package/plugins/particleEffects.js +2 -2
  19. package/plugins/pathFinder.js +2 -2
  20. package/plugins/pluginExport.js +1 -0
  21. package/plugins/render3d.js +131 -55
  22. package/plugins/render3dEditor.js +24 -10
  23. package/plugins/render3dExtras.js +41 -25
  24. package/plugins/render3dLevel.js +21 -12
  25. package/plugins/render3dVoxels.js +2 -2
  26. package/plugins/textureSheet.js +109 -7
  27. package/plugins/tweakables.js +1 -1
  28. package/plugins/tweenSystem.js +2 -2
  29. package/plugins/uiSystem.js +6 -2
  30. package/src/engine.js +60 -23
  31. package/src/engineAudio.js +16 -6
  32. package/src/engineBuild.mjs +49 -2
  33. package/src/engineDebug.js +4 -1
  34. package/src/engineDraw.js +40 -25
  35. package/src/engineEditor.js +82 -50
  36. package/src/engineExport.js +2 -0
  37. package/src/engineInput.js +21 -4
  38. package/src/engineMath.js +6 -2
  39. package/src/engineObject.js +675 -675
  40. package/src/engineParticles.js +1 -1
  41. package/src/engineSettings.js +16 -0
  42. package/src/engineTileLayer.js +85 -22
  43. package/src/engineUtilities.js +20 -8
  44. package/src/engineWebGL.js +15 -11
package/src/engine.js CHANGED
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
32
32
  * @type {string}
33
33
  * @default
34
34
  * @memberof Engine */
35
- const engineVersion = '1.24.1';
35
+ const engineVersion = '1.24.3';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -165,6 +165,7 @@ let timeFixedStart = 0, frameFixedStart = 0;
165
165
  let windowWidthLast = 0, windowHeightLast = 0, windowPixelRatioLast = 0;
166
166
  let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
167
167
  let engineFrameScheduled = false; // a frame of the loop is asked for and has not run yet
168
+ let engineFrameErrorLast; // the last error a release build's loop went on past, as text
168
169
 
169
170
  // the pairs of objects asked about a collision this update, so the other's own physics does not ask again: each
170
171
  // asker's others, with true for a pair both said to resolve, and false for one left overlapping, ignored or only
@@ -418,20 +419,30 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
418
419
  frameTimeBufferMS = 0;
419
420
  }
420
421
 
421
- // update multiple frames if necessary in case of slow framerate
422
- for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
422
+ // update multiple frames if necessary in case of slow framerate; a tick that throws still spends its time
423
+ // and the smoothing still comes back, so a release build going on past an error each tick keeps its rate
424
+ try
423
425
  {
424
- // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
425
- const frozenTick = paused || !(timeScale * debugScale);
426
-
427
- // increment frame and update time, frozen does not advance time
428
- if (!frozenTick)
429
- time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
430
- engineTick(frozenTick);
426
+ while (frameTimeBufferMS >= 0)
427
+ {
428
+ try
429
+ {
430
+ // read again each tick, so a pause set by the game stops the rest of this frame's catch-up ticks
431
+ const frozenTick = paused || !(timeScale * debugScale);
432
+
433
+ // increment frame and update time, frozen does not advance time
434
+ if (!frozenTick)
435
+ time = timeFixedStart + (frame++ - frameFixedStart) / frameRate;
436
+ engineTick(frozenTick);
437
+ }
438
+ finally { frameTimeBufferMS -= 1e3 / frameRate; }
439
+ }
440
+ }
441
+ finally
442
+ {
443
+ // add the time smoothing back in
444
+ frameTimeBufferMS += deltaSmooth;
431
445
  }
432
-
433
- // add the time smoothing back in
434
- frameTimeBufferMS += deltaSmooth;
435
446
  }
436
447
 
437
448
  // one tick of the loop: update game and objects, when frozen update everything except them
@@ -450,7 +461,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
450
461
  o.parent || o.updateTransforms();
451
462
 
452
463
  // objects made and destroyed while paused, like a menu's effects, still leave the list
453
- engineObjects = engineObjects.filter(o=>!o.destroyed);
464
+ engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
454
465
  }
455
466
  else
456
467
  engineObjectsUpdate();
@@ -497,7 +508,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
497
508
  // render the game and objects
498
509
  enginePreRender();
499
510
  gameRender();
500
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
511
+ engineObjectsSort();
501
512
  for (const o of engineObjects)
502
513
  {
503
514
  if (o.destroyed) continue;
@@ -600,7 +611,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
600
611
  }
601
612
 
602
613
  // the splash first, the images load under it, then the loading screen for the rest
603
- showSplashScreen && await promises.at(-1);
614
+ showSplashScreen && await promises[promises.length - 1];
604
615
  return startEngine(promises);
605
616
 
606
617
  // gameInit runs once the images are in, and the game loop starts once it and everything loaded while it ran are
@@ -723,7 +734,20 @@ function engineScheduleFrame()
723
734
  const next = (frameTimeMS)=>
724
735
  {
725
736
  engineFrameScheduled = false;
726
- engineManualStep || engineUpdateInternal(frameTimeMS);
737
+ if (engineManualStep) return;
738
+ if (debug) return engineUpdateInternal(frameTimeMS); // a debug build stops at an error, where it shows it
739
+ try { engineUpdateInternal(frameTimeMS); }
740
+ catch (error)
741
+ {
742
+ // a release build goes on past an error in a frame, a frozen game is the worst a player can get; an error
743
+ // is logged when it is not the last one again, one every frame would flood the console
744
+ const text = String(error);
745
+ text === engineFrameErrorLast || console.error(error);
746
+ engineFrameErrorLast = text;
747
+ // the frame's input is cleared as its tick would have, or a key press that threw would be pressed again
748
+ inputUpdatePost();
749
+ engineScheduleFrame();
750
+ }
727
751
  };
728
752
  if (typeof requestAnimationFrame === 'function')
729
753
  requestAnimationFrame(next);
@@ -769,14 +793,18 @@ function engineObjectsUpdate()
769
793
  ++engineObjectsUpdateCount;
770
794
  engineObjectsCollidePairs.clear();
771
795
  // objects update in render order, which rendering keeps them in, so a headless run or a frame that rendered
772
- // nothing updates them the same way; the sort is stable, and nearly free on a list that is already sorted
773
- engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
796
+ // nothing updates them the same way
797
+ engineObjectsSort();
774
798
  // get list of solid objects for physics optimization, in update order, which 3D collision pairs by;
775
799
  // 2D checks the static ones last, so a contact with a moving object can not leave something back inside a static
776
800
  // solid it was already pushed out of
777
- engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
778
- engineObjectsCollideStaticLast = engineObjectsCollide.filter(o=>o.mass)
779
- .concat(engineObjectsCollide.filter(o=>!o.mass));
801
+ const fixed = [];
802
+ engineObjectsCollide = [];
803
+ engineObjectsCollideStaticLast = [];
804
+ for (const o of engineObjects)
805
+ o.collideSolidObjects && (engineObjectsCollide.push(o), (o.mass ? engineObjectsCollideStaticLast : fixed).push(o));
806
+ for (const o of fixed)
807
+ engineObjectsCollideStaticLast.push(o);
780
808
 
781
809
  // update physics before object update
782
810
  for (const o of engineObjects)
@@ -826,7 +854,16 @@ function engineObjectsUpdate()
826
854
  updateTopObject(o);
827
855
 
828
856
  // remove destroyed objects
829
- engineObjects = engineObjects.filter(o=>!o.destroyed);
857
+ engineObjects.some(o=>o.destroyed) && (engineObjects = engineObjects.filter(o=>!o.destroyed));
858
+ }
859
+
860
+ // sort the objects by render order, keeping the order of equals, only when one is out of order, as they are kept
861
+ // sorted and most frames change none
862
+ function engineObjectsSort()
863
+ {
864
+ for (let i = engineObjects.length; --i > 0;)
865
+ if (engineObjects[i].renderOrder < engineObjects[i-1].renderOrder)
866
+ return void engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
830
867
  }
831
868
 
832
869
  /** Destroy and remove all objects
@@ -58,9 +58,19 @@ function audioInit()
58
58
  {
59
59
  if (!soundEnable || headlessMode) return;
60
60
 
61
+ audioSetSession();
61
62
  document.addEventListener('visibilitychange', audioVisibilityChange);
62
63
  }
63
64
 
65
+ // on an iPhone the silent switch mutes the page's sound, unless soundIgnoreSilentSwitch has it play as media,
66
+ // Safari 16.4 and up; elsewhere there is no audioSession and nothing changes
67
+ function audioSetSession()
68
+ {
69
+ const session = typeof navigator != 'undefined' && /** @type {any} */ (navigator).audioSession;
70
+ if (session)
71
+ session.type = soundIgnoreSilentSwitch ? 'playback' : 'auto';
72
+ }
73
+
64
74
  // a hidden page stops the game, so its sound stops too, and the audio clock with it so every sound picks up
65
75
  // exactly where it was; only a suspend made here is undone, not one the browser holds until the first input
66
76
  let audioSuspendedWhenHidden = false;
@@ -203,13 +213,13 @@ function audioParamRamp(param, value, fadeTime=0)
203
213
  * @memberof Audio
204
214
  * @example
205
215
  * // load an audio asset file
206
- * const sound_example = new Sound('sound.mp3');
216
+ * const music = new Sound('sound.mp3');
207
217
  *
208
218
  * // create a zzfx sound
209
- * const sound_example = new Sound([.5,.5]);
219
+ * const blip = new Sound([.5,.5]);
210
220
  *
211
221
  * // play a sound
212
- * sound_example.play();
222
+ * blip.play();
213
223
  */
214
224
  class Sound
215
225
  {
@@ -222,7 +232,7 @@ class Sound
222
232
  */
223
233
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
224
234
  {
225
- if (!soundEnable || headlessMode)
235
+ if (!soundEnable || headlessMode || !audioContext) // a browser with no audio makes none either
226
236
  {
227
237
  // no sound is made: it counts as loaded, so a game that waits for its sounds goes on
228
238
  this.loadedPercent = 1;
@@ -292,8 +302,8 @@ class Sound
292
302
  // load the audio file, a URL object as bundlers give works like its string;
293
303
  // report failures rather than leaving an unhandled rejection, the sound just stays unloaded and silent
294
304
  const filename = asset + '';
295
- engineAddLoad(this.loadSound(filename).catch(e=>
296
- LOG('Sound load failed for', filename, '-', e.message))); // startup waits for it
305
+ engineAddLoad(this.loadSound(filename).catch(e=> // startup waits for it, and a release build says why too
306
+ console.warn('Sound load failed for', filename, '-', e?.message ?? e)));
297
307
  }
298
308
  }
299
309
 
@@ -289,17 +289,64 @@ function typeScriptBuildStep(filename)
289
289
  try
290
290
  {
291
291
  const tsFilename = join(BUILD_FOLDER, `${ENGINE_NAME}.d.ts`);
292
- execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --outFile "${tsFilename}"`);
292
+ // strictNullChecks keeps undefined in the types, where tsc would drop it from accessors and object types
293
+ // whatever the JSDoc says, so a game in strict mode can set render3D.shader = undefined and is told a
294
+ // flare may be missing; a game not in strict mode reads the same types as before
295
+ execSync(`npx -p typescript tsc "${filename}" --declaration --allowJs --emitDeclarationOnly --strictNullChecks --outFile "${tsFilename}"`);
293
296
 
294
297
  // Make declare module part use the package name littlejsengine
295
298
  let fileContent = fs.readFileSync(tsFilename, 'utf8');
296
299
  fileContent = fileContent.replace(`${ENGINE_NAME}\.esm`, 'littlejsengine')
297
- fs.writeFileSync(tsFilename, fileContent);
300
+ fs.writeFileSync(tsFilename, typeScriptOptionalTidy(fileContent));
298
301
 
299
302
  }
300
303
  catch (e) { handleError(e, 'Failed to run TypeScript build step!'); }
301
304
  };
302
305
 
306
+ // strictNullChecks writes an optional parameter or field as x?: T | undefined, which is the same as x?: T, so the
307
+ // redundant | undefined is taken off: each ?: is followed to the end of its type, the first , ; ) or line end outside
308
+ // brackets, and only that type is changed; a required x: T | undefined keeps it
309
+ function typeScriptOptionalTidy(text)
310
+ {
311
+ let out = '', i = 0;
312
+ for (let at; (at = text.indexOf('?: ', i)) >= 0;)
313
+ {
314
+ const start = at + 3;
315
+ let end = start, depth = 0;
316
+ for (; end < text.length; ++end)
317
+ {
318
+ const c = text[end];
319
+ if (c === '=' && text[end + 1] === '>') { ++end; continue; } // an arrow is not a closing bracket
320
+ if ('([{<'.includes(c)) ++depth;
321
+ else if (')]}>'.includes(c) && depth) --depth;
322
+ else if (!depth && (',;)\n'.includes(c))) break;
323
+ }
324
+ let type = typeScriptOptionalTidy(text.slice(start, end)); // and the optional fields of a record type in it
325
+ if (type.endsWith(' | undefined'))
326
+ {
327
+ type = type.slice(0, -12);
328
+ // a function type was wrapped for the union, (() => void) | undefined, and needs it no more
329
+ if (type[0] === '(' && typeScriptCloseOf(type, 0) === type.length - 1)
330
+ type = type.slice(1, -1);
331
+ }
332
+ out += text.slice(i, start) + type;
333
+ i = end;
334
+ }
335
+ return out + text.slice(i);
336
+ }
337
+
338
+ // the index of the bracket that closes the one at start
339
+ function typeScriptCloseOf(text, start)
340
+ {
341
+ let depth = 0;
342
+ for (let k = start; k < text.length; ++k)
343
+ {
344
+ if (text[k] === '(') ++depth;
345
+ else if (text[k] === ')' && !--depth) return k;
346
+ }
347
+ return -1;
348
+ }
349
+
303
350
  // display the error and exit
304
351
  function handleError(e,message)
305
352
  {
@@ -269,7 +269,10 @@ function debugShowErrors()
269
269
  {
270
270
  // replace entire page with error message
271
271
  document.body.style.cssText = 'background-color:#111;margin:8px';
272
- document.body.innerHTML = `<pre style=color:#f00;font-size:28px;white-space:pre-wrap>` + message;
272
+ const pre = document.createElement('pre'); // the message as text, a file name in it never markup
273
+ pre.style.cssText = 'color:#f00;font-size:28px;white-space:pre-wrap';
274
+ pre.textContent = String(message);
275
+ document.body.replaceChildren(pre);
273
276
  }
274
277
 
275
278
  const originalAssert = console.assert;
package/src/engineDraw.js CHANGED
@@ -134,10 +134,13 @@ function tile(index=0, size=tileDefaultSize, texture=0, padding=tileDefaultPaddi
134
134
  // create tile info object
135
135
  const textureInfo = typeof texture === 'number' ?
136
136
  textureInfos[texture] : texture;
137
- if (headlessMode && !textureInfo?.size.x)
138
- return new TileInfo(new Vector2, size.copy(), textureInfo, padding, bleed); // no image loaded, no place in it
139
- ASSERT(textureInfo instanceof TextureInfo, 'tile texture is not loaded');
140
- ASSERT(textureInfo.size.x > 0, 'tile texture is not loaded');
137
+ if (!textureInfo?.size.x)
138
+ {
139
+ // no image: headless loads none and keeps the size; an image that failed to load, whose warning named it, gives
140
+ // a tile of no size, which draws nothing; a slot never given an image is a mistake a debug build points out
141
+ ASSERT(headlessMode || textureInfo instanceof TextureInfo, 'tile texture is not loaded', texture);
142
+ return new TileInfo(new Vector2, headlessMode ? size.copy() : new Vector2, textureInfo, padding, bleed);
143
+ }
141
144
 
142
145
  // get the position of the tile
143
146
  const sizePaddedX = size.x + padding*2;
@@ -182,6 +185,10 @@ class TileInfo
182
185
  this.padding = padding;
183
186
  /** @property {TextureInfo} - The texture info for this tile */
184
187
  this.textureInfo = textureInfo;
188
+ /** @property {Array<TileInfo>|undefined} - A tile set's tiles, each wherever it was packed, as loadTiles makes
189
+ * them: a tile layer given this tile info draws its tile n from tiles[n] and not from a grid on one sheet
190
+ * @type {Array<TileInfo>|undefined} */
191
+ this.tiles = undefined;
185
192
  /** @property {number} - Shrinks tile by this many pixels to prevent neighbors bleeding */
186
193
  this.bleed = bleed;
187
194
  /** @property {number} - How many frames per row for frame(), 0 to keep frames on a single row */
@@ -601,8 +608,8 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
601
608
  if (headlessMode && !context) return; // headless has no canvas, only a context passed in is drawn to
602
609
 
603
610
  const textureInfo = tileInfo?.textureInfo;
604
- if (textureInfo && !(tileInfo.size.x && tileInfo.size.y))
605
- return; // a tile with no area draws nothing, like a sprite still loading
611
+ if (textureInfo && !(tileInfo.size.x && tileInfo.size.y && textureInfo.size.x))
612
+ return; // a tile with no area draws nothing, like a sprite still loading, and nor does an image that failed
606
613
  const bleed = tileInfo?.bleed ?? 0;
607
614
  if (useWebGL && glEnable)
608
615
  {
@@ -696,7 +703,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
696
703
  * @param {boolean} [screenSpace=drawScreenSpace]
697
704
  * @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
698
705
  * @memberof Draw */
699
- function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
706
+ function drawRectGradient(pos, size=vec2(1), colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=drawScreenSpace, context)
700
707
  {
701
708
  ASSERT(isVector2(pos), 'pos must be a vec2');
702
709
  ASSERT(isVector2(size), 'size must be a vec2');
@@ -868,13 +875,15 @@ function drawLineList(points, width=.1, color=WHITE, wrap=false, pos=vec2(), ang
868
875
  if (useWebGL && glEnable)
869
876
  {
870
877
  ASSERT(!!glContext, 'WebGL is not enabled!');
871
- let size = vec2(1);
878
+ let sx = 1, sy = 1;
872
879
  if (screenSpace)
873
880
  {
874
881
  if (glSkipScreenSpace) return;
875
- [pos, size, angle] = screenToWorldTransform(pos, size, angle);
882
+ let size;
883
+ [pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
884
+ sx = size.x, sy = size.y;
876
885
  }
877
- glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, size.x, size.y, angle, wrap);
886
+ glDrawOutlineTransform(points, color.rgbaInt(), width, pos.x, pos.y, sx, sy, angle, wrap);
878
887
  }
879
888
  else
880
889
  {
@@ -940,17 +949,22 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
940
949
  ASSERT(isVector2(size), 'size must be a vec2');
941
950
  ASSERT(isNumber(sides), 'sides must be a number');
942
951
 
943
- // build regular polygon points
944
- const points = [];
952
+ // build regular polygon points, into vectors kept for the next, as drawPoly is done with them when it returns
953
+ const points = drawRegularPolyPoints, pool = drawRegularPolyPool;
954
+ points.length = 0;
945
955
  const sizeX = size.x/2, sizeY = size.y/2;
946
956
  for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
947
957
  {
948
- const a = (i/sides)*PI*2;
949
- points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
958
+ const a = (i/sides)*PI*2, point = pool[points.length] ||= vec2();
959
+ point.x = sin(a)*sizeX, point.y = cos(a)*sizeY;
960
+ points.push(point);
950
961
  }
951
962
  drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
952
963
  }
953
964
 
965
+ // the points drawRegularPoly draws and the vectors it writes them into
966
+ const drawRegularPolyPoints = [], drawRegularPolyPool = [];
967
+
954
968
  /** Draw colored polygon using passed in points
955
969
  * - WebGL fills a polygon whose edges do not cross, concave or not; a self crossing one, like a star through its
956
970
  * outer points, fills wrong there, so draw it as its simple outline or in parts
@@ -978,15 +992,17 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
978
992
  if (useWebGL && glEnable)
979
993
  {
980
994
  ASSERT(!!glContext, 'WebGL is not enabled!');
981
- let size = vec2(1);
995
+ let sx = 1, sy = 1;
982
996
  if (screenSpace)
983
997
  {
984
998
  if (glSkipScreenSpace) return;
985
- [pos, size, angle] = screenToWorldTransform(pos, size, angle);
999
+ let size;
1000
+ [pos, size, angle] = screenToWorldTransform(pos, vec2(1), angle);
1001
+ sx = size.x, sy = size.y;
986
1002
  }
987
- glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, size.x, size.y, angle);
1003
+ glDrawPointsTransform(points, color.rgbaInt(), pos.x, pos.y, sx, sy, angle);
988
1004
  if (lineWidth > 0)
989
- glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, size.x, size.y, angle);
1005
+ glDrawOutlineTransform(points, lineColor.rgbaInt(), lineWidth, pos.x, pos.y, sx, sy, angle);
990
1006
  }
991
1007
  else
992
1008
  {
@@ -1631,7 +1647,7 @@ function tintImageData(data, color, additiveColor)
1631
1647
  if (additiveColor && !isBlack(additiveColor))
1632
1648
  {
1633
1649
  // multiply + additive (slower), color.a is baked into the alpha channel here
1634
- const colorMultiply = [color.r, color.g, color.b, color.a];
1650
+ const colorMultiply = [clamp(color.r), clamp(color.g), clamp(color.b), clamp(color.a)];
1635
1651
  const colorAdd = [additiveColor.r * 255, additiveColor.g * 255,
1636
1652
  additiveColor.b * 255, additiveColor.a * 255];
1637
1653
  for (let i = 0; i < data.length; ++i)
@@ -1642,9 +1658,9 @@ function tintImageData(data, color, additiveColor)
1642
1658
  // RGB only, faster — alpha left intact for the caller
1643
1659
  for (let i = 0; i < data.length; i+=4)
1644
1660
  {
1645
- data[i ] *= color.r;
1646
- data[i+1] *= color.g;
1647
- data[i+2] *= color.b;
1661
+ data[i ] *= clamp(color.r);
1662
+ data[i+1] *= clamp(color.g);
1663
+ data[i+2] *= clamp(color.b);
1648
1664
  }
1649
1665
  return false;
1650
1666
  }
@@ -1682,8 +1698,7 @@ function bakeTintedImage(image, color, additiveColor)
1682
1698
  * @param {number} dHeight
1683
1699
  * @param {Color} color
1684
1700
  * @param {Color} [additiveColor]
1685
- * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding
1686
- * @memberof Draw */
1701
+ * @param {number} [bleed] - How many pixels to shrink the source, used to fix bleeding */
1687
1702
  function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight, color, additiveColor, bleed=0)
1688
1703
  {
1689
1704
  const sx2 = bleed;
@@ -1896,7 +1911,7 @@ function engineGlowTexture(falloff=1)
1896
1911
  ASSERT(isNumber(falloff) && falloff > 0, 'glowFalloff must be a number above 0');
1897
1912
  const key = max(round(falloff * 10), 1) / 10;
1898
1913
  let texture = engineGlowTextures.get(key);
1899
- if (texture || !glContext || typeof OffscreenCanvas == 'undefined') return texture;
1914
+ if (texture || !glContext || !canvasAvailable()) return texture;
1900
1915
  const size = 64, context = createCanvasContext(size), steps = 16;
1901
1916
  const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
1902
1917
  for (let i = 0; i <= steps; ++i)