littlejsengine 1.12.4 → 1.13.1

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 (88) hide show
  1. package/dist/box2d.wasm.js +630 -0
  2. package/dist/box2d.wasm.wasm +0 -0
  3. package/dist/littlejs.d.ts +437 -219
  4. package/dist/littlejs.esm.js +5950 -5307
  5. package/dist/littlejs.esm.min.js +4 -1
  6. package/dist/littlejs.js +5921 -5295
  7. package/dist/littlejs.min.js +4 -1
  8. package/dist/littlejs.release.js +5485 -4886
  9. package/examples/box2d/game.js +37 -42
  10. package/examples/box2d/gameObjects.js +41 -37
  11. package/examples/box2d/index.html +2 -2
  12. package/examples/box2d/scenes.js +24 -20
  13. package/examples/box2d/tiles.png +0 -0
  14. package/examples/breakout/game.js +1 -1
  15. package/examples/breakout/gameObjects.js +2 -2
  16. package/examples/breakout/index.html +1 -1
  17. package/examples/breakoutTutorial/README.md +2 -2
  18. package/examples/breakoutTutorial/game.js +1 -1
  19. package/examples/breakoutTutorial/index.html +1 -1
  20. package/examples/empty/index.html +1 -1
  21. package/examples/htmlMenu/index.html +1 -1
  22. package/examples/index.html +410 -159
  23. package/examples/module/game.js +1 -1
  24. package/examples/module/index.html +1 -1
  25. package/examples/platformer/game.js +6 -15
  26. package/examples/platformer/gameCharacter.js +6 -6
  27. package/examples/platformer/gameEffects.js +74 -57
  28. package/examples/platformer/gameLevel.js +61 -70
  29. package/examples/platformer/gameObjects.js +4 -4
  30. package/examples/platformer/index.html +1 -1
  31. package/examples/puzzle/index.html +1 -1
  32. package/examples/shorts/base.html +24 -3
  33. package/examples/shorts/box2d.js +46 -0
  34. package/examples/shorts/box2dCar.js +45 -0
  35. package/examples/shorts/flappyGame.js +52 -0
  36. package/examples/shorts/helloWorld.js +6 -3
  37. package/examples/shorts/hillGlideGame.js +56 -0
  38. package/examples/shorts/landerGame.js +32 -0
  39. package/examples/shorts/maze.js +47 -0
  40. package/examples/shorts/medals.js +42 -0
  41. package/examples/shorts/nineSlice.js +21 -0
  42. package/examples/shorts/piano.js +52 -0
  43. package/examples/shorts/platformer.js +24 -20
  44. package/examples/shorts/{pong.js → pongGame.js} +1 -1
  45. package/examples/shorts/postProcess.js +45 -0
  46. package/examples/shorts/raycasting.js +71 -0
  47. package/examples/shorts/slidingPuzzle.js +42 -0
  48. package/examples/shorts/spaceGame.js +56 -0
  49. package/examples/shorts/starfield.js +17 -0
  50. package/examples/shorts/tileLayer.js +3 -5
  51. package/examples/shorts/tiles.png +0 -0
  52. package/examples/shorts/tiltedView.js +8 -7
  53. package/examples/shorts/topDown.js +18 -26
  54. package/examples/shorts/uiSystem.js +36 -0
  55. package/examples/starter/build.bat +6 -1
  56. package/examples/starter/game.js +4 -2
  57. package/examples/starter/index.html +23 -13
  58. package/examples/stress/index.html +2 -3
  59. package/examples/style.css +136 -0
  60. package/examples/typescript/build.js +1 -2
  61. package/examples/typescript/game.js +2 -2
  62. package/examples/typescript/game.ts +1 -1
  63. package/examples/typescript/index.html +1 -1
  64. package/examples/uiSystem/game.js +9 -25
  65. package/examples/uiSystem/index.html +1 -1
  66. package/package.json +1 -1
  67. package/plugins/box2d.js +29 -35
  68. package/plugins/drawUtilities.js +126 -0
  69. package/plugins/newgrounds.js +1 -1
  70. package/plugins/pluginExport.js +8 -2
  71. package/plugins/postProcess.js +2 -2
  72. package/plugins/uiSystem.js +162 -68
  73. package/plugins/zzfxm.js +1 -1
  74. package/reference.md +10 -10
  75. package/src/engine.js +59 -39
  76. package/src/engineAudio.js +38 -20
  77. package/src/engineBuild.bat +6 -1
  78. package/src/engineBuild.js +32 -7
  79. package/src/engineDebug.js +53 -26
  80. package/src/engineDraw.js +108 -57
  81. package/src/engineExport.js +22 -9
  82. package/src/engineInput.js +112 -40
  83. package/src/engineObject.js +68 -67
  84. package/src/engineParticles.js +26 -9
  85. package/src/engineSettings.js +15 -16
  86. package/src/engineTileLayer.js +312 -153
  87. package/src/engineUtilities.js +198 -130
  88. package/src/engineWebGL.js +58 -35
