littlejsengine 1.12.6 → 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 (83) hide show
  1. package/dist/littlejs.d.ts +387 -177
  2. package/dist/littlejs.esm.js +5934 -5294
  3. package/dist/littlejs.esm.min.js +4 -1
  4. package/dist/littlejs.js +5887 -5263
  5. package/dist/littlejs.min.js +4 -1
  6. package/dist/littlejs.release.js +5426 -4829
  7. package/examples/box2d/game.js +1 -1
  8. package/examples/box2d/gameObjects.js +7 -6
  9. package/examples/box2d/index.html +1 -1
  10. package/examples/box2d/scenes.js +7 -7
  11. package/examples/breakout/game.js +1 -1
  12. package/examples/breakout/gameObjects.js +2 -2
  13. package/examples/breakout/index.html +1 -1
  14. package/examples/breakoutTutorial/README.md +2 -2
  15. package/examples/breakoutTutorial/game.js +1 -1
  16. package/examples/breakoutTutorial/index.html +1 -1
  17. package/examples/empty/index.html +1 -1
  18. package/examples/htmlMenu/index.html +1 -1
  19. package/examples/index.html +396 -153
  20. package/examples/module/game.js +1 -1
  21. package/examples/module/index.html +1 -1
  22. package/examples/platformer/game.js +4 -4
  23. package/examples/platformer/gameCharacter.js +6 -6
  24. package/examples/platformer/gameEffects.js +74 -57
  25. package/examples/platformer/gameLevel.js +61 -70
  26. package/examples/platformer/gameObjects.js +4 -4
  27. package/examples/platformer/index.html +1 -1
  28. package/examples/puzzle/index.html +1 -1
  29. package/examples/shorts/base.html +23 -3
  30. package/examples/shorts/box2dCar.js +8 -12
  31. package/examples/shorts/flappyGame.js +52 -0
  32. package/examples/shorts/helloWorld.js +6 -3
  33. package/examples/shorts/hillGlideGame.js +56 -0
  34. package/examples/shorts/landerGame.js +32 -0
  35. package/examples/shorts/maze.js +47 -0
  36. package/examples/shorts/medals.js +42 -0
  37. package/examples/shorts/nineSlice.js +21 -0
  38. package/examples/shorts/piano.js +52 -0
  39. package/examples/shorts/platformer.js +24 -20
  40. package/examples/shorts/{pong.js → pongGame.js} +1 -1
  41. package/examples/shorts/raycasting.js +71 -0
  42. package/examples/shorts/slidingPuzzle.js +42 -0
  43. package/examples/shorts/spaceGame.js +56 -0
  44. package/examples/shorts/starfield.js +17 -0
  45. package/examples/shorts/tileLayer.js +3 -5
  46. package/examples/shorts/tiles.png +0 -0
  47. package/examples/shorts/tiltedView.js +8 -7
  48. package/examples/shorts/topDown.js +18 -26
  49. package/examples/shorts/uiSystem.js +4 -7
  50. package/examples/starter/build.bat +6 -1
  51. package/examples/starter/game.js +2 -2
  52. package/examples/starter/index.html +23 -13
  53. package/examples/stress/index.html +2 -3
  54. package/examples/style.css +136 -0
  55. package/examples/typescript/build.js +1 -2
  56. package/examples/typescript/game.js +2 -2
  57. package/examples/typescript/game.ts +1 -1
  58. package/examples/typescript/index.html +1 -1
  59. package/examples/uiSystem/game.js +8 -24
  60. package/examples/uiSystem/index.html +1 -1
  61. package/package.json +1 -1
  62. package/plugins/box2d.js +8 -8
  63. package/plugins/drawUtilities.js +126 -0
  64. package/plugins/newgrounds.js +1 -1
  65. package/plugins/pluginExport.js +7 -1
  66. package/plugins/postProcess.js +2 -2
  67. package/plugins/uiSystem.js +153 -65
  68. package/plugins/zzfxm.js +1 -1
  69. package/reference.md +10 -10
  70. package/src/engine.js +50 -29
  71. package/src/engineAudio.js +14 -5
  72. package/src/engineBuild.bat +6 -1
  73. package/src/engineBuild.js +20 -5
  74. package/src/engineDebug.js +53 -26
  75. package/src/engineDraw.js +108 -57
  76. package/src/engineExport.js +21 -9
  77. package/src/engineInput.js +109 -37
  78. package/src/engineObject.js +68 -67
  79. package/src/engineParticles.js +26 -9
  80. package/src/engineSettings.js +15 -16
  81. package/src/engineTileLayer.js +312 -153
  82. package/src/engineUtilities.js +182 -130
  83. package/src/engineWebGL.js +47 -36
