littlejsengine 1.13.4 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +4 -3
  2. package/dist/littlejs.d.ts +395 -249
  3. package/dist/littlejs.esm.js +1550 -754
  4. package/dist/littlejs.esm.min.js +1 -1
  5. package/dist/littlejs.js +1528 -744
  6. package/dist/littlejs.min.js +1 -1
  7. package/dist/littlejs.release.js +1504 -720
  8. package/examples/box2d/game.js +4 -4
  9. package/examples/box2d/gameObjects.js +1 -1
  10. package/examples/breakout/game.js +30 -25
  11. package/examples/breakoutTutorial/README.md +1 -1
  12. package/examples/breakoutTutorial/game.js +1 -1
  13. package/examples/electron/build.js +1 -1
  14. package/examples/electron/index.html +2 -2
  15. package/examples/empty/game.js +1 -1
  16. package/examples/htmlMenu/index.html +7 -7
  17. package/examples/index.html +208 -97
  18. package/examples/platformer/gameEffects.js +20 -25
  19. package/examples/platformer/gameLevel.js +6 -1
  20. package/examples/shorts/base.html +1 -1
  21. package/examples/shorts/flappyGame.js +2 -1
  22. package/examples/shorts/helloWorld.js +1 -1
  23. package/examples/shorts/music.js +106 -0
  24. package/examples/shorts/parallax.js +71 -0
  25. package/examples/shorts/piano.js +7 -8
  26. package/examples/shorts/postProcess.js +25 -8
  27. package/examples/shorts/shapes.js +12 -18
  28. package/examples/shorts/song.mp3 +0 -0
  29. package/examples/shorts/uiSystem.js +15 -8
  30. package/examples/starter/index.html +2 -2
  31. package/examples/stress/index.html +14 -7
  32. package/examples/style.css +14 -4
  33. package/examples/typescript/build.js +1 -1
  34. package/examples/uiSystem/game.js +11 -11
  35. package/package.json +1 -1
  36. package/plugins/box2d.js +12 -10
  37. package/plugins/drawUtilities.js +5 -5
  38. package/plugins/newgrounds.js +6 -6
  39. package/plugins/pluginExport.js +1 -1
  40. package/plugins/uiSystem.js +103 -79
  41. package/plugins/zzfxm.js +10 -10
  42. package/reference.md +94 -56
  43. package/src/engine.js +28 -24
  44. package/src/engineAudio.js +284 -109
  45. package/src/engineBuild.js +11 -11
  46. package/src/engineDebug.js +29 -29
  47. package/src/engineDraw.js +285 -112
  48. package/src/engineExport.js +28 -16
  49. package/src/engineInput.js +57 -67
  50. package/src/engineMedals.js +25 -25
  51. package/src/engineObject.js +28 -25
  52. package/src/engineParticles.js +59 -59
  53. package/src/engineRelease.js +1 -1
  54. package/src/engineSettings.js +20 -13
  55. package/src/engineTileLayer.js +34 -35
  56. package/src/engineUtilities.js +66 -59
  57. package/src/engineWebGL.js +466 -66
  58. /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?6>
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,24 +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
- <input type=checkbox id=checkboxJumpToError>Jump to Error
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)'>
38
+ <input id=inputSearch placeholder='Search examples...' oninput=filterExamples() onkeydown='if(event.key===`Escape`)filterExamples(1)'>
34
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>
38
- <input type=checkbox id=checkboxWebGL checked>WebGL
43
+ <button id=buttonScreenshot>Screenshot</button>
44
+ <span class=nowrap><input type=checkbox id=checkboxWebGL checked>WebGL</span>
45
+ </span>
39
46
  </div>
40
- <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>
41
49
  </div>
42
50
  </div>
43
51
  <script>
@@ -66,21 +74,23 @@ const exampleList =
66
74
  new ExampleInfo('Timers', 'timers.js', 'Timer object and timing functions - [utilities, time]'),
67
75
  new ExampleInfo('Texture', 'texture.js', 'Display and manipulate textures - [graphics, sprites]'),
68
76
  new ExampleInfo('Particles', 'particles.js', 'Particle system basics - [effects, particles]'),