@@ -1,102 +1,41 @@
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
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
+ <button id=buttonRefresh onclick=refreshCode()>Refresh</button>
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 onclick=filterExamples(1)>Clear</button>
35
+ <button id=buttonPause>Pause</button>
36
+ <button id=buttonScreenshot>Screenshot</button>
37
+ <button id=buttonFullscreen>Fullscreen</button>
38
+ </div>
100
39
  <select id=selectExample autofocus onchange=setExample() size=2></select>
101
40
  </div>
102
41
  </div>
@@ -105,24 +44,24 @@ select
105
44
 
106
45
  class ExampleInfo
107
46
  {
108
- constructor(name, filename, description, fullExample=false)
47
+ constructor(name, filename, description='', largeExample=false)
109
48
  {
110
- if (filename && !fullExample)
111
- filename = 'shorts/' + filename; // short examples folder
49
+ if (filename && !largeExample)
50
+ filename = 'shorts/' + filename;
112
51
  this.name = name;
113
52
  this.filename = filename;
114
53
  this.description = description;
115
- this.fullExample = fullExample;
54
+ this.largeExample = largeExample;
116
55
  }
117
56
  }
118
57
 
119
58
  const exampleList =
120
59
  [
121
60
  new ExampleInfo('--- SHORT EXAMPLES ---'),
122
- new ExampleInfo('Hello World', 'helloWorld.js', 'Simplest example'),
61
+ new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal example'),
123
62
  new ExampleInfo('Shapes', 'shapes.js', 'How to draw geometric shapes'),
124
63
  new ExampleInfo('Colors', 'colors.js', 'How to use the color object'),
125
- new ExampleInfo('Blending', 'blending.js', 'Shows different blending modes'),
64
+ new ExampleInfo('Blending', 'blending.js', 'How to use blending modes'),
126
65
  new ExampleInfo('Timers', 'timers.js', 'How to use the timer object'),
127
66
  new ExampleInfo('Texture', 'texture.js', 'How to display a full texture'),
128
67
  new ExampleInfo('Particles', 'particles.js', 'How to use the particle system'),
@@ -130,13 +69,29 @@ const exampleList =
130
69
  new ExampleInfo('System Font', 'systemFont.js', 'Demo of the included system font'),
131
70
  new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'How to set up a simple sprite atlas'),
132
71
  new ExampleInfo('Animation', 'animation.js', 'How to animate sprites'),
133
- new ExampleInfo('Clock', 'clock.js', 'A simple clock showing the time'),
72
+ new ExampleInfo('Clock', 'clock.js', 'Simple clock showing the time'),
134
73
  new ExampleInfo('Tile Layer', 'tileLayer.js', 'How to use the tile layer object'),
135
- new ExampleInfo('Platformer Game', 'platformer.js', 'A platformer jumping game'),
136
- new ExampleInfo('Top Down Game', 'topDown.js', 'A top down adventure game'),
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'),
78
+ 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'),
137
81
  new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D view with tilted camera'),
138
- new ExampleInfo('Pong Game', 'pong.js', 'A simple pong game'),
139
-
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'),
89
+ 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'),
140
95
  new ExampleInfo('--- FULL EXAMPLES ---'),
141
96
  new ExampleInfo('Starter', 'starter', 'Clean starter project', true),
142
97
  new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Tutorial Breakout example', true),
@@ -146,25 +101,23 @@ const exampleList =
146
101
  new ExampleInfo('Stress Test', 'stress', 'Max sprite/object test and music system demo', true),
147
102
  new ExampleInfo('Box2D Plugin', 'box2d', 'Demo of the Box2D physics plugin', true),
