littlejsengine 1.12.6 → 1.13.4

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 (102) hide show
  1. package/README.md +15 -12
  2. package/dist/littlejs.d.ts +466 -236
  3. package/dist/littlejs.esm.js +6126 -5370
  4. package/dist/littlejs.esm.min.js +4 -1
  5. package/dist/littlejs.js +6093 -5359
  6. package/dist/littlejs.min.js +4 -1
  7. package/dist/littlejs.release.js +5614 -4907
  8. package/examples/box2d/game.js +2 -2
  9. package/examples/box2d/gameObjects.js +8 -7
  10. package/examples/box2d/index.html +1 -1
  11. package/examples/box2d/scenes.js +8 -8
  12. package/examples/breakout/game.js +1 -1
  13. package/examples/breakout/gameObjects.js +2 -2
  14. package/examples/breakout/index.html +1 -1
  15. package/examples/breakoutTutorial/README.md +46 -43
  16. package/examples/breakoutTutorial/game.js +3 -3
  17. package/examples/breakoutTutorial/index.html +1 -1
  18. package/examples/electron/build.bat +7 -0
  19. package/examples/electron/build.js +124 -0
  20. package/examples/electron/electron.js +35 -0
  21. package/examples/electron/game.js +140 -0
  22. package/examples/electron/index.html +13 -0
  23. package/examples/electron/package.json +12 -0
  24. package/examples/electron/tiles.png +0 -0
  25. package/examples/empty/game.js +1 -0
  26. package/examples/empty/index.html +1 -1
  27. package/examples/htmlMenu/game.js +1 -1
  28. package/examples/htmlMenu/index.html +1 -1
  29. package/examples/index.html +486 -182
  30. package/examples/module/game.js +6 -3
  31. package/examples/module/index.html +1 -1
  32. package/examples/particles/index.html +12 -3
  33. package/examples/platformer/game.js +4 -4
  34. package/examples/platformer/gameCharacter.js +6 -6
  35. package/examples/platformer/gameEffects.js +74 -57
  36. package/examples/platformer/gameLevel.js +61 -70
  37. package/examples/platformer/gameObjects.js +4 -4
  38. package/examples/platformer/index.html +1 -1
  39. package/examples/puzzle/index.html +1 -1
  40. package/examples/shorts/base.html +23 -3
  41. package/examples/shorts/blending.js +9 -5
  42. package/examples/shorts/box2d.js +1 -1
  43. package/examples/shorts/box2dCar.js +9 -13
  44. package/examples/shorts/clock.js +1 -1
  45. package/examples/shorts/colors.js +2 -2
  46. package/examples/shorts/flappyGame.js +52 -0
  47. package/examples/shorts/helloWorld.js +6 -3
  48. package/examples/shorts/hillGlideGame.js +56 -0
  49. package/examples/shorts/landerGame.js +32 -0
  50. package/examples/shorts/maze.js +47 -0
  51. package/examples/shorts/medals.js +42 -0
  52. package/examples/shorts/nineSlice.js +21 -0
  53. package/examples/shorts/piano.js +52 -0
  54. package/examples/shorts/platformer.js +24 -20
  55. package/examples/shorts/{pong.js → pongGame.js} +1 -1
  56. package/examples/shorts/raycasting.js +71 -0
  57. package/examples/shorts/shapes.js +9 -9
  58. package/examples/shorts/slidingPuzzle.js +42 -0
  59. package/examples/shorts/spaceGame.js +56 -0
  60. package/examples/shorts/starfield.js +15 -0
  61. package/examples/shorts/systemFont.js +1 -1
  62. package/examples/shorts/tileLayer.js +3 -5
  63. package/examples/shorts/tiles.png +0 -0
  64. package/examples/shorts/tiltedView.js +8 -7
  65. package/examples/shorts/topDown.js +18 -26
  66. package/examples/shorts/uiSystem.js +5 -7
  67. package/examples/starter/build.bat +6 -1
  68. package/examples/starter/build.js +9 -8
  69. package/examples/starter/game.js +7 -4
  70. package/examples/starter/index.html +23 -13
  71. package/examples/stress/index.html +7 -8
  72. package/examples/style.css +139 -0
  73. package/examples/typescript/build.js +2 -3
  74. package/examples/typescript/game.js +4 -4
  75. package/examples/typescript/game.ts +6 -3
  76. package/examples/typescript/index.html +1 -1
  77. package/examples/uiSystem/game.js +10 -25
  78. package/examples/uiSystem/index.html +1 -1
  79. package/package.json +2 -2
  80. package/plugins/box2d.js +22 -97
  81. package/plugins/drawUtilities.js +132 -0
  82. package/plugins/newgrounds.js +1 -1
  83. package/plugins/pluginExport.js +7 -1
  84. package/plugins/postProcess.js +9 -2
  85. package/plugins/uiSystem.js +155 -67
  86. package/plugins/zzfxm.js +1 -1
  87. package/reference.md +10 -10
  88. package/src/engine.js +56 -30
  89. package/src/engineAudio.js +15 -6
  90. package/src/engineBuild.bat +6 -1
  91. package/src/engineBuild.js +20 -5
  92. package/src/engineDebug.js +55 -28
  93. package/src/engineDraw.js +321 -177
  94. package/src/engineExport.js +27 -9
  95. package/src/engineInput.js +110 -38
  96. package/src/engineMedals.js +4 -4
  97. package/src/engineObject.js +71 -70
  98. package/src/engineParticles.js +33 -9
  99. package/src/engineSettings.js +69 -23
  100. package/src/engineTileLayer.js +312 -153
  101. package/src/engineUtilities.js +187 -135
  102. package/src/engineWebGL.js +70 -39
