littlejsengine 1.18.4 → 1.18.8

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 (176) hide show
  1. package/COPYRIGHT.txt +38 -0
  2. package/FAQ.md +633 -0
  3. package/README.md +19 -1
  4. package/dist/littlejs.d.ts +354 -55
  5. package/dist/littlejs.esm.js +1531 -489
  6. package/dist/littlejs.esm.min.js +1 -1
  7. package/dist/littlejs.js +1499 -476
  8. package/dist/littlejs.min.js +1 -1
  9. package/dist/littlejs.release.js +1493 -470
  10. package/package.json +8 -1
  11. package/plugins/box2d.js +32 -7
  12. package/{src/engineMedals.js → plugins/medalSystem.js} +248 -196
  13. package/plugins/pathFinder.js +758 -0
  14. package/plugins/pluginExport.js +19 -0
  15. package/plugins/{tween.js → tweenSystem.js} +7 -7
  16. package/plugins/uiSystem.js +186 -25
  17. package/src/engine.js +2 -141
  18. package/src/engineAudio.js +3 -3
  19. package/src/engineBuild.mjs +4 -2
  20. package/src/engineDebug.js +7 -7
  21. package/src/engineDraw.js +26 -8
  22. package/src/engineExport.js +13 -13
  23. package/src/engineInput.js +8 -8
  24. package/src/engineLogo.js +146 -0
  25. package/src/engineMath.js +15 -6
  26. package/src/engineObject.js +11 -5
  27. package/src/engineParticles.js +6 -3
  28. package/src/engineRelease.js +1 -1
  29. package/src/engineSettings.js +0 -47
  30. package/src/engineUtilities.js +13 -8
  31. package/src/engineWebGL.js +34 -7
  32. package/src/jsconfig.json +3 -1
  33. package/.github/workflows/test.yml +0 -17
  34. package/AI.md +0 -173
  35. package/CLAUDE.md +0 -1
  36. package/examples/box2d/game.js +0 -192
  37. package/examples/box2d/gameObjects.js +0 -564
  38. package/examples/box2d/index.html +0 -9
  39. package/examples/box2d/scenes.js +0 -194
  40. package/examples/box2d/tiles.png +0 -0
  41. package/examples/breakout/game.js +0 -175
  42. package/examples/breakout/gameObjects.js +0 -147
  43. package/examples/breakout/index.html +0 -8
  44. package/examples/breakout/tiles.png +0 -0
  45. package/examples/breakoutTutorial/README.md +0 -521
  46. package/examples/breakoutTutorial/game.js +0 -191
  47. package/examples/breakoutTutorial/images/1.png +0 -0
  48. package/examples/breakoutTutorial/images/10.png +0 -0
  49. package/examples/breakoutTutorial/images/11.png +0 -0
  50. package/examples/breakoutTutorial/images/2.png +0 -0
  51. package/examples/breakoutTutorial/images/3.png +0 -0
  52. package/examples/breakoutTutorial/images/4.png +0 -0
  53. package/examples/breakoutTutorial/images/5.png +0 -0
  54. package/examples/breakoutTutorial/images/6.png +0 -0
  55. package/examples/breakoutTutorial/images/7.png +0 -0
  56. package/examples/breakoutTutorial/images/8.png +0 -0
  57. package/examples/breakoutTutorial/images/9.png +0 -0
  58. package/examples/breakoutTutorial/index.html +0 -8
  59. package/examples/electron/build.mjs +0 -127
  60. package/examples/electron/electron.js +0 -35
  61. package/examples/electron/game.js +0 -54
  62. package/examples/electron/index.html +0 -13
  63. package/examples/electron/package.json +0 -5
  64. package/examples/electron/tiles.png +0 -0
  65. package/examples/empty/game.js +0 -51
  66. package/examples/empty/index.html +0 -5
  67. package/examples/empty/tiles.png +0 -0
  68. package/examples/favicon.png +0 -0
  69. package/examples/games.jpg +0 -0
  70. package/examples/htmlMenu/game.js +0 -82
  71. package/examples/htmlMenu/index.html +0 -45
  72. package/examples/htmlMenu/tiles.png +0 -0
  73. package/examples/index.html +0 -62
  74. package/examples/logo.png +0 -0
  75. package/examples/logo2.png +0 -0
  76. package/examples/module/build.mjs +0 -124
  77. package/examples/module/game.js +0 -132
  78. package/examples/module/index.html +0 -10
  79. package/examples/module/tiles.png +0 -0
  80. package/examples/particles/index.html +0 -426
  81. package/examples/particles/tiles.png +0 -0
  82. package/examples/platformer/data/gameLevelData.tmx +0 -143
  83. package/examples/platformer/data/gameLevelData.tsx +0 -4
  84. package/examples/platformer/game.js +0 -147
  85. package/examples/platformer/gameCharacter.js +0 -309
  86. package/examples/platformer/gameEffects.js +0 -278
  87. package/examples/platformer/gameLevel.js +0 -205
  88. package/examples/platformer/gameLevelData.json +0 -171
  89. package/examples/platformer/gameObjects.js +0 -378
  90. package/examples/platformer/gamePlayer.js +0 -35
  91. package/examples/platformer/index.html +0 -9
  92. package/examples/platformer/tiles.png +0 -0
  93. package/examples/platformer/tilesLevel.png +0 -0
  94. package/examples/puzzle/game.js +0 -331
  95. package/examples/puzzle/index.html +0 -8
  96. package/examples/puzzle/tiles.png +0 -0
  97. package/examples/screenshot.jpg +0 -0
  98. package/examples/shorts/animation.js +0 -18
  99. package/examples/shorts/base.html +0 -54
  100. package/examples/shorts/blending.js +0 -14
  101. package/examples/shorts/box2d.js +0 -51
  102. package/examples/shorts/box2dCar.js +0 -59
  103. package/examples/shorts/box2dPool.js +0 -107
  104. package/examples/shorts/box2dTileLayer.js +0 -48
  105. package/examples/shorts/cameraDrag.js +0 -22
  106. package/examples/shorts/clock.js +0 -21
  107. package/examples/shorts/colors.js +0 -25
  108. package/examples/shorts/debugDraw.js +0 -37
  109. package/examples/shorts/empty.js +0 -30
  110. package/examples/shorts/flappyGame.js +0 -55
  111. package/examples/shorts/fontImage.js +0 -17
  112. package/examples/shorts/fps.js +0 -90
  113. package/examples/shorts/helloWorld.js +0 -11
  114. package/examples/shorts/hillGlideGame.js +0 -63
  115. package/examples/shorts/input.js +0 -64
  116. package/examples/shorts/landerGame.js +0 -57
  117. package/examples/shorts/maze.js +0 -48
  118. package/examples/shorts/medals.js +0 -51
  119. package/examples/shorts/music.js +0 -78
  120. package/examples/shorts/musicPlayer.js +0 -137
  121. package/examples/shorts/nineSlice.js +0 -40
  122. package/examples/shorts/parallax.js +0 -72
  123. package/examples/shorts/particles.js +0 -28
  124. package/examples/shorts/piano.js +0 -44
  125. package/examples/shorts/platformer.js +0 -45
  126. package/examples/shorts/pongGame.js +0 -41
  127. package/examples/shorts/postProcess.js +0 -62
  128. package/examples/shorts/sequencer.js +0 -124
  129. package/examples/shorts/shader.js +0 -29
  130. package/examples/shorts/shapes.js +0 -21
  131. package/examples/shorts/slidingPuzzle.js +0 -52
  132. package/examples/shorts/song.mp3 +0 -0
  133. package/examples/shorts/sound.js +0 -36
  134. package/examples/shorts/spaceGame.js +0 -58
  135. package/examples/shorts/spriteAtlas.js +0 -31
  136. package/examples/shorts/starfield.js +0 -15
  137. package/examples/shorts/texture.js +0 -16
  138. package/examples/shorts/textureWrapped.js +0 -12
  139. package/examples/shorts/tileLayer.js +0 -48
  140. package/examples/shorts/tileRaycast.js +0 -39
  141. package/examples/shorts/tiles.png +0 -0
  142. package/examples/shorts/tiltedView.js +0 -63
  143. package/examples/shorts/timers.js +0 -53
  144. package/examples/shorts/topDown.js +0 -38
  145. package/examples/shorts/tween.js +0 -24
  146. package/examples/shorts/uiSystem.js +0 -61
  147. package/examples/shorts/video.webm +0 -0
  148. package/examples/shorts/videoPlayer.js +0 -34
  149. package/examples/shorts.js +0 -743
  150. package/examples/starter/build.bat +0 -7
  151. package/examples/starter/build.mjs +0 -126
  152. package/examples/starter/game.js +0 -137
  153. package/examples/starter/index.html +0 -35
  154. package/examples/starter/tiles.png +0 -0
  155. package/examples/stress/index.html +0 -173
  156. package/examples/style.css +0 -150
  157. package/examples/tweenSystem/game.js +0 -171
  158. package/examples/tweenSystem/index.html +0 -10
  159. package/examples/tweenSystem/tiles.png +0 -0
  160. package/examples/typescript/build.mjs +0 -60
  161. package/examples/typescript/game.js +0 -100
  162. package/examples/typescript/game.ts +0 -132
  163. package/examples/typescript/index.html +0 -10
  164. package/examples/typescript/tiles.png +0 -0
  165. package/examples/typescript/tsconfig.json +0 -17
  166. package/examples/uiSystem/game.js +0 -139
  167. package/examples/uiSystem/index.html +0 -10
  168. package/examples/uiSystem/tiles.png +0 -0
  169. package/jsconfig.json +0 -12
  170. package/plugins/desktop.ini +0 -2
  171. package/reference.md +0 -448
  172. package/test/math.test.mjs +0 -774
  173. package/test/setup.mjs +0 -22
  174. package/test/smoke.test.mjs +0 -274
  175. package/test/tween.test.mjs +0 -576
  176. package/test/util.test.mjs +0 -80
