littlejsengine 1.13.1 → 1.14.2

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 (83) hide show
  1. package/README.md +15 -11
  2. package/copyToGithub.bat +28 -0
  3. package/dist/littlejs.d.ts +430 -304
  4. package/dist/littlejs.esm.js +1745 -982
  5. package/dist/littlejs.esm.min.js +1 -1
  6. package/dist/littlejs.js +1716 -965
  7. package/dist/littlejs.min.js +1 -1
  8. package/dist/littlejs.release.js +1673 -922
  9. package/examples/box2d/game.js +2 -2
  10. package/examples/box2d/gameObjects.js +2 -2
  11. package/examples/box2d/scenes.js +1 -1
  12. package/examples/breakout/game.js +30 -25
  13. package/examples/breakoutTutorial/README.md +44 -41
  14. package/examples/breakoutTutorial/game.js +2 -2
  15. package/examples/electron/build.bat +7 -0
  16. package/examples/electron/build.js +124 -0
  17. package/examples/electron/electron.js +35 -0
  18. package/examples/electron/game.js +140 -0
  19. package/examples/electron/index.html +13 -0
  20. package/examples/electron/package.json +12 -0
  21. package/examples/electron/tiles.png +0 -0
  22. package/examples/empty/game.js +1 -0
  23. package/examples/htmlMenu/game.js +1 -1
  24. package/examples/index.html +332 -160
  25. package/examples/module/game.js +5 -2
  26. package/examples/particles/index.html +12 -3
  27. package/examples/platformer/gameEffects.js +20 -19
  28. package/examples/platformer/gameLevel.js +6 -1
  29. package/examples/shorts/base.html +1 -1
  30. package/examples/shorts/blending.js +9 -5
  31. package/examples/shorts/box2d.js +1 -1
  32. package/examples/shorts/box2dCar.js +1 -1
  33. package/examples/shorts/clock.js +1 -1
  34. package/examples/shorts/colors.js +2 -2
  35. package/examples/shorts/flappyGame.js +2 -1
  36. package/examples/shorts/helloWorld.js +1 -1
  37. package/examples/shorts/maze.js +1 -1
  38. package/examples/shorts/music.js +106 -0
  39. package/examples/shorts/nineSlice.js +9 -9
  40. package/examples/shorts/parallax.js +71 -0
  41. package/examples/shorts/piano.js +9 -10
  42. package/examples/shorts/postProcess.js +25 -8
  43. package/examples/shorts/raycasting.js +2 -2
  44. package/examples/shorts/shapes.js +7 -15
  45. package/examples/shorts/slidingPuzzle.js +2 -2
  46. package/examples/shorts/song.mp3 +0 -0
  47. package/examples/shorts/starfield.js +5 -7
  48. package/examples/shorts/systemFont.js +1 -1
  49. package/examples/shorts/uiSystem.js +16 -8
  50. package/examples/starter/build.js +9 -8
  51. package/examples/starter/game.js +5 -2
  52. package/examples/starter/index.html +2 -2
  53. package/examples/stress/index.html +13 -8
  54. package/examples/style.css +19 -6
  55. package/examples/typescript/build.js +1 -1
  56. package/examples/typescript/game.js +2 -2
  57. package/examples/typescript/game.ts +5 -2
  58. package/examples/uiSystem/game.js +13 -12
  59. package/package.json +2 -2
  60. package/plugins/box2d.js +24 -97
  61. package/plugins/drawUtilities.js +29 -23
  62. package/plugins/newgrounds.js +6 -6
  63. package/plugins/pluginExport.js +1 -1
  64. package/plugins/postProcess.js +7 -0
  65. package/plugins/uiSystem.js +106 -82
  66. package/plugins/zzfxm.js +10 -10
  67. package/reference.md +90 -54
  68. package/src/engine.js +28 -23
  69. package/src/engineAudio.js +285 -110
  70. package/src/engineBuild.js +9 -9
  71. package/src/engineDebug.js +28 -28
  72. package/src/engineDraw.js +346 -202
  73. package/src/engineExport.js +28 -16
  74. package/src/engineInput.js +58 -68
  75. package/src/engineMedals.js +29 -29
  76. package/src/engineObject.js +28 -25
  77. package/src/engineParticles.js +67 -60
  78. package/src/engineRelease.js +1 -1
  79. package/src/engineSettings.js +70 -16
  80. package/src/engineTileLayer.js +34 -34
  81. package/src/engineUtilities.js +69 -62
  82. package/src/engineWebGL.js +467 -65
  83. /package/examples/shorts/{playSound.js → sound.js} +0 -0