69
- new ExampleInfo('Play Sound', 'playSound.js', 'Sound effects with ZzFX - [audio, sound]'),
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]'),
70
79
  new ExampleInfo('System Font', 'systemFont.js', 'Built-in bitmap font rendering - [text, font]'),
71
80
  new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering - [graphics, sprites]'),
72
81
  new ExampleInfo('Animation', 'animation.js', 'Sprite animation techniques - [animation, sprites]'),
73
82
  new ExampleInfo('Clock', 'clock.js', 'Real-time clock display - [time, ui, utilities]'),
83
+ new ExampleInfo('Medals', 'medals.js', 'Achievement system demo - [ui, achievements]'),
74
84
  new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system - [tiles, levels]'),
75
85
  new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard - [audio, input, ui]'),
76
86
  new ExampleInfo('Maze Generator', 'maze.js', 'Procedural maze generation - [algorithm, generation]'),
77
- new ExampleInfo('Starfield', 'starfield.js', 'Animated parallax starfield - [effects, space]'),
78
- new ExampleInfo('Medals', 'medals.js', 'Achievement system demo - [ui, achievements]'),
87
+ new ExampleInfo('Starfield', 'starfield.js', 'Animated starfield - [effects, space]'),
88
+ new ExampleInfo('Parallax', 'parallax.js', 'Parallax mountains - [graphics, effects]'),
79
89
  new ExampleInfo('--- MINI GAMES ---'),
80
90
  new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run platformer - [game, platformer, physics]'),
81
91
  new ExampleInfo('Top Down Game', 'topDown.js', 'Adventure with collision - [game, topdown, rpg]'),
82
92
  new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D tilted perspective - [3d, camera, perspective]'),
83
- new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship with parallax scrolling - [game, space, shooter]'),
93
+ new ExampleInfo('Space Game', 'spaceGame.js', 'Spaceship with parallax starfield - [game, space, shooter]'),
84
94
  new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander physics game - [game, physics, thrust]'),
85
95
  new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird clone - [game, arcade, flying]'),
86
96
  new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style glider - [game, physics, flying]'),
@@ -108,32 +118,90 @@ const exampleList =
108
118
  ///////////////////////////////////////////////////////////////////////////////
109
119
 
110
120
  // global variables
111
- 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
112
124
 
113
- // load the examples into the list
114
- 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();
115
138
 
116
- // set the selected example from the URL parameters
117
- const urlParams = new URLSearchParams(window.location.search);
118
- const selectedExample = urlParams.get('example') || '';
119
- let exampleIndex = exampleList.findIndex((example) => example.filename == selectedExample);
120
- if (exampleIndex < 0)
121
- exampleIndex = 1;
122
- selectExample.selectedIndex = exampleIndex;
123
- setExample();
139
+ // apply responsive layout
140
+ addEventListener('resize', resizeWindow);
141
+ resizeWindow();
142
+ }
124
143
 
125
- onresize = () =>
144
+ function resizeWindow()
126
145
  {
127
- // resize iframe to fit half the window
128
- const aspect = 1920 / 1080;
129
- let w = innerWidth / 2 | 0;
130
- let h = w / aspect | 0;
131
- iframeContainer.style.width = w + 'px';
132
- iframeContainer.style.height = h + 'px';
133
- // fix code mirror width
134
- 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
+ }
135
204
  }
136
- onresize();
137
205
 
138
206
  ///////////////////////////////////////////////////////////////////////////////
139
207
  // setting examples
@@ -157,7 +225,7 @@ function setExample()
157
225
  exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
158
226
  const filename = 'examples/' + example.filename;
159
227
  exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
160
- exampleLink.innerText = filename;
228
+ exampleLink.innerText = 'View code on GitHub';
161
229
 
162
230
  // update URL parameter
163
231
  const url = new URL(window.location);
@@ -175,11 +243,7 @@ async function loadFile(filename, largeExample)
175
243
  textareaCode.disabled = largeExample;
176
244
 
177
245
  // disable buttons in large example mode
178
- buttonPause.disabled = largeExample;
179
- buttonRestart.disabled = largeExample;
180
- buttonScreenshot.disabled = largeExample;
181
- buttonFullscreen.disabled = largeExample;
182
- checkboxWebGL.disabled = largeExample;
246
+ setFrameControlsEnabled(!largeExample);
183
247
 
184
248
  if (largeExample)