package/src/engineDraw.js CHANGED
@@ -217,8 +217,9 @@ class TextureInfo
217
217
  * Create a TextureInfo, called automatically by the engine
218
218
  * @param {HTMLImageElement|OffscreenCanvas} image
219
219
  * @param {boolean} [useWebGL] - Should use WebGL if available?
220
+ * @param {boolean} [wrap] - Should the texture wrap (REPEAT) or clamp (CLAMP_TO_EDGE)?
220
221
  */
221
- constructor(image, useWebGL=true)
222
+ constructor(image, useWebGL=true, wrap=false)
222
223
  {
223
224
  /** @property {HTMLImageElement|OffscreenCanvas} - image source */
224
225
  this.image = image;
@@ -228,6 +229,8 @@ class TextureInfo
228
229
  this.sizeInverse = image ? vec2(1/image.width, 1/image.height) : vec2();
229
230
  /** @property {WebGLTexture} - WebGL texture */
230
231
  this.glTexture = undefined;
232
+ /** @property {boolean} - true for REPEAT wrap mode, false for CLAMP_TO_EDGE */
233
+ this.wrap = wrap;
231
234
  useWebGL && this.createWebGLTexture();
232
235
  }
233
236
 
@@ -240,6 +243,14 @@ class TextureInfo
240
243
  /** Check if the texture is webgl enabled
241
244
  * @return {boolean} */
