littlejsengine 1.18.4 → 1.18.7

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 (177) 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 +349 -54
  5. package/dist/littlejs.esm.js +1469 -448
  6. package/dist/littlejs.esm.min.js +1 -1
  7. package/dist/littlejs.js +1438 -436
  8. package/dist/littlejs.min.js +1 -1
  9. package/dist/littlejs.release.js +1438 -436
  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 +1 -1
  16. package/plugins/tweenSystem.js +509 -0
  17. package/plugins/uiSystem.js +150 -11
  18. package/src/engine.js +2 -141
  19. package/src/engineAudio.js +8 -5
  20. package/src/engineBuild.mjs +4 -2
  21. package/src/engineDebug.js +1 -1
  22. package/src/engineDraw.js +21 -3
  23. package/src/engineExport.js +12 -12
  24. package/src/engineInput.js +1 -1
  25. package/src/engineLogo.js +146 -0
  26. package/src/engineMath.js +10 -3
  27. package/src/engineObject.js +11 -5
  28. package/src/engineParticles.js +6 -3
  29. package/src/engineRelease.js +1 -1
  30. package/src/engineSettings.js +0 -47
  31. package/src/engineUtilities.js +7 -2
  32. package/src/engineWebGL.js +34 -7
  33. package/src/jsconfig.json +3 -1
  34. package/.github/workflows/test.yml +0 -17
  35. package/AI.md +0 -173
  36. package/CLAUDE.md +0 -1
  37. package/examples/box2d/game.js +0 -192
  38. package/examples/box2d/gameObjects.js +0 -564
  39. package/examples/box2d/index.html +0 -9
  40. package/examples/box2d/scenes.js +0 -194
  41. package/examples/box2d/tiles.png +0 -0
  42. package/examples/breakout/game.js +0 -175
  43. package/examples/breakout/gameObjects.js +0 -147
  44. package/examples/breakout/index.html +0 -8
  45. package/examples/breakout/tiles.png +0 -0
  46. package/examples/breakoutTutorial/README.md +0 -521
  47. package/examples/breakoutTutorial/game.js +0 -191
  48. package/examples/breakoutTutorial/images/1.png +0 -0
  49. package/examples/breakoutTutorial/images/10.png +0 -0
  50. package/examples/breakoutTutorial/images/11.png +0 -0
  51. package/examples/breakoutTutorial/images/2.png +0 -0
  52. package/examples/breakoutTutorial/images/3.png +0 -0
  53. package/examples/breakoutTutorial/images/4.png +0 -0
  54. package/examples/breakoutTutorial/images/5.png +0 -0
  55. package/examples/breakoutTutorial/images/6.png +0 -0
  56. package/examples/breakoutTutorial/images/7.png +0 -0
  57. package/examples/breakoutTutorial/images/8.png +0 -0
  58. package/examples/breakoutTutorial/images/9.png +0 -0
  59. package/examples/breakoutTutorial/index.html +0 -8
  60. package/examples/electron/build.mjs +0 -127
  61. package/examples/electron/electron.js +0 -35
  62. package/examples/electron/game.js +0 -54
  63. package/examples/electron/index.html +0 -13
  64. package/examples/electron/package.json +0 -5
  65. package/examples/electron/tiles.png +0 -0
  66. package/examples/empty/game.js +0 -51
  67. package/examples/empty/index.html +0 -5
  68. package/examples/empty/tiles.png +0 -0
  69. package/examples/favicon.png +0 -0
  70. package/examples/games.jpg +0 -0
  71. package/examples/htmlMenu/game.js +0 -82
  72. package/examples/htmlMenu/index.html +0 -45
  73. package/examples/htmlMenu/tiles.png +0 -0
  74. package/examples/index.html +0 -62
  75. package/examples/logo.png +0 -0
  76. package/examples/logo2.png +0 -0
  77. package/examples/module/build.mjs +0 -124
  78. package/examples/module/game.js +0 -132
  79. package/examples/module/index.html +0 -10
  80. package/examples/module/tiles.png +0 -0
  81. package/examples/particles/index.html +0 -426
  82. package/examples/particles/tiles.png +0 -0
  83. package/examples/platformer/data/gameLevelData.tmx +0 -143
  84. package/examples/platformer/data/gameLevelData.tsx +0 -4
  85. package/examples/platformer/game.js +0 -147
  86. package/examples/platformer/gameCharacter.js +0 -309
  87. package/examples/platformer/gameEffects.js +0 -278
  88. package/examples/platformer/gameLevel.js +0 -205
  89. package/examples/platformer/gameLevelData.json +0 -171
  90. package/examples/platformer/gameObjects.js +0 -378
  91. package/examples/platformer/gamePlayer.js +0 -35
  92. package/examples/platformer/index.html +0 -9
  93. package/examples/platformer/tiles.png +0 -0
  94. package/examples/platformer/tilesLevel.png +0 -0
  95. package/examples/puzzle/game.js +0 -331
  96. package/examples/puzzle/index.html +0 -8
  97. package/examples/puzzle/tiles.png +0 -0
  98. package/examples/screenshot.jpg +0 -0
  99. package/examples/shorts/animation.js +0 -18
  100. package/examples/shorts/base.html +0 -54
  101. package/examples/shorts/blending.js +0 -14
  102. package/examples/shorts/box2d.js +0 -51
  103. package/examples/shorts/box2dCar.js +0 -59
  104. package/examples/shorts/box2dPool.js +0 -107
  105. package/examples/shorts/box2dTileLayer.js +0 -48
  106. package/examples/shorts/cameraDrag.js +0 -22
  107. package/examples/shorts/clock.js +0 -21
  108. package/examples/shorts/colors.js +0 -25
  109. package/examples/shorts/debugDraw.js +0 -37
  110. package/examples/shorts/empty.js +0 -30
  111. package/examples/shorts/flappyGame.js +0 -55
  112. package/examples/shorts/fontImage.js +0 -17
  113. package/examples/shorts/fps.js +0 -90
  114. package/examples/shorts/helloWorld.js +0 -11
  115. package/examples/shorts/hillGlideGame.js +0 -63
  116. package/examples/shorts/input.js +0 -64
  117. package/examples/shorts/landerGame.js +0 -57
  118. package/examples/shorts/maze.js +0 -48
  119. package/examples/shorts/medals.js +0 -51
  120. package/examples/shorts/music.js +0 -78
  121. package/examples/shorts/musicPlayer.js +0 -137
  122. package/examples/shorts/nineSlice.js +0 -40
  123. package/examples/shorts/parallax.js +0 -72
  124. package/examples/shorts/particles.js +0 -28
  125. package/examples/shorts/piano.js +0 -44
  126. package/examples/shorts/platformer.js +0 -45
  127. package/examples/shorts/pongGame.js +0 -41
  128. package/examples/shorts/postProcess.js +0 -62
  129. package/examples/shorts/sequencer.js +0 -124
  130. package/examples/shorts/shader.js +0 -29
  131. package/examples/shorts/shapes.js +0 -21
  132. package/examples/shorts/slidingPuzzle.js +0 -52
  133. package/examples/shorts/song.mp3 +0 -0
  134. package/examples/shorts/sound.js +0 -36
  135. package/examples/shorts/spaceGame.js +0 -58
  136. package/examples/shorts/spriteAtlas.js +0 -31
  137. package/examples/shorts/starfield.js +0 -15
  138. package/examples/shorts/texture.js +0 -16
  139. package/examples/shorts/textureWrapped.js +0 -12
  140. package/examples/shorts/tileLayer.js +0 -48
  141. package/examples/shorts/tileRaycast.js +0 -39
  142. package/examples/shorts/tiles.png +0 -0
  143. package/examples/shorts/tiltedView.js +0 -63
  144. package/examples/shorts/timers.js +0 -53
  145. package/examples/shorts/topDown.js +0 -38
  146. package/examples/shorts/tween.js +0 -24
  147. package/examples/shorts/uiSystem.js +0 -61
  148. package/examples/shorts/video.webm +0 -0
  149. package/examples/shorts/videoPlayer.js +0 -34
  150. package/examples/shorts.js +0 -743
  151. package/examples/starter/build.bat +0 -7
  152. package/examples/starter/build.mjs +0 -126
  153. package/examples/starter/game.js +0 -137
  154. package/examples/starter/index.html +0 -35
  155. package/examples/starter/tiles.png +0 -0
  156. package/examples/stress/index.html +0 -173
  157. package/examples/style.css +0 -150
  158. package/examples/tweenSystem/game.js +0 -171
  159. package/examples/tweenSystem/index.html +0 -10
  160. package/examples/tweenSystem/tiles.png +0 -0
  161. package/examples/typescript/build.mjs +0 -60
  162. package/examples/typescript/game.js +0 -100
  163. package/examples/typescript/game.ts +0 -132
  164. package/examples/typescript/index.html +0 -10
  165. package/examples/typescript/tiles.png +0 -0
  166. package/examples/typescript/tsconfig.json +0 -17
  167. package/examples/uiSystem/game.js +0 -139
  168. package/examples/uiSystem/index.html +0 -10
  169. package/examples/uiSystem/tiles.png +0 -0
  170. package/jsconfig.json +0 -12
  171. package/plugins/desktop.ini +0 -2
  172. package/reference.md +0 -448
  173. package/test/math.test.mjs +0 -774
  174. package/test/setup.mjs +0 -22
  175. package/test/smoke.test.mjs +0 -274
  176. package/test/tween.test.mjs +0 -576
  177. package/test/util.test.mjs +0 -80