185
249
  {
@@ -254,7 +318,7 @@ function filterExamples(reset=0)
254
318
  if (!selectExample.options.length)
255
319
  {
256
320
  // show a message if no matches
257
- const o = new Option('No examples found matching "' + searchTerm + '"');
321
+ const o = new Option(`No examples found matching '${searchTerm}'`);
258
322
  o.disabled = true;
259
323
  selectExample.add(o);
260
324
  }
@@ -281,9 +345,20 @@ function restartCode()
281
345
  setCode(code);
282
346
  }
283
347
 
284
- function setCode(code, textFilename)
348
+ function setFrameControlsEnabled(enabled=true)
285
349
  {
286
- const largeExample = !!textFilename;
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)
358
+ {
359
+ const largeExample = !!filename;
360
+ filename = filename || 'shorts/base.html';
361
+
287
362
  clearTimeout(inputTimeout);
288
363
  if (iframeExample)
289
364
  iframeContainer.removeChild(iframeExample);
@@ -292,18 +367,26 @@ function setCode(code, textFilename)
292
367
  unsetConsoleMessage();
293
368
  iframeExample = document.createElement('iframe');
294
369
  iframeContainer.appendChild(iframeExample);
370
+
295
371
  iframeExample.onload = ()=>
296
372
  {
297
373
  if (largeExample)
298
374
  return;
299
375
 
376
+ // get the iframe content window and document
300
377
  const iframeContent = iframeExample.contentWindow;
301
378
  const iframeDocument = iframeContent.document;
379
+ if (!iframeContent.engineInit)
380
+ {
381
+ setErrorMessage(`Failed to load ${filename}`);
382
+ return;
383
+ }
302
384
 
303
385
  // intercept errors
304
386
  function getErrorLine(stack)
305
387
  {
306
- // try to extract line number from stack trace, look for <anonymous> or injectedScript to find the user code
388
+ // try to extract line number from stack trace
389
+ // look for <anonymous> or injectedScript to find user code
307
390
  const anonymousMatch = stack.match(/(<anonymous>|injectedScript):(\d+)/);
308
391
  return anonymousMatch ? parseInt(anonymousMatch[2]) : -1;
309
392
  }
@@ -332,7 +415,7 @@ function setCode(code, textFilename)
332
415
 
333
416
  // intercept asserts
334
417
  const originalAssert = iframeContent.console.assert;
335
- iframeContent.console.assert = function (condition, output)
418
+ iframeContent.console.assert = function (condition, ...output)
336
419
  {
337
420
  if (!condition)
338
421
  {
@@ -340,7 +423,10 @@ function setCode(code, textFilename)
340
423
  const errorLine = getErrorLine(stack);
341
424
  if (errorLine >= 0)
342
425
  setErrorLine(errorLine);
343
- setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + stack);
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);
344
430
  }
345
431
  originalAssert.apply(this, arguments);
346
432
  };
@@ -360,7 +446,7 @@ function setCode(code, textFilename)
360
446
  ['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
361
447
 
362
448
  {
363
- // setup buttons
449
+ // hook up buttons
364
450
  buttonScreenshot.onclick = () =>
365
451
  {
366
452
  if (iframeContent.debugScreenshot)
@@ -383,22 +469,34 @@ function setCode(code, textFilename)
383
469
  buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
384
470
  }
385
471
 
386
- // set WebGL based on checkbox
387
- iframeContent.setGLEnable(checkboxWebGL.checked);
388
- checkboxWebGL.onchange = ()=> iframeContent.setGLEnable(checkboxWebGL.checked);
389
-
390
472
  // create a script element that overrides the default functions
391
473
  const overrideScript = iframeDocument.createElement('script');
392
474
  iframeDocument.body.appendChild(overrideScript);
393
475
  overrideScript.text = code;
394
-
395
- if (textareaError.style.display == 'block')
396
- return; // stop if script error
397
-
476
+
477
+ if (textareaError.style.display === 'block')
478
+ return;
479
+
398
480
  // start LittleJS engine
399
- 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
+ })
400
497
  }
401
- iframeExample.src = textFilename || 'shorts/base.html?' + Date.now();
498
+
499
+ iframeExample.src = filename + '?' + Date.now();
402
500
  }
403
501
 