@@ -1,102 +1,42 @@
1
1
  <!DOCTYPE html><head>
2
2
  <title>LittleJS Example Browser</title>
3
3
  <link rel=icon type=image/png href=favicon.png>
4
+ <link rel=stylesheet href=style.css?6>
4
5
  <meta charset=utf-8>
5
- <style>
6
- html, body
7
- {
8
- height: 100%;
9
- display: flex;
10
- flex-direction: column;
11
- }
12
- #container1
13
- {
14
- height: 100%;
15
- display: flex;
16
- }
17
- #container2
18
- {
19
- display: flex;
20
- flex-direction: column;
21
- }
22
- #iframeContainer
23
- {
24
- border: 2px solid;
25
- background: #000;
26
- }
27
- iframe
28
- {
29
- width: 100%;
30
- height: 100%;
31
- border: none;
32
- }
33
- #selectExample
34
- {
35
- flex-grow: 1;
36
- }
37
- #textareaCode
38
- {
39
- flex-grow: 1;
40
- padding: 5px;
41
- resize: none;
42
- color: #fff;
43
- background: #000;
44
- }
45
- #textareaError
46
- {
47
- flex-grow: .3;
48
- padding: 5px;
49
- resize: none;
50
- display: none;
51
- color:#f22;
52
- background: #111;
53
- }
54
- #container3
55
- {
56
- width: 100%;
57
- height: 100%;
58
- display: flex;
59
- flex-direction: column;
60
- }
61
- #titleInfo
62
- {
63
- margin: 5px;
64
- text-align: center;
65
- font-size: 50px;
66
- }
67
- #exampleLink
68
- {
69
- margin: 0px;
70
- text-align: center;
71
- font-size: 30px;
72
- }
73
- #exampleInfo
74
- {
75
- margin: 0px;
76
- text-align: center;
77
- font-size: 22px;
78
- }
79
- select
80
- {
81
- color:#fff;
82
- background-color: #111;
83
- width:100%;
84
- }
85
- </style>
86
6
  </head><body>
87
7
  <div id=container1>
88
8
  <div id=container3>
89
9
  <p id=titleInfo>LittleJS Example Browser</p>
90
- <a id=exampleLink target="_blank">Example LINK</a>
91
- <p id=exampleInfo>Example Info</p>
10
+ <p id=exampleInfo></p>
11
+ <a id=exampleLink target='_blank'></a>
12
+ <hr>
13
+ <div>
14
+ Theme:
15
+ <select style=width:120px onchange=loadTheme() id=selectTheme></select>
16
+ <select style=width:40px onchange=loadTheme() id=selectFontSize>
17
+ <option value=10px>XS</option>
18
+ <option value=13px>S</option>
19
+ <option value=16px selected>M</option>
20
+ <option value=20px>L</option>
21
+ <option value=24px>XL</option>
22
+ </select>
23
+ <input type=checkbox id=checkboxLiveEdit checked>Live Edit
24
+ <input type=checkbox id=checkboxJumpToError>Jump to Error
25
+ </div>
92
26
  <textarea id=textareaCode oninput=codeInput() spellcheck=false></textarea>
93
27
  <textarea id=textareaError readonly spellcheck=false></textarea>
94
- <div><input type=checkbox id=checkboxLiveEdit checked>Live Edit
95
- <button id=buttonScreenshot>Screenshot</button>
96
- </div>
28
+ <textarea id=textareaConsole readonly spellcheck=false></textarea>
97
29
  </div>
98
30
  <div id=container2>
99
31
  <div id=iframeContainer></div>
32
+ <div>
33
+ <input id=inputSearch placeholder='Search examples...' oninput=filterExamples() onkeydown='if(event.key=="Escape")filterExamples(1)'>
34
+ <button id=buttonRestart onclick=restartCode()>Restart</button>
35
+ <button id=buttonPause>Pause</button>
36
+ <button id=buttonScreenshot>Screenshot</button>
37
+ <button id=buttonFullscreen>Fullscreen</button>
38
+ <input type=checkbox id=checkboxWebGL checked>WebGL
39
+ </div>
100
40
  <select id=selectExample autofocus onchange=setExample() size=2></select>