@@ -1,16 +1,17 @@
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?4>
4
+ <link rel=stylesheet href=style.css?9>
5
5
  <meta charset=utf-8>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
7
  </head><body>
7
8
  <div id=container1>
8
9
  <div id=container3>
9
10
  <p id=titleInfo>LittleJS Example Browser</p>
10
11
  <p id=exampleInfo></p>
11
12
  <a id=exampleLink target='_blank'></a>
13
+ <div id=divCodeOptions>
12
14
  <hr>
13
- <div>
14
15
  Theme:
15
16
  <select style=width:120px onchange=loadTheme() id=selectTheme></select>
16
17
  <select style=width:40px onchange=loadTheme() id=selectFontSize>
@@ -20,23 +21,31 @@ Theme:
20
21
  <option value=20px>L</option>
21
22
  <option value=24px>XL</option>
22
23
  </select>
23
- <input type=checkbox id=checkboxLiveEdit checked>Live Edit
24
- <button id=buttonRefresh onclick=refreshCode()>Refresh</button>
25
- </div>
24
+ <span class=nowrap><input type=checkbox id=checkboxLiveEdit checked>Live Edit</span>
25
+ <span class=nowrap><input type=checkbox id=checkboxJumpToErrors>Jump to Errors</span>
26
+ </div>
27
+ <div id=divTextareas>
26
28
  <textarea id=textareaCode oninput=codeInput() spellcheck=false></textarea>
27
29
  <textarea id=textareaError readonly spellcheck=false></textarea>
28
30
  <textarea id=textareaConsole readonly spellcheck=false></textarea>
29
31
  </div>
32
+ </div>
30
33
  <div id=container2>
34
+ <div style='display:flex;justify-content:center'>
31
35
  <div id=iframeContainer></div>
36
+ </div>
32
37
  <div>
33
- <input id=inputSearch placeholder='Search examples...' oninput=filterExamples() onkeydown='if(event.key=="Escape")filterExamples(1)'>
34
- <button onclick=filterExamples(1)>Clear</button>
38
+ <input id=inputSearch placeholder='Search examples...' oninput=filterExamples() onkeydown='if(event.key==="Escape")filterExamples(1)'>
39
+ <button id=buttonRestart onclick=restartCode()>Restart</button>
35
40
  <button id=buttonPause>Pause</button>
36
- <button id=buttonScreenshot>Screenshot</button>
41
+ <span id=container4>
37
42
  <button id=buttonFullscreen>Fullscreen</button>
43
+ <button id=buttonScreenshot>Screenshot</button>
44
+ <span class=nowrap><input type=checkbox id=checkboxWebGL checked>WebGL</span>
45
+ </span>
38
46
  </div>
39
- <select id=selectExample autofocus onchange=setExample() size=2></select>
47
+ <span id=selectExampleText class=nowrap>Select an example...</span>
48
+ <select id=selectExample onchange=setExample() size=2></select>
40
49
  </div>
41
50
  </div>
42
51
  <script>
@@ -58,80 +67,141 @@ class ExampleInfo
58
67
  const exampleList =
