littlejsengine 1.18.7 → 1.18.12
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 +2 -4
- package/dist/littlejs.d.ts +76 -8
- package/dist/littlejs.esm.js +333 -73
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +327 -72
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +317 -64
- package/package.json +1 -1
- package/plugins/medalSystem.js +2 -1
- package/plugins/newgrounds.js +7 -0
- package/plugins/pathFinder.js +35 -6
- package/plugins/tweenSystem.js +9 -1
- package/plugins/uiSystem.js +36 -14
- package/src/engine.js +3 -2
- package/src/engineAudio.js +7 -9
- package/src/engineDebug.js +10 -8
- package/src/engineDraw.js +94 -14
- package/src/engineExport.js +6 -1
- package/src/engineInput.js +7 -7
- package/src/engineMath.js +5 -3
- package/src/engineUtilities.js +49 -6
- package/src/engineWebGL.js +63 -1
- package/plugins/tween.js +0 -509
package/package.json
CHANGED
package/plugins/medalSystem.js
CHANGED
|
@@ -147,7 +147,8 @@ class Medal
|
|
|
147
147
|
/** @property {boolean} - Is the medal unlocked? */
|
|
148
148
|
this.unlocked = false;
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
/** @property {HTMLImageElement|undefined} - Source image for the medal icon, if any */
|
|
151
|
+
this.image = undefined;
|
|
151
152
|
if (src)
|
|
152
153
|
(this.image = new Image).src = src;
|
|
153
154
|
|
package/plugins/newgrounds.js
CHANGED
|
@@ -63,13 +63,18 @@ class NewgroundsPlugin
|
|
|
63
63
|
ASSERT(!cipher || cryptoJS, 'must provide cryptojs if there is a cipher');
|
|
64
64
|
|
|
65
65
|
newgrounds = this; // set global newgrounds object
|
|
66
|
+
/** @property {string} - The newgrounds App ID */
|
|
66
67
|
this.app_id = app_id;
|
|
68
|
+
/** @property {string|undefined} - AES-128/Base64 encryption key, if any */
|
|
67
69
|
this.cipher = cipher;
|
|
70
|
+
/** @property {Object|undefined} - CryptoJS instance used when cipher is set */
|
|
68
71
|
this.cryptoJS = cryptoJS;
|
|
72
|
+
/** @property {string} - Hostname used when logging views */
|
|
69
73
|
this.host = location ? location.hostname : '';
|
|
70
74
|
|
|
71
75
|
// get session id from url search params
|
|
72
76
|
const url = new URL(location.href);
|
|
77
|
+
/** @property {string|null} - Newgrounds session id from the URL (null when not logged in) */
|
|
73
78
|
this.session_id = url.searchParams.get('ngio_session_id');
|
|
74
79
|
|
|
75
80
|
if (!this.session_id)
|
|
@@ -77,6 +82,7 @@ class NewgroundsPlugin
|
|
|
77
82
|
|
|
78
83
|
// get medals
|
|
79
84
|
const medalsResult = this.call('Medal.getList');
|
|
85
|
+
/** @property {Array} - Medals fetched from Newgrounds (empty until session is active) */
|
|
80
86
|
this.medals = medalsResult ? medalsResult.result.data['medals'] : [];
|
|
81
87
|
debugMedals && LOG(this.medals);
|
|
82
88
|
for (const newgroundsMedal of this.medals)
|
|
@@ -100,6 +106,7 @@ class NewgroundsPlugin
|
|
|
100
106
|
|
|
101
107
|
// get scoreboards
|
|
102
108
|
const scoreboardResult = this.call('ScoreBoard.getBoards');
|
|
109
|
+
/** @property {Array} - Scoreboards fetched from Newgrounds */
|
|
103
110
|
this.scoreboards = scoreboardResult ? scoreboardResult.result.data.scoreboards : [];
|
|
104
111
|
debugMedals && LOG(this.scoreboards);
|
|
105
112
|
|
package/plugins/pathFinder.js
CHANGED
|
@@ -94,7 +94,9 @@ class PathFinder
|
|
|
94
94
|
// .size + .getCollisionData.
|
|
95
95
|
if (isVector2(source))
|
|
96
96
|
{
|
|
97
|
+
/** @property {Vector2} - Grid dimensions in tiles */
|
|
97
98
|
this.size = source.floor();
|
|
99
|
+
/** @property {TileCollisionLayer|undefined} - Tile layer driving walkability, if any */
|
|
98
100
|
this.tileLayer = undefined;
|
|
99
101
|
}
|
|
100
102
|
else
|
|
@@ -106,13 +108,18 @@ class PathFinder
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// Tunables (public, freely re-assignable).
|
|
111
|
+
/** @property {number} - A* heuristic multiplier (1 = admissible, higher = greedier) */
|
|
109
112
|
this.heuristicWeight = 1;
|
|
110
|
-
|
|
113
|
+
/** @property {number} - Maximum A* expansions before giving up */
|
|
114
|
+
this.maxLoop = 1e3;
|
|
115
|
+
/** @property {boolean} - If true, post-process paths with two-pass smoothing */
|
|
111
116
|
this.smoothPath = true;
|
|
117
|
+
/** @property {boolean} - If true, draw debug visualization during findPath */
|
|
112
118
|
this.debug = false;
|
|
113
|
-
|
|
119
|
+
/** @property {number} - Debug primitive lifetime in seconds (0 disables drawing) */
|
|
120
|
+
this.debugTime = 1;
|
|
114
121
|
|
|
115
|
-
|
|
122
|
+
/** @property {Array<PathFinderNode>} - Flat row-major array of size.x*size.y nodes */
|
|
116
123
|
this.nodes = new Array(this.size.x * this.size.y);
|
|
117
124
|
for (let y = 0; y < this.size.y; ++y)
|
|
118
125
|
for (let x = 0; x < this.size.x; ++x)
|
|
@@ -286,9 +293,12 @@ class PathFinder
|
|
|
286
293
|
// Best path so far through neighbor — record it.
|
|
287
294
|
neighbor.parent = current;
|
|
288
295
|
neighbor.g = tentativeG;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
296
|
+
// Octile heuristic — tightest admissible distance for an
|
|
297
|
+
// 8-connected grid with cardinal cost 1 and diagonal cost √2.
|
|
298
|
+
const adx = abs(endNode.pos.x - neighbor.pos.x);
|
|
299
|
+
const ady = abs(endNode.pos.y - neighbor.pos.y);
|
|
300
|
+
const h = max(adx, ady) + (Math.SQRT2 - 1) * min(adx, ady);
|
|
301
|
+
neighbor.f = neighbor.g + h * this.heuristicWeight;
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
|
|
@@ -570,6 +580,24 @@ class PathFinder
|
|
|
570
580
|
path.push(original[original.length - 1]);
|
|
571
581
|
}
|
|
572
582
|
|
|
583
|
+
/** Drop any middle node that lies exactly on the line through its two
|
|
584
|
+
* neighbors. Backstop for the smoothing passes — the corners pass
|
|
585
|
+
* intentionally keeps truly-straight runs, and the string-pulling pass
|
|
586
|
+
* checks collinearity against the original path, not the in-progress
|
|
587
|
+
* result, so it can leave 3+ collinear nodes in some edge cases.
|
|
588
|
+
* @param {PathFinderNode[]} path
|
|
589
|
+
* @private */
|
|
590
|
+
dropCollinearNodes(path)
|
|
591
|
+
{
|
|
592
|
+
for (let i = path.length - 2; i >= 1; --i)
|
|
593
|
+
{
|
|
594
|
+
const a = path[i - 1], b = path[i], c = path[i + 1];
|
|
595
|
+
if ((b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) ===
|
|
596
|
+
(b.pos.y - a.pos.y) * (c.pos.x - a.pos.x))
|
|
597
|
+
path.splice(i, 1);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
573
601
|
/** Lookup helper: true when the node at tile coords (x, y) is in-bounds
|
|
574
602
|
* and clear (walkable, zero-cost). Used by isLineClear's hot path.
|
|
575
603
|
* @param {number} x
|
|
@@ -737,6 +765,7 @@ class PathFinder
|
|
|
737
765
|
{
|
|
738
766
|
this.smoothPathCorners(nodePath);
|
|
739
767
|
this.smoothPathStringPull(nodePath);
|
|
768
|
+
this.dropCollinearNodes(nodePath);
|
|
740
769
|
}
|
|
741
770
|
|
|
742
771
|
// Convert to world-space Vector2 path. Return copies, not live node
|
package/plugins/tweenSystem.js
CHANGED
|
@@ -63,13 +63,21 @@ class Tween
|
|
|
63
63
|
}
|
|
64
64
|
ASSERT(isNumber(duration) && duration > 0, 'Tween duration must be > 0');
|
|
65
65
|
|
|
66
|
+
/** @property {function(number|Vector2|Color):void} - Called with the interpolated value each frame */
|
|
66
67
|
this.callback = callback;
|
|
68
|
+
/** @property {number|Vector2|Color} - Starting value */
|
|
67
69
|
this.start = start;
|
|
70
|
+
/** @property {number|Vector2|Color} - Ending value */
|
|
68
71
|
this.end = end;
|
|
72
|
+
/** @property {number} - Total duration in seconds */
|
|
69
73
|
this.duration = duration;
|
|
74
|
+
/** @property {number} - Remaining time in seconds (counts down from duration to 0) */
|
|
70
75
|
this.life = duration;
|
|
76
|
+
/** @property {function(number):number} - Easing curve mapping [0,1] -> [0,1] */
|
|
71
77
|
this.ease = options.ease || Ease.LINEAR;
|
|
78
|
+
/** @property {boolean} - If true, advance even when the game is paused */
|
|
72
79
|
this.useRealTime = !!options.useRealTime;
|
|
80
|
+
/** @property {boolean} - If true, stop advancing until cleared */
|
|
73
81
|
this.paused = !!options.paused;
|
|
74
82
|
|
|
75
83
|
/** @private completion callback set by then(), loop(), pingPong(). */
|
|
@@ -409,7 +417,7 @@ const Ease =
|
|
|
409
417
|
function tweenProperty(target, propertyPath, start, end, duration = 1, options = {})
|
|
410
418
|
{
|
|
411
419
|
ASSERT(target != null && typeof target === 'object', 'tweenProperty target must be an object');
|
|
412
|
-
ASSERT(
|
|
420
|
+
ASSERT(isStringLike(propertyPath) && propertyPath.length > 0, 'tweenProperty propertyPath must be a non-empty string');
|
|
413
421
|
|
|
414
422
|
const parts = propertyPath.split('.');
|
|
415
423
|
const lastKey = parts.pop();
|
package/plugins/uiSystem.js
CHANGED
|
@@ -125,12 +125,31 @@ class UISystemPlugin
|
|
|
125
125
|
|
|
126
126
|
engineAddPlugin(uiUpdate, uiRender);
|
|
127
127
|
|
|
128
|
-
// set object position
|
|
128
|
+
// set object position based on anchor target (parent box, or canvas for roots),
|
|
129
|
+
// self-pivot, and localPos offset
|
|
129
130
|
function updateTransforms(o)
|
|
130
131
|
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
let targetPos, targetSize;
|
|
133
|
+
if (o.parent)
|
|
134
|
+
{
|
|
135
|
+
targetPos = o.parent.pos;
|
|
136
|
+
targetSize = o.parent.size;
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
{
|
|
140
|
+
// anchor to canvas in native coords (handles nativeHeight if set)
|
|
141
|
+
targetPos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
142
|
+
targetSize = uiSystem.nativeHeight
|
|
143
|
+
? vec2(mainCanvasSize.x * uiSystem.nativeHeight / mainCanvasSize.y,
|
|
144
|
+
uiSystem.nativeHeight)
|
|
145
|
+
: mainCanvasSize;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const a = o.anchor;
|
|
149
|
+
o.pos = targetPos
|
|
150
|
+
.add(targetSize.multiply(a).scale(.5)) // anchor point on target
|
|
151
|
+
.subtract(o.size.multiply(a).scale(.5)) // pivot shift on self
|
|
152
|
+
.add(o.localPos); // user offset
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
// setup recursive update and render
|
|
@@ -597,9 +616,8 @@ class UISystemPlugin
|
|
|
597
616
|
// confirm menu
|
|
598
617
|
const confirmMenu = new UIObject(vec2(), size);
|
|
599
618
|
uiSystem.confirmDialog = confirmMenu;
|
|
600
|
-
confirmMenu.onRender = ()=>
|
|
619
|
+
confirmMenu.onRender = ()=>
|
|
601
620
|
{
|
|
602
|
-
confirmMenu.pos = uiSystem.screenToNative(mainCanvasSize.scale(.5));
|
|
603
621
|
const backgroundColor = hsl(0,0,0,.7);
|
|
604
622
|
uiSystem.drawRect(vec2(), vec2(1e9), backgroundColor);
|
|
605
623
|
}
|
|
@@ -732,7 +750,11 @@ class UIObject
|
|
|
732
750
|
this.navigationIndex = undefined;
|
|
733
751
|
/** @property {boolean} - Should this be auto selected by navigation? Must also have valid navigation index. */
|
|
734
752
|
this.navigationAutoSelect = false;
|
|
735
|
-
|
|
753
|
+
/** @property {Vector2} - Where on parent (or canvas if no parent) this object is anchored.
|
|
754
|
+
* Components in [-1, 1]: (0,0)=center, (-1,-1)=top-left, (1,1)=bottom-right.
|
|
755
|
+
* Also acts as self-pivot — e.g. (1,-1) puts your top-right corner at the anchor point. */
|
|
756
|
+
this.anchor = vec2();
|
|
757
|
+
|
|
736
758
|
uiSystem.uiObjects.push(this);
|
|
737
759
|
}
|
|
738
760
|
|
|
@@ -985,9 +1007,9 @@ class UIText extends UIObject
|
|
|
985
1007
|
{
|
|
986
1008
|
super(pos, size);
|
|
987
1009
|
|
|
988
|
-
ASSERT(
|
|
1010
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
989
1011
|
ASSERT(['left','center','right'].includes(align), 'ui text align must be left, center, or right');
|
|
990
|
-
ASSERT(
|
|
1012
|
+
ASSERT(isStringLike(font), 'ui text font must be a string');
|
|
991
1013
|
|
|
992
1014
|
// set properties
|
|
993
1015
|
this.text = text;
|
|
@@ -1035,7 +1057,7 @@ class UITextInput extends UIObject
|
|
|
1035
1057
|
{
|
|
1036
1058
|
super(pos, size);
|
|
1037
1059
|
|
|
1038
|
-
ASSERT(
|
|
1060
|
+
ASSERT(isStringLike(text), 'ui text must be a string');
|
|
1039
1061
|
|
|
1040
1062
|
/** @property {number} - Max length of input (0 = no limit) */
|
|
1041
1063
|
this.maxLength = 0;
|
|
@@ -1172,7 +1194,7 @@ class UIButton extends UIObject
|
|
|
1172
1194
|
{
|
|
1173
1195
|
super(pos, size);
|
|
1174
1196
|
|
|
1175
|
-
ASSERT(
|
|
1197
|
+
ASSERT(isStringLike(text), 'ui button must be a string');
|
|
1176
1198
|
ASSERT(isColor(color), 'ui button color must be a color');
|
|
1177
1199
|
|
|
1178
1200
|
/** @property {Vector2} - Text offset for the button */
|
|
@@ -1213,7 +1235,7 @@ class UICheckbox extends UIObject
|
|
|
1213
1235
|
{
|
|
1214
1236
|
super(pos, size);
|
|
1215
1237
|
|
|
1216
|
-
ASSERT(
|
|
1238
|
+
ASSERT(isStringLike(text), 'ui checkbox must be a string');
|
|
1217
1239
|
ASSERT(isColor(color), 'ui checkbox color must be a color');
|
|
1218
1240
|
|
|
1219
1241
|
/** @property {boolean} - Current percentage value of this slider 0-1 */
|
|
@@ -1270,7 +1292,7 @@ class UISlider extends UIObject
|
|
|
1270
1292
|
super(pos, size);
|
|
1271
1293
|
|
|
1272
1294
|
ASSERT(isNumber(value), 'ui slider value must be a number');
|
|
1273
|
-
ASSERT(
|
|
1295
|
+
ASSERT(isStringLike(text), 'ui slider must be a string');
|
|
1274
1296
|
ASSERT(isColor(color), 'ui slider color must be a color');
|
|
1275
1297
|
ASSERT(isColor(handleColor), 'ui slider handleColor must be a color');
|
|
1276
1298
|
|
|
@@ -1388,7 +1410,7 @@ class UIVideo extends UIObject
|
|
|
1388
1410
|
{
|
|
1389
1411
|
super(pos, size || vec2());
|
|
1390
1412
|
|
|
1391
|
-
ASSERT(
|
|
1413
|
+
ASSERT(isStringLike(src), 'video src must be a string');
|
|
1392
1414
|
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
1393
1415
|
|
|
1394
1416
|
this.color = BLACK; // default to black background
|
package/src/engine.js
CHANGED
|
@@ -32,7 +32,7 @@ const engineName = 'LittleJS';
|
|
|
32
32
|
* @type {string}
|
|
33
33
|
* @default
|
|
34
34
|
* @memberof Engine */
|
|
35
|
-
const engineVersion = '1.18.
|
|
35
|
+
const engineVersion = '1.18.12';
|
|
36
36
|
|
|
37
37
|
/** Frames per second to update
|
|
38
38
|
* @type {number}
|
|
@@ -204,7 +204,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
204
204
|
const combinedScale = timeScale * debugScale;
|
|
205
205
|
frameTimeDeltaMS *= combinedScale;
|
|
206
206
|
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
207
|
-
if (
|
|
207
|
+
if (combinedScale <= 1)
|
|
208
208
|
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
209
209
|
|
|
210
210
|
let wasUpdated = false;
|
|
@@ -291,6 +291,7 @@ async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, game
|
|
|
291
291
|
glFlush();
|
|
292
292
|
debugRenderPost();
|
|
293
293
|
drawCount = 0;
|
|
294
|
+
primitiveCount = 0;
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
|
package/src/engineAudio.js
CHANGED
|
@@ -14,12 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
/** Audio context used by the engine
|
|
18
|
-
* browser autoplay warnings about constructing an AudioContext before any
|
|
19
|
-
* user gesture.
|
|
17
|
+
/** Audio context used by the engine
|
|
20
18
|
* @type {AudioContext}
|
|
21
19
|
* @memberof Audio */
|
|
22
|
-
let audioContext;
|
|
20
|
+
let audioContext = new AudioContext;
|
|
23
21
|
|
|
24
22
|
/** Master gain node for all audio to pass through
|
|
25
23
|
* @type {GainNode}
|
|
@@ -35,13 +33,12 @@ const audioDefaultSampleRate = 44100;
|
|
|
35
33
|
* @return {boolean} - True if the audio context is running
|
|
36
34
|
* @memberof Audio */
|
|
37
35
|
function audioIsRunning()
|
|
38
|
-
{ return audioContext
|
|
36
|
+
{ return audioContext.state === 'running'; }
|
|
39
37
|
|
|
40
38
|
function audioInit()
|
|
41
39
|
{
|
|
42
40
|
if (!soundEnable || headlessMode) return;
|
|
43
41
|
|
|
44
|
-
audioContext = new AudioContext;
|
|
45
42
|
audioMasterGain = audioContext.createGain();
|
|
46
43
|
audioMasterGain.connect(audioContext.destination);
|
|
47
44
|
audioMasterGain.gain.value = soundVolume; // set starting value
|
|
@@ -87,7 +84,7 @@ class Sound
|
|
|
87
84
|
{
|
|
88
85
|
if (!soundEnable || headlessMode) return;
|
|
89
86
|
|
|
90
|
-
ASSERT(!asset || isArray(asset) ||
|
|
87
|
+
ASSERT(!asset || isArray(asset) || isStringLike(asset), 'asset must be a file name or zzfx array');
|
|
91
88
|
ASSERT(randomness === undefined || isNumber(randomness), 'randomness must be a number');
|
|
92
89
|
ASSERT(randomness === undefined || randomness >= 0 && randomness <=1, 'randomness must be between 0 and 1');
|
|
93
90
|
ASSERT(isNumber(range), 'range must be a number');
|
|
@@ -426,14 +423,15 @@ class SoundInstance
|
|
|
426
423
|
|
|
427
424
|
/** Speak text with passed in settings
|
|
428
425
|
* @param {string} text - The text to speak
|
|
429
|
-
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
430
426
|
* @param {number} [volume] - How much to scale volume by
|
|
431
427
|
* @param {number} [rate] - How quickly to speak
|
|
432
428
|
* @param {number} [pitch] - How much to change the pitch by
|
|
429
|
+
* @param {string} [language] - The language/accent to use (examples: en, it, ru, ja, zh)
|
|
433
430
|
* @return {SpeechSynthesisUtterance} - The utterance that was spoken
|
|
434
431
|
* @memberof Audio */
|
|
435
|
-
function speak(text,
|
|
432
|
+
function speak(text, volume=1, rate=1, pitch=1, language='')
|
|
436
433
|
{
|
|
434
|
+
ASSERT(typeof volume !== 'string', 'speak() signature changed: language is now the last parameter, after pitch');
|
|
437
435
|
if (!soundEnable || headlessMode) return;
|
|
438
436
|
if (!speechSynthesis) return;
|
|
439
437
|
|
package/src/engineDebug.js
CHANGED
|
@@ -79,7 +79,7 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false, s
|
|
|
79
79
|
{
|
|
80
80
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
81
81
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
82
|
-
ASSERT(
|
|
82
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
83
83
|
ASSERT(isNumber(time), 'time must be a number');
|
|
84
84
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
85
85
|
|
|
@@ -106,7 +106,7 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false, screen
|
|
|
106
106
|
{
|
|
107
107
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
108
108
|
ASSERT(isArray(points), 'points must be an array');
|
|
109
|
-
ASSERT(
|
|
109
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
110
110
|
ASSERT(isNumber(time), 'time must be a number');
|
|
111
111
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
112
112
|
|
|
@@ -130,7 +130,7 @@ function debugCircle(pos, size=0, color=WHITE, time=0, fill=false, screenSpace=f
|
|
|
130
130
|
{
|
|
131
131
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
132
132
|
ASSERT(isNumber(size), 'size must be a number');
|
|
133
|
-
ASSERT(
|
|
133
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
134
134
|
ASSERT(isNumber(time), 'time must be a number');
|
|
135
135
|
|
|
136
136
|
if (isColor(color))
|
|
@@ -208,13 +208,13 @@ function debugOverlap(posA, sizeA, posB, sizeB, color, time, screenSpace=false)
|
|
|
208
208
|
* @memberof Debug */
|
|
209
209
|
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace', screenSpace=false)
|
|
210
210
|
{
|
|
211
|
-
ASSERT(
|
|
211
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
212
212
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
213
213
|
ASSERT(isNumber(size), 'size must be a number');
|
|
214
|
-
ASSERT(
|
|
214
|
+
ASSERT(isStringLike(color) || isColor(color), 'color is invalid');
|
|
215
215
|
ASSERT(isNumber(time), 'time must be a number');
|
|
216
216
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
217
|
-
ASSERT(
|
|
217
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
218
218
|
|
|
219
219
|
if (isColor(color))
|
|
220
220
|
color = color.toString();
|
|
@@ -502,7 +502,8 @@ function debugRender()
|
|
|
502
502
|
debugContext.fillText('FPS: ' + averageFPS.toFixed(1) + (glEnable?' WebGL':' Canvas2D'),
|
|
503
503
|
x, y += h);
|
|
504
504
|
debugContext.fillText('Objects: ' + engineObjects.length, x, y += h);
|
|
505
|
-
debugContext.fillText('Draw
|
|
505
|
+
debugContext.fillText('Draw Calls: ' + drawCount, x, y += h);
|
|
506
|
+
debugContext.fillText('Primitives: ' + primitiveCount, x, y += h);
|
|
506
507
|
debugContext.fillText('---------', x, y += h);
|
|
507
508
|
debugContext.fillStyle = '#f00';
|
|
508
509
|
debugContext.fillText('ESC: Debug Overlay', x, y += h);
|
|
@@ -573,7 +574,8 @@ function debugRenderPost()
|
|
|
573
574
|
mainContext.font = '1em monospace';
|
|
574
575
|
mainContext.fillStyle = '#000';
|
|
575
576
|
const text = engineName + ' v' + engineVersion + ' / '
|
|
576
|
-
+ drawCount + ' / ' +
|
|
577
|
+
+ drawCount + ' / ' + primitiveCount + ' / '
|
|
578
|
+
+ engineObjects.length + ' / ' + averageFPS.toFixed(1)
|
|
577
579
|
+ (glEnable ? ' GL' : ' 2D') ;
|
|
578
580
|
mainContext.fillText(text, mainCanvas.width-3, 3);
|
|
579
581
|
mainContext.fillStyle = '#fff';
|
package/src/engineDraw.js
CHANGED
|
@@ -72,6 +72,12 @@ let textureInfos = [];
|
|
|
72
72
|
* @memberof Draw */
|
|
73
73
|
let drawCount;
|
|
74
74
|
|
|
75
|
+
/** Keeps track of how many primitives were drawn each frame for debugging
|
|
76
|
+
* A single draw call can render many primitives (e.g. a WebGL sprite batch).
|
|
77
|
+
* @type {number}
|
|
78
|
+
* @memberof Draw */
|
|
79
|
+
let primitiveCount;
|
|
80
|
+
|
|
75
81
|
// internal predicates for tint short-circuiting in canvas2D draw paths
|
|
76
82
|
// isWhite ignores alpha because alpha is applied via globalAlpha, not multiply
|
|
77
83
|
// isBlack includes alpha so additive colors that only contribute alpha are not skipped
|
|
@@ -312,19 +318,19 @@ function drawTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
|
312
318
|
}
|
|
313
319
|
else
|
|
314
320
|
{
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// color.add(additiveColor) on line ~337.
|
|
321
|
+
// untextured: glDrawUntextured picks the optimal path (poly
|
|
322
|
+
// tristrip if already in poly mode, otherwise instanced with
|
|
323
|
+
// uvs/rgba zeroed). Color+additive are folded together to match
|
|
324
|
+
// the Canvas2D path's color.add(additiveColor) on line ~337.
|
|
319
325
|
const combined = additiveColor ? color.add(additiveColor) : color;
|
|
320
|
-
|
|
321
|
-
0, combined.rgbaInt());
|
|
326
|
+
glDrawUntextured(pos.x, pos.y, size.x, size.y, angle, combined.rgbaInt());
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
else
|
|
325
330
|
{
|
|
326
331
|
// normal canvas 2D rendering method (slower)
|
|
327
332
|
++drawCount;
|
|
333
|
+
++primitiveCount;
|
|
328
334
|
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
329
335
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
330
336
|
{
|
|
@@ -364,13 +370,13 @@ function drawRect(pos, size, color, angle, useWebGL, screenSpace, context)
|
|
|
364
370
|
* @param {Vector2} pos
|
|
365
371
|
* @param {Vector2} [size=vec2(1)]
|
|
366
372
|
* @param {Color} [colorTop=WHITE]
|
|
367
|
-
* @param {Color} [colorBottom=
|
|
373
|
+
* @param {Color} [colorBottom=CLEAR_WHITE]
|
|
368
374
|
* @param {number} [angle]
|
|
369
375
|
* @param {boolean} [useWebGL=glEnable]
|
|
370
376
|
* @param {boolean} [screenSpace]
|
|
371
377
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
372
378
|
* @memberof Draw */
|
|
373
|
-
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=
|
|
379
|
+
function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=CLEAR_WHITE, angle=0, useWebGL=glEnable, screenSpace=false, context)
|
|
374
380
|
{
|
|
375
381
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
376
382
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -410,6 +416,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
410
416
|
{
|
|
411
417
|
// normal canvas 2D rendering method (slower)
|
|
412
418
|
++drawCount;
|
|
419
|
+
++primitiveCount;
|
|
413
420
|
size = new Vector2(size.x, -size.y); // fix upside down sprites
|
|
414
421
|
drawCanvas2D(pos, size, angle, false, (context)=>
|
|
415
422
|
{
|
|
@@ -472,8 +479,9 @@ function drawTextureWrapped(pos, size, wrapCount, texture=0, color=WHITE,
|
|
|
472
479
|
return;
|
|
473
480
|
}
|
|
474
481
|
|
|
475
|
-
// Canvas2D path — increment
|
|
482
|
+
// Canvas2D path — increment counts here (WebGL counts via glFlush)
|
|
476
483
|
++drawCount;
|
|
484
|
+
++primitiveCount;
|
|
477
485
|
|
|
478
486
|
if (!screenSpace)
|
|
479
487
|
{
|
|
@@ -547,6 +555,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
547
555
|
{
|
|
548
556
|
// normal canvas 2D rendering method (slower)
|
|
549
557
|
++drawCount;
|
|
558
|
+
++primitiveCount;
|
|
550
559
|
drawCanvas2D(pos, vec2(1), angle, false, (context)=>
|
|
551
560
|
{
|
|
552
561
|
context.strokeStyle = color.toString();
|
|
@@ -727,6 +736,77 @@ function drawCircle(pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, useW
|
|
|
727
736
|
drawEllipse(pos, vec2(size), color, 0, lineWidth, lineColor, useWebGL, screenSpace, context);
|
|
728
737
|
}
|
|
729
738
|
|
|
739
|
+
/** Draw a circle filled with a radial gradient from the center to the rim
|
|
740
|
+
* - Best when batched with other untextured polys
|
|
741
|
+
* - If drawing mostly textured sprites, bake the gradient into a texture and use drawTile instead
|
|
742
|
+
* - Stacking gradients at the exact same position may show a faint vertical artifact
|
|
743
|
+
* @param {Vector2} pos
|
|
744
|
+
* @param {number} [size=1] - Diameter
|
|
745
|
+
* @param {Color} [colorInner=WHITE]
|
|
746
|
+
* @param {Color} [colorOuter=CLEAR_WHITE]
|
|
747
|
+
* @param {boolean} [useWebGL=glEnable]
|
|
748
|
+
* @param {boolean} [screenSpace]
|
|
749
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context]
|
|
750
|
+
* @memberof Draw */
|
|
751
|
+
let drawCircleGradientOffset = 0;
|
|
752
|
+
function drawCircleGradient(pos, size=1, colorInner=WHITE, colorOuter=CLEAR_WHITE, useWebGL=glEnable, screenSpace=false, context)
|
|
753
|
+
{
|
|
754
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
755
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
756
|
+
ASSERT(isColor(colorInner) && isColor(colorOuter), 'color is invalid');
|
|
757
|
+
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
758
|
+
|
|
759
|
+
if (headlessMode) return;
|
|
760
|
+
|
|
761
|
+
if (useWebGL && glEnable)
|
|
762
|
+
{
|
|
763
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
764
|
+
if (screenSpace)
|
|
765
|
+
{
|
|
766
|
+
// convert to world space
|
|
767
|
+
pos = screenToWorld(pos);
|
|
768
|
+
size /= cameraScale;
|
|
769
|
+
}
|
|
770
|
+
// fan as tristrip; rotate the boundary vertex by one slice per call
|
|
771
|
+
// so back-to-back gradients at the same position have their hole
|
|
772
|
+
// (from gpu edge-rule on the boundary line-degen) at different rim
|
|
773
|
+
// verts and don't visibly stack
|
|
774
|
+
const sides = glCircleSides;
|
|
775
|
+
const radius = size/2;
|
|
776
|
+
const innerInt = colorInner.rgbaInt();
|
|
777
|
+
const outerInt = colorOuter.rgbaInt();
|
|
778
|
+
const offset = drawCircleGradientOffset++;
|
|
779
|
+
const startA = (offset%sides)/sides*PI*2;
|
|
780
|
+
const points = [vec2(pos.x + sin(startA)*radius, pos.y + cos(startA)*radius)];
|
|
781
|
+
const colors = [outerInt];
|
|
782
|
+
for (let i=sides; i--;)
|
|
783
|
+
{
|
|
784
|
+
const a = ((i+offset)%sides)/sides*PI*2;
|
|
785
|
+
points.push(pos);
|
|
786
|
+
colors.push(innerInt);
|
|
787
|
+
points.push(vec2(pos.x + sin(a)*radius, pos.y + cos(a)*radius));
|
|
788
|
+
colors.push(outerInt);
|
|
789
|
+
}
|
|
790
|
+
glDrawColoredPoints(points, colors);
|
|
791
|
+
}
|
|
792
|
+
else
|
|
793
|
+
{
|
|
794
|
+
// normal canvas 2D rendering method (slower)
|
|
795
|
+
++drawCount;
|
|
796
|
+
++primitiveCount;
|
|
797
|
+
drawCanvas2D(pos, vec2(size), 0, false, (context)=>
|
|
798
|
+
{
|
|
799
|
+
const gradient = context.createRadialGradient(0, 0, 0, 0, 0, .5);
|
|
800
|
+
gradient.addColorStop(0, colorInner.toString());
|
|
801
|
+
gradient.addColorStop(1, colorOuter.toString());
|
|
802
|
+
context.fillStyle = gradient;
|
|
803
|
+
context.beginPath();
|
|
804
|
+
context.ellipse(0, 0, .5, .5, 0, 0, 9);
|
|
805
|
+
context.fill();
|
|
806
|
+
}, screenSpace, context);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
730
810
|
/**
|
|
731
811
|
* @callback Canvas2DDrawFunction - A function that draws to a 2D canvas context
|
|
732
812
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} context
|
|
@@ -810,15 +890,15 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
810
890
|
* @memberof Draw */
|
|
811
891
|
function drawTextScreen(text, pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=drawContext)
|
|
812
892
|
{
|
|
813
|
-
ASSERT(
|
|
893
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
814
894
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
815
895
|
ASSERT(isNumber(size), 'size must be a number');
|
|
816
896
|
ASSERT(isColor(color), 'color must be a color');
|
|
817
897
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
818
898
|
ASSERT(isColor(lineColor), 'lineColor must be a color');
|
|
819
899
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
820
|
-
ASSERT(
|
|
821
|
-
ASSERT(
|
|
900
|
+
ASSERT(isStringLike(font), 'font must be a string');
|
|
901
|
+
ASSERT(isStringLike(fontStyle), 'fontStyle must be a string');
|
|
822
902
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
823
903
|
|
|
824
904
|
context.fillStyle = color.toString();
|
|
@@ -855,7 +935,7 @@ async function loadTexture(textureIndex, src)
|
|
|
855
935
|
{
|
|
856
936
|
ASSERT(isNumber(textureIndex), 'textureIndex must be a number');
|
|
857
937
|
ASSERT(!textureInfos[textureIndex], 'textureIndex is already loaded!');
|
|
858
|
-
ASSERT(!src ||
|
|
938
|
+
ASSERT(!src || isStringLike(src), 'image src must be a string');
|
|
859
939
|
|
|
860
940
|
const image = new Image;
|
|
861
941
|
if (src)
|
|
@@ -1243,7 +1323,7 @@ class FontImage
|
|
|
1243
1323
|
*/
|
|
1244
1324
|
drawTextScreen(text, pos, size, center=true, color=WHITE, useWebGL=glEnable, context)
|
|
1245
1325
|
{
|
|
1246
|
-
ASSERT(
|
|
1326
|
+
ASSERT(isStringLike(text), 'text must be a string');
|
|
1247
1327
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
1248
1328
|
ASSERT(isVector2(size) || typeof size === 'number', 'size must be a vec2 or number');
|
|
1249
1329
|
ASSERT(isColor(color), 'color must be a color');
|
package/src/engineExport.js
CHANGED
|
@@ -187,6 +187,8 @@ export
|
|
|
187
187
|
shareURL,
|
|
188
188
|
readSaveData,
|
|
189
189
|
writeSaveData,
|
|
190
|
+
noise1D,
|
|
191
|
+
noise2D,
|
|
190
192
|
|
|
191
193
|
// Random
|
|
192
194
|
rand,
|
|
@@ -208,7 +210,7 @@ export
|
|
|
208
210
|
isColor,
|
|
209
211
|
isVector2,
|
|
210
212
|
isNumber,
|
|
211
|
-
|
|
213
|
+
isStringLike,
|
|
212
214
|
isArray,
|
|
213
215
|
|
|
214
216
|
// Default Colors
|
|
@@ -240,6 +242,7 @@ export
|
|
|
240
242
|
mainCanvasSize,
|
|
241
243
|
textureInfos,
|
|
242
244
|
drawCount,
|
|
245
|
+
primitiveCount,
|
|
243
246
|
screenToWorld,
|
|
244
247
|
worldToScreen,
|
|
245
248
|
screenToWorldDelta,
|
|
@@ -255,6 +258,7 @@ export
|
|
|
255
258
|
drawRegularPoly,
|
|
256
259
|
drawEllipse,
|
|
257
260
|
drawCircle,
|
|
261
|
+
drawCircleGradient,
|
|
258
262
|
drawCanvas2D,
|
|
259
263
|
drawText,
|
|
260
264
|
drawTextScreen,
|
|
@@ -284,6 +288,7 @@ export
|
|
|
284
288
|
glCopyToContext,
|
|
285
289
|
glSetAntialias,
|
|
286
290
|
glDraw,
|
|
291
|
+
glDrawUntextured,
|
|
287
292
|
glDrawPointsTransform,
|
|
288
293
|
glDrawOutlineTransform,
|
|
289
294
|
glDrawPoints,
|