101
41
  </div>
102
42
  </div>
@@ -105,70 +45,81 @@ select
105
45
 
106
46
  class ExampleInfo
107
47
  {
108
- constructor(name, filename, description, fullExample=false)
48
+ constructor(name, filename, description='', largeExample=false)
109
49
  {
110
- if (filename && !fullExample)
111
- filename = 'shorts/' + filename; // short examples folder
50
+ if (filename && !largeExample)
51
+ filename = 'shorts/' + filename;
112
52
  this.name = name;
113
53
  this.filename = filename;
114
54
  this.description = description;
115
- this.fullExample = fullExample;
55
+ this.largeExample = largeExample;
116
56
  }
117
57
  }
118
58
 
119
59
  const exampleList =
120
60
  [
121
61
  new ExampleInfo('--- SHORT EXAMPLES ---'),
122
- new ExampleInfo('Hello World', 'helloWorld.js', 'Simplest example'),
123
- new ExampleInfo('Shapes', 'shapes.js', 'How to draw geometric shapes'),
124
- new ExampleInfo('Colors', 'colors.js', 'How to use the color object'),
125
- new ExampleInfo('Blending', 'blending.js', 'How to use blending modes'),
126
- new ExampleInfo('Timers', 'timers.js', 'How to use the timer object'),
127
- new ExampleInfo('Texture', 'texture.js', 'How to display a full texture'),
128
- new ExampleInfo('Particles', 'particles.js', 'How to use the particle system'),
129
- new ExampleInfo('Play Sound', 'playSound.js', 'How to play sounds with zzfx'),
130
- new ExampleInfo('System Font', 'systemFont.js', 'Demo of the included system font'),
131
- new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'How to set up a simple sprite atlas'),
132
- new ExampleInfo('Animation', 'animation.js', 'How to animate sprites'),
133
- new ExampleInfo('Clock', 'clock.js', 'Simple clock showing the time'),
134
- new ExampleInfo('Tile Layer', 'tileLayer.js', 'How to use the tile layer object'),
135
- new ExampleInfo('Platformer Game', 'platformer.js', 'Platformer jumping game'),
136
- new ExampleInfo('Top Down Game', 'topDown.js', 'Rop down adventure game'),
137
- new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D view with tilted camera'),
138
- new ExampleInfo('Pong Game', 'pong.js', 'Simple pong game'),
139
- new ExampleInfo('Box2d Demo', 'box2d.js', 'Demo of the Box2D physics plugin'),
140
- new ExampleInfo('Box2d Car', 'box2dCar.js', 'Controllable vehicle using the Box2D physics plugin'),
141
- new ExampleInfo('Post Processing', 'postProcess.js', 'Example of the post processing plugin'),
142
- new ExampleInfo('UI System Plugin', 'uiSystem.js', 'Simple example of LittleJS UI plugin'),
143
-
62
+ new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal starter example - [basic, beginner]'),
63
+ new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes - [graphics, primitives, basic]'),
64
+ new ExampleInfo('Colors', 'colors.js', 'Color object and HSL usage - [graphics, color, basic]'),
65
+ new ExampleInfo('Blending', 'blending.js', 'Blending modes and transparency - [graphics, effects]'),
66
+ new ExampleInfo('Timers', 'timers.js', 'Timer object and timing functions - [utilities, time]'),
67
+ new ExampleInfo('Texture', 'texture.js', 'Display and manipulate textures - [graphics, sprites]'),
68
+ new ExampleInfo('Particles', 'particles.js', 'Particle system basics - [effects, particles]'),
69
+ new ExampleInfo('Play Sound', 'playSound.js', 'Sound effects with ZzFX - [audio, sound]'),
70
+ new ExampleInfo('System Font', 'systemFont.js', 'Built-in bitmap font rendering - [text, font]'),
71
+ new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering - [graphics, sprites]'),
72
+ new ExampleInfo('Animation', 'animation.js', 'Sprite animation techniques - [animation, sprites]'),
73
+ new ExampleInfo('Clock', 'clock.js', 'Real-time clock display - [time, ui, utilities]'),
74
+ new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system - [tiles, levels]'),
75
+ new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard - [audio, input, ui]'),
76
+ new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation - [algorithm, generation]'),
77
+ new ExampleInfo('Starfield', 'starfield.js', 'Animated parallax starfield - [effects, space]'),
78
+ new ExampleInfo('Medals', 'medals.js', 'Achievement system demo - [ui, achievements]'),
79
+ new ExampleInfo('--- MINI GAMES ---'),
80
+ new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run platformer - [game, platformer, physics]'),
81
+ new ExampleInfo('Top Down Game', 'topDown.js', 'Adventure with collision - [game, topdown, rpg]'),
82
+ new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D tilted perspective - [3d, camera, perspective]'),
83
+ new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship with parallax scrolling - [game, space, shooter]'),
84
+ new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander physics game - [game, physics, thrust]'),
85
+ new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird clone - [game, arcade, flying]'),
86
+ new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style glider - [game, physics, flying]'),
87
+ new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball game - [game, classic, arcade]'),
88
+ new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', 'Tile sliding puzzle game - [game, puzzle, logic]'),
89
+ new ExampleInfo('Raycasting Game', 'raycasting.js', 'Pseudo 3D raycasting demo - [3d, raycasting, wolfenstein]'),
90
+ new ExampleInfo('--- PLUGINS ---'),
91
+ new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin demo - [physics, box2d, plugin]'),
92
+ new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D - [physics, box2d, vehicle]'),
93
+ new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters - [graphics, shaders, effects]'),
94
+ new ExampleInfo('UI System Plugin', 'uiSystem.js', 'UI elements and widgets - [ui, plugin, interface]'),
95
+ new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panel rendering - [ui, graphics, scaling]'),
144
96
  new ExampleInfo('--- FULL EXAMPLES ---'),