148
103
  new ExampleInfo('HTML Menus', 'htmlMenu', 'Shows how to combine HTML with LittleJS', true),
149
- new ExampleInfo('UI System Plugin', 'uiSystem', 'Example of how to use LittleJS UI plugin', true),
150
- // Add more examples here!
104
+ new ExampleInfo('UI System Plugin', 'uiSystem', 'How to use LittleJS UI plugin', true),
151
105
  ];
152
106
 
153
107
  ///////////////////////////////////////////////////////////////////////////////
154
108
 
155
- let iframeExample, inputTimeout;
109
+ // global variables
110
+ let iframeExample, inputTimeout, codeMirror, consoleAutoScroll, errorLineMarker, codeIsJS;
156
111
 
157
112
  // load the examples into the list
158
- for (const example of exampleList)
159
- {
160
- const text = example.name + (example.description?' - ' + example.description : '');
161
- const o = new Option(text);
162
- o.disabled = !example.filename;
163
- selectExample.add(o);
164
- }
113
+ filterExamples();
165
114
 
166
115
  // set the selected example from the URL hash
167
- selectExample.selectedIndex = location.hash.split`#`[1] | 0
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;
168
121
  setExample();
169
122
 
170
123
  onresize = () =>
@@ -175,79 +128,167 @@ onresize = () =>
175
128
  let h = w / aspect | 0;
176
129
  iframeContainer.style.width = w + 'px';
177
130
  iframeContainer.style.height = h + 'px';
131
+ // fix code mirror width
132
+ container3.style.width = w-30 + 'px';
178
133
  }
179
134
  onresize();
180
135
 
181
136
  ///////////////////////////////////////////////////////////////////////////////
137
+ // setting examples
182
138
 
183
139
  function setExample()
184
140
  {
185
- if (selectExample.selectedIndex < 1 || selectExample.selectedIndex >= exampleList.length)
186
- selectExample.selectedIndex = 1; // reset to default
187
- let example = exampleList[selectExample.selectedIndex];
188
- if (!example.filename)
189
- example = exampleList[selectExample.selectedIndex = 1]; // reset to default
141
+ // get the original index if this is a filtered result
142
+ const selectedOption = selectExample.options[selectExample.selectedIndex];
143
+ let exampleIndex = selectedOption && selectedOption.originalIndex ?
144
+ parseInt(selectedOption.originalIndex) :
145
+ selectExample.selectedIndex;
146
+
147
+ // make sure we have a valid example
148
+ if (exampleIndex < 0 || exampleIndex >= exampleList.length)
149
+ exampleIndex = 1; // reset to default
150
+ if (!exampleList[exampleIndex].filename)
151
+ exampleIndex = 1;
152
+
153
+ // load the example
154
+ const example = exampleList[exampleIndex];
190
155
  exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
191
156
  const filename = 'examples/' + example.filename;
192
157
  exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
193
158
  exampleLink.innerText = filename;
194
- loadFile(example.filename, example.fullExample);
195
- location.hash = selectExample.selectedIndex;
159
+ location.hash = example.filename;
160
+ loadFile(example.filename, example.largeExample);
196
161
  }
197
162
 
198
- function codeInput()
163
+ async function loadFile(filename, largeExample)
199
164
  {
200
- if (!checkboxLiveEdit.checked)
165
+ if (codeMirror)
166
+ codeMirror.setOption('readOnly', largeExample);
167
+ else
168
+ textareaCode.disabled = largeExample;
169
+
170
+ // disable buttons in large example mode
171
+ buttonPause.disabled = largeExample;
172
+ buttonRefresh.disabled = largeExample;
173
+ buttonScreenshot.disabled = largeExample;
174
+ buttonFullscreen.disabled = largeExample;
175
+
176
+ if (largeExample)
177
+ {
178
+
179
+ // Show message in code view that full examples can't be edited
180
+ const text = 'Code view not available for large examples.';
181
+ setCode(text, filename);
182
+ codeIsJS = false;
183
+ if (codeMirror)
184
+ {
185
+ codeMirror.off('change', codeInput);
186
+ codeMirror.setOption('mode', 'text');
187
+ codeMirror.setValue(text);
188
+ }
189
+ else
190
+ textareaCode.value = text;
191
+ clearTimeout(inputTimeout);
201
192
  return;
193
+ }
202
194
 
203
- // debounce input
204
- clearTimeout(inputTimeout);
205
- inputTimeout = setTimeout(() => setCode(textareaCode.value), 500);
195
+ try
196
+ {
197
+ const response = await fetch(filename);
198
+ if (!response.ok)
199
+ throw new Error('Could not load file: ' + filename);
200
+ const text = await response.text();
201
+
202
+ // set the code in both code mirror and textarea
203
+ codeIsJS = true;
204
+ if (codeMirror)
205
+ {
206
+ codeMirror.on('change', codeInput);
207
+ codeMirror.setOption('mode', 'javascript');
208
+ codeMirror.setValue(text);
209
+ }
210
+ else
211
+ textareaCode.value = text;
212
+ clearTimeout(inputTimeout);
213
+ setCode(text);
214
+ }
215
+ catch (error)
216
+ {
217
+ setErrorMessage(error.message);
218
+ }
206
219
  }