59
68
  [
60
69
  new ExampleInfo('--- SHORT EXAMPLES ---'),
61
- new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal example'),
62
- new ExampleInfo('Shapes', 'shapes.js', 'How to draw geometric shapes'),
63
- new ExampleInfo('Colors', 'colors.js', 'How to use the color object'),
64
- new ExampleInfo('Blending', 'blending.js', 'How to use blending modes'),
65
- new ExampleInfo('Timers', 'timers.js', 'How to use the timer object'),
66
- new ExampleInfo('Texture', 'texture.js', 'How to display a full texture'),
67
- new ExampleInfo('Particles', 'particles.js', 'How to use the particle system'),
68
- new ExampleInfo('Play Sound', 'playSound.js', 'How to play sounds with zzfx'),
69
- new ExampleInfo('System Font', 'systemFont.js', 'Demo of the included system font'),
70
- new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'How to set up a simple sprite atlas'),
71
- new ExampleInfo('Animation', 'animation.js', 'How to animate sprites'),
72
- new ExampleInfo('Clock', 'clock.js', 'Simple clock showing the time'),
73
- new ExampleInfo('Tile Layer', 'tileLayer.js', 'How to use the tile layer object'),
74
- new ExampleInfo('Piano', 'piano.js', 'A simple piano keyboard demo'),
75
- new ExampleInfo('Maze Generator', 'maze.js', 'Generates a random maze'),
76
- new ExampleInfo('Starfield', 'starfield.js', 'A moving starfield effect'),
77
- new ExampleInfo('Medals', 'medals.js', 'How to use the medal system'),
70
+ new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal starter example - [basic, beginner]'),
71
+ new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes - [graphics, primitives, basic]'),
72
+ new ExampleInfo('Colors', 'colors.js', 'Color object and HSL usage - [graphics, color, basic]'),
73
+ new ExampleInfo('Blending', 'blending.js', 'Blending modes and transparency - [graphics, effects]'),
74
+ new ExampleInfo('Timers', 'timers.js', 'Timer object and timing functions - [utilities, time]'),
75
+ new ExampleInfo('Texture', 'texture.js', 'Display and manipulate textures - [graphics, sprites]'),
76
+ new ExampleInfo('Particles', 'particles.js', 'Particle system basics - [effects, particles]'),
77
+ new ExampleInfo('Sound Effects', 'sound.js', 'Example sound effects with ZzFX - [audio, sound]'),
78
+ new ExampleInfo('Music', 'music.js', 'Example sound loading, play, pause, seek, and stop - [audio, music]'),
79
+ new ExampleInfo('System Font', 'systemFont.js', 'Built-in bitmap font rendering - [text, font]'),
80
+ new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering - [graphics, sprites]'),
81
+ new ExampleInfo('Animation', 'animation.js', 'Sprite animation techniques - [animation, sprites]'),
82
+ new ExampleInfo('Clock', 'clock.js', 'Real-time clock display - [time, ui, utilities]'),
83
+ new ExampleInfo('Medals', 'medals.js', 'Achievement system demo - [ui, achievements]'),
84
+ new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system - [tiles, levels]'),
85
+ new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard - [audio, input, ui]'),
86
+ new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation - [algorithm, generation]'),
87
+ new ExampleInfo('Starfield', 'starfield.js', 'Animated starfield - [effects, space]'),
88
+ new ExampleInfo('Parallax', 'parallax.js', 'Parallax mountains - [graphics, effects]'),
78
89
  new ExampleInfo('--- MINI GAMES ---'),
79
- new ExampleInfo('Platformer Game', 'platformer.js', 'Platformer jumping game'),
80
- new ExampleInfo('Top Down Game', 'topDown.js', 'Top down adventure game'),
81
- new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D view with tilted camera'),
82
- new ExampleInfo('Space Game', 'spaceGame.js', 'Space ship game with parallax starfield'),
83
- new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander style game'),
84
- new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird style game'),
85
- new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style game'),
86
- new ExampleInfo('Pong Game', 'pongGame.js', 'Simple pong style game'),
87
- new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', 'Sliding tile puzzle game'),
88
- new ExampleInfo('Raycasting Game', 'raycasting.js', 'Pseudo 3D raycasting system'),
90
+ new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run platformer - [game, platformer, physics]'),
91
+ new ExampleInfo('Top Down Game', 'topDown.js', 'Adventure with collision - [game, topdown, rpg]'),
92
+ new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D tilted perspective - [3d, camera, perspective]'),
93
+ new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship with parallax starfield - [game, space, shooter]'),
94
+ new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander physics game - [game, physics, thrust]'),
95
+ new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird clone - [game, arcade, flying]'),
96
+ new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style glider - [game, physics, flying]'),
97
+ new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball game - [game, classic, arcade]'),
98
+ new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', 'Tile sliding puzzle game - [game, puzzle, logic]'),
99
+ new ExampleInfo('Raycasting Game', 'raycasting.js', 'Pseudo 3D raycasting demo - [3d, raycasting, wolfenstein]'),
89
100
  new ExampleInfo('--- PLUGINS ---'),
90
- new ExampleInfo('Box2d Demo', 'box2d.js', 'Demo of the Box2D physics plugin'),
91
- new ExampleInfo('Box2d Car', 'box2dCar.js', 'Controllable vehicle using the Box2D physics plugin'),
92
- new ExampleInfo('Post Processing', 'postProcess.js', 'Post processing plugin demo'),
93
- new ExampleInfo('UI System Plugin', 'uiSystem.js', 'Simple example of LittleJS UI plugin'),
94
- new ExampleInfo('Nine Slice', 'nineSlice.js', 'Demo of nine slice and three slice drawing'),
101
+ new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin demo - [physics, box2d, plugin]'),
102
+ new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D - [physics, box2d, vehicle]'),
103
+ new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters - [graphics, shaders, effects]'),
104
+ new ExampleInfo('UI System Plugin', 'uiSystem.js', 'UI elements and widgets - [ui, plugin, interface]'),
105
+ new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panel rendering - [ui, graphics, scaling]'),
95
106
  new ExampleInfo('--- FULL EXAMPLES ---'),