@@ -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,21 +44,21 @@ 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
64
  new ExampleInfo('Blending', 'blending.js', 'How to use blending modes'),
@@ -132,15 +71,27 @@ const exampleList =
132
71
  new ExampleInfo('Animation', 'animation.js', 'How to animate sprites'),
133
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'),
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 ---'),
135
79
  new ExampleInfo('Platformer Game', 'platformer.js', 'Platformer jumping game'),
136
- new ExampleInfo('Top Down Game', 'topDown.js', 'Rop down adventure 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', 'Simple pong game'),
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 ---'),
139
90
  new ExampleInfo('Box2d Demo', 'box2d.js', 'Demo of the Box2D physics plugin'),
140
91
  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'),
92
+ new ExampleInfo('Post Processing', 'postProcess.js', 'Post processing plugin demo'),
142
93
  new ExampleInfo('UI System Plugin', 'uiSystem.js', 'Simple example of LittleJS UI plugin'),
143
-
94
+ new ExampleInfo('Nine Slice', 'nineSlice.js', 'Demo of nine slice and three slice drawing'),
144
95
  new ExampleInfo('--- FULL EXAMPLES ---'),
145
96
  new ExampleInfo('Starter', 'starter', 'Clean starter project', true),
146
97
  new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Tutorial Breakout example', true),
@@ -150,25 +101,23 @@ const exampleList =
150
101
  new ExampleInfo('Stress Test', 'stress', 'Max sprite/object test and music system demo', true),
151
102
  new ExampleInfo('Box2D Plugin', 'box2d', 'Demo of the Box2D physics plugin', true),
152
103
  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!
104
+ new ExampleInfo('UI System Plugin', 'uiSystem', 'How to use LittleJS UI plugin', true),
155
105
  ];
156
106
 
157
107
  ///////////////////////////////////////////////////////////////////////////////
158
108
 
159
- let iframeExample, inputTimeout;
109
+ // global variables
110
+ let iframeExample, inputTimeout, codeMirror, consoleAutoScroll, errorLineMarker, codeIsJS;
160
111
 
161
112
  // 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
- }
113
+ filterExamples();
169
114
 
170
115
  // set the selected example from the URL hash
171
- 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;
172
121
  setExample();
173
122
 
174
123
  onresize = () =>
@@ -179,55 +128,67 @@ onresize = () =>
179
128
  let h = w / aspect | 0;
180
129
  iframeContainer.style.width = w + 'px';
181
130
  iframeContainer.style.height = h + 'px';
131
+ // fix code mirror width
132
+ container3.style.width = w-30 + 'px';
182
133
  }
183
134
  onresize();
184
135
 
185
136
  ///////////////////////////////////////////////////////////////////////////////
137
+ // setting examples
186
138
 
187
139
  function setExample()
188
140
  {
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
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];
194
155
  exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
195
156
  const filename = 'examples/' + example.filename;
196
157
  exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
197
158
  exampleLink.innerText = filename;
198
- loadFile(example.filename, example.fullExample);
199
- location.hash = selectExample.selectedIndex;
200
- }
201
-
202
- function codeInput()
203
- {
204
- if (!checkboxLiveEdit.checked)
205
- return;
206
-
207
- // debounce input
208
- clearTimeout(inputTimeout);
209
- inputTimeout = setTimeout(() => setCode(textareaCode.value), 500);
159
+ location.hash = example.filename;
160
+ loadFile(example.filename, example.largeExample);
210
161
  }
211
162
 
212
- async function loadFile(filename, fullExample)
163
+ async function loadFile(filename, largeExample)
213
164
  {
214
- textareaCode.disabled = fullExample;
215
- if (fullExample)
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)
216
177
  {
217
- if (iframeExample)
218
- iframeContainer.removeChild(iframeExample);
219
178
 
220
- setErrorMessage();
221
- iframeExample = document.createElement('iframe');
222
- iframeContainer.appendChild(iframeExample);
223
- iframeExample.onload = () =>
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)
224
184
  {
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';
185
+ codeMirror.off('change', codeInput);
186
+ codeMirror.setOption('mode', 'text');
187
+ codeMirror.setValue(text);
229
188
  }
