littlejsengine 1.12.6 → 1.13.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.
- package/README.md +15 -12
- package/dist/littlejs.d.ts +466 -236
- package/dist/littlejs.esm.js +6126 -5370
- package/dist/littlejs.esm.min.js +4 -1
- package/dist/littlejs.js +6093 -5359
- package/dist/littlejs.min.js +4 -1
- package/dist/littlejs.release.js +5614 -4907
- package/examples/box2d/game.js +2 -2
- package/examples/box2d/gameObjects.js +8 -7
- package/examples/box2d/index.html +1 -1
- package/examples/box2d/scenes.js +8 -8
- package/examples/breakout/game.js +1 -1
- package/examples/breakout/gameObjects.js +2 -2
- package/examples/breakout/index.html +1 -1
- package/examples/breakoutTutorial/README.md +46 -43
- package/examples/breakoutTutorial/game.js +3 -3
- package/examples/breakoutTutorial/index.html +1 -1
- 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/empty/index.html +1 -1
- package/examples/htmlMenu/game.js +1 -1
- package/examples/htmlMenu/index.html +1 -1
- package/examples/index.html +486 -182
- package/examples/module/game.js +6 -3
- package/examples/module/index.html +1 -1
- package/examples/particles/index.html +12 -3
- package/examples/platformer/game.js +4 -4
- package/examples/platformer/gameCharacter.js +6 -6
- package/examples/platformer/gameEffects.js +74 -57
- package/examples/platformer/gameLevel.js +61 -70
- package/examples/platformer/gameObjects.js +4 -4
- package/examples/platformer/index.html +1 -1
- package/examples/puzzle/index.html +1 -1
- package/examples/shorts/base.html +23 -3
- package/examples/shorts/blending.js +9 -5
- package/examples/shorts/box2d.js +1 -1
- package/examples/shorts/box2dCar.js +9 -13
- package/examples/shorts/clock.js +1 -1
- package/examples/shorts/colors.js +2 -2
- package/examples/shorts/flappyGame.js +52 -0
- package/examples/shorts/helloWorld.js +6 -3
- package/examples/shorts/hillGlideGame.js +56 -0
- package/examples/shorts/landerGame.js +32 -0
- package/examples/shorts/maze.js +47 -0
- package/examples/shorts/medals.js +42 -0
- package/examples/shorts/nineSlice.js +21 -0
- package/examples/shorts/piano.js +52 -0
- package/examples/shorts/platformer.js +24 -20
- package/examples/shorts/{pong.js → pongGame.js} +1 -1
- package/examples/shorts/raycasting.js +71 -0
- package/examples/shorts/shapes.js +9 -9
- package/examples/shorts/slidingPuzzle.js +42 -0
- package/examples/shorts/spaceGame.js +56 -0
- package/examples/shorts/starfield.js +15 -0
- package/examples/shorts/systemFont.js +1 -1
- package/examples/shorts/tileLayer.js +3 -5
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +8 -7
- package/examples/shorts/topDown.js +18 -26
- package/examples/shorts/uiSystem.js +5 -7
- package/examples/starter/build.bat +6 -1
- package/examples/starter/build.js +9 -8
- package/examples/starter/game.js +7 -4
- package/examples/starter/index.html +23 -13
- package/examples/stress/index.html +7 -8
- package/examples/style.css +139 -0
- package/examples/typescript/build.js +2 -3
- package/examples/typescript/game.js +4 -4
- package/examples/typescript/game.ts +6 -3
- package/examples/typescript/index.html +1 -1
- package/examples/uiSystem/game.js +10 -25
- package/examples/uiSystem/index.html +1 -1
- package/package.json +2 -2
- package/plugins/box2d.js +22 -97
- package/plugins/drawUtilities.js +132 -0
- package/plugins/newgrounds.js +1 -1
- package/plugins/pluginExport.js +7 -1
- package/plugins/postProcess.js +9 -2
- package/plugins/uiSystem.js +155 -67
- package/plugins/zzfxm.js +1 -1
- package/reference.md +10 -10
- package/src/engine.js +56 -30
- package/src/engineAudio.js +15 -6
- package/src/engineBuild.bat +6 -1
- package/src/engineBuild.js +20 -5
- package/src/engineDebug.js +55 -28
- package/src/engineDraw.js +321 -177
- package/src/engineExport.js +27 -9
- package/src/engineInput.js +110 -38
- package/src/engineMedals.js +4 -4
- package/src/engineObject.js +71 -70
- package/src/engineParticles.js +33 -9
- package/src/engineSettings.js +69 -23
- package/src/engineTileLayer.js +312 -153
- package/src/engineUtilities.js +187 -135
- package/src/engineWebGL.js +70 -39
package/examples/index.html
CHANGED
|
@@ -1,102 +1,42 @@
|
|
|
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
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
|
-
<
|
|
91
|
-
<
|
|
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
|
+
<input type=checkbox id=checkboxJumpToError>Jump to Error
|
|
25
|
+
</div>
|
|
92
26
|
<textarea id=textareaCode oninput=codeInput() spellcheck=false></textarea>
|
|
93
27
|
<textarea id=textareaError readonly spellcheck=false></textarea>
|
|
94
|
-
<
|
|
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 id=buttonRestart onclick=restartCode()>Restart</button>
|
|
35
|
+
<button id=buttonPause>Pause</button>
|
|
36
|
+
<button id=buttonScreenshot>Screenshot</button>
|
|
37
|
+
<button id=buttonFullscreen>Fullscreen</button>
|
|
38
|
+
<input type=checkbox id=checkboxWebGL checked>WebGL
|
|
39
|
+
</div>
|
|
100
40
|
<select id=selectExample autofocus onchange=setExample() size=2></select>
|
|
101
41
|
</div>
|
|
102
42
|
</div>
|
|
@@ -105,70 +45,81 @@ select
|
|
|
105
45
|
|
|
106
46
|
class ExampleInfo
|
|
107
47
|
{
|
|
108
|
-
constructor(name, filename, description,
|
|
48
|
+
constructor(name, filename, description='', largeExample=false)
|
|
109
49
|
{
|
|
110
|
-
if (filename && !
|
|
111
|
-
filename = 'shorts/' + filename;
|
|
50
|
+
if (filename && !largeExample)
|
|
51
|
+
filename = 'shorts/' + filename;
|
|
112
52
|
this.name = name;
|
|
113
53
|
this.filename = filename;
|
|
114
54
|
this.description = description;
|
|
115
|
-
this.
|
|
55
|
+
this.largeExample = largeExample;
|
|
116
56
|
}
|
|
117
57
|
}
|
|
118
58
|
|
|
119
59
|
const exampleList =
|
|
120
60
|
[
|
|
121
61
|
new ExampleInfo('--- SHORT EXAMPLES ---'),
|
|
122
|
-
new ExampleInfo('Hello World', 'helloWorld.js', '
|
|
123
|
-
new ExampleInfo('Shapes', 'shapes.js', '
|
|
124
|
-
new ExampleInfo('Colors', 'colors.js', '
|
|
125
|
-
new ExampleInfo('Blending', 'blending.js', '
|
|
126
|
-
new ExampleInfo('Timers', 'timers.js', '
|
|
127
|
-
new ExampleInfo('Texture', 'texture.js', '
|
|
128
|
-
new ExampleInfo('Particles', 'particles.js', '
|
|
129
|
-
new ExampleInfo('Play Sound', 'playSound.js', '
|
|
130
|
-
new ExampleInfo('System Font', 'systemFont.js', '
|
|
131
|
-
new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', '
|
|
132
|
-
new ExampleInfo('Animation', 'animation.js', '
|
|
133
|
-
new ExampleInfo('Clock', 'clock.js', '
|
|
134
|
-
new ExampleInfo('Tile Layer', 'tileLayer.js', '
|
|
135
|
-
new ExampleInfo('
|
|
136
|
-
new ExampleInfo('
|
|
137
|
-
new ExampleInfo('
|
|
138
|
-
new ExampleInfo('
|
|
139
|
-
new ExampleInfo('
|
|
140
|
-
new ExampleInfo('
|
|
141
|
-
new ExampleInfo('
|
|
142
|
-
new ExampleInfo('
|
|
143
|
-
|
|
62
|
+
new ExampleInfo('Hello World', 'helloWorld.js', 'Minimal starter example - [basic, beginner]'),
|
|
63
|
+
new ExampleInfo('Shapes', 'shapes.js', 'Draw geometric shapes - [graphics, primitives, basic]'),
|
|
64
|
+
new ExampleInfo('Colors', 'colors.js', 'Color object and HSL usage - [graphics, color, basic]'),
|
|
65
|
+
new ExampleInfo('Blending', 'blending.js', 'Blending modes and transparency - [graphics, effects]'),
|
|
66
|
+
new ExampleInfo('Timers', 'timers.js', 'Timer object and timing functions - [utilities, time]'),
|
|
67
|
+
new ExampleInfo('Texture', 'texture.js', 'Display and manipulate textures - [graphics, sprites]'),
|
|
68
|
+
new ExampleInfo('Particles', 'particles.js', 'Particle system basics - [effects, particles]'),
|
|
69
|
+
new ExampleInfo('Play Sound', 'playSound.js', 'Sound effects with ZzFX - [audio, sound]'),
|
|
70
|
+
new ExampleInfo('System Font', 'systemFont.js', 'Built-in bitmap font rendering - [text, font]'),
|
|
71
|
+
new ExampleInfo('Sprite Atlas', 'spriteAtlas.js', 'Sprite atlas and tile rendering - [graphics, sprites]'),
|
|
72
|
+
new ExampleInfo('Animation', 'animation.js', 'Sprite animation techniques - [animation, sprites]'),
|
|
73
|
+
new ExampleInfo('Clock', 'clock.js', 'Real-time clock display - [time, ui, utilities]'),
|
|
74
|
+
new ExampleInfo('Tile Layer', 'tileLayer.js', 'Tile layer rendering system - [tiles, levels]'),
|
|
75
|
+
new ExampleInfo('Piano', 'piano.js', 'Interactive piano keyboard - [audio, input, ui]'),
|
|
76
|
+
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]'),
|
|
79
|
+
new ExampleInfo('--- MINI GAMES ---'),
|
|
80
|
+
new ExampleInfo('Platformer Game', 'platformer.js', 'Jump and run platformer - [game, platformer, physics]'),
|
|
81
|
+
new ExampleInfo('Top Down Game', 'topDown.js', 'Adventure with collision - [game, topdown, rpg]'),
|
|
82
|
+
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]'),
|
|
84
|
+
new ExampleInfo('Lander Game', 'landerGame.js', 'Lunar lander physics game - [game, physics, thrust]'),
|
|
85
|
+
new ExampleInfo('Flappy Game', 'flappyGame.js', 'Flappy bird clone - [game, arcade, flying]'),
|
|
86
|
+
new ExampleInfo('Hill Glide Game', 'hillGlideGame.js', 'Tiny wings style glider - [game, physics, flying]'),
|
|
87
|
+
new ExampleInfo('Pong Game', 'pongGame.js', 'Classic paddle ball game - [game, classic, arcade]'),
|
|
88
|
+
new ExampleInfo('Sliding Puzzle', 'slidingPuzzle.js', 'Tile sliding puzzle game - [game, puzzle, logic]'),
|
|
89
|
+
new ExampleInfo('Raycasting Game', 'raycasting.js', 'Pseudo 3D raycasting demo - [3d, raycasting, wolfenstein]'),
|
|
90
|
+
new ExampleInfo('--- PLUGINS ---'),
|
|
91
|
+
new ExampleInfo('Box2d Demo', 'box2d.js', 'Box2D physics plugin demo - [physics, box2d, plugin]'),
|
|
92
|
+
new ExampleInfo('Box2d Car', 'box2dCar.js', 'Drivable car with Box2D - [physics, box2d, vehicle]'),
|
|
93
|
+
new ExampleInfo('Post Processing', 'postProcess.js', 'Shader effects and filters - [graphics, shaders, effects]'),
|
|
94
|
+
new ExampleInfo('UI System Plugin', 'uiSystem.js', 'UI elements and widgets - [ui, plugin, interface]'),
|
|
95
|
+
new ExampleInfo('Nine Slice', 'nineSlice.js', 'Scalable UI panel rendering - [ui, graphics, scaling]'),
|
|
144
96
|
new ExampleInfo('--- FULL EXAMPLES ---'),
|
|
145
|
-
new ExampleInfo('Starter', 'starter', 'Clean
|
|
146
|
-
new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', '
|
|
147
|
-
new ExampleInfo('Breakout Game', 'breakout', '
|
|
148
|
-
new ExampleInfo('Platforming Game', 'platformer', '
|
|
149
|
-
new ExampleInfo('Puzzle Game', 'puzzle', 'Match
|
|
150
|
-
new ExampleInfo('Stress Test', 'stress', '
|
|
151
|
-
new ExampleInfo('Box2D Plugin', 'box2d', '
|
|
152
|
-
new ExampleInfo('HTML Menus', 'htmlMenu', '
|
|
153
|
-
new ExampleInfo('UI System Plugin', 'uiSystem', '
|
|
154
|
-
// Add more examples here!
|
|
97
|
+
new ExampleInfo('Starter', 'starter', 'Clean project template - [template, beginner]', true),
|
|
98
|
+
new ExampleInfo('Breakout Tutorial', 'breakoutTutorial', 'Step-by-step breakout guide - [tutorial, breakout]', true),
|
|
99
|
+
new ExampleInfo('Breakout Game', 'breakout', 'Complete breakout with effects - [game, breakout, complete]', true),
|
|
100
|
+
new ExampleInfo('Platforming Game', 'platformer', 'Full platformer with levels - [game, platformer, complete]', true),
|
|
101
|
+
new ExampleInfo('Puzzle Game', 'puzzle', 'Match-3 puzzle with HD graphics - [game, puzzle, match3]', true),
|
|
102
|
+
new ExampleInfo('Stress Test', 'stress', 'Performance test with music - [performance, test, music]', true),
|
|
103
|
+
new ExampleInfo('Box2D Plugin', 'box2d', 'Complete Box2D physics demo - [physics, box2d, complete]', true),
|
|
104
|
+
new ExampleInfo('HTML Menus', 'htmlMenu', 'HTML integration example - [html, ui, integration]', true),
|
|
105
|
+
new ExampleInfo('UI System Plugin', 'uiSystem', 'Complete UI system demo - [ui, plugin, complete]', true),
|
|
155
106
|
];
|
|
156
107
|
|
|
157
108
|
///////////////////////////////////////////////////////////////////////////////
|
|
158
109
|
|
|
159
|
-
|
|
110
|
+
// global variables
|
|
111
|
+
let iframeExample, inputTimeout, codeMirror, consoleAutoScroll, errorLineMarker, codeIsJS;
|
|
160
112
|
|
|
161
113
|
// load the examples into the list
|
|
162
|
-
|
|
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
|
-
}
|
|
114
|
+
filterExamples();
|
|
169
115
|
|
|
170
|
-
// set the selected example from the URL
|
|
171
|
-
|
|
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;
|
|
172
123
|
setExample();
|
|
173
124
|
|
|
174
125
|
onresize = () =>
|
|
@@ -179,55 +130,73 @@ onresize = () =>
|
|
|
179
130
|
let h = w / aspect | 0;
|
|
180
131
|
iframeContainer.style.width = w + 'px';
|
|
181
132
|
iframeContainer.style.height = h + 'px';
|
|
133
|
+
// fix code mirror width
|
|
134
|
+
container3.style.width = w-30 + 'px';
|
|
182
135
|
}
|
|
183
136
|
onresize();
|
|
184
137
|
|
|
185
138
|
///////////////////////////////////////////////////////////////////////////////
|
|
139
|
+
// setting examples
|
|
186
140
|
|
|
187
141
|
function setExample()
|
|
188
142
|
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
let
|
|
192
|
-
|
|
193
|
-
|
|
143
|
+
// get the original index if this is a filtered result
|
|
144
|
+
const selectedOption = selectExample.options[selectExample.selectedIndex];
|
|
145
|
+
let exampleIndex = selectedOption && selectedOption.originalIndex ?
|
|
146
|
+
parseInt(selectedOption.originalIndex) :
|
|
147
|
+
selectExample.selectedIndex;
|
|
148
|
+
|
|
149
|
+
// make sure we have a valid example
|
|
150
|
+
if (exampleIndex < 0 || exampleIndex >= exampleList.length)
|
|
151
|
+
exampleIndex = 1; // reset to default
|
|
152
|
+
if (!exampleList[exampleIndex].filename)
|
|
153
|
+
exampleIndex = 1;
|
|
154
|
+
|
|
155
|
+
// load the example
|
|
156
|
+
const example = exampleList[exampleIndex];
|
|
194
157
|
exampleInfo.innerText = example.name + (example.description ? ' - ' + example.description : '');
|
|
195
158
|
const filename = 'examples/' + example.filename;
|
|
196
159
|
exampleLink.href = 'https://github.com/KilledByAPixel/LittleJS/tree/main/' + filename;
|
|
197
160
|
exampleLink.innerText = filename;
|
|
198
|
-
|
|
199
|
-
|
|
161
|
+
|
|
162
|
+
// update URL parameter
|
|
163
|
+
const url = new URL(window.location);
|
|
164
|
+
url.searchParams.set('example', example.filename);
|
|
165
|
+
window.history.replaceState({}, '', url);
|
|
166
|
+
|
|
167
|
+
loadFile(example.filename, example.largeExample);
|
|
200
168
|
}
|
|
201
169
|
|
|
202
|
-
function
|
|
170
|
+
async function loadFile(filename, largeExample)
|
|
203
171
|
{
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
172
|
+
if (codeMirror)
|
|
173
|
+
codeMirror.setOption('readOnly', largeExample);
|
|
174
|
+
else
|
|
175
|
+
textareaCode.disabled = largeExample;
|
|
176
|
+
|
|
177
|
+
// 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;
|
|
183
|
+
|
|
184
|
+
if (largeExample)
|
|
216
185
|
{
|
|
217
|
-
if (iframeExample)
|
|
218
|
-
iframeContainer.removeChild(iframeExample);
|
|
219
186
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
187
|
+
// Show message in code view that full examples can't be edited
|
|
188
|
+
const text = 'Code view not available for large examples.';
|
|
189
|
+
setCode(text, filename);
|
|
190
|
+
codeIsJS = false;
|
|
191
|
+
if (codeMirror)
|
|
224
192
|
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
textareaCode.value = 'Editor code view not available for large examples.\n';
|
|
193
|
+
codeMirror.off('change', codeInput);
|
|
194
|
+
codeMirror.setOption('mode', 'text');
|
|
195
|
+
codeMirror.setValue(text);
|
|
229
196
|
}
|
|
230
|
-
|
|
197
|
+
else
|
|
198
|
+
textareaCode.value = text;
|
|
199
|
+
clearTimeout(inputTimeout);
|
|
231
200
|
return;
|
|
232
201
|
}
|
|
233
202
|
|
|
@@ -237,7 +206,19 @@ async function loadFile(filename, fullExample)
|
|
|
237
206
|
if (!response.ok)
|
|
238
207
|
throw new Error('Could not load file: ' + filename);
|
|
239
208
|
const text = await response.text();
|
|
240
|
-
|
|
209
|
+
|
|
210
|
+
// set the code in both code mirror and textarea
|
|
211
|
+
codeIsJS = true;
|
|
212
|
+
if (codeMirror)
|
|
213
|
+
{
|
|
214
|
+
codeMirror.on('change', codeInput);
|
|
215
|
+
codeMirror.setOption('mode', 'javascript');
|
|
216
|
+
codeMirror.setValue(text);
|
|
217
|
+
}
|
|
218
|
+
else
|
|
219
|
+
textareaCode.value = text;
|
|
220
|
+
clearTimeout(inputTimeout);
|
|
221
|
+
setCode(text);
|
|
241
222
|
}
|
|
242
223
|
catch (error)
|
|
243
224
|
{
|
|
@@ -245,45 +226,167 @@ async function loadFile(filename, fullExample)
|
|
|
245
226
|
}
|
|
246
227
|
}
|
|
247
228
|
|
|
248
|
-
function
|
|
229
|
+
function filterExamples(reset=0)
|
|
230
|
+
{
|
|
231
|
+
if (reset)
|
|
232
|
+
inputSearch.value = '';
|
|
233
|
+
|
|
234
|
+
// clear current options
|
|
235
|
+
selectExample.options.length = 0;
|
|
236
|
+
|
|
237
|
+
// filter and add matching examples
|
|
238
|
+
const searchTerm = inputSearch.value.toLowerCase().trim();
|
|
239
|
+
for (let i = 0; i < exampleList.length; i++)
|
|
240
|
+
{
|
|
241
|
+
// check if search term matches name or description
|
|
242
|
+
const example = exampleList[i];
|
|
243
|
+
const text = example.name + (example.description ? ` - ${example.description}` : '');
|
|
244
|
+
if (example.name.toLowerCase().includes(searchTerm) ||
|
|
245
|
+
example.description.toLowerCase().includes(searchTerm))
|
|
246
|
+
{
|
|
247
|
+
const o = new Option(text);
|
|
248
|
+
o.disabled = !example.filename;
|
|
249
|
+
o.originalIndex = i;
|
|
250
|
+
selectExample.add(o);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!selectExample.options.length)
|
|
255
|
+
{
|
|
256
|
+
// show a message if no matches
|
|
257
|
+
const o = new Option('No examples found matching "' + searchTerm + '"');
|
|
258
|
+
o.disabled = true;
|
|
259
|
+
selectExample.add(o);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
264
|
+
// setting code
|
|
265
|
+
|
|
266
|
+
function codeInput()
|
|
249
267
|
{
|
|
268
|
+
if (!checkboxLiveEdit.checked)
|
|
269
|
+
return;
|
|
270
|
+
|
|
271
|
+
// debounce input - get content from code mirror if available, otherwise from textarea
|
|
272
|
+
clearTimeout(inputTimeout);
|
|
273
|
+
const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
|
|
274
|
+
inputTimeout = setTimeout(() => setCode(code), 500);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function restartCode()
|
|
278
|
+
{
|
|
279
|
+
// manually restart/run the code regardless of live edit setting
|
|
280
|
+
const code = codeMirror ? codeMirror.getValue() : textareaCode.value;
|
|
281
|
+
setCode(code);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function setCode(code, textFilename)
|
|
285
|
+
{
|
|
286
|
+
const largeExample = !!textFilename;
|
|
250
287
|
clearTimeout(inputTimeout);
|
|
251
288
|
if (iframeExample)
|
|
252
289
|
iframeContainer.removeChild(iframeExample);
|
|
253
290
|
|
|
254
|
-
|
|
291
|
+
unsetErrorMessage();
|
|
292
|
+
unsetConsoleMessage();
|
|
255
293
|
iframeExample = document.createElement('iframe');
|
|
256
294
|
iframeContainer.appendChild(iframeExample);
|
|
257
|
-
iframeExample.onload = ()
|
|
295
|
+
iframeExample.onload = ()=>
|
|
258
296
|
{
|
|
297
|
+
if (largeExample)
|
|
298
|
+
return;
|
|
299
|
+
|
|
259
300
|
const iframeContent = iframeExample.contentWindow;
|
|
260
301
|
const iframeDocument = iframeContent.document;
|
|
261
302
|
|
|
262
|
-
//
|
|
303
|
+
// intercept errors
|
|
304
|
+
function getErrorLine(stack)
|
|
305
|
+
{
|
|
306
|
+
// try to extract line number from stack trace, look for <anonymous> or injectedScript to find the user code
|
|
307
|
+
const anonymousMatch = stack.match(/(<anonymous>|injectedScript):(\d+)/);
|
|
308
|
+
return anonymousMatch ? parseInt(anonymousMatch[2]) : -1;
|
|
309
|
+
}
|
|
263
310
|
iframeContent.onerror = (message, source, lineno, colno, error) =>
|
|
264
311
|
{
|
|
265
312
|
let text = message;
|
|
266
313
|
if (lineno)
|
|
267
314
|
text += ` (Line:${lineno}, Column:${colno})`
|
|
268
315
|
if (error && error.stack)
|
|
269
|
-
text += `\n
|
|
316
|
+
text += `\n` + error.stack;
|
|
270
317
|
setErrorMessage(text);
|
|
318
|
+
setErrorLine(lineno);
|
|
271
319
|
}
|
|
272
320
|
iframeContent.onunhandledrejection = (event) =>
|
|
273
321
|
{
|
|
274
322
|
let text = event.reason;
|
|
275
|
-
if (event.reason.stack)
|
|
276
|
-
text += `\n\n` + event.reason.stack;
|
|
277
323
|
setErrorMessage(text);
|
|
324
|
+
if (event.reason && event.reason.stack)
|
|
325
|
+
{
|
|
326
|
+
text = event.reason.stack;
|
|
327
|
+
const errorLine = getErrorLine(event.reason.stack);
|
|
328
|
+
if (errorLine >= 0)
|
|
329
|
+
setErrorLine(errorLine);
|
|
330
|
+
}
|
|
278
331
|
};
|
|
332
|
+
|
|
333
|
+
// intercept asserts
|
|
279
334
|
const originalAssert = iframeContent.console.assert;
|
|
280
335
|
iframeContent.console.assert = function (condition, output)
|
|
281
336
|
{
|
|
282
|
-
|
|
283
|
-
|
|
337
|
+
if (!condition)
|
|
338
|
+
{
|
|
339
|
+
const stack = (new Error).stack;
|
|
340
|
+
const errorLine = getErrorLine(stack);
|
|
341
|
+
if (errorLine >= 0)
|
|
342
|
+
setErrorLine(errorLine);
|
|
343
|
+
setErrorMessage('Assertion failed!\n' + (output || '') + '\n' + stack);
|
|
344
|
+
}
|
|
284
345
|
originalAssert.apply(this, arguments);
|
|
285
346
|
};
|
|
286
347
|
|
|
348
|
+
// intercept console prints
|
|
349
|
+
const originalConsole = iframeContent.console;
|
|
350
|
+
function interceptConsole(method)
|
|
351
|
+
{
|
|
352
|
+
const original = originalConsole[method];
|
|
353
|
+
iframeContent.console[method] = function(...args)
|
|
354
|
+
{
|
|
355
|
+
const message = args.map(m=>stringifyMessage(m)).join('\n');
|
|
356
|
+
setConsoleMessage(message);
|
|
357
|
+
original.apply(originalConsole, args);
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
['log','info','warn','error','debug'].forEach(f=>interceptConsole(f));
|
|
361
|
+
|
|
362
|
+
{
|
|
363
|
+
// setup buttons
|
|
364
|
+
buttonScreenshot.onclick = () =>
|
|
365
|
+
{
|
|
366
|
+
if (iframeContent.debugScreenshot)
|
|
367
|
+
iframeContent.debugScreenshot();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// pause/resume functionality
|
|
371
|
+
buttonPause.onclick = () =>
|
|
372
|
+
{
|
|
373
|
+
if (!iframeContent.getPaused || !iframeContent.setPaused)
|
|
374
|
+
return;
|
|
375
|
+
|
|
376
|
+
const paused = !iframeContent.getPaused();
|
|
377
|
+
iframeContent.setPaused(paused);
|
|
378
|
+
buttonPause.textContent = paused ? 'Resume' : 'Pause';
|
|
379
|
+
}
|
|
380
|
+
buttonPause.textContent = 'Pause';
|
|
381
|
+
|
|
382
|
+
// fullscreen functionality
|
|
383
|
+
buttonFullscreen.onclick = ()=> iframeContent.toggleFullscreen();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// set WebGL based on checkbox
|
|
387
|
+
iframeContent.setGLEnable(checkboxWebGL.checked);
|
|
388
|
+
checkboxWebGL.onchange = ()=> iframeContent.setGLEnable(checkboxWebGL.checked);
|
|
389
|
+
|
|
287
390
|
// create a script element that overrides the default functions
|
|
288
391
|
const overrideScript = iframeDocument.createElement('script');
|
|
289
392
|
iframeDocument.body.appendChild(overrideScript);
|
|
@@ -293,23 +396,224 @@ function setCode(code, largeExample)
|
|
|
293
396
|
return; // stop if script error
|
|
294
397
|
|
|
295
398
|
// start LittleJS engine
|
|
296
|
-
iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?
|
|
399
|
+
iframeContent.engineInit(iframeContent.gameInit, iframeContent.gameUpdate, iframeContent.gameUpdatePost, iframeContent.gameRender, iframeContent.gameRenderPost, ['tiles.png?'+Date.now()]);
|
|
400
|
+
}
|
|
401
|
+
iframeExample.src = textFilename || 'shorts/base.html?' + Date.now();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
405
|
+
// error and console messages
|
|
406
|
+
|
|
407
|
+
function stringifyMessage(message)
|
|
408
|
+
{
|
|
409
|
+
// make sure message is a string
|
|
410
|
+
if (message === null)
|
|
411
|
+
return 'null';
|
|
412
|
+
if (Number.isNaN(message))
|
|
413
|
+
return 'NaN';
|
|
414
|
+
if (message === undefined)
|
|
415
|
+
return 'undefined';
|
|
416
|
+
if (message === 0)
|
|
417
|
+
return '0';
|
|
418
|
+
if (message === false)
|
|
419
|
+
return 'false';
|
|
420
|
+
return message;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function setMessage(message, element, clear=true)
|
|
424
|
+
{
|
|
425
|
+
message = stringifyMessage(message);
|
|
426
|
+
if (clear || !element.value)
|
|
427
|
+
element.value = message;
|
|
428
|
+
else
|
|
429
|
+
element.value += '\n' + message;
|
|
430
|
+
element.style.display = message ? 'block' : 'none';
|
|
431
|
+
|
|
432
|
+
if (element == textareaConsole)
|
|
433
|
+
{
|
|
434
|
+
const maxConsoleLines = 100;
|
|
435
|
+
const lines = element.value.split('\n');
|
|
436
|
+
if (lines.length > maxConsoleLines)
|
|
437
|
+
{
|
|
438
|
+
// limit max lines, prevents slowdown from too many lines
|
|
439
|
+
const excessLines = lines.length - maxConsoleLines;
|
|
440
|
+
element.value = lines.slice(excessLines).join('\n');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// auto scroll to bottom only if user hasn't manually scrolled
|
|
444
|
+
if (consoleAutoScroll)
|
|
445
|
+
element.scrollTop = element.scrollHeight;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function unsetMessage(element)
|
|
450
|
+
{
|
|
451
|
+
element.style.display = 'none';
|
|
452
|
+
element.value = '';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function clearErrorLine()
|
|
456
|
+
{
|
|
457
|
+
if (!codeMirror || !errorLineMarker)
|
|
458
|
+
return;
|
|
459
|
+
codeMirror.getDoc().removeLineClass(errorLineMarker, 'background', 'error-line');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function setErrorLine(lineNumber)
|
|
463
|
+
{
|
|
464
|
+
if (!lineNumber || lineNumber <= 0)
|
|
465
|
+
return;
|
|
297
466
|
|
|
298
|
-
|
|
467
|
+
if (codeMirror)
|
|
468
|
+
{
|
|
469
|
+
clearErrorLine();
|
|
470
|
+
--lineNumber; // codeMirror uses 0-based line numbers
|
|
471
|
+
errorLineMarker = codeMirror.getDoc().addLineClass(lineNumber, 'background', 'error-line');
|
|
472
|
+
if (checkboxJumpToError.checked)
|
|
299
473
|
{
|
|
300
|
-
|
|
301
|
-
|
|
474
|
+
codeMirror.scrollIntoView({line: lineNumber, ch: 0});
|
|
475
|
+
codeMirror.focus();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (textareaCode && checkboxJumpToError.checked)
|
|
479
|
+
{
|
|
480
|
+
// for textarea, calculate character position and set cursor
|
|
481
|
+
const lines = textareaCode.value.split('\n');
|
|
482
|
+
let charPos = 0;
|
|
483
|
+
for (let i = 0; i < lineNumber - 1 && i < lines.length; i++)
|
|
484
|
+
{
|
|
485
|
+
charPos += lines[i].length + 1; // +1 for newline character
|
|
486
|
+
}
|
|
487
|
+
textareaCode.setSelectionRange(charPos, charPos);
|
|
488
|
+
if (checkboxJumpToError.checked)
|
|
489
|
+
{
|
|
490
|
+
const line = textareaCode.value.split('\n');
|
|
491
|
+
lineNumber = Math.min(Math.max(0, lineNumber-1), lines.length-1);
|
|
492
|
+
|
|
493
|
+
// select error line in textarea
|
|
494
|
+
let start = 0;
|
|
495
|
+
for (let i = 0; i < lineNumber; i++)
|
|
496
|
+
start += lines[i].length + 1;
|
|
497
|
+
textareaCode.selectionStart = start;
|
|
498
|
+
textareaCode.selectionEnd = start + lines[lineNumber].length;
|
|
499
|
+
textareaCode.focus();
|
|
302
500
|
}
|
|
303
501
|
}
|
|
304
|
-
iframeExample.src = 'shorts/base.html';
|
|
305
502
|
}
|
|
306
503
|
|
|
307
|
-
function setErrorMessage(message
|
|
504
|
+
function setErrorMessage(message)
|
|
505
|
+
{
|
|
506
|
+
// prevent overwriting an existing error message
|
|
507
|
+
const errorMessageIsVisible = textareaError.style.display == 'block';
|
|
508
|
+
if (!errorMessageIsVisible)
|
|
509
|
+
setMessage(message, textareaError);
|
|
510
|
+
}
|
|
511
|
+
function unsetErrorMessage() { unsetMessage(textareaError); clearErrorLine(); }
|
|
512
|
+
function setConsoleMessage(message) { setMessage(message, textareaConsole, false); }
|
|
513
|
+
function unsetConsoleMessage() { unsetMessage(textareaConsole); consoleAutoScroll = true; }
|
|
514
|
+
function onScrollConsole()
|
|
515
|
+
{
|
|
516
|
+
// only auto scroll console is it is scrolled to bottom
|
|
517
|
+
const tolerance = 5; // pixels of tolerance
|
|
518
|
+
consoleAutoScroll = Math.abs(textareaConsole.scrollTop - (textareaConsole.scrollHeight - textareaConsole.clientHeight)) < tolerance;
|
|
519
|
+
}
|
|
520
|
+
textareaConsole.addEventListener('scroll', onScrollConsole);
|
|
521
|
+
|
|
522
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
523
|
+
// setup code mirror
|
|
524
|
+
|
|
525
|
+
const defaultTheme = 'littlejs';
|
|
526
|
+
const defaultFontSize = '16px';
|
|
527
|
+
const themes =
|
|
528
|
+
[
|
|
529
|
+
'littlejs',
|
|
530
|
+
'3024-night',
|
|
531
|
+
'abcdef',
|
|
532
|
+
'ambiance',
|
|
533
|
+
'blackboard',
|
|
534
|
+
'monokai',
|
|
535
|
+
'duotone-light',
|
|
536
|
+
'icecoder',
|
|
537
|
+
'lesser-dark',
|
|
538
|
+
'night',
|
|
539
|
+
'yonce'
|
|
540
|
+
];
|
|
541
|
+
|
|
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
|
+
|
|
547
|
+
for (const theme of themes)
|
|
548
|
+
{
|
|
549
|
+
if (theme != 'littlejs')
|
|
550
|
+
addCodeMirrorElement(`theme/${theme}.min.css`, 'link', 'stylesheet');
|
|
551
|
+
const o = new Option(theme);
|
|
552
|
+
o.selected = theme == savedTheme;
|
|
553
|
+
selectTheme.add(o);
|
|
554
|
+
}
|
|
555
|
+
|
|
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()
|
|
308
566
|
{
|
|
309
|
-
|
|
310
|
-
|
|
567
|
+
if (codeMirror)
|
|
568
|
+
return; // prevent duplicate initialization
|
|
569
|
+
|
|
570
|
+
const textareaCode = document.getElementById('textareaCode');
|
|
571
|
+
codeMirror = CodeMirror.fromTextArea(textareaCode,
|
|
572
|
+
{
|
|
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();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function addCodeMirrorElement(filename, type, rel)
|
|
585
|
+
{
|
|
586
|
+
// add element for code mirror
|
|
587
|
+
const e = document.createElement(type);
|
|
588
|
+
e.rel = rel;
|
|
589
|
+
filename = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/6.65.7/' + filename;
|
|
590
|
+
if (type == 'link')
|
|
591
|
+
e.href = filename;
|
|
592
|
+
else
|
|
593
|
+
e.src = filename;
|
|
594
|
+
e.crossOrigin = 'anonymous';
|
|
595
|
+
document.head.appendChild(e);
|
|
596
|
+
return e;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function loadTheme()
|
|
600
|
+
{
|
|
601
|
+
const theme = selectTheme.value;
|
|
602
|
+
const size = selectFontSize.value;
|
|
603
|
+
|
|
604
|
+
// Save preferences to localStorage
|
|
605
|
+
localStorage.setItem(savePrefix+'theme', theme);
|
|
606
|
+
localStorage.setItem(savePrefix+'fontSize', size);
|
|
607
|
+
|
|
608
|
+
// Apply the theme and font size
|
|
609
|
+
textareaCode.style.fontSize = size;
|
|
610
|
+
if (codeMirror)
|
|
611
|
+
{
|
|
612
|
+
codeMirror.getWrapperElement().style.fontSize = size;
|
|
613
|
+
codeMirror.setOption('theme', theme);
|
|
614
|
+
}
|
|
311
615
|
}
|
|
312
616
|
|
|
313
617
|
</script>
|
|
314
618
|
</body>
|
|
315
|
-
</html>
|
|
619
|
+
</html>
|