96
- new ExampleInfo('Starter', 'starter', 'Clean starter project', true),
97
- new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Tutorial Breakout example', true),
98
- new ExampleInfo('Breakout Game', 'breakout', 'Block breaking game with post-processing effects', true),
99
- new ExampleInfo('Platforming Game', 'platformer', 'Platformer/shooter with level data loading', true),
100
- new ExampleInfo('Puzzle Game', 'puzzle', 'Match 3 puzzle game with HD rendering', true),
101
- new ExampleInfo('Stress Test', 'stress', 'Max sprite/object test and music system demo', true),
102
- new ExampleInfo('Box2D Plugin', 'box2d', 'Demo of the Box2D physics plugin', true),
103
- new ExampleInfo('HTML Menus', 'htmlMenu', 'Shows how to combine HTML with LittleJS', true),
104
- new ExampleInfo('UI System Plugin', 'uiSystem', 'How to use LittleJS UI plugin', true),
107
+ new ExampleInfo('Starter', 'starter', 'Clean project template - [template, beginner]', true),
108
+ new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout guide - [tutorial, breakout]', true),
109
+ new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout with effects - [game, breakout, complete]', true),
110
+ new ExampleInfo('Platforming Game', 'platformer', 'Full platformer with levels - [game, platformer, complete]', true),
111
+ new ExampleInfo('Puzzle Game', 'puzzle', 'Match-3 puzzle with HD graphics - [game, puzzle, match3]', true),
112
+ new ExampleInfo('Stress Test', 'stress', 'Performance test with music - [performance, test, music]', true),
113
+ new ExampleInfo('Box2D Plugin', 'box2d', 'Complete Box2D physics demo - [physics, box2d, complete]', true),
114
+ new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML integration example - [html, ui, integration]', true),
115
+ new ExampleInfo('UI System Plugin', 'uiSystem', 'Complete UI system demo - [ui, plugin, complete]', true),
105
116
  ];
106
117
 
107
118
  ///////////////////////////////////////////////////////////////////////////////
108
119
 
109
120
  // global variables
110
- let iframeExample, inputTimeout, codeMirror, consoleAutoScroll, errorLineMarker, codeIsJS;
121
+ let iframeExample; // iframe of the current loaded example
122
+ let inputTimeout; // timeout to debounce input
123
+ let consoleAutoScroll; // allow console to scroll automatically
111
124
 
112
- // load the examples into the list
113
- filterExamples();
125
+ function initExampleBrowser()
126
+ {
127
+ // load the examples into the list
128
+ filterExamples();
129
+
130
+ // set the selected example from the URL parameters
131
+ const urlParams = new URLSearchParams(window.location.search);
132
+ const selectedExample = urlParams.get('example') || '';
133
+ let exampleIndex = exampleList.findIndex((example) => example.filename === selectedExample);
134
+ if (exampleIndex < 0)
135
+ exampleIndex = 1;
136
+ selectExample.selectedIndex = exampleIndex;
137
+ setExample();
114
138
 
115
- // set the selected example from the URL hash
116
- const selectedExample = location.hash.split`#`[1] || '';
117
- let exampleIndex = exampleList.findIndex((example) => example.filename == selectedExample);
118
- if (exampleIndex < 0)
119
- exampleIndex = 1;
120
- selectExample.selectedIndex = exampleIndex;
121
- setExample();
139
+ // apply responsive layout
140
+ addEventListener('resize', resizeWindow);
141
+ resizeWindow();
142
+ }
122
143
 