145
- new ExampleInfo('Starter', 'starter', 'Clean starter project', true),
146
- new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Tutorial Breakout example', true),
147
- new ExampleInfo('Breakout Game', 'breakout', 'Block breaking game with post-processing effects', true),
148
- new ExampleInfo('Platforming Game', 'platformer', 'Platformer/shooter with level data loading', true),
149
- new ExampleInfo('Puzzle Game', 'puzzle', 'Match 3 puzzle game with HD rendering', true),
150
- new ExampleInfo('Stress Test', 'stress', 'Max sprite/object test and music system demo', true),
151
- new ExampleInfo('Box2D Plugin', 'box2d', 'Demo of the Box2D physics plugin', true),
152
- new ExampleInfo('HTML Menus', 'htmlMenu', 'Shows how to combine HTML with LittleJS', true),
153
- new ExampleInfo('UI System Plugin', 'uiSystem', 'Example of how to use LittleJS UI plugin', true),
154
- // Add more examples here!
97
+ new ExampleInfo('Starter', 'starter', 'Clean project template - [template, beginner]', true),
98
+ new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout guide - [tutorial, breakout]', true),
99
+ new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout with effects - [game, breakout, complete]', true),
100
+ new ExampleInfo('Platforming Game', 'platformer', 'Full platformer with levels - [game, platformer, complete]', true),
101
+ new ExampleInfo('Puzzle Game', 'puzzle', 'Match-3 puzzle with HD graphics - [game, puzzle, match3]', true),
102
+ new ExampleInfo('Stress Test', 'stress', 'Performance test with music - [performance, test, music]', true),
103
+ new ExampleInfo('Box2D Plugin', 'box2d', 'Complete Box2D physics demo - [physics, box2d, complete]', true),
104
+ new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML integration example - [html, ui, integration]', true),
105
+ new ExampleInfo('UI System Plugin', 'uiSystem', 'Complete UI system demo - [ui, plugin, complete]', true),
155
106
  ];
156
107
 
157
108
  ///////////////////////////////////////////////////////////////////////////////
158
109
 
159
- let iframeExample, inputTimeout;
110
+ // global variables
111
+ let iframeExample, inputTimeout, codeMirror, consoleAutoScroll, errorLineMarker, codeIsJS;
160
112
 
161
113
  // load the examples into the list
162
- for (const example of exampleList)
163
- {
164
- const text = example.name + (example.description?' - ' + example.description : '');
165
- const o = new Option(text);
166
- o.disabled = !example.filename;
167
- selectExample.add(o);
168
- }
114
+ filterExamples();
169
115
 
170
- // set the selected example from the URL hash
171
- selectExample.selectedIndex = location.hash.split`#`[1] | 0
116
+ // set the selected example from the URL parameters
117
+ const urlParams = new URLSearchParams(window.location.search);
118
+ const selectedExample = urlParams.get('example') || '';
119
+ let exampleIndex = exampleList.findIndex((example) => example.filename == selectedExample);
120
+ if (exampleIndex < 0)
121
+ exampleIndex = 1;
122
+ selectExample.selectedIndex = exampleIndex;
172
123
  setExample();
173
124
 
174
125
  onresize = () =>
@@ -179,55 +130,73 @@ onresize = () =>
179
130
  let h = w / aspect | 0;
180
131
  iframeContainer.style.width = w + 'px';
181
132
  iframeContainer.style.height = h + 'px';
133
+ // fix code mirror width
134
+ container3.style.width = w-30 + 'px';
182
135
  }
183
136
  onresize();
184
137
 
185
138
  ///////////////////////////////////////////////////////////////////////////////
139
+ // setting examples
186
140
 
187
141
  function setExample()