404
502
  ///////////////////////////////////////////////////////////////////////////////
@@ -427,9 +525,9 @@ function setMessage(message, element, clear=true)
427
525
  element.value = message;
428
526
  else
429
527
  element.value += '\n' + message;
430
- element.style.display = message ? 'block' : 'none';
528
+ element.style.display = message ? 'block' : '';
431
529
 
432
- if (element == textareaConsole)
530
+ if (element === textareaConsole)
433
531
  {
434
532
  const maxConsoleLines = 100;
435
533
  const lines = element.value.split('\n');
@@ -448,7 +546,7 @@ function setMessage(message, element, clear=true)
448
546
 
449
547
  function unsetMessage(element)
450
548
  {
451
- element.style.display = 'none';
549
+ element.style.display = '';
452
550
  element.value = '';
453
551
  }
454
552
 
@@ -469,13 +567,13 @@ function setErrorLine(lineNumber)
469
567
  clearErrorLine();
470
568
  --lineNumber; // codeMirror uses 0-based line numbers
471
569
  errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
472
- if (checkboxJumpToError.checked)
570
+ if (checkboxJumpToErrors.checked)
473
571
  {
474
572
  codeMirror.scrollIntoView({line: lineNumber, ch: 0});
475
573
  codeMirror.focus();
476
574
  }
477
575
  }
478
- else if (textareaCode && checkboxJumpToError.checked)
576
+ else if (textareaCode && checkboxJumpToErrors.checked)
479
577
  {
480
578
  // for textarea, calculate character position and set cursor
481
579
  const lines = textareaCode.value.split('\n');
@@ -485,7 +583,7 @@ function setErrorLine(lineNumber)
485
583
  charPos += lines[i].length + 1; // +1 for newline character
486
584
  }
487
585
  textareaCode.setSelectionRange(charPos, charPos);
488
- if (checkboxJumpToError.checked)
586
+ if (checkboxJumpToErrors.checked)
489
587
  {
490
588
  const line = textareaCode.value.split('\n');
491
589
  lineNumber = Math.min(Math.max(0, lineNumber-1), lines.length-1);
@@ -504,7 +602,8 @@ function setErrorLine(lineNumber)
504
602
  function setErrorMessage(message)
505
603
  {
506
604
  // prevent overwriting an existing error message
507
- const errorMessageIsVisible = textareaError.style.display == 'block';
605
+ const errorMessageIsVisible = textareaError.style.display === 'block';
606
+ setFrameControlsEnabled(false);
508
607
  if (!errorMessageIsVisible)
509
608
  setMessage(message, textareaError);
510
609
  }
@@ -520,10 +619,23 @@ function onScrollConsole()
520
619
  textareaConsole.addEventListener('scroll', onScrollConsole);
521
620
 
522
621
  ///////////////////////////////////////////////////////////////////////////////
523
- // setup code mirror
524
-
622
+ // load saved preferences from localStorage
525
623
  const defaultTheme = 'littlejs';
526
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
+
527
639
  const themes =
528
640
  [
529
641
  'littlejs',
@@ -539,46 +651,40 @@ const themes =
539
651
  'yonce'
540
652
  ];
541
653
 
542
- // Load saved preferences from localStorage
543
- const savePrefix = 'LittleJSExamples_';
544
- const savedTheme = localStorage.getItem(savePrefix+'theme') || defaultTheme;
545
- const savedFontSize = localStorage.getItem(savePrefix+'fontSize') || defaultFontSize;
546
-
654
+ // load theme and font size
547
655
  for (const theme of themes)
548
656
  {
549
657
  if (theme != 'littlejs')
550
658
  addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
551
659
  const o = new Option(theme);
552
- o.selected = theme == savedTheme;
660
+ o.selected = theme === savedTheme;
553
661
  selectTheme.add(o);
554
662
  }
555
663
 
556
- // Set the saved font size
557
- selectFontSize.value = savedFontSize;
558
- textareaCode.style.fontSize = savedFontSize;
559
- addCodeMirrorElement('codemirror.min.js', 'script').onload = ()=>
560
- addCodeMirrorElement('codemirror.min.css', 'link', 'stylesheet').onload = ()=>
561
- addCodeMirrorElement('addon/edit/matchbrackets.js', 'script').onload = ()=>
562
- addCodeMirrorElement('mode/javascript/javascript.min.js', 'script').onload = ()=>
563
- initCodeMirror();
564
-
565
- function initCodeMirror()
664
+ if (useCodeMirror)
566
665
  {
567
- if (codeMirror)
568
- return; // prevent duplicate initialization
569
-
570
- const textareaCode = document.getElementById('textareaCode');
571
- 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 = ()=>
572
670
  {
573
- theme: savedTheme,
574
- indentUnit: 4,
575
- mode: codeIsJS ? 'javascript' : 'text',
576
- lineNumbers: true,
577
- lineWrapping: true,
578
- matchBrackets: true,
579
- });
580
- codeMirror.on('change', codeInput);
581
- loadTheme();
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
+ }
582
688
  }