123
- onresize = () =>
144
+ function resizeWindow()
124
145
  {
125
- // resize iframe to fit half the window
126
- const aspect = 1920 / 1080;
127
- let w = innerWidth / 2 | 0;
128
- let h = w / aspect | 0;
129
- iframeContainer.style.width = w + 'px';
130
- iframeContainer.style.height = h + 'px';
131
- // fix code mirror width
132
- container3.style.width = w-30 + 'px';
146
+ // tweak layout for touch devices
147
+ const isTouchDevice = window.ontouchstart !== undefined;
148
+ if (isTouchDevice)
149
+ container4.style.display = 'none';
150
+ else
151
+ selectExampleText.style.display = 'none';
152
+
153
+ const windowAspect = innerWidth / innerHeight;
154
+ const verticalLayout = windowAspect < 1;
155
+ if (verticalLayout)
156
+ {
157
+ // vertical layout for thin screens
158
+ if (container2.parentNode != container3)
159
+ container3.insertBefore(container2, divCodeOptions);
160
+ // resize iframe to the window
161
+ setFrameSize(innerWidth-35);
162
+
163
+ // fix code mirror sizing glitch
164
+ codeMirror && codeMirror.setSize(innerWidth-35, null);
165
+ }
166
+ else
167
+ {
168
+ // horizontal layout for wide screens
169
+ if (container2.parentNode != container1)
170
+ container1.appendChild(container2);
171
+
172
+ // show full controls
173
+ container4.style.display = '';
174
+
175
+ // resize iframe to fit half the window
176
+ setFrameSize(innerWidth/2);
177
+
178
+ // fix code mirror sizing glitch
179
+ codeMirror && codeMirror.setSize(innerWidth/2 - 35, null);
180
+ }
181
+
182
+ function setFrameSize(w)
183
+ {
184
+ const aspect = 16 / 9; // HD aspect ratio
185
+ w = w | 0;
186
+ const h = w / aspect | 0;
187
+ iframeContainer.style.width = w + 'px';
188
+ iframeContainer.style.height = h + 'px';
189
+
190
+ if (iframeExample)
191
+ {
192
+ // ensure iframe fills container tightly
193
+ iframeExample.style.width = w + 'px';
194
+ iframeExample.style.height = h + 'px';
195
+ }
196
+
197
+ // fix code mirror after layout changes
198
+ if (codeMirror)
199
+ {
200
+ // fix glitch with code mirror sizing
201
+ setTimeout(()=>codeMirror.refresh(), 500);
202
+ }
203
+ }
133
204
  }
134
- onresize();
135
205
 
136
206
  ///////////////////////////////////////////////////////////////////////////////
137
207
  // setting examples
@@ -155,8 +225,13 @@ function setExample()
155
225
  exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
156
226
  const filename = 'examples/' + example.filename;
157
227
  exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
158
- exampleLink.innerText = filename;
159
- location.hash = example.filename;
228
+ exampleLink.innerText = 'View code on GitHub';
229
+
230
+ // update URL parameter
231
+ const url = new URL(window.location);
232
+ url.searchParams.set('example', example.filename);
233
+ window.history.replaceState({}, '', url);
234
+
160
235
  loadFile(example.filename, example.largeExample);
161
236
  }
162
237
 
@@ -168,10 +243,7 @@ async function loadFile(filename, largeExample)
168
243
  textareaCode.disabled = largeExample;
169
244
 
170
245
  // disable buttons in large example mode
171
- buttonPause.disabled = largeExample;
172
- buttonRefresh.disabled = largeExample;
173
- buttonScreenshot.disabled = largeExample;
174
- buttonFullscreen.disabled = largeExample;
246
+ setFrameControlsEnabled(!largeExample);
175
247
 
176
248
  if (largeExample)
177
249
  {
@@ -266,16 +338,27 @@ function codeInput()
266
338
  inputTimeout = setTimeout(() => setCode(code), 500);
267
339
  }
268
340
 
269
- function refreshCode()
341
+ function restartCode()
270
342
  {
271
- // manually refresh/run the code regardless of live edit setting
343
+ // manually restart/run the code regardless of live edit setting
272
344
  const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
273
345
  setCode(code);
274
346
  }
275
347
 