207
220
 
208
- function loadFile(filename, fullExample)
221
+ function filterExamples(reset=0)
209
222
  {
210
- textareaCode.disabled = fullExample;
211
- if (fullExample)
223
+ if (reset)
224
+ inputSearch.value = '';
225
+
226
+ // clear current options
227
+ selectExample.options.length = 0;
228
+
229
+ // filter and add matching examples
230
+ const searchTerm = inputSearch.value.toLowerCase().trim();
231
+ for (let i = 0; i < exampleList.length; i++)
212
232
  {
213
- if (iframeExample)
214
- iframeContainer.removeChild(iframeExample);
215
-
216
- setErrorMessage();
217
- iframeExample = document.createElement('iframe');
218
- iframeContainer.appendChild(iframeExample);
219
- iframeExample.onload = () =>
233
+ // check if search term matches name or description
234
+ const example = exampleList[i];
235
+ const text = example.name + (example.description ? ` - ${example.description}` : '');
236
+ if (example.name.toLowerCase().includes(searchTerm) ||
237
+ example.description.toLowerCase().includes(searchTerm))
220
238
  {
221
- const iframeContent = iframeExample.contentWindow;
222
- const iframeDocument = iframeContent.document;
223
- //textareaCode.value = iframeDocument.body.innerHTML;
224
- textareaCode.value = 'Editor code view not available for large examples.\n';
239
+ const o = new Option(text);
240
+ o.disabled = !example.filename;
241
+ o.originalIndex = i;
242
+ selectExample.add(o);
225
243
  }
226
- iframeExample.src = filename;
227
- return;
228
244
  }
245
+
246
+ if (!selectExample.options.length)
247
+ {
248
+ // show a message if no matches
249
+ const o = new Option('No examples found matching "' + searchTerm + '"');
250
+ o.disabled = true;
251
+ selectExample.add(o);
252
+ }
253
+ }
254
+
255
+ ///////////////////////////////////////////////////////////////////////////////
256
+ // setting code
257
+
258
+ function codeInput()
259
+ {
260
+ if (!checkboxLiveEdit.checked)
261
+ return;
262
+
263
+ // debounce input - get content from code mirror if available, otherwise from textarea
264
+ clearTimeout(inputTimeout);
265
+ const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
266
+ inputTimeout = setTimeout(() => setCode(code), 500);
267
+ }
229
268
 
230
- fetch(filename)
231
- .then(response => {
232
- if (!response.ok)
233
- throw new Error('Could not load file: '+ filename);
234
- return response.text();
235
- })
236
- .then(text => setCode(textareaCode.value = text, fullExample))
237
- .catch(error => setErrorMessage(error.message));
269
+ function refreshCode()
270
+ {
271
+ // manually refresh/run the code regardless of live edit setting
272
+ const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
273
+ setCode(code);
238
274
  }
239
275
 
