littlejsengine 1.9.5 → 1.9.6
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/dist/littlejs.d.ts +57 -45
- package/dist/littlejs.esm.js +277 -192
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +275 -192
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +272 -188
- package/examples/breakout/index.html +3 -3
- package/examples/breakoutTutorial/index.html +2 -2
- package/examples/electron/index.html +2 -2
- package/examples/js13k/build.js +1 -0
- package/examples/js13k/index.html +13 -13
- package/examples/module/index.html +1 -1
- package/examples/particles/index.html +1 -1
- package/examples/platformer/index.html +8 -8
- package/examples/puzzle/index.html +2 -2
- package/examples/starter/index.html +13 -13
- package/examples/stress/index.html +1 -1
- package/examples/typescript/index.html +1 -1
- package/package.json +1 -1
- package/src/engine.js +63 -40
- package/src/engineAudio.js +20 -16
- package/src/engineDebug.js +3 -4
- package/src/engineDraw.js +13 -10
- package/src/engineExport.js +2 -0
- package/src/engineInput.js +80 -64
- package/src/engineObject.js +18 -7
- package/src/engineParticles.js +14 -13
- package/src/engineSettings.js +13 -2
- package/src/engineTileLayer.js +15 -4
- package/src/engineUtilities.js +16 -14
- package/src/engineWebGL.js +20 -18
- package/LittleJS.zip +0 -0
package/dist/littlejs.esm.js
CHANGED
|
@@ -153,7 +153,7 @@ function debugClear() { debugPrimitives = []; }
|
|
|
153
153
|
* @param {String} [filename]
|
|
154
154
|
* @param {String} [type]
|
|
155
155
|
* @memberof Debug */
|
|
156
|
-
function debugSaveCanvas(canvas, filename=
|
|
156
|
+
function debugSaveCanvas(canvas, filename='screenshot', type='image/png')
|
|
157
157
|
{ debugSaveDataURL(canvas.toDataURL(type), filename); }
|
|
158
158
|
|
|
159
159
|
/** Save a text file to disk
|
|
@@ -161,7 +161,7 @@ function debugSaveCanvas(canvas, filename=engineName, type='image/png')
|
|
|
161
161
|
* @param {String} [filename]
|
|
162
162
|
* @param {String} [type]
|
|
163
163
|
* @memberof Debug */
|
|
164
|
-
function debugSaveText(text, filename=
|
|
164
|
+
function debugSaveText(text, filename='text', type='text/plain')
|
|
165
165
|
{ debugSaveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename); }
|
|
166
166
|
|
|
167
167
|
/** Save a data url to disk
|
|
@@ -181,8 +181,7 @@ function debugSaveDataURL(dataURL, filename)
|
|
|
181
181
|
function debugInit()
|
|
182
182
|
{
|
|
183
183
|
// create link for saving screenshots
|
|
184
|
-
|
|
185
|
-
downloadLink.style.display = 'none';
|
|
184
|
+
downloadLink = document.createElement('a');
|
|
186
185
|
}
|
|
187
186
|
|
|
188
187
|
function debugUpdate()
|
|
@@ -485,7 +484,7 @@ function clamp(value, min=0, max=1) { return value < min ? min : value > max ? m
|
|
|
485
484
|
* @return {Number}
|
|
486
485
|
* @memberof Utilities */
|
|
487
486
|
function percent(value, valueA, valueB)
|
|
488
|
-
{ return valueB
|
|
487
|
+
{ return (valueB-=valueA) ? clamp((value-valueA)/valueB) : 0; }
|
|
489
488
|
|
|
490
489
|
/** Linearly interpolates between values passed in using percent
|
|
491
490
|
* @param {Number} percent
|
|
@@ -692,7 +691,7 @@ class RandomGenerator
|
|
|
692
691
|
this.seed ^= this.seed << 13;
|
|
693
692
|
this.seed ^= this.seed >>> 17;
|
|
694
693
|
this.seed ^= this.seed << 5;
|
|
695
|
-
return valueB + (valueA - valueB) * abs(this.seed %
|
|
694
|
+
return valueB + (valueA - valueB) * abs(this.seed % 1e8) / 1e8;
|
|
696
695
|
}
|
|
697
696
|
|
|
698
697
|
/** Returns a floored seeded random value the two values passed in
|
|
@@ -703,7 +702,7 @@ class RandomGenerator
|
|
|
703
702
|
|
|
704
703
|
/** Randomly returns either -1 or 1 deterministically
|
|
705
704
|
* @return {Number} */
|
|
706
|
-
sign() { return this.
|
|
705
|
+
sign() { return this.float() > .5 ? 1 : -1; }
|
|
707
706
|
}
|
|
708
707
|
|
|
709
708
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -751,6 +750,7 @@ class Vector2
|
|
|
751
750
|
* @param {Number} [y] - Y axis location */
|
|
752
751
|
constructor(x=0, y=0)
|
|
753
752
|
{
|
|
753
|
+
ASSERT(typeof x === 'number' && typeof y === 'number');
|
|
754
754
|
/** @property {Number} - X axis location */
|
|
755
755
|
this.x = x;
|
|
756
756
|
/** @property {Number} - Y axis location */
|
|
@@ -1077,12 +1077,14 @@ class Color
|
|
|
1077
1077
|
* @return {Color} */
|
|
1078
1078
|
setHSLA(h=0, s=0, l=1, a=1)
|
|
1079
1079
|
{
|
|
1080
|
+
h = mod(h,1);
|
|
1081
|
+
s = clamp(s);
|
|
1082
|
+
l = clamp(l);
|
|
1080
1083
|
const q = l < .5 ? l*(1+s) : l+s-l*s, p = 2*l-q,
|
|
1081
1084
|
f = (p, q, t)=>
|
|
1082
|
-
(t = (
|
|
1083
|
-
t < 1
|
|
1084
|
-
t < 2
|
|
1085
|
-
|
|
1085
|
+
(t = mod(t,1))*6 < 1 ? p+(q-p)*6*t :
|
|
1086
|
+
t*2 < 1 ? q :
|
|
1087
|
+
t*3 < 2 ? p+(q-p)*(4-t*6) : p;
|
|
1086
1088
|
this.r = f(p, q, h + 1/3);
|
|
1087
1089
|
this.g = f(p, q, h);
|
|
1088
1090
|
this.b = f(p, q, h - 1/3);
|
|
@@ -1133,13 +1135,12 @@ class Color
|
|
|
1133
1135
|
).clamp();
|
|
1134
1136
|
}
|
|
1135
1137
|
|
|
1136
|
-
/** Returns this color expressed as a
|
|
1138
|
+
/** Returns this color expressed as a rgb color code
|
|
1137
1139
|
* @param {Boolean} [useAlpha] - if alpha should be included in result
|
|
1138
1140
|
* @return {String} */
|
|
1139
|
-
toString(useAlpha = true)
|
|
1140
|
-
{
|
|
1141
|
-
|
|
1142
|
-
return '#' + toHex(this.r) + toHex(this.g) + toHex(this.b) + (useAlpha ? toHex(this.a) : '');
|
|
1141
|
+
toString(useAlpha = true)
|
|
1142
|
+
{
|
|
1143
|
+
return `rgb(${this.r*255},${this.g*255},${this.b*255},${useAlpha ? this.a : 0})`;
|
|
1143
1144
|
}
|
|
1144
1145
|
|
|
1145
1146
|
/** Set this color from a hex code
|
|
@@ -1197,11 +1198,11 @@ class Timer
|
|
|
1197
1198
|
|
|
1198
1199
|
/** Returns true if set and has not elapsed
|
|
1199
1200
|
* @return {Boolean} */
|
|
1200
|
-
active() { return time
|
|
1201
|
+
active() { return time < this.time; }
|
|
1201
1202
|
|
|
1202
1203
|
/** Returns true if set and elapsed
|
|
1203
1204
|
* @return {Boolean} */
|
|
1204
|
-
elapsed() { return time
|
|
1205
|
+
elapsed() { return time >= this.time; }
|
|
1205
1206
|
|
|
1206
1207
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1207
1208
|
* @return {Number} */
|
|
@@ -1276,6 +1277,12 @@ let fontDefault = 'arial';
|
|
|
1276
1277
|
* @memberof Settings */
|
|
1277
1278
|
let showSplashScreen = false;
|
|
1278
1279
|
|
|
1280
|
+
/** Disables all rendering, audio, and input for servers
|
|
1281
|
+
* @type {Boolean}
|
|
1282
|
+
* @default
|
|
1283
|
+
* @memberof Settings */
|
|
1284
|
+
let headlessMode = false;
|
|
1285
|
+
|
|
1279
1286
|
///////////////////////////////////////////////////////////////////////////////
|
|
1280
1287
|
// WebGL settings
|
|
1281
1288
|
|
|
@@ -1304,7 +1311,7 @@ let tileSizeDefault = vec2(16);
|
|
|
1304
1311
|
* @type {Number}
|
|
1305
1312
|
* @default
|
|
1306
1313
|
* @memberof Settings */
|
|
1307
|
-
let tileFixBleedScale = .
|
|
1314
|
+
let tileFixBleedScale = .5;
|
|
1308
1315
|
|
|
1309
1316
|
///////////////////////////////////////////////////////////////////////////////
|
|
1310
1317
|
// Object settings
|
|
@@ -1429,7 +1436,7 @@ let soundEnable = true;
|
|
|
1429
1436
|
* @type {Number}
|
|
1430
1437
|
* @default
|
|
1431
1438
|
* @memberof Settings */
|
|
1432
|
-
let soundVolume = .
|
|
1439
|
+
let soundVolume = .3;
|
|
1433
1440
|
|
|
1434
1441
|
/** Default range where sound no longer plays
|
|
1435
1442
|
* @type {Number}
|
|
@@ -1514,6 +1521,11 @@ function setFontDefault(font) { fontDefault = font; }
|
|
|
1514
1521
|
* @memberof Settings */
|
|
1515
1522
|
function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
1516
1523
|
|
|
1524
|
+
/** Set to disalbe rendering, audio, and input for servers
|
|
1525
|
+
* @param {Boolean} headless
|
|
1526
|
+
* @memberof Settings */
|
|
1527
|
+
function setHeadlessMode(headless) { headlessMode = headless; }
|
|
1528
|
+
|
|
1517
1529
|
/** Set if webgl rendering is enabled
|
|
1518
1530
|
* @param {Boolean} enable
|
|
1519
1531
|
* @memberof Settings */
|
|
@@ -1783,18 +1795,30 @@ class EngineObject
|
|
|
1783
1795
|
engineObjects.push(this);
|
|
1784
1796
|
}
|
|
1785
1797
|
|
|
1786
|
-
/** Update the object transform
|
|
1787
|
-
|
|
1798
|
+
/** Update the object transform, called automatically by engine even when paused */
|
|
1799
|
+
updateTransforms()
|
|
1788
1800
|
{
|
|
1789
1801
|
const parent = this.parent;
|
|
1790
1802
|
if (parent)
|
|
1791
1803
|
{
|
|
1792
1804
|
// copy parent pos/angle
|
|
1793
|
-
|
|
1794
|
-
this.
|
|
1795
|
-
|
|
1805
|
+
const mirror = parent.getMirrorSign();
|
|
1806
|
+
this.pos = this.localPos.multiply(vec2(mirror,1)).rotate(-parent.angle).add(parent.pos);
|
|
1807
|
+
this.angle = mirror*this.localAngle + parent.angle;
|
|
1796
1808
|
}
|
|
1797
1809
|
|
|
1810
|
+
// update children
|
|
1811
|
+
for (const child of this.children)
|
|
1812
|
+
child.updateTransforms();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/** Update the object physics, called automatically by engine once each frame */
|
|
1816
|
+
update()
|
|
1817
|
+
{
|
|
1818
|
+
// child objects do not have physics
|
|
1819
|
+
if (this.parent)
|
|
1820
|
+
return;
|
|
1821
|
+
|
|
1798
1822
|
// limit max speed to prevent missing collisions
|
|
1799
1823
|
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
1800
1824
|
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
@@ -1809,8 +1833,7 @@ class EngineObject
|
|
|
1809
1833
|
// physics sanity checks
|
|
1810
1834
|
ASSERT(this.angleDamping >= 0 && this.angleDamping <= 1);
|
|
1811
1835
|
ASSERT(this.damping >= 0 && this.damping <= 1);
|
|
1812
|
-
|
|
1813
|
-
if (!enablePhysicsSolver || !this.mass) // do not update collision for fixed objects
|
|
1836
|
+
if (!enablePhysicsSolver || !this.mass) // dont do collision for fixed objects
|
|
1814
1837
|
return;
|
|
1815
1838
|
|
|
1816
1839
|
const wasMovingDown = this.velocity.y < 0;
|
|
@@ -2140,6 +2163,9 @@ let drawCount;
|
|
|
2140
2163
|
*/
|
|
2141
2164
|
function tile(pos=vec2(), size=tileSizeDefault, textureIndex=0)
|
|
2142
2165
|
{
|
|
2166
|
+
if (headlessMode)
|
|
2167
|
+
return new TileInfo;
|
|
2168
|
+
|
|
2143
2169
|
// if size is a number, make it a vector
|
|
2144
2170
|
if (typeof size === 'number')
|
|
2145
2171
|
{
|
|
@@ -2173,9 +2199,9 @@ class TileInfo
|
|
|
2173
2199
|
constructor(pos=vec2(), size=tileSizeDefault, textureIndex=0)
|
|
2174
2200
|
{
|
|
2175
2201
|
/** @property {Vector2} - Top left corner of tile in pixels */
|
|
2176
|
-
this.pos = pos;
|
|
2202
|
+
this.pos = pos.copy();
|
|
2177
2203
|
/** @property {Vector2} - Size of tile in pixels */
|
|
2178
|
-
this.size = size;
|
|
2204
|
+
this.size = size.copy();
|
|
2179
2205
|
/** @property {Number} - Texture index to use */
|
|
2180
2206
|
this.textureIndex = textureIndex;
|
|
2181
2207
|
}
|
|
@@ -2267,7 +2293,7 @@ function getCameraSize() { return mainCanvasSize.scale(1/cameraScale); }
|
|
|
2267
2293
|
* @param {Color} [additiveColor=(0,0,0,0)] - Additive color to be applied
|
|
2268
2294
|
* @param {Boolean} [useWebGL=glEnable] - Use accelerated WebGL rendering
|
|
2269
2295
|
* @param {Boolean} [screenSpace=false] - If true the pos and size are in screen space
|
|
2270
|
-
* @param {CanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
2296
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to
|
|
2271
2297
|
* @memberof Draw */
|
|
2272
2298
|
function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
|
|
2273
2299
|
angle=0, mirror, additiveColor=new Color(0,0,0,0), useWebGL=glEnable, screenSpace, context)
|
|
@@ -2340,7 +2366,7 @@ function drawTile(pos, size=vec2(1), tileInfo, color=new Color,
|
|
|
2340
2366
|
* @param {Number} [angle]
|
|
2341
2367
|
* @param {Boolean} [useWebGL=glEnable]
|
|
2342
2368
|
* @param {Boolean} [screenSpace=false]
|
|
2343
|
-
* @param {CanvasRenderingContext2D} [context]
|
|
2369
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
2344
2370
|
* @memberof Draw */
|
|
2345
2371
|
function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
2346
2372
|
{
|
|
@@ -2354,7 +2380,7 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
2354
2380
|
* @param {Color} [color=(1,1,1,1)]
|
|
2355
2381
|
* @param {Boolean} [useWebGL=glEnable]
|
|
2356
2382
|
* @param {Boolean} [screenSpace=false]
|
|
2357
|
-
* @param {CanvasRenderingContext2D} [context]
|
|
2383
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
2358
2384
|
* @memberof Draw */
|
|
2359
2385
|
function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, context)
|
|
2360
2386
|
{
|
|
@@ -2370,7 +2396,7 @@ function drawLine(posA, posB, thickness=.1, color, useWebGL, screenSpace, contex
|
|
|
2370
2396
|
* @param {Boolean} mirror
|
|
2371
2397
|
* @param {Function} drawFunction
|
|
2372
2398
|
* @param {Boolean} [screenSpace=false]
|
|
2373
|
-
* @param {CanvasRenderingContext2D} [context=mainContext]
|
|
2399
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
|
|
2374
2400
|
* @memberof Draw */
|
|
2375
2401
|
function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, context=mainContext)
|
|
2376
2402
|
{
|
|
@@ -2391,7 +2417,7 @@ function drawCanvas2D(pos, size, angle, mirror, drawFunction, screenSpace, conte
|
|
|
2391
2417
|
/** Enable normal or additive blend mode
|
|
2392
2418
|
* @param {Boolean} [additive]
|
|
2393
2419
|
* @param {Boolean} [useWebGL=glEnable]
|
|
2394
|
-
* @param {CanvasRenderingContext2D} [context=mainContext]
|
|
2420
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=mainContext]
|
|
2395
2421
|
* @memberof Draw */
|
|
2396
2422
|
function setBlendMode(additive, useWebGL=glEnable, context)
|
|
2397
2423
|
{
|
|
@@ -2416,7 +2442,7 @@ function setBlendMode(additive, useWebGL=glEnable, context)
|
|
|
2416
2442
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
2417
2443
|
* @param {CanvasTextAlign} [textAlign='center']
|
|
2418
2444
|
* @param {String} [font=fontDefault]
|
|
2419
|
-
* @param {CanvasRenderingContext2D} [context=overlayContext]
|
|
2445
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
2420
2446
|
* @memberof Draw */
|
|
2421
2447
|
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, context)
|
|
2422
2448
|
{
|
|
@@ -2433,7 +2459,7 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
2433
2459
|
* @param {Color} [lineColor=(0,0,0,1)]
|
|
2434
2460
|
* @param {CanvasTextAlign} [textAlign]
|
|
2435
2461
|
* @param {String} [font=fontDefault]
|
|
2436
|
-
* @param {CanvasRenderingContext2D} [context=overlayContext]
|
|
2462
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
2437
2463
|
* @memberof Draw */
|
|
2438
2464
|
function drawTextScreen(text, pos, size=1, color=new Color, lineWidth=0, lineColor=new Color(0,0,0), textAlign='center', font=fontDefault, context=overlayContext)
|
|
2439
2465
|
{
|
|
@@ -2476,7 +2502,7 @@ class FontImage
|
|
|
2476
2502
|
* @param {HTMLImageElement} [image] - Image for the font, if undefined default font is used
|
|
2477
2503
|
* @param {Vector2} [tileSize=(8,8)] - Size of the font source tiles
|
|
2478
2504
|
* @param {Vector2} [paddingSize=(0,1)] - How much extra space to add between characters
|
|
2479
|
-
* @param {CanvasRenderingContext2D} [context=overlayContext] - context to draw to
|
|
2505
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext] - context to draw to
|
|
2480
2506
|
*/
|
|
2481
2507
|
constructor(image, tileSize=vec2(8), paddingSize=vec2(0,1), context=overlayContext)
|
|
2482
2508
|
{
|
|
@@ -2684,7 +2710,7 @@ function gamepadWasReleased(button, gamepad=0)
|
|
|
2684
2710
|
* @return {Vector2}
|
|
2685
2711
|
* @memberof Input */
|
|
2686
2712
|
function gamepadStick(stick, gamepad=0)
|
|
2687
|
-
{ return
|
|
2713
|
+
{ return gamepadStickData[gamepad] ? gamepadStickData[gamepad][stick] || vec2() : vec2(); }
|
|
2688
2714
|
|
|
2689
2715
|
///////////////////////////////////////////////////////////////////////////////
|
|
2690
2716
|
// Input update called by engine
|
|
@@ -2695,6 +2721,8 @@ let inputData = [[]];
|
|
|
2695
2721
|
|
|
2696
2722
|
function inputUpdate()
|
|
2697
2723
|
{
|
|
2724
|
+
if (headlessMode) return;
|
|
2725
|
+
|
|
2698
2726
|
// clear input when lost focus (prevent stuck keys)
|
|
2699
2727
|
isTouchDevice || document.hasFocus() || clearInput();
|
|
2700
2728
|
|
|
@@ -2707,6 +2735,8 @@ function inputUpdate()
|
|
|
2707
2735
|
|
|
2708
2736
|
function inputUpdatePost()
|
|
2709
2737
|
{
|
|
2738
|
+
if (headlessMode) return;
|
|
2739
|
+
|
|
2710
2740
|
// clear input to prepare for next frame
|
|
2711
2741
|
for (const deviceInputData of inputData)
|
|
2712
2742
|
for (const i in deviceInputData)
|
|
@@ -2715,9 +2745,12 @@ function inputUpdatePost()
|
|
|
2715
2745
|
}
|
|
2716
2746
|
|
|
2717
2747
|
///////////////////////////////////////////////////////////////////////////////
|
|
2718
|
-
//
|
|
2748
|
+
// Input event handlers
|
|
2719
2749
|
|
|
2750
|
+
function inputInit()
|
|
2720
2751
|
{
|
|
2752
|
+
if (headlessMode) return;
|
|
2753
|
+
|
|
2721
2754
|
onkeydown = (e)=>
|
|
2722
2755
|
{
|
|
2723
2756
|
if (debug && e.target != document.body) return;
|
|
@@ -2748,21 +2781,29 @@ function inputUpdatePost()
|
|
|
2748
2781
|
c == 'KeyA' ? 'ArrowLeft' :
|
|
2749
2782
|
c == 'KeyD' ? 'ArrowRight' : c : c;
|
|
2750
2783
|
}
|
|
2784
|
+
|
|
2785
|
+
// mouse event handlers
|
|
2786
|
+
onmousedown = (e)=>
|
|
2787
|
+
{
|
|
2788
|
+
isUsingGamepad = false;
|
|
2789
|
+
inputData[0][e.button] = 3;
|
|
2790
|
+
mousePosScreen = mouseToScreen(e);
|
|
2791
|
+
e.button && e.preventDefault();
|
|
2792
|
+
}
|
|
2793
|
+
onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
|
|
2794
|
+
onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
|
|
2795
|
+
onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
|
|
2796
|
+
oncontextmenu = (e)=> false; // prevent right click menu
|
|
2797
|
+
|
|
2798
|
+
// init touch input
|
|
2799
|
+
if (isTouchDevice)
|
|
2800
|
+
touchInputInit();
|
|
2751
2801
|
}
|
|
2752
2802
|
|
|
2753
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2754
|
-
// Mouse event handlers
|
|
2755
|
-
|
|
2756
|
-
onmousedown = (e)=> {isUsingGamepad = false; inputData[0][e.button] = 3; mousePosScreen = mouseToScreen(e); e.button && e.preventDefault();}
|
|
2757
|
-
onmouseup = (e)=> inputData[0][e.button] = inputData[0][e.button] & 2 | 4;
|
|
2758
|
-
onmousemove = (e)=> mousePosScreen = mouseToScreen(e);
|
|
2759
|
-
onwheel = (e)=> mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY);
|
|
2760
|
-
oncontextmenu = (e)=> false; // prevent right click menu
|
|
2761
|
-
|
|
2762
2803
|
// convert a mouse or touch event position to screen space
|
|
2763
2804
|
function mouseToScreen(mousePos)
|
|
2764
2805
|
{
|
|
2765
|
-
if (!mainCanvas)
|
|
2806
|
+
if (!mainCanvas || headlessMode)
|
|
2766
2807
|
return vec2(); // fix bug that can occur if user clicks before page loads
|
|
2767
2808
|
|
|
2768
2809
|
const rect = mainCanvas.getBoundingClientRect();
|
|
@@ -2774,7 +2815,7 @@ function mouseToScreen(mousePos)
|
|
|
2774
2815
|
// Gamepad input
|
|
2775
2816
|
|
|
2776
2817
|
// gamepad internal variables
|
|
2777
|
-
const
|
|
2818
|
+
const gamepadStickData = [];
|
|
2778
2819
|
|
|
2779
2820
|
// gamepads are updated by engine every frame automatically
|
|
2780
2821
|
function gamepadsUpdate()
|
|
@@ -2791,14 +2832,11 @@ function gamepadsUpdate()
|
|
|
2791
2832
|
// update touch gamepad if enabled
|
|
2792
2833
|
if (touchGamepadEnable && isTouchDevice)
|
|
2793
2834
|
{
|
|
2794
|
-
|
|
2795
|
-
if (!touchGamepadButtons)
|
|
2796
|
-
createTouchGamepad();
|
|
2797
|
-
|
|
2835
|
+
ASSERT(touchGamepadButtons, 'set touchGamepadEnable before calling init!');
|
|
2798
2836
|
if (touchGamepadTimer.isSet())
|
|
2799
2837
|
{
|
|
2800
2838
|
// read virtual analog stick
|
|
2801
|
-
const sticks =
|
|
2839
|
+
const sticks = gamepadStickData[0] || (gamepadStickData[0] = []);
|
|
2802
2840
|
sticks[0] = vec2();
|
|
2803
2841
|
if (touchGamepadAnalog)
|
|
2804
2842
|
sticks[0] = applyDeadZones(touchGamepadStick);
|
|
@@ -2815,7 +2853,8 @@ function gamepadsUpdate()
|
|
|
2815
2853
|
for (let i=10; i--;)
|
|
2816
2854
|
{
|
|
2817
2855
|
const j = i == 3 ? 2 : i == 2 ? 3 : i; // fix button locations
|
|
2818
|
-
|
|
2856
|
+
const wasDown = gamepadIsDown(j,0);
|
|
2857
|
+
data[j] = touchGamepadButtons[i] ? wasDown ? 1 : 3 : wasDown ? 4 : 0;
|
|
2819
2858
|
}
|
|
2820
2859
|
}
|
|
2821
2860
|
}
|
|
@@ -2835,7 +2874,7 @@ function gamepadsUpdate()
|
|
|
2835
2874
|
// get or create gamepad data
|
|
2836
2875
|
const gamepad = gamepads[i];
|
|
2837
2876
|
const data = inputData[i+1] || (inputData[i+1] = []);
|
|
2838
|
-
const sticks =
|
|
2877
|
+
const sticks = gamepadStickData[i] || (gamepadStickData[i] = []);
|
|
2839
2878
|
|
|
2840
2879
|
if (gamepad)
|
|
2841
2880
|
{
|
|
@@ -2874,28 +2913,44 @@ function gamepadsUpdate()
|
|
|
2874
2913
|
* @param {Number|Array} [pattern] - single value in ms or vibration interval array
|
|
2875
2914
|
* @memberof Input */
|
|
2876
2915
|
function vibrate(pattern=100)
|
|
2877
|
-
{ vibrateEnable && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
2916
|
+
{ vibrateEnable && !headlessMode && navigator && navigator.vibrate && navigator.vibrate(pattern); }
|
|
2878
2917
|
|
|
2879
2918
|
/** Cancel any ongoing vibration
|
|
2880
2919
|
* @memberof Input */
|
|
2881
2920
|
function vibrateStop() { vibrate(0); }
|
|
2882
2921
|
|
|
2883
2922
|
///////////////////////////////////////////////////////////////////////////////
|
|
2884
|
-
// Touch input
|
|
2923
|
+
// Touch input & virtual on screen gamepad
|
|
2885
2924
|
|
|
2886
2925
|
/** True if a touch device has been detected
|
|
2887
2926
|
* @memberof Input */
|
|
2888
|
-
const isTouchDevice = window.ontouchstart !== undefined;
|
|
2927
|
+
const isTouchDevice = !headlessMode && window.ontouchstart !== undefined;
|
|
2928
|
+
|
|
2929
|
+
// touch gamepad internal variables
|
|
2930
|
+
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
|
|
2889
2931
|
|
|
2890
2932
|
// try to enable touch mouse
|
|
2891
|
-
|
|
2933
|
+
function touchInputInit()
|
|
2892
2934
|
{
|
|
2935
|
+
// add non passive touch event listeners
|
|
2936
|
+
let handleTouch = handleTouchDefault;
|
|
2937
|
+
if (touchGamepadEnable)
|
|
2938
|
+
{
|
|
2939
|
+
// touch input internal variables
|
|
2940
|
+
handleTouch = handleTouchGamepad;
|
|
2941
|
+
touchGamepadButtons = [];
|
|
2942
|
+
touchGamepadStick = vec2();
|
|
2943
|
+
}
|
|
2944
|
+
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
2945
|
+
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
2946
|
+
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
2947
|
+
|
|
2893
2948
|
// override mouse events
|
|
2894
|
-
let wasTouching;
|
|
2895
2949
|
onmousedown = onmouseup = ()=> 0;
|
|
2896
2950
|
|
|
2897
2951
|
// handle all touch events the same way
|
|
2898
|
-
|
|
2952
|
+
let wasTouching;
|
|
2953
|
+
function handleTouchDefault(e)
|
|
2899
2954
|
{
|
|
2900
2955
|
// fix stalled audio requiring user interaction
|
|
2901
2956
|
if (soundEnable && audioContext && audioContext.state != 'running')
|
|
@@ -2924,27 +2979,14 @@ if (isTouchDevice)
|
|
|
2924
2979
|
// must return true so the document will get focus
|
|
2925
2980
|
return true;
|
|
2926
2981
|
}
|
|
2927
|
-
}
|
|
2928
2982
|
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
// touch input internal variables
|
|
2933
|
-
let touchGamepadTimer = new Timer, touchGamepadButtons, touchGamepadStick;
|
|
2934
|
-
|
|
2935
|
-
// create the touch gamepad, called automatically by the engine
|
|
2936
|
-
function createTouchGamepad()
|
|
2937
|
-
{
|
|
2938
|
-
// touch input internal variables
|
|
2939
|
-
touchGamepadButtons = [];
|
|
2940
|
-
touchGamepadStick = vec2();
|
|
2941
|
-
|
|
2942
|
-
const touchHandler = ontouchstart;
|
|
2943
|
-
ontouchstart = ontouchmove = ontouchend = (e)=>
|
|
2983
|
+
// special handling for virtual gamepad mode
|
|
2984
|
+
function handleTouchGamepad(e)
|
|
2944
2985
|
{
|
|
2945
2986
|
// clear touch gamepad input
|
|
2946
2987
|
touchGamepadStick = vec2();
|
|
2947
2988
|
touchGamepadButtons = [];
|
|
2989
|
+
isUsingGamepad = true;
|
|
2948
2990
|
|
|
2949
2991
|
const touching = e.touches.length;
|
|
2950
2992
|
if (touching)
|
|
@@ -2985,9 +3027,8 @@ function createTouchGamepad()
|
|
|
2985
3027
|
}
|
|
2986
3028
|
}
|
|
2987
3029
|
|
|
2988
|
-
// call default touch handler
|
|
2989
|
-
|
|
2990
|
-
isUsingGamepad = true;
|
|
3030
|
+
// call default touch handler so normal touch events still work
|
|
3031
|
+
handleTouchDefault(e);
|
|
2991
3032
|
|
|
2992
3033
|
// must return true so the document will get focus
|
|
2993
3034
|
return true;
|
|
@@ -3006,32 +3047,33 @@ function touchGamepadRender()
|
|
|
3006
3047
|
return;
|
|
3007
3048
|
|
|
3008
3049
|
// setup the canvas
|
|
3009
|
-
overlayContext
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3050
|
+
const context = overlayContext;
|
|
3051
|
+
context.save();
|
|
3052
|
+
context.globalAlpha = alpha*touchGamepadAlpha;
|
|
3053
|
+
context.strokeStyle = '#fff';
|
|
3054
|
+
context.lineWidth = 3;
|
|
3013
3055
|
|
|
3014
3056
|
// draw left analog stick
|
|
3015
|
-
|
|
3016
|
-
|
|
3057
|
+
context.fillStyle = touchGamepadStick.lengthSquared() > 0 ? '#fff' : '#000';
|
|
3058
|
+
context.beginPath();
|
|
3017
3059
|
|
|
3018
3060
|
const leftCenter = vec2(touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
3019
3061
|
if (touchGamepadAnalog) // draw circle shaped gamepad
|
|
3020
3062
|
{
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3063
|
+
context.arc(leftCenter.x, leftCenter.y, touchGamepadSize/2, 0, 9);
|
|
3064
|
+
context.fill();
|
|
3065
|
+
context.stroke();
|
|
3024
3066
|
}
|
|
3025
3067
|
else // draw cross shaped gamepad
|
|
3026
3068
|
{
|
|
3027
3069
|
for(let i=10; i--;)
|
|
3028
3070
|
{
|
|
3029
3071
|
const angle = i*PI/4;
|
|
3030
|
-
|
|
3031
|
-
i%2 &&
|
|
3032
|
-
i==1 &&
|
|
3072
|
+
context.arc(leftCenter.x, leftCenter.y,touchGamepadSize*.6, angle + PI/8, angle + PI/8);
|
|
3073
|
+
i%2 && context.arc(leftCenter.x, leftCenter.y, touchGamepadSize*.33, angle, angle);
|
|
3074
|
+
i==1 && context.fill();
|
|
3033
3075
|
}
|
|
3034
|
-
|
|
3076
|
+
context.stroke();
|
|
3035
3077
|
}
|
|
3036
3078
|
|
|
3037
3079
|
// draw right face buttons
|
|
@@ -3039,15 +3081,15 @@ function touchGamepadRender()
|
|
|
3039
3081
|
for (let i=4; i--;)
|
|
3040
3082
|
{
|
|
3041
3083
|
const pos = rightCenter.add(vec2().setDirection(i, touchGamepadSize/2));
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3084
|
+
context.fillStyle = touchGamepadButtons[i] ? '#fff' : '#000';
|
|
3085
|
+
context.beginPath();
|
|
3086
|
+
context.arc(pos.x, pos.y, touchGamepadSize/4, 0,9);
|
|
3087
|
+
context.fill();
|
|
3088
|
+
context.stroke();
|
|
3047
3089
|
}
|
|
3048
3090
|
|
|
3049
3091
|
// set canvas back to normal
|
|
3050
|
-
|
|
3092
|
+
context.restore();
|
|
3051
3093
|
}
|
|
3052
3094
|
/**
|
|
3053
3095
|
* LittleJS Audio System
|
|
@@ -3082,7 +3124,7 @@ class Sound
|
|
|
3082
3124
|
*/
|
|
3083
3125
|
constructor(zzfxSound, range=soundDefaultRange, taper=soundDefaultTaper)
|
|
3084
3126
|
{
|
|
3085
|
-
if (!soundEnable) return;
|
|
3127
|
+
if (!soundEnable || headlessMode) return;
|
|
3086
3128
|
|
|
3087
3129
|
/** @property {Number} - World space max range of sound, will not play if camera is farther away */
|
|
3088
3130
|
this.range = range;
|
|
@@ -3096,8 +3138,8 @@ class Sound
|
|
|
3096
3138
|
if (zzfxSound)
|
|
3097
3139
|
{
|
|
3098
3140
|
// generate zzfx sound now for fast playback
|
|
3099
|
-
|
|
3100
|
-
zzfxSound[1]
|
|
3141
|
+
const defaultRandomness = .05;
|
|
3142
|
+
this.randomness = zzfxSound[1] || defaultRandomness;
|
|
3101
3143
|
this.sampleChannels = [zzfxG(...zzfxSound)];
|
|
3102
3144
|
this.sampleRate = zzfxR;
|
|
3103
3145
|
}
|
|
@@ -3113,7 +3155,7 @@ class Sound
|
|
|
3113
3155
|
*/
|
|
3114
3156
|
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false)
|
|
3115
3157
|
{
|
|
3116
|
-
if (!soundEnable || !this.sampleChannels) return;
|
|
3158
|
+
if (!soundEnable || !this.sampleChannels || headlessMode) return;
|
|
3117
3159
|
|
|
3118
3160
|
let pan;
|
|
3119
3161
|
if (pos)
|
|
@@ -3196,7 +3238,9 @@ class SoundWave extends Sound
|
|
|
3196
3238
|
super(undefined, range, taper);
|
|
3197
3239
|
this.randomness = randomness;
|
|
3198
3240
|
|
|
3199
|
-
if (!soundEnable) return;
|
|
3241
|
+
if (!soundEnable || headlessMode) return;
|
|
3242
|
+
if (!audioContext)
|
|
3243
|
+
audioContext = new AudioContext; // create audio context
|
|
3200
3244
|
|
|
3201
3245
|
fetch(filename)
|
|
3202
3246
|
.then(response => response.arrayBuffer())
|
|
@@ -3250,7 +3294,7 @@ class Music extends Sound
|
|
|
3250
3294
|
{
|
|
3251
3295
|
super(undefined);
|
|
3252
3296
|
|
|
3253
|
-
if (!soundEnable) return;
|
|
3297
|
+
if (!soundEnable || headlessMode) return;
|
|
3254
3298
|
this.randomness = 0;
|
|
3255
3299
|
this.sampleChannels = zzfxM(...zzfxMusic);
|
|
3256
3300
|
this.sampleRate = zzfxR;
|
|
@@ -3273,7 +3317,7 @@ class Music extends Sound
|
|
|
3273
3317
|
* @memberof Audio */
|
|
3274
3318
|
function playAudioFile(filename, volume=1, loop=false)
|
|
3275
3319
|
{
|
|
3276
|
-
if (!soundEnable) return;
|
|
3320
|
+
if (!soundEnable || headlessMode) return;
|
|
3277
3321
|
|
|
3278
3322
|
const audio = new Audio(filename);
|
|
3279
3323
|
audio.volume = soundVolume * volume;
|
|
@@ -3292,7 +3336,7 @@ function playAudioFile(filename, volume=1, loop=false)
|
|
|
3292
3336
|
* @memberof Audio */
|
|
3293
3337
|
function speak(text, language='', volume=1, rate=1, pitch=1)
|
|
3294
3338
|
{
|
|
3295
|
-
if (!soundEnable || !speechSynthesis) return;
|
|
3339
|
+
if (!soundEnable || !speechSynthesis || headlessMode) return;
|
|
3296
3340
|
|
|
3297
3341
|
// common languages (not supported by all browsers)
|
|
3298
3342
|
// en - english, it - italian, fr - french, de - german, es - spanish
|
|
@@ -3325,7 +3369,7 @@ function getNoteFrequency(semitoneOffset, rootFrequency=220)
|
|
|
3325
3369
|
/** Audio context used by the engine
|
|
3326
3370
|
* @type {AudioContext}
|
|
3327
3371
|
* @memberof Audio */
|
|
3328
|
-
let audioContext
|
|
3372
|
+
let audioContext;
|
|
3329
3373
|
|
|
3330
3374
|
/** Keep track if audio was suspended when last sound was played
|
|
3331
3375
|
* @type {Boolean}
|
|
@@ -3343,7 +3387,9 @@ let audioSuspended = false;
|
|
|
3343
3387
|
* @memberof Audio */
|
|
3344
3388
|
function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sampleRate=zzfxR)
|
|
3345
3389
|
{
|
|
3346
|
-
if (!soundEnable) return;
|
|
3390
|
+
if (!soundEnable || headlessMode) return;
|
|
3391
|
+
if (!audioContext)
|
|
3392
|
+
audioContext = new AudioContext; // create audio context
|
|
3347
3393
|
|
|
3348
3394
|
// prevent sounds from building up if they can't be played
|
|
3349
3395
|
const audioWasSuspended = audioSuspended;
|
|
@@ -3389,7 +3435,7 @@ function playSamples(sampleChannels, volume=1, rate=1, pan=0, loop=false, sample
|
|
|
3389
3435
|
* @param {Array} zzfxSound - Array of ZzFX parameters, ex. [.5,.5]
|
|
3390
3436
|
* @return {AudioBufferSourceNode} - The audio node of the sound played
|
|
3391
3437
|
* @memberof Audio */
|
|
3392
|
-
function zzfx(...zzfxSound) { return
|
|
3438
|
+
function zzfx(...zzfxSound) { return new Sound(zzfxSound).play(); }
|
|
3393
3439
|
|
|
3394
3440
|
/** Sample rate used for all ZzFX sounds
|
|
3395
3441
|
* @default 44100
|
|
@@ -3398,7 +3444,7 @@ const zzfxR = 44100;
|
|
|
3398
3444
|
|
|
3399
3445
|
/** Generate samples for a ZzFX sound
|
|
3400
3446
|
* @param {Number} [volume] - Volume scale (percent)
|
|
3401
|
-
* @param {Number} [randomness] -
|
|
3447
|
+
* @param {Number} [randomness] - Unused in this fuction, handled by Sound class
|
|
3402
3448
|
* @param {Number} [frequency] - Frequency of sound (Hz)
|
|
3403
3449
|
* @param {Number} [attack] - Attack time, how fast sound starts (seconds)
|
|
3404
3450
|
* @param {Number} [sustain] - Sustain time, how long sound holds (seconds)
|
|
@@ -3424,17 +3470,18 @@ const zzfxR = 44100;
|
|
|
3424
3470
|
function zzfxG
|
|
3425
3471
|
(
|
|
3426
3472
|
// parameters
|
|
3427
|
-
volume = 1, randomness =
|
|
3473
|
+
volume = 1, randomness = 0, frequency = 220, attack = 0, sustain = 0,
|
|
3428
3474
|
release = .1, shape = 0, shapeCurve = 1, slide = 0, deltaSlide = 0,
|
|
3429
3475
|
pitchJump = 0, pitchJumpTime = 0, repeatTime = 0, noise = 0, modulation = 0,
|
|
3430
3476
|
bitCrush = 0, delay = 0, sustainVolume = 1, decay = 0, tremolo = 0, filter = 0
|
|
3431
3477
|
)
|
|
3432
3478
|
{
|
|
3479
|
+
// LJS Note: ZZFX modded so randomness is handled by Sound class
|
|
3480
|
+
|
|
3433
3481
|
// init parameters
|
|
3434
3482
|
let PI2 = PI*2, sampleRate = zzfxR,
|
|
3435
3483
|
startSlide = slide *= 500 * PI2 / sampleRate / sampleRate,
|
|
3436
|
-
startFrequency = frequency *=
|
|
3437
|
-
rand(1 + randomness, 1-randomness) * PI2 / sampleRate,
|
|
3484
|
+
startFrequency = frequency *= PI2 / sampleRate,
|
|
3438
3485
|
b = [], t = 0, tm = 0, i = 0, j = 1, r = 0, c = 0, s = 0, f, length,
|
|
3439
3486
|
|
|
3440
3487
|
// biquad LP/HP filter
|
|
@@ -3456,7 +3503,6 @@ function zzfxG
|
|
|
3456
3503
|
pitchJump *= PI2 / sampleRate;
|
|
3457
3504
|
pitchJumpTime *= sampleRate;
|
|
3458
3505
|
repeatTime = repeatTime * sampleRate | 0;
|
|
3459
|
-
volume *= soundVolume;
|
|
3460
3506
|
|
|
3461
3507
|
// generate waveform
|
|
3462
3508
|
for(length = attack + decay + sustain + release + delay | 0;
|
|
@@ -3802,7 +3848,7 @@ class TileLayer extends EngineObject
|
|
|
3802
3848
|
|
|
3803
3849
|
/** @property {HTMLCanvasElement} - The canvas used by this tile layer */
|
|
3804
3850
|
this.canvas = document.createElement('canvas');
|
|
3805
|
-
/** @property {CanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
|
|
3851
|
+
/** @property {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this tile layer */
|
|
3806
3852
|
this.context = this.canvas.getContext('2d');
|
|
3807
3853
|
/** @property {Vector2} - How much to scale this layer when rendered */
|
|
3808
3854
|
this.scale = scale;
|
|
@@ -3813,6 +3859,17 @@ class TileLayer extends EngineObject
|
|
|
3813
3859
|
this.data = [];
|
|
3814
3860
|
for (let j = this.size.area(); j--;)
|
|
3815
3861
|
this.data.push(new TileLayerData);
|
|
3862
|
+
|
|
3863
|
+
if (headlessMode)
|
|
3864
|
+
{
|
|
3865
|
+
// disable rendering
|
|
3866
|
+
this.redraw = () => {};
|
|
3867
|
+
this.render = () => {};
|
|
3868
|
+
this.redrawStart = () => {};
|
|
3869
|
+
this.redrawEnd = () => {};
|
|
3870
|
+
this.drawTileData = () => {};
|
|
3871
|
+
this.drawCanvas2D = () => {};
|
|
3872
|
+
}
|
|
3816
3873
|
}
|
|
3817
3874
|
|
|
3818
3875
|
/** Set data at a given position in the array
|
|
@@ -3843,7 +3900,7 @@ class TileLayer extends EngineObject
|
|
|
3843
3900
|
ASSERT(mainContext != this.context, 'must call redrawEnd() after drawing tiles');
|
|
3844
3901
|
|
|
3845
3902
|
// flush and copy gl canvas because tile canvas does not use webgl
|
|
3846
|
-
|
|
3903
|
+
!glOverlay && !this.isOverlay && glCopyToContext(mainContext);
|
|
3847
3904
|
|
|
3848
3905
|
// draw the entire cached level onto the canvas
|
|
3849
3906
|
const pos = worldToScreen(this.pos.add(vec2(0,this.size.y*this.scale.y)));
|
|
@@ -3893,14 +3950,14 @@ class TileLayer extends EngineObject
|
|
|
3893
3950
|
this.context.imageSmoothingEnabled = !canvasPixelated;
|
|
3894
3951
|
|
|
3895
3952
|
// setup gl rendering if enabled
|
|
3896
|
-
|
|
3953
|
+
glPreRender();
|
|
3897
3954
|
}
|
|
3898
3955
|
|
|
3899
3956
|
/** Call to end the redraw process */
|
|
3900
3957
|
redrawEnd()
|
|
3901
3958
|
{
|
|
3902
3959
|
ASSERT(mainContext == this.context, 'must call redrawStart() before drawing tiles');
|
|
3903
|
-
|
|
3960
|
+
glCopyToContext(mainContext, true);
|
|
3904
3961
|
//debugSaveCanvas(this.canvas);
|
|
3905
3962
|
|
|
3906
3963
|
// set stuff back to normal
|
|
@@ -4225,20 +4282,21 @@ class ParticleEmitter extends EngineObject
|
|
|
4225
4282
|
class Particle extends EngineObject
|
|
4226
4283
|
{
|
|
4227
4284
|
/**
|
|
4228
|
-
* Create a particle with the
|
|
4229
|
-
*
|
|
4230
|
-
* @param {
|
|
4231
|
-
* @param {
|
|
4232
|
-
* @param {
|
|
4233
|
-
* @param {Color}
|
|
4234
|
-
* @param {
|
|
4235
|
-
* @param {Number}
|
|
4236
|
-
* @param {Number}
|
|
4237
|
-
* @param {Number}
|
|
4238
|
-
* @param {
|
|
4239
|
-
* @param {
|
|
4285
|
+
* Create a particle with the passed in settings
|
|
4286
|
+
* Typically this is created automatically by a ParticleEmitter
|
|
4287
|
+
* @param {Vector2} position - World space position of the particle
|
|
4288
|
+
* @param {TileInfo} tileInfo - Tile info to render particles
|
|
4289
|
+
* @param {Number} angle - Angle to rotate the particle
|
|
4290
|
+
* @param {Color} colorStart - Color at start of life
|
|
4291
|
+
* @param {Color} colorEnd - Color at end of life
|
|
4292
|
+
* @param {Number} lifeTime - How long to live for
|
|
4293
|
+
* @param {Number} sizeStart - Size at start of life
|
|
4294
|
+
* @param {Number} sizeEnd - Size at end of life
|
|
4295
|
+
* @param {Number} fadeRate - How quick to fade in/out
|
|
4296
|
+
* @param {Boolean} additive - Does it use additive blend mode
|
|
4297
|
+
* @param {Number} trailScale - If a trail, how long to make it
|
|
4240
4298
|
* @param {ParticleEmitter} [localSpaceEmitter] - Parent emitter if local space
|
|
4241
|
-
* @param {Function}
|
|
4299
|
+
* @param {Function} [destroyCallback] - Callback when particle dies
|
|
4242
4300
|
*/
|
|
4243
4301
|
constructor(position, tileInfo, angle, colorStart, colorEnd, lifeTime, sizeStart, sizeEnd, fadeRate, additive, trailScale, localSpaceEmitter, destroyCallback
|
|
4244
4302
|
)
|
|
@@ -4647,6 +4705,8 @@ let glShader, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData,
|
|
|
4647
4705
|
// Initalize WebGL, called automatically by the engine
|
|
4648
4706
|
function glInit()
|
|
4649
4707
|
{
|
|
4708
|
+
if (!glEnable || headlessMode) return;
|
|
4709
|
+
|
|
4650
4710
|
// create the canvas and textures
|
|
4651
4711
|
glCanvas = document.createElement('canvas');
|
|
4652
4712
|
glContext = glCanvas.getContext('webgl2');
|
|
@@ -4659,11 +4719,11 @@ function glInit()
|
|
|
4659
4719
|
'#version 300 es\n' + // specify GLSL ES version
|
|
4660
4720
|
'precision highp float;'+ // use highp for better accuracy
|
|
4661
4721
|
'uniform mat4 m;'+ // transform matrix
|
|
4662
|
-
'in vec2 g;'+ // geometry
|
|
4663
|
-
'in vec4 p,u,c,a;'+ // position/size, uvs, color, additiveColor
|
|
4664
|
-
'in float r;'+ // rotation
|
|
4665
|
-
'out vec2 v;'+ //
|
|
4666
|
-
'out vec4 d,e;'+ //
|
|
4722
|
+
'in vec2 g;'+ // in: geometry
|
|
4723
|
+
'in vec4 p,u,c,a;'+ // in: position/size, uvs, color, additiveColor
|
|
4724
|
+
'in float r;'+ // in: rotation
|
|
4725
|
+
'out vec2 v;'+ // out: uv
|
|
4726
|
+
'out vec4 d,e;'+ // out: color, additiveColor
|
|
4667
4727
|
'void main(){'+ // shader entry point
|
|
4668
4728
|
'vec2 s=(g-.5)*p.zw;'+ // get size offset
|
|
4669
4729
|
'gl_Position=m*vec4(p.xy+s*cos(r)-vec2(-s.y,s)*sin(r),1,1);'+ // transform position
|
|
@@ -4673,10 +4733,10 @@ function glInit()
|
|
|
4673
4733
|
,
|
|
4674
4734
|
'#version 300 es\n' + // specify GLSL ES version
|
|
4675
4735
|
'precision highp float;'+ // use highp for better accuracy
|
|
4676
|
-
'in vec2 v;'+ // uv
|
|
4677
|
-
'in vec4 d,e;'+ // color, additiveColor
|
|
4678
4736
|
'uniform sampler2D s;'+ // texture
|
|
4679
|
-
'
|
|
4737
|
+
'in vec2 v;'+ // in: uv
|
|
4738
|
+
'in vec4 d,e;'+ // in: color, additiveColor
|
|
4739
|
+
'out vec4 c;'+ // out: color
|
|
4680
4740
|
'void main(){'+ // shader entry point
|
|
4681
4741
|
'c=texture(s,v)*d+e;'+ // modulate texture by color plus additive
|
|
4682
4742
|
'}' // end of shader
|
|
@@ -4698,9 +4758,11 @@ function glInit()
|
|
|
4698
4758
|
// Setup render each frame, called automatically by engine
|
|
4699
4759
|
function glPreRender()
|
|
4700
4760
|
{
|
|
4761
|
+
if (!glEnable || headlessMode) return;
|
|
4762
|
+
|
|
4701
4763
|
// clear and set to same size as main canvas
|
|
4702
4764
|
glContext.viewport(0, 0, glCanvas.width=mainCanvas.width, glCanvas.height=mainCanvas.height);
|
|
4703
|
-
glContext.clear(gl_COLOR_BUFFER_BIT);
|
|
4765
|
+
//glContext.clear(gl_COLOR_BUFFER_BIT); // auto cleared when size is set
|
|
4704
4766
|
|
|
4705
4767
|
// set up the shader
|
|
4706
4768
|
glContext.useProgram(glShader);
|
|
@@ -4734,12 +4796,12 @@ function glPreRender()
|
|
|
4734
4796
|
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
4735
4797
|
const p = vec2(-1).subtract(cameraPos.multiply(s));
|
|
4736
4798
|
glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), false,
|
|
4737
|
-
|
|
4799
|
+
[
|
|
4738
4800
|
s.x, 0, 0, 0,
|
|
4739
4801
|
0, s.y, 0, 0,
|
|
4740
4802
|
1, 1, 1, 1,
|
|
4741
4803
|
p.x, p.y, 0, 0
|
|
4742
|
-
]
|
|
4804
|
+
]
|
|
4743
4805
|
);
|
|
4744
4806
|
}
|
|
4745
4807
|
|
|
@@ -4750,7 +4812,7 @@ function glPreRender()
|
|
|
4750
4812
|
function glSetTexture(texture)
|
|
4751
4813
|
{
|
|
4752
4814
|
// must flush cache with the old texture to set a new one
|
|
4753
|
-
if (texture == glActiveTexture)
|
|
4815
|
+
if (headlessMode || texture == glActiveTexture)
|
|
4754
4816
|
return;
|
|
4755
4817
|
|
|
4756
4818
|
glFlush();
|
|
@@ -4810,7 +4872,6 @@ function glCreateTexture(image)
|
|
|
4810
4872
|
const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
|
|
4811
4873
|
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
|
|
4812
4874
|
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
|
|
4813
|
-
|
|
4814
4875
|
return texture;
|
|
4815
4876
|
}
|
|
4816
4877
|
|
|
@@ -4834,12 +4895,12 @@ function glFlush()
|
|
|
4834
4895
|
}
|
|
4835
4896
|
|
|
4836
4897
|
/** Draw any sprites still in the buffer, copy to main canvas and clear
|
|
4837
|
-
* @param {CanvasRenderingContext2D} context
|
|
4898
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
4838
4899
|
* @param {Boolean} [forceDraw]
|
|
4839
4900
|
* @memberof WebGL */
|
|
4840
4901
|
function glCopyToContext(context, forceDraw=false)
|
|
4841
4902
|
{
|
|
4842
|
-
if (!glInstanceCount && !forceDraw) return;
|
|
4903
|
+
if (!glEnable || !glInstanceCount && !forceDraw) return;
|
|
4843
4904
|
|
|
4844
4905
|
glFlush();
|
|
4845
4906
|
|
|
@@ -4896,7 +4957,7 @@ let glPostShader, glPostTexture, glPostIncludeOverlay;
|
|
|
4896
4957
|
function glInitPostProcess(shaderCode, includeOverlay=false)
|
|
4897
4958
|
{
|
|
4898
4959
|
ASSERT(!glPostShader, 'can only have 1 post effects shader');
|
|
4899
|
-
|
|
4960
|
+
if (headlessMode) return;
|
|
4900
4961
|
if (!shaderCode) // default shader pass through
|
|
4901
4962
|
shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
|
|
4902
4963
|
|
|
@@ -4935,8 +4996,7 @@ function glInitPostProcess(shaderCode, includeOverlay=false)
|
|
|
4935
4996
|
// Render the post processing shader, called automatically by the engine
|
|
4936
4997
|
function glRenderPostProcess()
|
|
4937
4998
|
{
|
|
4938
|
-
if (!glPostShader)
|
|
4939
|
-
return;
|
|
4999
|
+
if (!glPostShader || headlessMode) return;
|
|
4940
5000
|
|
|
4941
5001
|
// prepare to render post process shader
|
|
4942
5002
|
if (glEnable)
|
|
@@ -5042,7 +5102,7 @@ const engineName = 'LittleJS';
|
|
|
5042
5102
|
* @type {String}
|
|
5043
5103
|
* @default
|
|
5044
5104
|
* @memberof Engine */
|
|
5045
|
-
const engineVersion = '1.9.
|
|
5105
|
+
const engineVersion = '1.9.6';
|
|
5046
5106
|
|
|
5047
5107
|
/** Frames per second to update
|
|
5048
5108
|
* @type {Number}
|
|
@@ -5119,7 +5179,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5119
5179
|
mainContext.imageSmoothingEnabled = !canvasPixelated;
|
|
5120
5180
|
|
|
5121
5181
|
// setup gl rendering if enabled
|
|
5122
|
-
|
|
5182
|
+
glPreRender();
|
|
5123
5183
|
}
|
|
5124
5184
|
|
|
5125
5185
|
// internal update loop for engine
|
|
@@ -5138,11 +5198,14 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5138
5198
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
5139
5199
|
if (!debugSpeedUp)
|
|
5140
5200
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp in case of slow framerate
|
|
5201
|
+
|
|
5141
5202
|
updateCanvas();
|
|
5142
5203
|
|
|
5143
5204
|
if (paused)
|
|
5144
5205
|
{
|
|
5145
|
-
//
|
|
5206
|
+
// update object transforms even when paused
|
|
5207
|
+
for (const o of engineObjects)
|
|
5208
|
+
o.parent || o.updateTransforms();
|
|
5146
5209
|
inputUpdate();
|
|
5147
5210
|
debugUpdate();
|
|
5148
5211
|
gameUpdatePost();
|
|
@@ -5154,7 +5217,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5154
5217
|
let deltaSmooth = 0;
|
|
5155
5218
|
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
5156
5219
|
{
|
|
5157
|
-
// force
|
|
5220
|
+
// force at least one update each frame since it is waiting for refresh
|
|
5158
5221
|
deltaSmooth = frameTimeBufferMS;
|
|
5159
5222
|
frameTimeBufferMS = 0;
|
|
5160
5223
|
}
|
|
@@ -5179,34 +5242,37 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5179
5242
|
// add the time smoothing back in
|
|
5180
5243
|
frameTimeBufferMS += deltaSmooth;
|
|
5181
5244
|
}
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
enginePreRender();
|
|
5185
|
-
gameRender();
|
|
5186
|
-
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
5187
|
-
for (const o of engineObjects)
|
|
5188
|
-
o.destroyed || o.render();
|
|
5189
|
-
gameRenderPost();
|
|
5190
|
-
glRenderPostProcess();
|
|
5191
|
-
medalsRender();
|
|
5192
|
-
touchGamepadRender();
|
|
5193
|
-
debugRender();
|
|
5194
|
-
glEnable && glCopyToContext(mainContext);
|
|
5195
|
-
|
|
5196
|
-
if (showWatermark)
|
|
5245
|
+
|
|
5246
|
+
if (!headlessMode)
|
|
5197
5247
|
{
|
|
5198
|
-
//
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5248
|
+
// render sort then render while removing destroyed objects
|
|
5249
|
+
enginePreRender();
|
|
5250
|
+
gameRender();
|
|
5251
|
+
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
5252
|
+
for (const o of engineObjects)
|
|
5253
|
+
o.destroyed || o.render();
|
|
5254
|
+
gameRenderPost();
|
|
5255
|
+
glRenderPostProcess();
|
|
5256
|
+
medalsRender();
|
|
5257
|
+
touchGamepadRender();
|
|
5258
|
+
debugRender();
|
|
5259
|
+
glCopyToContext(mainContext);
|
|
5260
|
+
|
|
5261
|
+
if (showWatermark)
|
|
5262
|
+
{
|
|
5263
|
+
// update fps
|
|
5264
|
+
overlayContext.textAlign = 'right';
|
|
5265
|
+
overlayContext.textBaseline = 'top';
|
|
5266
|
+
overlayContext.font = '1em monospace';
|
|
5267
|
+
overlayContext.fillStyle = '#000';
|
|
5268
|
+
const text = engineName + ' ' + 'v' + engineVersion + ' / '
|
|
5269
|
+
+ drawCount + ' / ' + engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
5270
|
+
+ (glEnable ? ' GL' : ' 2D') ;
|
|
5271
|
+
overlayContext.fillText(text, mainCanvas.width-3, 3);
|
|
5272
|
+
overlayContext.fillStyle = '#fff';
|
|
5273
|
+
overlayContext.fillText(text, mainCanvas.width-2, 2);
|
|
5274
|
+
drawCount = 0;
|
|
5275
|
+
}
|
|
5210
5276
|
}
|
|
5211
5277
|
|
|
5212
5278
|
requestAnimationFrame(engineUpdate);
|
|
@@ -5214,6 +5280,8 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5214
5280
|
|
|
5215
5281
|
function updateCanvas()
|
|
5216
5282
|
{
|
|
5283
|
+
if (headlessMode) return;
|
|
5284
|
+
|
|
5217
5285
|
if (canvasFixedSize.x)
|
|
5218
5286
|
{
|
|
5219
5287
|
// clear canvas and set fixed size
|
|
@@ -5241,8 +5309,20 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5241
5309
|
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
5242
5310
|
}
|
|
5243
5311
|
|
|
5312
|
+
function startEngine()
|
|
5313
|
+
{
|
|
5314
|
+
gameInit();
|
|
5315
|
+
engineUpdate();
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
if (headlessMode)
|
|
5319
|
+
{
|
|
5320
|
+
startEngine();
|
|
5321
|
+
return;
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5244
5324
|
// setup html
|
|
5245
|
-
|
|
5325
|
+
const styleBody =
|
|
5246
5326
|
'margin:0;overflow:hidden;' + // fill the window
|
|
5247
5327
|
'background:#000;' + // set background color
|
|
5248
5328
|
'touch-action:none;' + // prevent mobile pinch to resize
|
|
@@ -5254,8 +5334,9 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5254
5334
|
mainContext = mainCanvas.getContext('2d');
|
|
5255
5335
|
|
|
5256
5336
|
// init stuff and start engine
|
|
5337
|
+
inputInit();
|
|
5257
5338
|
debugInit();
|
|
5258
|
-
|
|
5339
|
+
glInit();
|
|
5259
5340
|
|
|
5260
5341
|
// create overlay canvas for hud to appear above gl canvas
|
|
5261
5342
|
document.body.appendChild(overlayCanvas = document.createElement('canvas'));
|
|
@@ -5296,12 +5377,7 @@ function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRender
|
|
|
5296
5377
|
}));
|
|
5297
5378
|
|
|
5298
5379
|
// load all of the images
|
|
5299
|
-
Promise.all(promises).then(
|
|
5300
|
-
{
|
|
5301
|
-
// start the engine
|
|
5302
|
-
gameInit();
|
|
5303
|
-
engineUpdate();
|
|
5304
|
-
});
|
|
5380
|
+
Promise.all(promises).then(startEngine);
|
|
5305
5381
|
}
|
|
5306
5382
|
|
|
5307
5383
|
/** Update each engine object, remove destroyed objects, and update time
|
|
@@ -5322,7 +5398,14 @@ function engineObjectsUpdate()
|
|
|
5322
5398
|
}
|
|
5323
5399
|
}
|
|
5324
5400
|
for (const o of engineObjects)
|
|
5325
|
-
|
|
5401
|
+
{
|
|
5402
|
+
// update top level objects
|
|
5403
|
+
if (!o.parent)
|
|
5404
|
+
{
|
|
5405
|
+
updateObject(o);
|
|
5406
|
+
o.updateTransforms();
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5326
5409
|
|
|
5327
5410
|
// remove destroyed objects
|
|
5328
5411
|
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
@@ -5608,6 +5691,7 @@ export {
|
|
|
5608
5691
|
canvasPixelated,
|
|
5609
5692
|
fontDefault,
|
|
5610
5693
|
showSplashScreen,
|
|
5694
|
+
headlessMode,
|
|
5611
5695
|
tileSizeDefault,
|
|
5612
5696
|
tileFixBleedScale,
|
|
5613
5697
|
enablePhysicsSolver,
|
|
@@ -5646,6 +5730,7 @@ export {
|
|
|
5646
5730
|
setCanvasPixelated,
|
|
5647
5731
|
setFontDefault,
|
|
5648
5732
|
setShowSplashScreen,
|
|
5733
|
+
setHeadlessMode,
|
|
5649
5734
|
setGlEnable,
|
|
5650
5735
|
setGlOverlay,
|
|
5651
5736
|
setTileSizeDefault,
|