276
- function setCode(code, textFilename)
348
+ function setFrameControlsEnabled(enabled=true)
349
+ {
350
+ buttonPause.disabled = !enabled;
351
+ buttonRestart.disabled = !enabled;
352
+ buttonScreenshot.disabled = !enabled;
353
+ buttonFullscreen.disabled = !enabled;
354
+ checkboxWebGL.disabled = !enabled;
355
+ }
356
+
357
+ function setCode(code, filename)
277
358
  {
278
- const largeExample = !!textFilename;
359
+ const largeExample = !!filename;
360
+ filename = filename || 'shorts/base.html';
361
+
279
362
  clearTimeout(inputTimeout);
280
363
  if (iframeExample)
281
364
  iframeContainer.removeChild(iframeExample);
@@ -284,15 +367,29 @@ function setCode(code, textFilename)
284
367
  unsetConsoleMessage();
285
368
  iframeExample = document.createElement('iframe');
286
369
  iframeContainer.appendChild(iframeExample);
370
+
287
371
  iframeExample.onload = ()=>
288
372
  {
289
373
  if (largeExample)
290
374
  return;
291
375
 
376
+ // get the iframe content window and document
292
377
  const iframeContent = iframeExample.contentWindow;
293
378
  const iframeDocument = iframeContent.document;
379
+ if (!iframeContent.engineInit)
380
+ {
381
+ setErrorMessage(`Failed to load ${filename}`);
382
+ return;
383
+ }
294
384
 
295
- // create error event listeners
385
+ // intercept errors
386
+ function getErrorLine(stack)
387
+ {
388
+ // try to extract line number from stack trace
389
+ // look for <anonymous> or injectedScript to find user code
390
+ const anonymousMatch = stack.match(/(<anonymous>|injectedScript):(\d+)/);
391
+ return anonymousMatch ? parseInt(anonymousMatch[2]) : -1;
392
+ }
296
393
  iframeContent.onerror = (message, source, lineno, colno, error) =>
297
394
  {
298
395
  let text = message;
@@ -306,18 +403,35 @@ function setCode(code, textFilename)
306
403
  iframeContent.onunhandledrejection = (event) =>
307
404
  {
308
405
  let text = event.reason;
406
+ setErrorMessage(text);
309
407
  if (event.reason && event.reason.stack)
310
408
  {
311
- text += `\n\n` + event.reason.stack;
312
-
313
- // try to extract line number from stack trace
314
- const lineMatch = event.reason.stack.match(/:(\d+):(\d+)/);
315
- setErrorLine(parseInt(lineMatch[1]));
409
+ text = event.reason.stack;
410
+ const errorLine = getErrorLine(event.reason.stack);
411
+ if (errorLine >= 0)
412
+ setErrorLine(errorLine);
316
413
  }
317
- setErrorMessage(text);
318
414
  };
319
415
 
320
- // intercept console methods
416
+ // intercept asserts
417
+ const originalAssert = iframeContent.console.assert;
418
+ iframeContent.console.assert = function (condition, ...output)
419
+ {
420
+ if (!condition)
421
+ {
422
+ const stack = (new Error).stack;
423
+ const errorLine = getErrorLine(stack);
424
+ if (errorLine >= 0)
425
+ setErrorLine(errorLine);
426
+
427
+ // format output parameters properly
428
+ const outputMessage = output.length > 0 ? output.map(m => stringifyMessage(m)).join(' ') : '';
429
+ setErrorMessage('Assertion failed!\n' + outputMessage + '\n' + stack);
430
+ }
431
+ originalAssert.apply(this, arguments);
432
+ };
433
+
434
+ // intercept console prints
321
435
  const originalConsole = iframeContent.console;
322
436
  function interceptConsole(method)
323
437
  {
@@ -331,48 +445,58 @@ function setCode(code, textFilename)
331
445
  }
332
446
  ['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
333
447
 
334
- // intercept asserts
335
- const originalAssert = iframeContent.console.assert;
336
- iframeContent.console.assert = function (condition, output)
337
448
  {
338
- if (!condition)
339
- setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + (new Error).stack);
340
- originalAssert.apply(this, arguments);
341
- };
449
+ // hook up buttons
450
+ buttonScreenshot.onclick = () =>
451
+ {
452
+ if (iframeContent.debugScreenshot)
453
+ iframeContent.debugScreenshot();
454
+ }
342
455
 
343
- buttonScreenshot.onclick = () =>
344
- {
345
- if (iframeContent.debugScreenshot)
346
- iframeContent.debugScreenshot();
347
- }
456
+ // pause/resume functionality
457
+ buttonPause.onclick = () =>
458
+ {
459
+ if (!iframeContent.getPaused || !iframeContent.setPaused)
460
+ return;
461
+
462
+ const paused = !iframeContent.getPaused();
463
+ iframeContent.setPaused(paused);
464
+ buttonPause.textContent = paused ? 'Resume' : 'Pause';
465
+ }
466
+ buttonPause.textContent = 'Pause';
348
467
 
349
- // pause/resume functionality
350
- buttonPause.onclick = () =>
351
- {
352
- if (!iframeContent.getPaused || !iframeContent.setPaused)
353
- return;
354
-
355
- const paused = !iframeContent.getPaused();
356
- iframeContent.setPaused(paused);
357
- buttonPause.textContent = paused ? 'Resume' : 'Pause';
468
+ // fullscreen functionality
469
+ buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
358
470
  }
359
- buttonPause.textContent = 'Pause';
360
-
361
- // fullscreen functionality
362
- buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
363
471
 
364
472
  // create a script element that overrides the default functions
365
473
  const overrideScript = iframeDocument.createElement('script');
366
474
  iframeDocument.body.appendChild(overrideScript);
367
475
  overrideScript.text = code;
368
-
369
- if (textareaError.style.display == 'block')
370
- return; // stop if script error
371
-
476
+
477
+ if (textareaError.style.display === 'block')
478
+ return;
479
+
372
480
  // start LittleJS engine
373
- iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()]);
481
+ iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()])
482
+ .catch(error =>
483
+ {
484
+ const errorLine = getErrorLine(error.stack);
485
+ if (errorLine >= 0)
486
+ setErrorLine(errorLine);
487
+ setErrorMessage(error.message + '\n' + error.stack);
488
+ throw error;
489
+ })
490
+ .then(()=>
491
+ {
492
+ // setup frame controls
493
+ setFrameControlsEnabled();
494
+ iframeContent.setGLEnable(checkboxWebGL.checked);
495
+ checkboxWebGL.onchange = ()=> iframeContent.setGLEnable(checkboxWebGL.checked);
496
+ })
374
497
  }