240
- function setCode(code, largeExample)
276
+ function setCode(code, textFilename)
241
277
  {
278
+ const largeExample = !!textFilename;
242
279
  clearTimeout(inputTimeout);
243
280
  if (iframeExample)
244
281
  iframeContainer.removeChild(iframeExample);
245
282
 
246
- setErrorMessage();
283
+ unsetErrorMessage();
284
+ unsetConsoleMessage();
247
285
  iframeExample = document.createElement('iframe');
248
286
  iframeContainer.appendChild(iframeExample);
249
- iframeExample.onload = () =>
287
+ iframeExample.onload = ()=>
250
288
  {
289
+ if (largeExample)
290
+ return;
291
+
251
292
  const iframeContent = iframeExample.contentWindow;
252
293
  const iframeDocument = iframeContent.document;
253
294
 
@@ -258,24 +299,68 @@ function setCode(code, largeExample)
258
299
  if (lineno)
259
300
  text += ` (Line:${lineno}, Column:${colno})`
260
301
  if (error && error.stack)
261
- text += `\n\n` + error.stack;
302
+ text += `\n` + error.stack;
262
303
  setErrorMessage(text);
304
+ setErrorLine(lineno);
263
305
  }
264
306
  iframeContent.onunhandledrejection = (event) =>
265
307
  {
266
308
  let text = event.reason;
267
- if (event.reason.stack)
309
+ if (event.reason && event.reason.stack)
310
+ {
268
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]));
316
+ }
269
317
  setErrorMessage(text);
270
318
  };
319
+
320
+ // intercept console methods
321
+ const originalConsole = iframeContent.console;
322
+ function interceptConsole(method)
323
+ {
324
+ const original = originalConsole[method];
325
+ iframeContent.console[method] = function(...args)
326
+ {
327
+ const message = args.map(m=>stringifyMessage(m)).join('\n');
328
+ setConsoleMessage(message);
329
+ original.apply(originalConsole, args);
330
+ };
331
+ }
332
+ ['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
333
+
334
+ // intercept asserts
271
335
  const originalAssert = iframeContent.console.assert;
272
336
  iframeContent.console.assert = function (condition, output)
273
337
  {
274
- if (!condition)
338
+ if (!condition)
275
339
  setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + (new Error).stack);
276
340
  originalAssert.apply(this, arguments);
277
341
  };
278
342
 
343
+ buttonScreenshot.onclick = () =>
344
+ {
345
+ if (iframeContent.debugScreenshot)
346
+ iframeContent.debugScreenshot();
347
+ }
348
+
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';
358
+ }
359
+ buttonPause.textContent = 'Pause';
360
+
361
+ // fullscreen functionality
362
+ buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
363
+
279
364
  // create a script element that overrides the default functions
280
365
  const overrideScript = iframeDocument.createElement('script');
281
366
  iframeDocument.body.appendChild(overrideScript);
@@ -285,23 +370,189 @@ function setCode(code, largeExample)
285
370
  return; // stop if script error
286
371
 
287
372
  // start LittleJS engine
288
- iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?1']);
373
+ iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()]);
374
+ }
375
+ iframeExample.src = textFilename || 'shorts/base.html?' + Date.now();
376
+ }
289
377
 
290
- buttonScreenshot.onclick = () =>
378
+ ///////////////////////////////////////////////////////////////////////////////
379
+ // error and console messages
380
+
381
+ function stringifyMessage(message)
382
+ {
383
+ // make sure message is a string
384
+ if (message === null)
385
+ return 'null';
386
+ if (Number.isNaN(message))
387
+ return 'NaN';
388
+ if (message === undefined)
389
+ return 'undefined';
390
+ if (message === 0)
391
+ return '0';
392
+ if (message === false)
393
+ return 'false';
394
+ return message;
395
+ }
396
+
397
+ function setMessage(message, element, clear=true)
398
+ {
399
+ message = stringifyMessage(message);
400
+ if (clear || !element.value)
401
+ element.value = message;
402
+ else
403
+ element.value += '\n' + message;
404
+ element.style.display = message ? 'block' : 'none';
405
+
406
+ if (element == textareaConsole)
407
+ {
408
+ const maxConsoleLines = 100;
409
+ const lines = element.value.split('\n');
410
+ if (lines.length > maxConsoleLines)
291
411
  {
292
- if (iframeContent.debugScreenshot)
293
- iframeContent.debugScreenshot();
412
+ // limit max lines, prevents slowdown from too many lines
413
+ const excessLines = lines.length - maxConsoleLines;
414
+ element.value = lines.slice(excessLines).join('\n');
294
415
  }
416
+
417
+ // auto scroll to bottom only if user hasn't manually scrolled
418
+ if (consoleAutoScroll)
419
+ element.scrollTop = element.scrollHeight;
295
420
  }
296
- iframeExample.src = 'shorts/base.html';
297
421
  }
298
422
 