@@ -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
@@ -470,6 +474,7 @@ class RandomGenerator
470
474
  * @param {number} [seed] - Starting seed or engine default seed */
471
475
  constructor(seed = 123456789)
472
476
  {
477
+ ASSERT(seed !== 0, 'RandomGenerator seed must be non-zero (xorshift is fixed at 0)');
473
478
  /** @property {number} - random seed */
474
479
  this.seed = seed;
475
480
  }
@@ -763,8 +768,10 @@ class Vector2
763
768
  * @return {Vector2} */
764
769
  floor() { return new Vector2(floor(this.x), floor(this.y)); }
765
770
 
766
- /** Returns a copy of this vector snapped to a grid
767
- * @param {number} grid - grid size to snap to
771
+ /** Returns a copy of this vector snapped to a grid. Note that `grid` is
772
+ * the number of snap steps per unit (so `grid=2` snaps to halves and
773
+ * `grid=0.5` snaps to twos), not the cell size.
774
+ * @param {number} grid - snap steps per unit
768
775
  * @return {Vector2} */
769
776
  snap(grid)
770
777
  {
@@ -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 */
@@ -190,9 +190,14 @@ function readSaveData(saveName, defaultSaveData)
190
190
  {
191
191
  ASSERT(isString(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
 
@@ -99,7 +99,7 @@ function glInit(rootElement)
99
99
  // reinit WebGL and restore textures
100
100
  initWebGL();
101
101
  for (const info of glTextureInfos)
102
- info.glTexture = glCreateTexture(info.image);
102
+ info.glTexture = glCreateTexture(info.image, info.wrap);
103
103
  pluginList.forEach(plugin=>plugin.glContextRestored?.());
104
104
  });
105
105
 
@@ -315,6 +315,30 @@ function glSetTexture(texture)
315
315
  glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
316
316
  }
317
317
 
318
+ /** Set the wrap mode (REPEAT or CLAMP_TO_EDGE) on an existing WebGL texture
319
+ * Flushes the current batch only if the texture is the active one
320
+ * @param {WebGLTexture} texture
321
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
322
+ * @memberof WebGL */
323
+ function glSetTextureWrap(texture, wrap=true)
324
+ {
325
+ if (!glContext || !texture) return;
326
+
327
+ // flush only if changing wrap on the currently bound texture
328
+ const isCurrent = texture === glActiveTexture;
329
+ if (isCurrent)
330
+ glFlush();
331
+ else
332
+ glContext.bindTexture(glContext.TEXTURE_2D, texture);
333
+
334
+ const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
335
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
336
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
337
+
338
+ if (!isCurrent && glActiveTexture)
339
+ glContext.bindTexture(glContext.TEXTURE_2D, glActiveTexture);
340
+ }
341
+
318
342
  /** Compile WebGL shader of the given type, will throw errors if in debug mode
319
343
  * @param {string} source
320
344
  * @param {number} type
@@ -359,9 +383,10 @@ function glCreateProgram(vsSource, fsSource)
359
383
  /** Create WebGL texture from an image and init the texture settings
360
384
  * Restores the active texture when done
361
385
  * @param {HTMLImageElement|HTMLCanvasElement|OffscreenCanvas} [image]
386
+ * @param {boolean} [wrap] - true for REPEAT, false for CLAMP_TO_EDGE
362
387
  * @return {WebGLTexture}
363
388
  * @memberof WebGL */
364
- function glCreateTexture(image)
389
+ function glCreateTexture(image, wrap=false)
365
390
  {
366
391
  if (!glContext) return;
367
392
 
@@ -387,8 +412,9 @@ function glCreateTexture(image)
387
412
  const minFilter = mipMap ? glContext.LINEAR_MIPMAP_LINEAR : magFilter;
388
413
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MAG_FILTER, magFilter);
389
414
  glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_MIN_FILTER, minFilter);