188
142
  {
189
- if (selectExample.selectedIndex < 1 || selectExample.selectedIndex >= exampleList.length)
190
- selectExample.selectedIndex = 1; // reset to default
191
- let example = exampleList[selectExample.selectedIndex];
192
- if (!example.filename)
193
- example = exampleList[selectExample.selectedIndex = 1]; // reset to default
143
+ // get the original index if this is a filtered result
144
+ const selectedOption = selectExample.options[selectExample.selectedIndex];
145
+ let exampleIndex = selectedOption && selectedOption.originalIndex ?
146
+ parseInt(selectedOption.originalIndex) :
147
+ selectExample.selectedIndex;
148
+
149
+ // make sure we have a valid example
150
+ if (exampleIndex < 0 || exampleIndex >= exampleList.length)
151
+ exampleIndex = 1; // reset to default
152
+ if (!exampleList[exampleIndex].filename)
153
+ exampleIndex = 1;
154
+
155
+ // load the example
156
+ const example = exampleList[exampleIndex];
194
157
  exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
195
158
  const filename = 'examples/' + example.filename;
196
159
  exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
197
160
  exampleLink.innerText = filename;
198
- loadFile(example.filename, example.fullExample);
199
- location.hash = selectExample.selectedIndex;
161
+
162
+ // update URL parameter
163
+ const url = new URL(window.location);
164
+ url.searchParams.set('example', example.filename);
165
+ window.history.replaceState({}, '', url);
166
+
167
+ loadFile(example.filename, example.largeExample);
200
168
  }
201
169
 
202
- function codeInput()
170
+ async function loadFile(filename, largeExample)
203
171
  {
204
- if (!checkboxLiveEdit.checked)
205
- return;
206
-
207
- // debounce input
208
- clearTimeout(inputTimeout);
209
- inputTimeout = setTimeout(() => setCode(textareaCode.value), 500);
210
- }
211
-
212
- async function loadFile(filename, fullExample)
213
- {
214
- textareaCode.disabled = fullExample;
215
- if (fullExample)
172
+ if (codeMirror)
173
+ codeMirror.setOption('readOnly', largeExample);
174
+ else
175
+ textareaCode.disabled = largeExample;
176
+
177
+ // disable buttons in large example mode
178
+ buttonPause.disabled = largeExample;
179
+ buttonRestart.disabled = largeExample;
180
+ buttonScreenshot.disabled = largeExample;
181
+ buttonFullscreen.disabled = largeExample;
182
+ checkboxWebGL.disabled = largeExample;
183
+
184
+ if (largeExample)
216
185
  {
217
- if (iframeExample)
218
- iframeContainer.removeChild(iframeExample);
219
186
 
220
- setErrorMessage();
221
- iframeExample = document.createElement('iframe');
222
- iframeContainer.appendChild(iframeExample);
223
- iframeExample.onload = () =>
187
+ // Show message in code view that full examples can't be edited
188
+ const text = 'Code view not available for large examples.';
189
+ setCode(text, filename);
190
+ codeIsJS = false;
191
+ if (codeMirror)
224
192
  {
225
- const iframeContent = iframeExample.contentWindow;
226
- const iframeDocument = iframeContent.document;
227
- //textareaCode.value = iframeDocument.body.innerHTML;
228
- textareaCode.value = 'Editor code view not available for large examples.\n';
193
+ codeMirror.off('change', codeInput);
194
+ codeMirror.setOption('mode', 'text');
195
+ codeMirror.setValue(text);
229
196
  }
230
- iframeExample.src = filename;
197
+ else
198
+ textareaCode.value = text;
199
+ clearTimeout(inputTimeout);
231
200
  return;
232
201
  }
233
202
 
@@ -237,7 +206,19 @@ async function loadFile(filename, fullExample)
237
206
  if (!response.ok)
238
207
  throw new Error('Could not load file: ' + filename);
239
208
  const text = await response.text();
240
- setCode(textareaCode.value = text, fullExample);
209
+
210
+ // set the code in both code mirror and textarea
211
+ codeIsJS = true;
212
+ if (codeMirror)
213
+ {
214
+ codeMirror.on('change', codeInput);
215
+ codeMirror.setOption('mode', 'javascript');
216
+ codeMirror.setValue(text);
217
+ }
218
+ else
219
+ textareaCode.value = text;
220
+ clearTimeout(inputTimeout);
221
+ setCode(text);
241
222
  }
242
223
  catch (error)
243
224
  {
@@ -245,45 +226,167 @@ async function loadFile(filename, fullExample)
245
226
  }
246
227
  }
247
228
 