375
- iframeExample.src = textFilename || 'shorts/base.html?' + Date.now();
498
+
499
+ iframeExample.src = filename + '?' + Date.now();
376
500
  }
377
501
 
378
502
  ///////////////////////////////////////////////////////////////////////////////
@@ -401,9 +525,9 @@ function setMessage(message, element, clear=true)
401
525
  element.value = message;
402
526
  else
403
527
  element.value += '\n' + message;
404
- element.style.display = message ? 'block' : 'none';
528
+ element.style.display = message ? 'block' : '';
405
529
 
406
- if (element == textareaConsole)
530
+ if (element === textareaConsole)
407
531
  {
408
532
  const maxConsoleLines = 100;
409
533
  const lines = element.value.split('\n');
@@ -422,7 +546,7 @@ function setMessage(message, element, clear=true)
422
546
 
423
547
  function unsetMessage(element)
424
548
  {
425
- element.style.display = 'none';
549
+ element.style.display = '';
426
550
  element.value = '';
427
551
  }
428
552
 
@@ -435,19 +559,51 @@ function clearErrorLine()
435
559
 
436
560
  function setErrorLine(lineNumber)
437
561
  {
438
- if (!codeMirror || !(lineNumber > 0))
562
+ if (!lineNumber || lineNumber <= 0)
439
563
  return;
440
564
 
441
- clearErrorLine();
442
- --lineNumber; // codeMirror uses 0-based line numbers
443
- errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
444
- //codeMirror.scrollIntoView({line: lineNumber});
565
+ if (codeMirror)
566
+ {
567
+ clearErrorLine();
568
+ --lineNumber; // codeMirror uses 0-based line numbers
569
+ errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
570
+ if (checkboxJumpToErrors.checked)
571
+ {
572
+ codeMirror.scrollIntoView({line: lineNumber, ch: 0});
573
+ codeMirror.focus();
574
+ }
575
+ }
576
+ else if (textareaCode && checkboxJumpToErrors.checked)
577
+ {
578
+ // for textarea, calculate character position and set cursor
579
+ const lines = textareaCode.value.split('\n');
580
+ let charPos = 0;
581
+ for (let i = 0; i < lineNumber - 1 && i < lines.length; i++)
582
+ {
583
+ charPos += lines[i].length + 1; // +1 for newline character
584
+ }
585
+ textareaCode.setSelectionRange(charPos, charPos);
586
+ if (checkboxJumpToErrors.checked)
587
+ {
588
+ const line = textareaCode.value.split('\n');
589
+ lineNumber = Math.min(Math.max(0, lineNumber-1), lines.length-1);
590
+
591
+ // select error line in textarea
592
+ let start = 0;
593
+ for (let i = 0; i < lineNumber; i++)
594
+ start += lines[i].length + 1;
595
+ textareaCode.selectionStart = start;
596
+ textareaCode.selectionEnd = start + lines[lineNumber].length;
597
+ textareaCode.focus();
598
+ }
599
+ }
445
600
  }
446
601
 
447
602
  function setErrorMessage(message)
448
603
  {
449
604
  // prevent overwriting an existing error message
450
- const errorMessageIsVisible = textareaError.style.display == 'block';
605
+ const errorMessageIsVisible = textareaError.style.display === 'block';
606
+ setFrameControlsEnabled(false);
451
607
  if (!errorMessageIsVisible)
452
608
  setMessage(message, textareaError);
453
609
  }
@@ -463,10 +619,23 @@ function onScrollConsole()
463
619
  textareaConsole.addEventListener('scroll', onScrollConsole);
464
620
 
465
621
  ///////////////////////////////////////////////////////////////////////////////
466
- // setup code mirror
467
-
622
+ // load saved preferences from localStorage
468
623
  const defaultTheme = 'littlejs';
469
624
  const defaultFontSize = '16px';
625
+ const savePrefix = 'LittleJSExamples_';
626
+ const savedTheme = localStorage.getItem(savePrefix+'theme') || defaultTheme;
627
+ const savedFontSize = localStorage.getItem(savePrefix+'fontSize') || defaultFontSize;
628
+ selectFontSize.value = savedFontSize;
629
+ textareaCode.style.fontSize = savedFontSize;
630
+
631
+ ///////////////////////////////////////////////////////////////////////////////
632
+ // setup code mirror
633
+
634
+ const useCodeMirror = true;
635
+ let codeMirror; // code mirror instance
636
+ let errorLineMarker; // marker for error line in code mirror
637
+ let codeIsJS; // is the current code javascript
638
+
470
639
  const themes =
471
640
  [
472
641
  'littlejs',
@@ -482,42 +651,40 @@ const themes =
482
651
  'yonce'
483
652
  ];
484
653
 
485
- // Load saved preferences from localStorage
486
- const savedTheme = localStorage.getItem('littleJS-theme') || defaultTheme;
487
- const savedFontSize = localStorage.getItem('littleJS-fontSize') || defaultFontSize;
488
-
654
+ // load theme and font size
489
655
  for (const theme of themes)
490
656
  {
491
657
  if (theme != 'littlejs')
492
658
  addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
493
659
  const o = new Option(theme);
494
- o.selected = theme == savedTheme;
660
+ o.selected = theme === savedTheme;
495
661
  selectTheme.add(o);
496
662
  }
497
663
 
498
- // Set the saved font size
499
- selectFontSize.value = savedFontSize;
500
- textareaCode.style.fontSize = savedFontSize;
501
- addCodeMirrorElement('codemirror.min.css', 'link', 'stylesheet');
502
- addCodeMirrorElement('codemirror.min.js', 'script').onload = ()=>
503
- addCodeMirrorElement('addon/edit/matchbrackets.js', 'script').onload = ()=>
504
- addCodeMirrorElement('mode/javascript/javascript.min.js', 'script').onload = ()=>
505
- initCodeMirror();
506
-
507
- function initCodeMirror()
664
+ if (useCodeMirror)
508
665
  {
509
- const textareaCode = document.getElementById('textareaCode');
510
- codeMirror = CodeMirror.fromTextArea(textareaCode,
666
+ addCodeMirrorElement('codemirror.min.js', 'script').onload = ()=>
667
+ addCodeMirrorElement('codemirror.min.css', 'link', 'stylesheet').onload = ()=>
668
+ addCodeMirrorElement('addon/edit/matchbrackets.js', 'script').onload = ()=>
669
+ addCodeMirrorElement('mode/javascript/javascript.min.js', 'script').onload = ()=>
511
670
  {
512
- theme: savedTheme,
513
- indentUnit: 4,
514
- mode: codeIsJS ? 'javascript' : 'text',
515
- lineNumbers: true,
516
- lineWrapping: true,
517
- matchBrackets: true,
518
- });
519
- codeMirror.on('change', codeInput);
520
- loadTheme();
671
+ if (codeMirror)
672
+ return; // prevent duplicate initialization
673
+
674
+ const textareaCode = document.getElementById('textareaCode');
675
+ codeMirror = CodeMirror.fromTextArea(textareaCode,
676
+ {
677
+ theme: savedTheme,
678
+ indentUnit: 4,
679
+ mode: codeIsJS ? 'javascript' : 'text',
680
+ lineNumbers: true,
681
+ lineWrapping: true,
682
+ matchBrackets: true,
683
+ });
684
+ codeMirror.on('change', codeInput);
685
+ loadTheme();
686
+ resizeWindow(); // fix cursor positioning issue on startup
687
+ }
521
688
  }