390
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, glContext.REPEAT);
391
- glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, glContext.REPEAT);
415
+ const wrapMode = wrap ? glContext.REPEAT : glContext.CLAMP_TO_EDGE;
416
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_S, wrapMode);
417
+ glContext.texParameteri(glContext.TEXTURE_2D, glContext.TEXTURE_WRAP_T, wrapMode);
392
418
  if (mipMap)
393
419
  glContext.generateMipmap(glContext.TEXTURE_2D);
394
420
 
@@ -440,7 +466,7 @@ function glRegisterTextureInfo(textureInfo)
440
466
  if (textureInfo.glTexture)
441
467
  glSetTextureData(textureInfo.glTexture, textureInfo.image);
442
468
  else
443
- textureInfo.glTexture = glCreateTexture(textureInfo.image);
469
+ textureInfo.glTexture = glCreateTexture(textureInfo.image, textureInfo.wrap);
444
470
  }
445
471
 
446
472
  /** Tells WebGL to destroy the glTexture and stop tracking it
@@ -659,6 +685,7 @@ function glSetRenderTarget(texture, clear=false)
659
685
  glFlush();
660
686
  glRenderTarget = undefined;
661
687
  glContext.bindFramebuffer(glContext.FRAMEBUFFER, null);
688
+ glContext.viewport(0, 0, mainCanvasSize.x, mainCanvasSize.y);
662
689
  }
663
690
  }
664
691
 
@@ -781,9 +808,9 @@ function glPolyStrip(points)
781
808
  return area;
782
809
  }
783
810
 
784
- // ensure counter-clockwise winding
811
+ // ensure counter-clockwise winding (slice first so we don't mutate caller's array)
785
812
  if (signedArea(points) < 0)
786
- points = points.reverse();
813
+ points = points.slice().reverse();
787
814
 
788
815
  // check if point is inside triangle
789
816
  const e = 1e-9;
package/src/jsconfig.json CHANGED
@@ -2,7 +2,9 @@
2
2
  "compilerOptions": {
3
3
  "checkJs": true,
4
4
  "target": "ES2020",
5
- "module": "esnext"
5
+ "module": "esnext",
6
+ "noImplicitAny": false,
7
+ "strict": false
6
8
  },
7
9
  "exclude": [
8
10
  "engineRelease.js",
@@ -1,17 +0,0 @@
1
- name: Test
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- test:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v5
12
- - uses: actions/setup-node@v5
13
- with:
14
- node-version: '22'
15
- - run: npm install
16
- - run: npm run build
17
- - run: npm test
package/AI.md DELETED
@@ -1,173 +0,0 @@
1
- # LittleJS Project - AI Agent Instructions
2
-
3
- These instructions are for making changes in the LittleJS repo safely. Optimize for small diffs, clarity, and ease of use.
4
-
5
- ## Non-negotiable rules
6
-
7
- - **Prefer minimal, local changes.** Do not refactor for style unless asked.
8
- - **No new runtime dependencies.** Keep LittleJS dependency-free at runtime.
9
- - **Do not hand-edit generated build artifacts.**
10
- - Treat `dist/` as generated output.
11
- - Make changes in `src/` (and `plugins/` when appropriate), then run the build.
12
- - **Match surrounding style.** Follow the conventions in the files you touch.
13
- - **Avoid breaking public APIs.** If a change could break users, call it out clearly and offer a compatible alternative.
14
-
15
- If anything in this doc conflicts with the actual repo behavior, follow the repo behavior and update this doc.
16
-
17
- ## Key resources
18
-
19
- - `README.md` - Overview and getting started
20
- - `reference.md` - API quick reference
21
- - `examples/` - Working examples demonstrating engine features
22
-
23
- ## Architecture overview
24
-
25
- LittleJS is a modular HTML5 game engine with:
26
-
27
- - **Core engine**: `src/engine*.js` (main loop, objects, rendering, physics, input, etc.)
28
- - **Plugins**: `plugins/*.js` (optional features like Box2D, post-processing, UI, audio helpers, etc.)
29
- - **Build system**: `src/engineBuild.mjs` (concatenates modules into distributable bundles)
30
-
31
- ## Repo structure and file types
32
-
33
- ### Engine source (`src/*.js`)
34
- - Modular architecture (one subsystem per file)
35
- - Concatenated at build time (internal source does not use ES modules)
36
- - Code is vanilla JavaScript with type info expressed via JSDoc comments
37
-
38
- ### Build output (`dist/`)
39
- Common outputs include:
40
- - `littlejs.js` - Full bundle (debug features included)
41
- - `littlejs.release.js` - Production bundle (debug stripped)
42
- - `littlejs.esm.js` - ES module build (import/export)
43
- - `littlejs.esm.min.js` - Minified ES module
44
- - `littlejs.d.ts` - TypeScript definitions
45
-
46
- Use via script tag or ES module import:
47
- - `<script src="dist/littlejs.js"></script>`
48
- - `import * as LJS from './dist/littlejs.esm.js'`
49
-
50
- Prefer adding new optional features as plugins when it keeps the core simpler.
51
-
52
- ### Examples
53
- - `examples/starter/` - Plain JavaScript global usage via `<script>` (recommended starting point)
54
- - `examples/module/` - ES module import pattern
55
- - `examples/typescript/` - TypeScript example usage
56
- - `examples/shorts/*.js` - Single-file demos loaded by the shorts harness
57
-
58
- ### Short examples (`examples/shorts/*.js`)
59
- Short examples are special:
60
- - Pure JS code file, no HTML
61
- - No imports, do not use LJS namespace - engine APIs are available globally
62
- - Override hooks: `gameInit()`, `gameUpdate()`, `gameUpdatePost()`, `gameRender()`, `gameRenderPost()`
63
-
64
- ## Coding conventions
65
-
66
- ### Factory functions vs constructors
67
- Prefer factory functions for core types:
68
- - `vec2(x, y)` not `new Vector2(x, y)`
69
- - `rgb(r, g, b, a)` or `hsl(h, s, l, a)` not `new Color(...)`
70
- - `tile(index, size)` for tile info
71
-
72
- Use constructors for game objects and complex types:
73
- - `new EngineObject(pos, size)`
74
- - `new ParticleEmitter(...)`
75
- - `new Sound(zzfxParams)`
76
- - `new Timer(duration)`
77
-
78
- ### Naming
79
- - `camelCase` for variables and functions
80
- - `PascalCase` for classes
81
- - `UPPER_CASE` for constants that are truly constant (like `PI`)
82
-
83
- ### Code style
84
- - Use JSDoc with `@memberof` grouping (namespaces: Engine, Math, Draw, Input, Audio, Debug, Settings, etc.)
85
- - Prefer single-line comments: `// comment`
86
- - Use `ASSERT(condition, 'error message')` for validation (stripped in release)
87
- - Use `LOG(...)` for debug output (stripped in release)
88
-
89
- ### Type checking
90
- Use built-in type helpers for validation:
91
- ```javascript
92
- isNumber(n) // true if number and not NaN
93
- isString(s) // true if not null/undefined (has toString)
94
- isArray(a) // true if array
95
- isVector2(v) // true if valid Vector2
96
- isColor(c) // true if valid Color
97
- ```
98
-
99
- ### Global variables
100
- - Engine time: `time`, `timeReal`, `frame`, `timeDelta`
101
- - Camera: `cameraPos`, `cameraScale`, `cameraAngle`
102
- - Input: `mousePos`, `mousePosScreen`, `mouseWheel`
103
- - State: `paused`, `debug`, `debugOverlay`
104
- - Settings are in `engineSettings.js` with corresponding setter functions
105
-
106
- ## Common patterns
107
-
108
- ### Game structure
109
- ```javascript
110
- function gameInit() { } // Called once after engine starts
111
- function gameUpdate() { } // Called every frame for game logic
112
- function gameUpdatePost() { } // Called after physics, even when paused
113
- function gameRender() { } // Called before objects render
114
- function gameRenderPost() { } // Called after objects render
115
-
116
- engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']);
117
- ```
118
-
119
- ### Creating objects
120
- ```javascript
121
- class Player extends EngineObject {
122
- constructor(pos) {
123
- super(pos, vec2(1), tile(0, 16));
124
- this.setCollision();
125
- }
126
- update() {
127
- super.update();
128
- // custom logic
129
- }
130
- }
131
- ```
132
-
133
- ### Common drawing functions
134
- ```javascript
135
- drawRect(pos, size, color) // solid rectangle
136
- drawTile(pos, size, tileInfo, color) // sprite from tile sheet
137
- drawText(text, pos, size, color) // text rendering
138
- drawLine(posA, posB, thickness, color) // line between points
139
- drawEllipse(pos, size, color) // filled ellipse
140
- ```
141
-
142
- ## Common pitfalls
143
-
144
- - **New public APIs must be added to `src/engineExport.js`** - Variables and functions added to engine source files are accessible in script-tag builds automatically, but the ESM build (`littlejs.esm.js`) and TypeScript definitions (`littlejs.d.ts`) only include what's listed in `engineExport.js`. Plugin exports go in `plugins/pluginExport.js`. Forgetting this means ESM/TS users can't access the new API.
145
- - **ASSERT and LOG are stripped in release builds** - Don't rely on side effects
146
- - **Don't modify constant colors** - `WHITE`, `BLACK`, `RED`, etc. are frozen; use `.copy()` first
147
- - **Time variables are global** - `time`, `frame` update automatically each frame
148
- - **Fixed 60 FPS timestep** - Physics runs at 60 FPS regardless of display refresh rate
149
- - **WebGL is enabled by default** - Set `glEnable = false` before `engineInit()` for Canvas2D only
150
- - **Tile coordinates are bottom-left origin** - Y increases upward in world space
151
-
152
- ## Developer workflows
153
-
154
- ### Build
155
- ```bash
156
- npm run build
157
- ```
158
-
159
- ### Testing
160
- ```bash
161
- npm test
162
- ```
163
-
164
- - Tests target `dist/littlejs.esm.js` — rebuild with `npm run build` after changing source.
165
- - [test/setup.mjs](test/setup.mjs) stubs minimal DOM and enables headless mode. Tests shouldn't call `engineInit`, `render()`, or assume `time` advances.
166
- - Zero test dependencies — uses Node's built-in `node --test`. Match the style in [test/](test/) when adding new ones.
167
- - CI runs build + test on every push/PR ([.github/workflows/test.yml](.github/workflows/test.yml)).
168
-
169
- ### Debug features
170
- - Press `Esc` to toggle debug overlay
171
- - Number keys toggle visualizations
172
- - `+`/`-` keys control time scale
173
- - Debug functions: `debugRect()`, `debugCircle()`, `debugLine()`, `debugText()`
package/CLAUDE.md DELETED
@@ -1 +0,0 @@
1
- See @AI.md for instructions.