230
- iframeExample.src = filename;
189
+ else
190
+ textareaCode.value = text;
191
+ clearTimeout(inputTimeout);
231
192
  return;
232
193
  }
233
194
 
@@ -237,7 +198,19 @@ async function loadFile(filename, fullExample)
237
198
  if (!response.ok)
238
199
  throw new Error('Could not load file: ' + filename);
239
200
  const text = await response.text();
240
- setCode(textareaCode.value = text, fullExample);
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);
241
214
  }
242
215
  catch (error)
243
216
  {
@@ -245,17 +218,77 @@ async function loadFile(filename, fullExample)
245
218
  }
246
219
  }
247
220
 
248
- function setCode(code, largeExample)
221
+ function filterExamples(reset=0)
222
+ {
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++)
232
+ {
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))
238
+ {
239
+ const o = new Option(text);
240
+ o.disabled = !example.filename;
241
+ o.originalIndex = i;
242
+ selectExample.add(o);
243
+ }
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
+ }
268
+
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);
274
+ }
275
+
276
+ function setCode(code, textFilename)
249
277
  {
278
+ const largeExample = !!textFilename;
250
279
  clearTimeout(inputTimeout);
251
280
  if (iframeExample)
252
281
  iframeContainer.removeChild(iframeExample);
253
282
 
254
- setErrorMessage();
283
+ unsetErrorMessage();
284
+ unsetConsoleMessage();
255
285
  iframeExample = document.createElement('iframe');
256
286
  iframeContainer.appendChild(iframeExample);
257
- iframeExample.onload = () =>
287
+ iframeExample.onload = ()=>
258
288
  {
289
+ if (largeExample)
290
+ return;
291
+
259
292
  const iframeContent = iframeExample.contentWindow;
260
293
  const iframeDocument = iframeContent.document;
261
294
 
@@ -266,24 +299,68 @@ function setCode(code, largeExample)
266
299
  if (lineno)
267
300
  text += ` (Line:${lineno}, Column:${colno})`
268
301
  if (error && error.stack)
269
- text += `\n\n` + error.stack;
302
+ text += `\n` + error.stack;
270
303
  setErrorMessage(text);
304
+ setErrorLine(lineno);
271
305
  }
272
306
  iframeContent.onunhandledrejection = (event) =>
273
307
  {
274
308
  let text = event.reason;
275
- if (event.reason.stack)
309
+ if (event.reason && event.reason.stack)
310
+ {
276
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
+ }
277
317
  setErrorMessage(text);
278
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
279
335
  const originalAssert = iframeContent.console.assert;
280
336
  iframeContent.console.assert = function (condition, output)
281
337
  {
282
- if (!condition)
338
+ if (!condition)
283
339
  setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + (new Error).stack);
284
340
  originalAssert.apply(this, arguments);
285
341
  };
286
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
+
287
364
  // create a script element that overrides the default functions
288
365
  const overrideScript = iframeDocument.createElement('script');
289
366
  iframeDocument.body.appendChild(overrideScript);
@@ -293,23 +370,189 @@ function setCode(code, largeExample)
293
370
  return; // stop if script error
294
371
 
295
372
  // start LittleJS engine
296
- 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
+ }
297
377
 
298
- 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)
299
411
  {
300
- if (iframeContent.debugScreenshot)
301
- 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');
302
415
  }
416
+
417
+ // auto scroll to bottom only if user hasn't manually scrolled
418
+ if (consoleAutoScroll)
419
+ element.scrollTop = element.scrollHeight;
303
420
  }
304
- iframeExample.src = 'shorts/base.html';
305
421
  }
306
422
 
307
- function setErrorMessage(message='')
423
+ function unsetMessage(element)
424
+ {
425
+ element.style.display = 'none';
426
+ element.value = '';
427
+ }
428
+
429
+ function clearErrorLine()
308
430
  {
309
- textareaError.value = message
310
- textareaError.style.display = message ? 'block' : 'none';
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()
539
+ {
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
+ }
311
554
  }
312
555
 
313
556
  </script>
314
557
  </body>
315
- </html>
558
+ </html>