248
- function setCode(code, largeExample)
229
+ function filterExamples(reset=0)
230
+ {
231
+ if (reset)
232
+ inputSearch.value = '';
233
+
234
+ // clear current options
235
+ selectExample.options.length = 0;
236
+
237
+ // filter and add matching examples
238
+ const searchTerm = inputSearch.value.toLowerCase().trim();
239
+ for (let i = 0; i < exampleList.length; i++)
240
+ {
241
+ // check if search term matches name or description
242
+ const example = exampleList[i];
243
+ const text = example.name + (example.description ? ` - ${example.description}` : '');
244
+ if (example.name.toLowerCase().includes(searchTerm) ||
245
+ example.description.toLowerCase().includes(searchTerm))
246
+ {
247
+ const o = new Option(text);
248
+ o.disabled = !example.filename;
249
+ o.originalIndex = i;
250
+ selectExample.add(o);
251
+ }
252
+ }
253
+
254
+ if (!selectExample.options.length)
255
+ {
256
+ // show a message if no matches
257
+ const o = new Option('No examples found matching "' + searchTerm + '"');
258
+ o.disabled = true;
259
+ selectExample.add(o);
260
+ }
261
+ }
262
+
263
+ ///////////////////////////////////////////////////////////////////////////////
264
+ // setting code
265
+
266
+ function codeInput()
249
267
  {
268
+ if (!checkboxLiveEdit.checked)
269
+ return;
270
+
271
+ // debounce input - get content from code mirror if available, otherwise from textarea
272
+ clearTimeout(inputTimeout);
273
+ const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
274
+ inputTimeout = setTimeout(() => setCode(code), 500);
275
+ }
276
+
277
+ function restartCode()
278
+ {
279
+ // manually restart/run the code regardless of live edit setting
280
+ const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
281
+ setCode(code);
282
+ }
283
+
284
+ function setCode(code, textFilename)
285
+ {
286
+ const largeExample = !!textFilename;
250
287
  clearTimeout(inputTimeout);
251
288
  if (iframeExample)
252
289
  iframeContainer.removeChild(iframeExample);
253
290
 
254
- setErrorMessage();
291
+ unsetErrorMessage();
292
+ unsetConsoleMessage();
255
293
  iframeExample = document.createElement('iframe');
256
294
  iframeContainer.appendChild(iframeExample);
257
- iframeExample.onload = () =>
295
+ iframeExample.onload = ()=>
258
296
  {
297
+ if (largeExample)
298
+ return;
299
+
259
300
  const iframeContent = iframeExample.contentWindow;
260
301
  const iframeDocument = iframeContent.document;
261
302
 
262
- // create error event listeners
303
+ // intercept errors
304
+ function getErrorLine(stack)
305
+ {
306
+ // try to extract line number from stack trace, look for <anonymous> or injectedScript to find the user code
307
+ const anonymousMatch = stack.match(/(<anonymous>|injectedScript):(\d+)/);
308
+ return anonymousMatch ? parseInt(anonymousMatch[2]) : -1;
309
+ }
263
310
  iframeContent.onerror = (message, source, lineno, colno, error) =>
264
311
  {
265
312
  let text = message;
266
313
  if (lineno)
267
314
  text += ` (Line:${lineno}, Column:${colno})`
268
315
  if (error && error.stack)
269
- text += `\n\n` + error.stack;
316
+ text += `\n` + error.stack;
270
317
  setErrorMessage(text);
318
+ setErrorLine(lineno);
271
319
  }
272
320
  iframeContent.onunhandledrejection = (event) =>
273
321
  {
274
322
  let text = event.reason;
275
- if (event.reason.stack)
276
- text += `\n\n` + event.reason.stack;
277
323
  setErrorMessage(text);
324
+ if (event.reason && event.reason.stack)
325
+ {
326
+ text = event.reason.stack;
327
+ const errorLine = getErrorLine(event.reason.stack);
328
+ if (errorLine >= 0)
329
+ setErrorLine(errorLine);
330
+ }
278
331
  };
332
+
333
+ // intercept asserts
279
334
  const originalAssert = iframeContent.console.assert;
280
335
  iframeContent.console.assert = function (condition, output)
281
336
  {
282
- if (!condition)
283
- setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + (new Error).stack);
337
+ if (!condition)
338
+ {
339
+ const stack = (new Error).stack;
340
+ const errorLine = getErrorLine(stack);
341
+ if (errorLine >= 0)
342
+ setErrorLine(errorLine);
343
+ setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + stack);
344
+ }
284
345
  originalAssert.apply(this, arguments);
285
346
  };
286
347
 