299
- function setErrorMessage(message='')
423
+ function unsetMessage(element)
424
+ {
425
+ element.style.display = 'none';
426
+ element.value = '';
427
+ }
428
+
429
+ function clearErrorLine()
430
+ {
431
+ if (!codeMirror || !errorLineMarker)
432
+ return;
433
+ codeMirror.getDoc().removeLineClass(errorLineMarker, 'background', 'error-line');
434
+ }
435
+
436
+ function setErrorLine(lineNumber)
437
+ {
438
+ if (!codeMirror || !(lineNumber > 0))
439
+ return;
440
+
441
+ clearErrorLine();
442
+ --lineNumber; // codeMirror uses 0-based line numbers
443
+ errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
444
+ //codeMirror.scrollIntoView({line: lineNumber});
445
+ }
446
+
447
+ function setErrorMessage(message)
448
+ {
449
+ // prevent overwriting an existing error message
450
+ const errorMessageIsVisible = textareaError.style.display == 'block';
451
+ if (!errorMessageIsVisible)
452
+ setMessage(message, textareaError);
453
+ }
454
+ function unsetErrorMessage() { unsetMessage(textareaError); clearErrorLine(); }
455
+ function setConsoleMessage(message) { setMessage(message, textareaConsole, false); }
456
+ function unsetConsoleMessage() { unsetMessage(textareaConsole); consoleAutoScroll = true; }
457
+ function onScrollConsole()
458
+ {
459
+ // only auto scroll console is it is scrolled to bottom
460
+ const tolerance = 5; // pixels of tolerance
461
+ consoleAutoScroll = Math.abs(textareaConsole.scrollTop - (textareaConsole.scrollHeight - textareaConsole.clientHeight)) < tolerance;
462
+ }
463
+ textareaConsole.addEventListener('scroll', onScrollConsole);
464
+
465
+ ///////////////////////////////////////////////////////////////////////////////
466
+ // setup code mirror
467
+
468
+ const defaultTheme = 'littlejs';
469
+ const defaultFontSize = '16px';
470
+ const themes =
471
+ [
472
+ 'littlejs',
473
+ '3024-night',
474
+ 'abcdef',
475
+ 'ambiance',
476
+ 'blackboard',
477
+ 'monokai',
478
+ 'duotone-light',
479
+ 'icecoder',
480
+ 'lesser-dark',
481
+ 'night',
482
+ 'yonce'
483
+ ];
484
+
485
+ // Load saved preferences from localStorage
486
+ const savedTheme = localStorage.getItem('littleJS-theme') || defaultTheme;
487
+ const savedFontSize = localStorage.getItem('littleJS-fontSize') || defaultFontSize;
488
+
489
+ for (const theme of themes)
490
+ {
491
+ if (theme != 'littlejs')
492
+ addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
493
+ const o = new Option(theme);
494
+ o.selected = theme == savedTheme;
495
+ selectTheme.add(o);
496
+ }
497
+
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()
508
+ {
509
+ const textareaCode = document.getElementById('textareaCode');
510
+ codeMirror = CodeMirror.fromTextArea(textareaCode,
511
+ {
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();
521
+ }
522
+
523
+ function addCodeMirrorElement(filename, type, rel)
524
+ {
525
+ // add element for code mirror
526
+ const e = document.createElement(type);
527
+ e.rel = rel;
528
+ filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/' + filename;
529
+ if (type == 'link')
530
+ e.href = filename;
531
+ else
532
+ e.src = filename;
533
+ e.crossOrigin = 'anonymous';
534
+ document.head.appendChild(e);
535
+ return e;
536
+ }
537
+
538
+ function loadTheme()
300
539
  {
301
- textareaError.value = message
302
- textareaError.style.display = message ? 'block' : 'none';
540
+ const theme = selectTheme.value;
541
+ const size = selectFontSize.value;
542
+
543
+ // Save preferences to localStorage
544
+ localStorage.setItem('littleJSExamples_theme', theme);
545
+ localStorage.setItem('littleJSExamples_fontSize', size);
546
+
547
+ // Apply the theme and font size
548
+ textareaCode.style.fontSize = size;
549
+ if (codeMirror)
550
+ {
551
+ codeMirror.getWrapperElement().style.fontSize = size;
552
+ codeMirror.setOption('theme', theme);
553
+ }
303
554
  }
304
555
 
305
556
  </script>
306
557
  </body>
307
- </html>
558
+ </html>