522
689
 
523
690
  function addCodeMirrorElement(filename, type, rel)
@@ -525,8 +692,8 @@ function addCodeMirrorElement(filename, type, rel)
525
692
  // add element for code mirror
526
693
  const e = document.createElement(type);
527
694
  e.rel = rel;
528
- filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/' + filename;
529
- if (type == 'link')
695
+ filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/' + filename;
696
+ if (type === 'link')
530
697
  e.href = filename;
531
698
  else
532
699
  e.src = filename;
@@ -541,8 +708,8 @@ function loadTheme()
541
708
  const size = selectFontSize.value;
542
709
 
543
710
  // Save preferences to localStorage
544
- localStorage.setItem('littleJSExamples_theme', theme);
545
- localStorage.setItem('littleJSExamples_fontSize', size);
711
+ localStorage.setItem(savePrefix+'theme', theme);
712
+ localStorage.setItem(savePrefix+'fontSize', size);
546
713
 
547
714
  // Apply the theme and font size
548
715
  textareaCode.style.fontSize = size;
@@ -553,6 +720,11 @@ function loadTheme()
553
720
  }
554
721
  }
555
722
 
723
+ ///////////////////////////////////////////////////////////////////////////////
724
+
725
+ // start up the browser
726
+ initExampleBrowser()
727
+
556
728
  </script>
557
729
  </body>
558
730
  </html>