583
689
 
584
690
  function addCodeMirrorElement(filename, type, rel)
@@ -587,7 +693,7 @@ function addCodeMirrorElement(filename, type, rel)
587
693
  const e = document.createElement(type);
588
694
  e.rel = rel;
589
695
  filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/' + filename;
590
- if (type == 'link')
696
+ if (type === 'link')
591
697
  e.href = filename;
592
698
  else
593
699
  e.src = filename;
@@ -614,6 +720,11 @@ function loadTheme()
614
720
  }
615
721
  }
616
722
 
723
+ ///////////////////////////////////////////////////////////////////////////////
724
+
725
+ // start up the browser
726
+ initExampleBrowser()
727
+
617
728
  </script>
618
729
  </body>
619
730
  </html>
@@ -82,7 +82,7 @@ export function explosion(pos, radius=3)
82
82
  GameLevel.decorateTile(pos.add(vec2(x,y)).floor());
83
83
  }
84
84
 
85
- // update webgl texture
85
+ // update WebGL texture
86
86
  GameLevel.updateWebGL(GameLevel.foregroundTileLayer);
87
87
 
88
88
  // kill/push objects
@@ -153,7 +153,7 @@ export function destroyTile(pos, makeSound = 1, cleanup = 1)
153
153
  layer.setData(pos, new LJS.TileLayerData, true);
154
154
  layer.setCollisionData(pos, GameLevel.tileType_empty);
155
155
 
156
- // cleanup neighbors and rebuild webgl
156
+ // cleanup neighbors and rebuild WebGL
157
157
  if (cleanup)
