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.
- package/README.md +15 -11
- package/copyToGithub.bat +28 -0
- package/dist/littlejs.d.ts +430 -304
- package/dist/littlejs.esm.js +1745 -982
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +1716 -965
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +1673 -922
- package/examples/box2d/game.js +2 -2
- package/examples/box2d/gameObjects.js +2 -2
- package/examples/box2d/scenes.js +1 -1
- package/examples/breakout/game.js +30 -25
- package/examples/breakoutTutorial/README.md +44 -41
- package/examples/breakoutTutorial/game.js +2 -2
- package/examples/electron/build.bat +7 -0
- package/examples/electron/build.js +124 -0
- package/examples/electron/electron.js +35 -0
- package/examples/electron/game.js +140 -0
- package/examples/electron/index.html +13 -0
- package/examples/electron/package.json +12 -0
- package/examples/electron/tiles.png +0 -0
- package/examples/empty/game.js +1 -0
- package/examples/htmlMenu/game.js +1 -1
- package/examples/index.html +332 -160
- package/examples/module/game.js +5 -2
- package/examples/particles/index.html +12 -3
- package/examples/platformer/gameEffects.js +20 -19
- package/examples/platformer/gameLevel.js +6 -1
- package/examples/shorts/base.html +1 -1
- package/examples/shorts/blending.js +9 -5
- package/examples/shorts/box2d.js +1 -1
- package/examples/shorts/box2dCar.js +1 -1
- package/examples/shorts/clock.js +1 -1
- package/examples/shorts/colors.js +2 -2
- package/examples/shorts/flappyGame.js +2 -1
- package/examples/shorts/helloWorld.js +1 -1
- package/examples/shorts/maze.js +1 -1
- package/examples/shorts/music.js +106 -0
- package/examples/shorts/nineSlice.js +9 -9
- package/examples/shorts/parallax.js +71 -0
- package/examples/shorts/piano.js +9 -10
- package/examples/shorts/postProcess.js +25 -8
- package/examples/shorts/raycasting.js +2 -2
- package/examples/shorts/shapes.js +7 -15
- package/examples/shorts/slidingPuzzle.js +2 -2
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/starfield.js +5 -7
- package/examples/shorts/systemFont.js +1 -1
- package/examples/shorts/uiSystem.js +16 -8
- package/examples/starter/build.js +9 -8
- package/examples/starter/game.js +5 -2
- package/examples/starter/index.html +2 -2
- package/examples/stress/index.html +13 -8
- package/examples/style.css +19 -6
- package/examples/typescript/build.js +1 -1
- package/examples/typescript/game.js +2 -2
- package/examples/typescript/game.ts +5 -2
- package/examples/uiSystem/game.js +13 -12
- package/package.json +2 -2
- package/plugins/box2d.js +24 -97
- package/plugins/drawUtilities.js +29 -23
- package/plugins/newgrounds.js +6 -6
- package/plugins/pluginExport.js +1 -1
- package/plugins/postProcess.js +7 -0
- package/plugins/uiSystem.js +106 -82
- package/plugins/zzfxm.js +10 -10
- package/reference.md +90 -54
- package/src/engine.js +28 -23
- package/src/engineAudio.js +285 -110
- package/src/engineBuild.js +9 -9
- package/src/engineDebug.js +28 -28
- package/src/engineDraw.js +346 -202
- package/src/engineExport.js +28 -16
- package/src/engineInput.js +58 -68
- package/src/engineMedals.js +29 -29
- package/src/engineObject.js +28 -25
- package/src/engineParticles.js +67 -60
- package/src/engineRelease.js +1 -1
- package/src/engineSettings.js +70 -16
- package/src/engineTileLayer.js +34 -34
- package/src/engineUtilities.js +69 -62
- package/src/engineWebGL.js +467 -65
- /package/examples/shorts/{playSound.js → sound.js} +0 -0
package/examples/index.html
CHANGED
|
@@ -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
|
+
<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
|
-
<
|
|
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
|
|
34
|
-
<button onclick=
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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', '
|
|
63
|
-
new ExampleInfo('Colors', 'colors.js', '
|
|
64
|
-
new ExampleInfo('Blending', 'blending.js', '
|
|
65
|
-
new ExampleInfo('Timers', 'timers.js', '
|
|
66
|
-
new ExampleInfo('Texture', 'texture.js', '
|
|
67
|
-
new ExampleInfo('Particles', 'particles.js', '
|
|
68
|
-
new ExampleInfo('
|
|
69
|
-
new ExampleInfo('
|
|
70
|
-
new ExampleInfo('
|
|
71
|
-
new ExampleInfo('
|
|
72
|
-
new ExampleInfo('
|
|
73
|
-
new ExampleInfo('
|
|
74
|
-
new ExampleInfo('
|
|
75
|
-
new ExampleInfo('
|
|
76
|
-
new ExampleInfo('
|
|
77
|
-
new ExampleInfo('
|
|
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', '
|
|
80
|
-
new ExampleInfo('Top Down Game', 'topDown.js', '
|
|
81
|
-
new ExampleInfo('Tilted View', 'tiltedView.js', 'Pseudo 3D
|
|
82
|
-
new ExampleInfo('Space Game', 'spaceGame.js', '
|
|
83
|
-
new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander
|
|
84
|
-
new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird
|
|
85
|
-
new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style game'),
|
|
86
|
-
new ExampleInfo('Pong Game', 'pongGame.js', '
|
|
87
|
-
new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', '
|
|
88
|
-
new ExampleInfo('Raycasting Game', 'raycasting.js', 'Pseudo 3D raycasting
|
|
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', '
|
|
91
|
-
new ExampleInfo('Box2d Car', 'box2dCar.js', '
|
|
92
|
-
new ExampleInfo('Post Processing', 'postProcess.js', '
|
|
93
|
-
new ExampleInfo('UI System Plugin', 'uiSystem.js', '
|
|
94
|
-
new ExampleInfo('Nine Slice', 'nineSlice.js', '
|
|
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
|
|
97
|
-
new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', '
|
|
98
|
-
new ExampleInfo('Breakout Game', 'breakout', '
|
|
99
|
-
new ExampleInfo('Platforming Game', 'platformer', '
|
|
100
|
-
new ExampleInfo('Puzzle Game', 'puzzle', 'Match
|
|
101
|
-
new ExampleInfo('Stress Test', 'stress', '
|
|
102
|
-
new ExampleInfo('Box2D Plugin', 'box2d', '
|
|
103
|
-
new ExampleInfo('HTML Menus', 'htmlMenu', '
|
|
104
|
-
new ExampleInfo('UI System Plugin', 'uiSystem', '
|
|
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
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
exampleIndex = 1;
|
|
120
|
-
selectExample.selectedIndex = exampleIndex;
|
|
121
|
-
setExample();
|
|
139
|
+
// apply responsive layout
|
|
140
|
+
addEventListener('resize', resizeWindow);
|
|
141
|
+
resizeWindow();
|
|
142
|
+
}
|
|
122
143
|
|
|
123
|
-
|
|
144
|
+
function resizeWindow()
|
|
124
145
|
{
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 =
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
341
|
+
function restartCode()
|
|
270
342
|
{
|
|
271
|
-
// manually
|
|
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
|
|
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 = !!
|
|
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
|
-
//
|
|
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
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
449
|
+
// hook up buttons
|
|
450
|
+
buttonScreenshot.onclick = () =>
|
|
451
|
+
{
|
|
452
|
+
if (iframeContent.debugScreenshot)
|
|
453
|
+
iframeContent.debugScreenshot();
|
|
454
|
+
}
|
|
342
455
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
iframeContent.
|
|
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
|
-
|
|
350
|
-
|
|
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
|
|
370
|
-
return;
|
|
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
|
-
|
|
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' : '
|
|
528
|
+
element.style.display = message ? 'block' : '';
|
|
405
529
|
|
|
406
|
-
if (element
|
|
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 = '
|
|
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 (!
|
|
562
|
+
if (!lineNumber || lineNumber <= 0)
|
|
439
563
|
return;
|
|
440
564
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
660
|
+
o.selected = theme === savedTheme;
|
|
495
661
|
selectTheme.add(o);
|
|
496
662
|
}
|
|
497
663
|
|
|
498
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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/
|
|
529
|
-
if (type
|
|
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('
|
|
545
|
-
localStorage.setItem('
|
|
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>
|