242
245
  hasWebGL() { return !!this.glTexture; }
246
+
247
+ /** Set the wrap mode for this texture
248
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE */
249
+ setWrap(wrap=true)
250
+ {
251
+ this.wrap = wrap;
252
+ glSetTextureWrap(this.glTexture, wrap);
253
+ }
243
254
  }
244
255
 
245
256
  ///////////////////////////////////////////////////////////////////////////////
@@ -301,8 +312,13 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
301
312
  }
302
313
  else
303
314
  {
304
- // if no tile info, force untextured
305
- glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0, 0, color.rgbaInt());
315
+ // if no tile info, force untextured by zeroing rgba (so whatever
316
+ // texture is bound doesn't leak in) and folding color+additive
317
+ // into the additive slot — matches the Canvas2D path's
318
+ // color.add(additiveColor) on line ~337.
319
+ const combined = additiveColor ? color.add(additiveColor) : color;
320
+ glDraw(pos.x, pos.y, size.x, size.y, angle, 0, 0, 0, 0,
321
+ 0, combined.rgbaInt());
306
322
  }
307
323
  }
308
324
  else
@@ -441,6 +457,8 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
441
457
  const textureInfo = typeof texture === 'number' ? textureInfos[texture] : texture;
442
458
  ASSERT(textureInfo instanceof TextureInfo, 'texture not loaded');
443
459
  ASSERT(textureInfo.size.x > 0, 'texture not loaded');
460
+ ASSERT(textureInfo.wrap,
461
+ 'drawTextureWrapped requires a wrap-enabled texture; call textureInfo.setWrap(true) first');
444
462
 
445
463
  if (useWebGL && glEnable)