158
158
  {
159
159
  for (let i=-1;i<=1;++i)
@@ -184,13 +184,7 @@ export class Sky extends LJS.EngineObject
184
184
  {
185
185
  // fill background with a gradient
186
186
  const canvas = LJS.mainCanvas;
187
- const gradientDelta = 5;
188
- for (let y=0; y<canvas.height; y+=gradientDelta)
189
- {
190
- // draw horizontal lines to create a gradient
191
- const color = this.skyColor.lerp(this.horizonColor, y/canvas.height);
192
- LJS.drawRect(vec2(0,y), vec2(canvas.width*2, gradientDelta*2), color, 0, undefined, true)
193
- }
187
+ LJS.drawRectGradient(LJS.cameraPos, LJS.getCameraSize(), this.skyColor, this.horizonColor);
194
188
 
195
189
  // draw stars
196
190
  LJS.setBlendMode(true);
@@ -216,21 +210,20 @@ export class Sky extends LJS.EngineObject
216
210
 
217
211
  export class ParallaxLayer extends LJS.CanvasLayer
218
212
  {
219
- constructor(depth)
213
+ constructor(pos, topColor, bottomColor, depth)
220
214
  {
221
215
  const renderOrder = depth - 3e3;
222
216
  const canvasSize = vec2(512, 256);
223
- super(vec2(), vec2(), 0, renderOrder, canvasSize);
224
- this.canvasSize = canvasSize;
217
+ super(pos, vec2(), 0, renderOrder, canvasSize);
218
+ this.centerPos = pos;
225
219
  this.depth = depth;
226
220
 
227
221
  // create a gradient for the mountains
228
- const topColor = GameLevel.levelColor.mutate(.2).lerp(GameLevel.sky.skyColor, .8 - depth*.15);
229
- const bottomColor = GameLevel.levelColor.subtract(LJS.CLEAR_WHITE).mutate(.2);
230
- for (let i = canvasSize.y; i--;)
222
+ const w = canvasSize.x, h = canvasSize.y;
223
+ for (let i = h; i--;)
231
224
  {
232
225
  // draw a 1 pixel gradient line on the left side of the canvas
233
- const p = i/canvasSize.y;
226
+ const p = i/h;
234
227
  this.context.fillStyle = topColor.lerp(bottomColor, p);
235
228
  this.context.fillRect(0, i, 1, 1);
236
229
  }
@@ -239,40 +232,42 @@ export class ParallaxLayer extends LJS.CanvasLayer
239
232
  const pointiness = .2; // how pointy the mountains are
240
233
  const levelness = .005; // how much the mountains level out
241
234
  const slopeRange = 1; // max slope of the mountains
242
- const startGroundLevel = canvasSize.y/2;
235
+ const startGroundLevel = h/2;
243
236
  let y = startGroundLevel, groundSlope = LJS.rand(-slopeRange, slopeRange);
244
- for (let x=canvasSize.x; x--;)
237
+ for (let x=w; x--;)
245
238
  {
246
239
  // pull slope towards start ground level
247
240
  y += groundSlope -= (y-startGroundLevel)*levelness;
248
241
 
249
- // random change slope
242
+ // randomly change slope
250
243
  if (LJS.rand() < pointiness)
251
244
  groundSlope = LJS.rand(-slopeRange, slopeRange);
252
245
 
253
246
  // draw 1 pixel wide vertical slice of mountain
254
- this.context.drawImage(this.canvas, 0, 0, 1, canvasSize.y, x, y, 1, canvasSize.y - y);
247
+ this.context.drawImage(this.canvas, 0, 0, 1, h, x, y, 1, h - y);
255
248
  }
256
249
 
257
250
  // remove gradient sliver from left side
258
- this.context.clearRect(0,0,1,canvasSize.y);
251
+ this.context.clearRect(0,0,1,h);
259
252
 
260
- // make webgl texture
253
+ // make WebGL texture
261
254
  this.useWebGL(LJS.glEnable);
262
255
  }
263
256
 
264
257
  render()
265
258
  {
259
+ const canvasSize = vec2(this.canvas.width, this.canvas.height);
266
260
  const depth = this.depth
267
261
  const distance = 4 + depth;
268
262
  const parallax = vec2(150, 30).scale(depth**2+1);
269
- const levelCenter = GameLevel.levelSize.scale(.5);
270
- const cameraDeltaFromCenter = LJS.cameraPos.subtract(levelCenter).divide(levelCenter.scale(-1).divide(parallax));
263
+ const levelCenter = this.centerPos;
264
+ const cameraDeltaFromCenter = LJS.cameraPos.subtract(levelCenter)
265
+ .divide(levelCenter.scale(-1).divide(parallax));
271
266
  const scale = distance/LJS.cameraScale;
272
267
  const positonOffset = vec2(0, 2-depth);
273
268
  const cameraOffset = cameraDeltaFromCenter.scale(1/LJS.cameraScale);
274
269
  this.pos = LJS.cameraPos.add(positonOffset).add(cameraOffset);
275
- this.size = this.canvasSize.scale(scale);
270
+ this.size = canvasSize.scale(scale);
276
271
  super.render();
277
272
  }
278
273
  }
@@ -39,7 +39,12 @@ export function buildLevel()
39
39
 
40
40
  // create parallax layers
41
41
  for (let i=3; i--;)
42
- new GameEffects.ParallaxLayer(i);
42
+ {
43
+ const pos = levelSize.scale(.5);
44
+ const topColor = levelColor.mutate(.2).lerp(sky.skyColor, .8 - i*.15);
45
+ const bottomColor = levelColor.subtract(LJS.CLEAR_WHITE).mutate(.2);
46
+ new GameEffects.ParallaxLayer(pos, topColor, bottomColor, i );
47
+ }
43
48
  }
44
49
 
45
50
  function loadLevelData()
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html><meta charset=utf-8><body>
2
- <script src=../../dist/littlejs.js?1133></script>
2
+ <script src=../../dist/littlejs.js?1134></script>
3
3
  <script src=../../dist/box2d.wasm.js></script>
4
4
 
5
5
  <!-- LittleJS Engine Source