348
+ // intercept console prints
349
+ const originalConsole = iframeContent.console;
350
+ function interceptConsole(method)
351
+ {
352
+ const original = originalConsole[method];
353
+ iframeContent.console[method] = function(...args)
354
+ {
355
+ const message = args.map(m=>stringifyMessage(m)).join('\n');
356
+ setConsoleMessage(message);
357
+ original.apply(originalConsole, args);
358
+ };
359
+ }
360
+ ['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
361
+
362
+ {
363
+ // setup buttons
364
+ buttonScreenshot.onclick = () =>
365
+ {
366
+ if (iframeContent.debugScreenshot)
367
+ iframeContent.debugScreenshot();
368
+ }
369
+
370
+ // pause/resume functionality
371
+ buttonPause.onclick = () =>
372
+ {
373
+ if (!iframeContent.getPaused || !iframeContent.setPaused)
374
+ return;
375
+
376
+ const paused = !iframeContent.getPaused();
377
+ iframeContent.setPaused(paused);
378
+ buttonPause.textContent = paused ? 'Resume' : 'Pause';
379
+ }
380
+ buttonPause.textContent = 'Pause';
381
+
382
+ // fullscreen functionality
383
+ buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
384
+ }
385
+
386
+ // set WebGL based on checkbox
387
+ iframeContent.setGLEnable(checkboxWebGL.checked);
388
+ checkboxWebGL.onchange = ()=> iframeContent.setGLEnable(checkboxWebGL.checked);
389
+
287
390
  // create a script element that overrides the default functions
288
391
  const overrideScript = iframeDocument.createElement('script');
289
392
  iframeDocument.body.appendChild(overrideScript);
@@ -293,23 +396,224 @@ function setCode(code, largeExample)
293
396
  return; // stop if script error
294
397
 
295
398
  // start LittleJS engine
296
- iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?1']);
399
+ iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()]);
400
+ }
401
+ iframeExample.src = textFilename || 'shorts/base.html?' + Date.now();
402
+ }
403
+
404
+ ///////////////////////////////////////////////////////////////////////////////
405
+ // error and console messages
406
+
407
+ function stringifyMessage(message)
408
+ {
409
+ // make sure message is a string
410
+ if (message === null)
411
+ return 'null';
412
+ if (Number.isNaN(message))
413
+ return 'NaN';
414
+ if (message === undefined)
415
+ return 'undefined';
416
+ if (message === 0)
417
+ return '0';
418
+ if (message === false)
419
+ return 'false';
420
+ return message;
421
+ }
422
+
423
+ function setMessage(message, element, clear=true)
424
+ {
425
+ message = stringifyMessage(message);
426
+ if (clear || !element.value)
427
+ element.value = message;
428
+ else
429
+ element.value += '\n' + message;
430
+ element.style.display = message ? 'block' : 'none';
431
+
432
+ if (element == textareaConsole)
433
+ {
434
+ const maxConsoleLines = 100;
435
+ const lines = element.value.split('\n');
436
+ if (lines.length > maxConsoleLines)
437
+ {
438
+ // limit max lines, prevents slowdown from too many lines
439
+ const excessLines = lines.length - maxConsoleLines;
440
+ element.value = lines.slice(excessLines).join('\n');
441
+ }
442
+
443
+ // auto scroll to bottom only if user hasn't manually scrolled
444
+ if (consoleAutoScroll)
445
+ element.scrollTop = element.scrollHeight;
446
+ }
447
+ }
448
+
449
+ function unsetMessage(element)
450
+ {
451
+ element.style.display = 'none';
452
+ element.value = '';
453
+ }
454
+
455
+ function clearErrorLine()
456
+ {
457
+ if (!codeMirror || !errorLineMarker)
458
+ return;
459
+ codeMirror.getDoc().removeLineClass(errorLineMarker, 'background', 'error-line');
460
+ }
461
+
462
+ function setErrorLine(lineNumber)
463
+ {
464
+ if (!lineNumber || lineNumber <= 0)
465
+ return;
297
466
 
298
- buttonScreenshot.onclick = () =>
467
+ if (codeMirror)
468
+ {
469
+ clearErrorLine();
470
+ --lineNumber; // codeMirror uses 0-based line numbers
471
+ errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
472
+ if (checkboxJumpToError.checked)
299
473
  {
300
- if (iframeContent.debugScreenshot)
301
- iframeContent.debugScreenshot();
474
+ codeMirror.scrollIntoView({line: lineNumber, ch: 0});
475
+ codeMirror.focus();
476
+ }
477
+ }
478
+ else if (textareaCode && checkboxJumpToError.checked)
479
+ {
480
+ // for textarea, calculate character position and set cursor
481
+ const lines = textareaCode.value.split('\n');
482
+ let charPos = 0;
483
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++)
484
+ {
485
+ charPos += lines[i].length + 1; // +1 for newline character
486
+ }
487
+ textareaCode.setSelectionRange(charPos, charPos);
488
+ if (checkboxJumpToError.checked)
489
+ {
490
+ const line = textareaCode.value.split('\n');
491
+ lineNumber = Math.min(Math.max(0, lineNumber-1), lines.length-1);
492
+
493
+ // select error line in textarea
494
+ let start = 0;
495
+ for (let i = 0; i < lineNumber; i++)
496
+ start += lines[i].length + 1;
497
+ textareaCode.selectionStart = start;
498
+ textareaCode.selectionEnd = start + lines[lineNumber].length;
499
+ textareaCode.focus();
302
500
  }
303
501
  }