446
464
  {
@@ -792,15 +810,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
792
810
  * @memberof Draw */
793
811
  function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
794
812
  {
795
- ASSERT(isString(text), 'text must be a string');
813
+ ASSERT(isStringLike(text), 'text must be a string');
796
814
  ASSERT(isVector2(pos), 'pos must be a vec2');
797
815
  ASSERT(isNumber(size), 'size must be a number');
798
816
  ASSERT(isColor(color), 'color must be a color');
799
817
  ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
800
818
  ASSERT(isColor(lineColor), 'lineColor must be a color');
801
819
  ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
802
- ASSERT(isString(font), 'font must be a string');
803
- ASSERT(isString(fontStyle), 'fontStyle must be a string');
820
+ ASSERT(isStringLike(font), 'font must be a string');
821
+ ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
804
822
  ASSERT(isNumber(angle), 'angle must be a number');
805
823
 
806
824
  context.fillStyle = color.toString();
@@ -837,7 +855,7 @@ async function loadTexture(textureIndex, src)
837
855
  {
838
856
  ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
839
857
  ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
840
- ASSERT(!src || isString(src), 'image src must be a string');
858
+ ASSERT(!src || isStringLike(src), 'image src must be a string');
841
859
 
842
860
  const image = new Image;
843
861
  if (src)
@@ -1225,7 +1243,7 @@ class FontImage
1225
1243
  */
1226
1244
  drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
1227
1245
  {
1228
- ASSERT(isString(text), 'text must be a string');
1246
+ ASSERT(isStringLike(text), 'text must be a string');
1229
1247
  ASSERT(isVector2(pos), 'pos must be a vec2');
1230
1248
  ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
1231
1249
  ASSERT(isColor(color), 'color must be a color');
@@ -11,6 +11,7 @@ export
11
11
  frameRate,
12
12
  timeDelta,
13
13
  engineObjects,
14
+ engineObjectsCollide,
14
15
  frame,
15
16
  time,
16
17
  timeReal,
@@ -33,6 +34,7 @@ export
33
34
  // Debug
34
35
  ASSERT,
35
36
  LOG,
37
+ debugPointSize,
36
38
  debugRect,
37
39
  debugPoly,
38
40
  debugCircle,
@@ -77,11 +79,14 @@ export
77
79
  gravity,
78
80
  particleEmitRateScale,
79
81
  glEnable,
82
+ glCircleSides,
80
83
  gamepadsEnable,
81
84
  gamepadDirectionEmulateStick,
82
85
  inputWASDEmulateDirection,
86
+ touchInputEnable,
83
87
  touchGamepadEnable,
84
88
  touchGamepadCenterButtonSize,
89
+ touchGamepadButtonCount,
85
90
  touchGamepadAnalog,
86
91
  touchGamepadSize,
87
92
  touchGamepadAlpha,
@@ -91,9 +96,6 @@ export
91
96
  soundVolume,
92
97
  soundDefaultRange,
93
98
  soundDefaultTaper,
94
- medalDisplayTime,
95
- medalDisplaySlideTime,
96
- medalDisplaySize,
97
99
 
98
100
  // Setters for globals
99
101
  setCameraPos,
@@ -125,6 +127,7 @@ export
125
127
  setObjectMaxSpeed,
126
128
  setGravity,
127
129
  setParticleEmitRateScale,
130
+ setGLCircleSides,
128
131
  setTouchInputEnable,
129
132
  setGamepadsEnable,
130
133
  setGamepadDirectionEmulateStick,
@@ -140,10 +143,6 @@ export
140
143
  setSoundVolume,
141
144
  setSoundDefaultRange,
142
145
  setSoundDefaultTaper,
143
- setMedalDisplayTime,
144
- setMedalDisplaySlideTime,
145
- setMedalDisplaySize,
146
- setMedalsPreventUnlock,
147
146
  setDebugWatermark,
148
147
  setDebugKey,
149
148
 
@@ -173,6 +172,7 @@ export
173
172
  percentLerp,
174
173
  smoothStep,
175
174
  nearestPowerOfTwo,
175
+ isPowerOfTwo,
176
176
  isOverlapping,
177
177
  isIntersecting,
178
178
  lineTest,
@@ -208,7 +208,7 @@ export
208
208
  isColor,
209
209
  isVector2,
210
210
  isNumber,
211
- isString,
211
+ isStringLike,
212
212
  isArray,
213
213
 
214
214
  // Default Colors
@@ -252,6 +252,7 @@ export
252
252
  drawLineList,
253
253
  drawLine,
254
254
  drawPoly,
255
+ drawRegularPoly,
255
256
  drawEllipse,
256
257
  drawCircle,
257
258
  drawCanvas2D,
@@ -265,6 +266,7 @@ export
265
266
  toggleFullscreen,
266
267
  setCursor,
267
268
  getCameraSize,
269
+ isOnScreen,
268
270
 
269
271
  // WebGL
270
272
  glCanvas,
@@ -272,6 +274,7 @@ export
272
274
  glAntialias,
273
275
  glClearCanvas,
274
276
  glSetTexture,
277
+ glSetTextureWrap,
275
278
  glCompileShader,
276
279
  glCreateProgram,
277
280
  glCreateTexture,
@@ -313,6 +316,7 @@ export
313
316
  gamepadWasPressed,
314
317
  gamepadWasReleased,
315
318
  gamepadStick,
319
+ gamepadStickCount,
316
320
  gamepadDpad,
317
321
  gamepadConnected,
318
322
  gamepadVibrate,
@@ -327,6 +331,7 @@ export
327
331
  audioContext,
328
332
  audioMasterGain,
329
333
  audioDefaultSampleRate,
334
+ audioIsRunning,
330
335
  Sound,
331
336
  SoundInstance,
332
337
  speak,
@@ -354,9 +359,4 @@ export
354
359
  ParticleEmitter,
355
360
  Particle,
356
361
 
357
- // Medals
358
- medals,
359
- medalsPreventUnlock,
360
- medalsInit,
361
- Medal,
362
362
  }
@@ -66,7 +66,7 @@ const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
66
66
  * This is useful to disable for html menus so the browser can handle input normally
67
67
  * @param {boolean} preventDefault
68
68
  * @memberof Input */
69
- function setInputPreventDefault(preventDefault) { inputPreventDefault = preventDefault; }
69
+ function setInputPreventDefault(preventDefault=true) { inputPreventDefault = preventDefault; }
70
70
 
71
71
  /** Clears an input key state
72
72
  * @param {string|number} key
@@ -103,7 +103,7 @@ function inputClear()
103
103
  * @memberof Input */
104
104
  function keyIsDown(key, device=0)
105
105
  {
106
- ASSERT(isString(key), 'key must be a number or string');
106
+ ASSERT(isStringLike(key), 'key must be a number or string');
107
107
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
108
108
  return !!(inputData[device]?.[key] & 1);
109
109
  }
@@ -115,7 +115,7 @@ function keyIsDown(key, device=0)
115
115
  * @memberof Input */
116
116
  function keyWasPressed(key, device=0)
117
117
  {
118
- ASSERT(isString(key), 'key must be a number or string');
118
+ ASSERT(isStringLike(key), 'key must be a number or string');
119
119
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
120
120
  return !!(inputData[device]?.[key] & 2);
121
121
  }
@@ -127,7 +127,7 @@ function keyWasPressed(key, device=0)
127
127
  * @memberof Input */
128
128
  function keyWasReleased(key, device=0)
129
129
  {
130
- ASSERT(isString(key), 'key must be a number or string');
130
+ ASSERT(isStringLike(key), 'key must be a number or string');
131
131
  ASSERT(device > 0 || typeof key !== 'number' || key < 3, 'use code string for keyboard');
132
132
  return !!(inputData[device]?.[key] & 4);
133
133
  }
@@ -141,10 +141,10 @@ function keyWasReleased(key, device=0)
141
141
  * @memberof Input */
142
142
  function keyDirection(up='ArrowUp', down='ArrowDown', left='ArrowLeft', right='ArrowRight')
143
143
  {
144
- ASSERT(isString(up), 'up key must be a string');
145
- ASSERT(isString(down), 'down key must be a string');
146
- ASSERT(isString(left), 'left key must be a string');
147
- ASSERT(isString(right), 'right key must be a string');
144
+ ASSERT(isStringLike(up), 'up key must be a string');
145
+ ASSERT(isStringLike(down), 'down key must be a string');
146
+ ASSERT(isStringLike(left), 'left key must be a string');
147
+ ASSERT(isStringLike(right), 'right key must be a string');
148
148
  const k = (key)=> keyIsDown(key) ? 1 : 0;
149
149
  return vec2(k(right) - k(left), k(up) - k(down));
150
150
  }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * LittleJS Engine Logo
3
+ * - Draws the LittleJS splash screen logo
4
+ * - Used internally during engine startup
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ ///////////////////////////////////////////////////////////////////////////////
10
+ function drawEngineLogo(t)
11
+ {
12
+ const blackAndWhite = 0;
13
+ const showName = 1;
14
+
15
+ // LittleJS Logo and Splash Screen
16
+ const x = mainContext;
17
+ const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
18
+ const w = mainCanvas.width = innerWidth * dpr;
19
+ const h = mainCanvas.height = innerHeight * dpr;
20
+ {
21
+ // background
22
+ const p3 = percent(t, 1, .8);
23
+ const p4 = percent(t, 0, .5);
24
+ const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.6);
25
+ g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
26
+ g.addColorStop(1,hsl(0,0,0,p3).toString());
27
+ x.save();
28
+ x.fillStyle = g;
29
+ x.fillRect(0,0,w,h);
30
+ }
31
+ const gradient = (X1,Y1,X2,Y2,C,S=1)=>
32
+ {
33
+ if (C >= 0)
34
+ {
35
+ if (blackAndWhite)
36
+ x.fillStyle = '#fff';
37
+ else
38
+ {
39
+ const g = x.fillStyle = x.createLinearGradient(X1,Y1,X2,Y2);
40
+ g.addColorStop(0,color(C,2));
41
+ g.addColorStop(1,color(C,1));
42
+ }
43
+ }
44
+ else
45
+ x.fillStyle = '#000';
46
+ C >= -1 ? (x.fill(), S && x.stroke()) : x.stroke();
47
+ }
48
+ const circle = (X,Y,R,A=0,B=2*PI,C,S)=>
49
+ {
50
+ x.beginPath();
51
+ x.arc(X,Y,R,p*A,p*B);
52
+ gradient(X,Y-R,X,Y+R,C,S);
53
+ }
54
+ const rect = (X,Y,W,H,C)=>
55
+ {
56
+ x.beginPath();
57
+ x.rect(X,Y,W,H*p);
58
+ gradient(X,Y+H,X+W,Y,C);
59
+ }
60
+ const poly = (points,C,Y,H)=>
61
+ {
62
+ x.beginPath();
63
+ for (const p of points)
64
+ x.lineTo(p.x, p.y);
65
+ x.closePath();
66
+ gradient(0, Y, 0, Y+H,C);
67
+ }
68
+ const color = (c,l)=> l?`hsl(${[.95,.56,.13][c%3]*360} 99%${[0,50,75][l]}%`:'#000';
69
+
70
+ // center and fit to screen
71
+ const alpha = oscillate(1,1,t);
72
+ const p = percent(alpha, .1, .5);
73
+ const size = min(6, min(w,h)/99);
74
+ x.translate(w/2,h/2);
75
+ x.scale(size,size);
76
+ x.translate(-40,-35);
77
+ p < 1 && x.setLineDash([99*p,99]);
78
+ x.lineJoin = x.lineCap = 'round';
79
+ x.lineWidth = .1 + p*1.9;
80
+ //x.strokeStyle='#fff7';
81
+
82
+ if (showName)
83
+ {
84
+ // engine name text
85
+ const Y = 54;
86
+ const s = 'LittleJS';
87
+ x.font = '900 15.5px arial';
88
+ x.lineWidth = .1+p*3.9;
89
+ x.textAlign = 'center';
90
+ x.textBaseline = 'top';
91
+ rect(11,Y+1,59,8*p,-1);
92
+ x.beginPath();
93
+
94
+ let w2 = 0;
95
+ for (let i=0;i<s.length;++i)
96
+ w2 += x.measureText(s[i]).width;
97
+ for (let j=2;j--;)
98
+ for (let i=0,X=40-w2/2;i<s.length;++i)
99
+ {
100
+ const w = x.measureText(s[i]).width, X2 = X+w/2;
101
+ gradient(X2,Y,X2+2,Y+13,i>5?1:0);
102
+ x[j?'strokeText':'fillText'](s[i],X2,Y+.5,17*p);
103
+ X += w;
104
+ }
105
+
106
+ x.lineWidth = .1 + p*1.9;
107
+ rect(3,Y,73,0); // bottom
108
+ }
109
+
110
+ rect(7,15,26,-7,0); // cab top
111
+ rect(25,15,8,25,-1); // cab front
112
+ rect(10,40,15,-25,1); // cab back
113
+ rect(14,21,7,9,2); // cab window
114
+ rect(38,20,6,-6,2); // little stack
115
+
116
+ // big stack
117
+ rect(49,20,10,-6,0);
118
+ const stackPoints = [vec2(44,8),vec2(64,8),vec2(59,8+6*p),vec2(49,8+6*p)];
119
+ poly(stackPoints,2,8,6*p);
120
+ rect(44,8,20,-7,0);
121
+
122
+ // engine
123
+ for (let i=5;i--;) circle(59-i*6*p,30,10,0,2*PI,1,0);
124
+ circle(59,30,4,0,7,2); // light
125
+
126
+ // engine outline
127
+ rect(35,20,24,0); // top
128
+ circle(59,30,10); // front
129
+ circle(47,30,10,PI/2,PI*3/2); // middle
130
+ circle(35,30,10,PI/2,PI*3/2); // back
131
+ rect(7,40,13,7,-1); // bottom back
132
+ rect(17,40,43,14,-1); // bottom center
133
+
134
+ // wheels
135
+ for (let i=3;i--;) for (let j=2;j--;) circle(17+15*i,47,j?7:1,0,2*PI,2);
136
+
137
+ // cowcatcher
138
+ for (let i=2;i--;)
139
+ {
140
+ let w=6, s=7, o=53+w*p*i
141
+ const points = [vec2(o+s,54),vec2(o,40),vec2(o+w*p,40),vec2(o+s+w*p,54)];
142
+ poly(points,0,40,14);
143
+ }
144
+
145
+ x.restore();
146
+ }
package/src/engineMath.js CHANGED
@@ -153,7 +153,11 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
153
153
  * @return {number}
154
154
  * @memberof Math */
155
155
  function distanceWrap(valueA, valueB, wrapSize=1)
156
- { const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
156
+ {
157
+ ASSERT(wrapSize > 0, 'distanceWrap wrapSize must be > 0');
158
+ const d = (valueA - valueB) % wrapSize;
159
+ return d*2 % wrapSize - d;
160
+ }
157
161
 
158
162
  /** Linearly interpolates between values passed in with wrapping
159
163
  * @param {number} valueA
@@ -288,13 +292,15 @@ function oscillate(frequency=1, amplitude=1, t=time, offset=0, type=0)
288
292
  function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
289
293
 
290
294
  /**
291
- * Check if object can be converted to a string (has a toString method)
295
+ * Check if a value is stringifiable i.e. it has a toString that returns
296
+ * a string. Use this for ASSERTs and inputs that will be coerced to text;
297
+ * use `typeof x === 'string'` inline if you need strict-string semantics.
292
298
  * - Returns true for strings, numbers, and most objects
293
299
  * - Returns false for null and undefined
294
300
  * @param {any} s
295
301
  * @return {boolean}
296
302
  * @memberof Math */
297
- function isString(s) { return s != null && typeof s?.toString() === 'string'; }
303
+ function isStringLike(s) { return s != null && typeof s?.toString() === 'string'; }
298
304
 
299
305
  /**
300
306
  * Check if object is an array
@@ -470,6 +476,7 @@ class RandomGenerator
470
476
  * @param {number} [seed] - Starting seed or engine default seed */
471
477
  constructor(seed = 123456789)
472
478
  {
479
+ ASSERT(seed !== 0, 'RandomGenerator seed must be non-zero (xorshift is fixed at 0)');
473
480
  /** @property {number} - random seed */
474
481
  this.seed = seed;
475
482
  }
@@ -763,8 +770,10 @@ class Vector2
763
770
  * @return {Vector2} */
764
771
  floor() { return new Vector2(floor(this.x), floor(this.y)); }
765
772
 
766
- /** Returns a copy of this vector snapped to a grid
767
- * @param {number} grid - grid size to snap to
773
+ /** Returns a copy of this vector snapped to a grid. Note that `grid` is
774
+ * the number of snap steps per unit (so `grid=2` snaps to halves and
775
+ * `grid=0.5` snaps to twos), not the cell size.
776
+ * @param {number} grid - snap steps per unit
768
777
  * @return {Vector2} */
769
778
  snap(grid)
770
779
  {
@@ -1035,7 +1044,7 @@ class Color
1035
1044
  * @return {Color} */
1036
1045
  setHex(hex)
1037
1046
  {
1038
- ASSERT(isString(hex), 'Color hex code must be a string');
1047
+ ASSERT(isStringLike(hex), 'Color hex code must be a string');
1039
1048
  ASSERT(hex[0] === '#', 'Color hex code must start with #');
1040
1049
  ASSERT([4,5,7,9].includes(hex.length), 'Invalid hex');
1041
1050
 
@@ -132,10 +132,18 @@ class EngineObject
132
132
  const parent = this.parent;
133
133
  if (parent)
134
134
  {
135
- // copy parent pos/angle
135
+ // compose with parent transform inline to avoid intermediate vector allocs
136
136
  const mirror = parent.getMirrorSign();
137
- this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(parent.angle).add(parent.pos);
138
- this.angle = mirror*this.localAngle + parent.angle;
137
+ const lp = this.localPos, pp = parent.pos;
138
+ const lx = lp.x*mirror, ly = lp.y, pa = parent.angle;
139
+ if (pa)
140
+ {
141
+ const c = cos(-pa), s = sin(-pa);
142
+ this.pos = new Vector2(lx*c - ly*s + pp.x, lx*s + ly*c + pp.y);
143
+ }
144
+ else
145
+ this.pos = new Vector2(lx + pp.x, ly + pp.y);
146
+ this.angle = mirror*this.localAngle + pa;
139
147
  }
140
148
 
141
149
  // update children
@@ -501,8 +509,6 @@ class EngineObject
501
509
  * @return {string} */
502
510
  toString()
503
511
  {
504
- if (!debug) return;
505
-
506
512
  let text = 'type = ' + this.constructor.name;
507
513
  if (this.pos.x || this.pos.y)
508
514
  text += '\npos = ' + this.pos;
@@ -315,11 +315,14 @@ class ParticleEmitter extends EngineObject
315
315
  }
316
316
 
317
317
  ///////////////////////////////////////////////////////////////////////////////
318
+ // scratch vector reused by Particle.render to avoid per-frame allocations
319
+ const particleDrawPos = new Vector2;
320
+
318
321
  /**
319
322
  * Particle Object - Created automatically by Particle Emitters
320
323
  * @memberof Particles
321
324
  */
322
- class Particle
325
+ class Particle
323
326
  {
324
327
  /**
325
328
  * Create a particle with the passed in settings
@@ -494,14 +497,14 @@ class Particle
494
497
  additive && setBlendMode(true);
495
498
 
496
499
  // update the position and angle for drawing
497
- const pos = this.pos.copy();
500
+ const pos = particleDrawPos.set(this.pos.x, this.pos.y);
498
501
  let angle = this.angle;
499
502
  if (localSpace)
500
503
  {
501
504
  // in local space of emitter
502
505
  const a = emitter.angle;
503
506
  const c = cos(a), s = sin(a);
504
- pos.set(emitter.pos.x + pos.x*c - pos.y*s,
507
+ pos.set(emitter.pos.x + pos.x*c - pos.y*s,
505
508
  emitter.pos.y + pos.x*s + pos.y*c);
506
509
  angle += a;
507
510
  }
@@ -18,7 +18,7 @@ const debugPhysics = 0;
18
18
  const debugParticles = 0;
19
19
  const debugRaycast = 0;
20
20
  const debugGamepads = 0;
21
- const debugMedals = 0;
21
+ const debugPointSize = .5;
22
22
 
23
23
  // debug commands are automatically removed from the final build
24
24
  function ASSERT (){}
@@ -322,33 +322,6 @@ let soundDefaultRange = 40;
322
322
  * @memberof Settings */
323
323
  let soundDefaultTaper = .7;
324
324
 
325
- ///////////////////////////////////////////////////////////////////////////////
326
- // Medals settings
327
-
328
- /** How long to show medals for in seconds
329
- * @type {number}
330
- * @default
331
- * @memberof Settings */
332
- let medalDisplayTime = 5;
333
-
334
- /** How quickly to slide on/off medals in seconds
335
- * @type {number}
336
- * @default
337
- * @memberof Settings */
338
- let medalDisplaySlideTime = .5;
339
-
340
- /** Size of medal display
341
- * @type {Vector2}
342
- * @default Vector2(640,80)
343
- * @memberof Settings */
344
- let medalDisplaySize = vec2(640, 80);
345
-
346
- /** Set to stop medals from being unlockable (like if cheats are enabled)
347
- * @type {boolean}
348
- * @default
349
- * @memberof Settings */
350
- let medalsPreventUnlock = false;
351
-
352
325
  ///////////////////////////////////////////////////////////////////////////////
353
326
  // Setters for global variables
354
327
 
@@ -610,26 +583,6 @@ function setSoundDefaultRange(range) { soundDefaultRange = range; }
610
583
  * @memberof Settings */
611
584
  function setSoundDefaultTaper(taper) { soundDefaultTaper = taper; }
612
585
 
613
- /** Set how long to show medals for in seconds
614
- * @param {number} time
615
- * @memberof Settings */
616
- function setMedalDisplayTime(time) { medalDisplayTime = time; }
617
-
618
- /** Set how quickly to slide on/off medals in seconds
619
- * @param {number} time
620
- * @memberof Settings */
621
- function setMedalDisplaySlideTime(time) { medalDisplaySlideTime = time; }
622
-
623
- /** Set size of medal display
624
- * @param {Vector2} size
625
- * @memberof Settings */
626
- function setMedalDisplaySize(size) { medalDisplaySize = size.copy(); }
627
-
628
- /** Set to stop medals from being unlockable
629
- * @param {boolean} preventUnlock
630
- * @memberof Settings */
631
- function setMedalsPreventUnlock(preventUnlock) { medalsPreventUnlock = preventUnlock; }
632
-
633
586
  /** Set if watermark with FPS should be shown
634
587
  * @param {boolean} show
635
588
  * @memberof Debug */
@@ -155,8 +155,8 @@ function saveCanvas(canvas, filename='screenshot', type='image/png')
155
155
  * @memberof Utilities */
156
156
  function saveDataURL(url, filename='download', revokeTime)
157
157
  {
158
- ASSERT(isString(url), 'saveDataURL requires url string');
159
- ASSERT(isString(filename), 'saveDataURL requires filename string');
158
+ ASSERT(isStringLike(url), 'saveDataURL requires url string');
159
+ ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
160
160
 
161
161
  // create link for saving screenshots
162
162
  const link = document.createElement('a');
@@ -174,8 +174,8 @@ function saveDataURL(url, filename='download', revokeTime)
174
174
  * @memberof Utilities */
175
175
  function shareURL(title, url, callback)
176
176
  {
177
- ASSERT(isString(title), 'shareURL requires title string');
178
- ASSERT(isString(url), 'shareURL requires url string');
177
+ ASSERT(isStringLike(title), 'shareURL requires title string');
178
+ ASSERT(isStringLike(url), 'shareURL requires url string');
179
179
  navigator.share?.({title, url}).then(()=>callback?.());
180
180
  }
181
181
 
@@ -188,11 +188,16 @@ function shareURL(title, url, callback)
188
188
  * @memberof Utilities */
189
189
  function readSaveData(saveName, defaultSaveData)
190
190
  {
191
- ASSERT(isString(saveName), 'loadData requires saveName string');
191
+ ASSERT(isStringLike(saveName), 'loadData requires saveName string');
192
192
 
193
- // replace undefined values with defaults
193
+ // replace undefined values with defaults; tolerate corrupt JSON
194
194
  const data = localStorage[saveName];
195
- const loadedData = data ? JSON.parse(data) : {};
195
+ let loadedData = {};
196
+ if (data)
197
+ {
198
+ try { loadedData = JSON.parse(data); }
199
+ catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
200
+ }
196
201
  return { ...defaultSaveData, ...loadedData };
197
202
  }
198
203
 
@@ -202,6 +207,6 @@ function readSaveData(saveName, defaultSaveData)
202
207
  * @memberof Utilities */
203
208
  function writeSaveData(saveName, saveData)
204
209
  {
205
- ASSERT(isString(saveName), 'saveData requires saveName string');
210
+ ASSERT(isStringLike(saveName), 'saveData requires saveName string');
206
211
  localStorage[saveName] = JSON.stringify(saveData);
207
212
  }