304
- iframeExample.src = 'shorts/base.html';
305
502
  }
306
503
 
307
- function setErrorMessage(message='')
504
+ function setErrorMessage(message)
505
+ {
506
+ // prevent overwriting an existing error message
507
+ const errorMessageIsVisible = textareaError.style.display == 'block';
508
+ if (!errorMessageIsVisible)
509
+ setMessage(message, textareaError);
510
+ }
511
+ function unsetErrorMessage() { unsetMessage(textareaError); clearErrorLine(); }
512
+ function setConsoleMessage(message) { setMessage(message, textareaConsole, false); }
513
+ function unsetConsoleMessage() { unsetMessage(textareaConsole); consoleAutoScroll = true; }
514
+ function onScrollConsole()
515
+ {
516
+ // only auto scroll console is it is scrolled to bottom
517
+ const tolerance = 5; // pixels of tolerance
518
+ consoleAutoScroll = Math.abs(textareaConsole.scrollTop - (textareaConsole.scrollHeight - textareaConsole.clientHeight)) < tolerance;
519
+ }
520
+ textareaConsole.addEventListener('scroll', onScrollConsole);
521
+
522
+ ///////////////////////////////////////////////////////////////////////////////
523
+ // setup code mirror
524
+
525
+ const defaultTheme = 'littlejs';
526
+ const defaultFontSize = '16px';
527
+ const themes =
528
+ [
529
+ 'littlejs',
530
+ '3024-night',
531
+ 'abcdef',
532
+ 'ambiance',
533
+ 'blackboard',
534
+ 'monokai',
535
+ 'duotone-light',
536
+ 'icecoder',
537
+ 'lesser-dark',
538
+ 'night',
539
+ 'yonce'
540
+ ];
541
+
542
+ // Load saved preferences from localStorage
543
+ const savePrefix = 'LittleJSExamples_';
544
+ const savedTheme = localStorage.getItem(savePrefix+'theme') || defaultTheme;
545
+ const savedFontSize = localStorage.getItem(savePrefix+'fontSize') || defaultFontSize;
546
+
547
+ for (const theme of themes)
548
+ {
549
+ if (theme != 'littlejs')
550
+ addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
551
+ const o = new Option(theme);
552
+ o.selected = theme == savedTheme;
553
+ selectTheme.add(o);
554
+ }
555
+
556
+ // Set the saved font size
557
+ selectFontSize.value = savedFontSize;
558
+ textareaCode.style.fontSize = savedFontSize;
559
+ addCodeMirrorElement('codemirror.min.js', 'script').onload = ()=>
560
+ addCodeMirrorElement('codemirror.min.css', 'link', 'stylesheet').onload = ()=>
561
+ addCodeMirrorElement('addon/edit/matchbrackets.js', 'script').onload = ()=>
562
+ addCodeMirrorElement('mode/javascript/javascript.min.js', 'script').onload = ()=>
563
+ initCodeMirror();
564
+
565
+ function initCodeMirror()
308
566
  {
309
- textareaError.value = message
310
- textareaError.style.display = message ? 'block' : 'none';
567
+ if (codeMirror)
568
+ return; // prevent duplicate initialization
569
+
570
+ const textareaCode = document.getElementById('textareaCode');
571
+ codeMirror = CodeMirror.fromTextArea(textareaCode,
572
+ {
573
+ theme: savedTheme,
574
+ indentUnit: 4,
575
+ mode: codeIsJS ? 'javascript' : 'text',
576
+ lineNumbers: true,
577
+ lineWrapping: true,
578
+ matchBrackets: true,
579
+ });
580
+ codeMirror.on('change', codeInput);
581
+ loadTheme();
582
+ }
583
+
584
+ function addCodeMirrorElement(filename, type, rel)
585
+ {
586
+ // add element for code mirror
587
+ const e = document.createElement(type);
588
+ e.rel = rel;
589
+ filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/' + filename;
590
+ if (type == 'link')
591
+ e.href = filename;
592
+ else
593
+ e.src = filename;
594
+ e.crossOrigin = 'anonymous';
595
+ document.head.appendChild(e);
596
+ return e;
597
+ }
598
+
599
+ function loadTheme()
600
+ {
601
+ const theme = selectTheme.value;
602
+ const size = selectFontSize.value;
603
+
604
+ // Save preferences to localStorage
605
+ localStorage.setItem(savePrefix+'theme', theme);
606
+ localStorage.setItem(savePrefix+'fontSize', size);
607
+
608
+ // Apply the theme and font size
609
+ textareaCode.style.fontSize = size;
610
+ if (codeMirror)
611
+ {
612
+ codeMirror.getWrapperElement().style.fontSize = size;
613
+ codeMirror.setOption('theme', theme);
614
+ }
311
615
  }
312
616
 
313
617
  </script>
314
618
  </body>
315
- </html>
619
+ </html>