littlejsengine 1.23.0 → 1.24.0
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/CHANGELOG.md +234 -0
- package/COPYRIGHT.txt +38 -38
- package/EDITOR.md +359 -0
- package/FAQ.md +4 -9
- package/README.md +14 -8
- package/REFERENCE.md +2327 -0
- package/dist/littlejs.d.ts +149 -104
- package/dist/littlejs.esm.js +1652 -1332
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +1651 -1328
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +1553 -1294
- package/package.json +5 -3
- package/plugins/gltf.js +29 -13
- package/plugins/pluginExport.js +0 -4
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +136 -31
- package/plugins/render3dEditor.js +51 -17
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +52 -6
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/src/engine.js +1 -1
- package/src/engineAudio.js +9 -3
- package/src/engineBuild.mjs +17 -3
- package/src/engineDraw.js +11 -7
- package/src/engineEditor.js +52 -18
- package/src/engineExport.js +1 -0
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +5 -1
- package/src/engineTileLayer.js +119 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
- package/plugins/zzfxm.js +0 -160
package/src/engineUtilities.js
CHANGED
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Utility Classes and Functions
|
|
3
|
-
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
4
|
-
* - Time formatting helper
|
|
5
|
-
* - JSON file fetching
|
|
6
|
-
* - File saving (text, canvas, data URLs)
|
|
7
|
-
* - Native share dialog support
|
|
8
|
-
* - Local storage save data management
|
|
9
|
-
* - Value noise (1D and 2D)
|
|
10
|
-
* @namespace Utilities
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
'use strict';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Timer object tracks how long has passed since it was set
|
|
17
|
-
* @memberof Engine
|
|
18
|
-
* @example
|
|
19
|
-
* let a = new Timer; // creates a timer that is not set
|
|
20
|
-
* a.set(3); // sets the timer to 3 seconds
|
|
21
|
-
*
|
|
22
|
-
* let b = new Timer(1); // creates a timer with 1 second left
|
|
23
|
-
* b.unset(); // unset the timer
|
|
24
|
-
*/
|
|
25
|
-
class Timer
|
|
26
|
-
{
|
|
27
|
-
/** Create a timer object set time passed in
|
|
28
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
29
|
-
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
30
|
-
constructor(timeLeft, useRealTime=false)
|
|
31
|
-
{
|
|
32
|
-
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
33
|
-
this.useRealTime = useRealTime;
|
|
34
|
-
const globalTime = this.getGlobalTime();
|
|
35
|
-
/** @type {number|undefined} */
|
|
36
|
-
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
37
|
-
/** @type {number|undefined} */
|
|
38
|
-
this.setTime = timeLeft;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Set the timer with seconds passed in
|
|
42
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
43
|
-
set(timeLeft=0)
|
|
44
|
-
{
|
|
45
|
-
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
46
|
-
const globalTime = this.getGlobalTime();
|
|
47
|
-
this.time = globalTime + timeLeft;
|
|
48
|
-
this.setTime = timeLeft;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Set if the timer should keep running even when the game is paused
|
|
52
|
-
* @param {boolean} [useRealTime] */
|
|
53
|
-
setUseRealTime(useRealTime=true)
|
|
54
|
-
{
|
|
55
|
-
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
56
|
-
this.useRealTime = useRealTime;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Unset the timer */
|
|
60
|
-
unset() { this.time = undefined; }
|
|
61
|
-
|
|
62
|
-
/** Returns true if set
|
|
63
|
-
* @return {boolean} */
|
|
64
|
-
isSet() { return this.time !== undefined; }
|
|
65
|
-
|
|
66
|
-
/** Returns true if set and has not elapsed
|
|
67
|
-
* @return {boolean} */
|
|
68
|
-
active() { return this.getGlobalTime() < this.time; }
|
|
69
|
-
|
|
70
|
-
/** Returns true if set and elapsed
|
|
71
|
-
* @return {boolean} */
|
|
72
|
-
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
73
|
-
|
|
74
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
75
|
-
* @return {number} */
|
|
76
|
-
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
77
|
-
|
|
78
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
79
|
-
* Zero-duration timers report 1 (already elapsed).
|
|
80
|
-
* @return {number} */
|
|
81
|
-
getPercent()
|
|
82
|
-
{
|
|
83
|
-
if (!this.isSet()) return 0;
|
|
84
|
-
if (!this.setTime) return 1;
|
|
85
|
-
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Get the time this timer was set to, returns 0 if not set
|
|
89
|
-
* @return {number} */
|
|
90
|
-
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
91
|
-
|
|
92
|
-
/** Get the current global time this timer is based on
|
|
93
|
-
* @return {number} */
|
|
94
|
-
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
95
|
-
|
|
96
|
-
/** Returns this timer expressed as a string
|
|
97
|
-
* @return {string} */
|
|
98
|
-
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
99
|
-
|
|
100
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
101
|
-
* @return {number} */
|
|
102
|
-
valueOf() { return this.get(); }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
-
|
|
107
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
108
|
-
* @param {number} t - time in seconds
|
|
109
|
-
* @return {string}
|
|
110
|
-
* @memberof Utilities */
|
|
111
|
-
function formatTime(t)
|
|
112
|
-
{
|
|
113
|
-
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
114
|
-
t = abs(t)|0;
|
|
115
|
-
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
119
|
-
* @param {string} url - URL of JSON file
|
|
120
|
-
* @return {Promise<object>}
|
|
121
|
-
* @memberof Utilities */
|
|
122
|
-
async function fetchJSON(url)
|
|
123
|
-
{
|
|
124
|
-
const response = await fetch(url);
|
|
125
|
-
if (!response.ok)
|
|
126
|
-
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
127
|
-
const json = await response.json();
|
|
128
|
-
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
129
|
-
return json;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
-
|
|
134
|
-
/** Save a text file to disk
|
|
135
|
-
* @param {string} text
|
|
136
|
-
* @param {string} [filename]
|
|
137
|
-
* @param {string} [type]
|
|
138
|
-
* @memberof Utilities */
|
|
139
|
-
function saveText(text, filename='text', type='text/plain')
|
|
140
|
-
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
141
|
-
|
|
142
|
-
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
|
-
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
-
* @param {number} width - In pixels
|
|
145
|
-
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
|
-
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
147
|
-
* @return {OffscreenCanvasRenderingContext2D}
|
|
148
|
-
* @memberof Utilities */
|
|
149
|
-
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
|
-
{
|
|
151
|
-
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
-
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Save a canvas to disk
|
|
156
|
-
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
|
-
* @param {string} [filename]
|
|
158
|
-
* @param {string} [type]
|
|
159
|
-
* @memberof Utilities */
|
|
160
|
-
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
|
-
{
|
|
162
|
-
if (canvas instanceof OffscreenCanvas)
|
|
163
|
-
{
|
|
164
|
-
// copy to temporary canvas and save
|
|
165
|
-
const saveCanvas = document.createElement('canvas');
|
|
166
|
-
saveCanvas.width = canvas.width;
|
|
167
|
-
saveCanvas.height = canvas.height;
|
|
168
|
-
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
169
|
-
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
|
-
}
|
|
171
|
-
else
|
|
172
|
-
saveDataURL(canvas.toDataURL(type), filename);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Save a data url to disk
|
|
176
|
-
* @param {string} url
|
|
177
|
-
* @param {string} [filename]
|
|
178
|
-
* @param {number} [revokeTime] - how long before revoking the url
|
|
179
|
-
* @memberof Utilities */
|
|
180
|
-
function saveDataURL(url, filename='download', revokeTime)
|
|
181
|
-
{
|
|
182
|
-
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
183
|
-
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
184
|
-
|
|
185
|
-
// create link for saving screenshots
|
|
186
|
-
const link = document.createElement('a');
|
|
187
|
-
link.download = filename;
|
|
188
|
-
link.href = url;
|
|
189
|
-
link.click();
|
|
190
|
-
if (revokeTime !== undefined)
|
|
191
|
-
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Share content using the native share dialog if available
|
|
195
|
-
* @param {string} title - title of the share
|
|
196
|
-
* @param {string} url - url to share
|
|
197
|
-
* @param {Function} [callback] - Called when share is complete
|
|
198
|
-
* @memberof Utilities */
|
|
199
|
-
function shareURL(title, url, callback)
|
|
200
|
-
{
|
|
201
|
-
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
202
|
-
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
203
|
-
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
207
|
-
|
|
208
|
-
/** Read save data from local storage
|
|
209
|
-
* - The result has the type of defaultSaveData, or any when there is none
|
|
210
|
-
* @template {Object<string, any>} [T=any]
|
|
211
|
-
* @param {string} saveName - unique name for the game/save
|
|
212
|
-
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
213
|
-
* @return {T}
|
|
214
|
-
* @memberof Utilities */
|
|
215
|
-
function readSaveData(saveName, defaultSaveData)
|
|
216
|
-
{
|
|
217
|
-
ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
218
|
-
ASSERT(defaultSaveData === undefined ||
|
|
219
|
-
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
220
|
-
'readSaveData: default must be an object - the result is ' +
|
|
221
|
-
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
222
|
-
'Use readSaveData(key, {best:0}).best');
|
|
223
|
-
|
|
224
|
-
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
225
|
-
// iframes) and corrupt JSON in stored data
|
|
226
|
-
let loadedData = {};
|
|
227
|
-
try
|
|
228
|
-
{
|
|
229
|
-
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
230
|
-
if (data)
|
|
231
|
-
{
|
|
232
|
-
try { loadedData = JSON.parse(data); }
|
|
233
|
-
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
237
|
-
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Write save data to local storage
|
|
241
|
-
* @param {string} saveName - unique name for the game/save
|
|
242
|
-
* @param {object} saveData - object containing data to be saved
|
|
243
|
-
* @memberof Utilities */
|
|
244
|
-
function writeSaveData(saveName, saveData)
|
|
245
|
-
{
|
|
246
|
-
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
247
|
-
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
-
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
-
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
253
|
-
|
|
254
|
-
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
255
|
-
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
256
|
-
function noiseHash(i)
|
|
257
|
-
{
|
|
258
|
-
let h = (i | 0) ^ 0x9e3779b9;
|
|
259
|
-
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
260
|
-
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
261
|
-
h ^= h >>> 16;
|
|
262
|
-
return (h >>> 0) / 2**32;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
266
|
-
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
267
|
-
* are interpolated with smoothStep for C1 continuity.
|
|
268
|
-
* @param {number} x
|
|
269
|
-
* @return {number}
|
|
270
|
-
* @memberof Utilities */
|
|
271
|
-
function noise1D(x)
|
|
272
|
-
{
|
|
273
|
-
const i = floor(x);
|
|
274
|
-
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
278
|
-
* @param {number} x
|
|
279
|
-
* @param {number} y
|
|
280
|
-
* @return {number}
|
|
281
|
-
* @memberof Utilities */
|
|
282
|
-
function noise2D(x, y)
|
|
283
|
-
{
|
|
284
|
-
const ix = floor(x), iy = floor(y);
|
|
285
|
-
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
-
// large prime decorrelates neighboring rows
|
|
287
|
-
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
288
|
-
return lerp(
|
|
289
|
-
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
|
-
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
291
|
-
fy);
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Utility Classes and Functions
|
|
3
|
+
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
4
|
+
* - Time formatting helper
|
|
5
|
+
* - JSON file fetching
|
|
6
|
+
* - File saving (text, canvas, data URLs)
|
|
7
|
+
* - Native share dialog support
|
|
8
|
+
* - Local storage save data management
|
|
9
|
+
* - Value noise (1D and 2D)
|
|
10
|
+
* @namespace Utilities
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Timer object tracks how long has passed since it was set
|
|
17
|
+
* @memberof Engine
|
|
18
|
+
* @example
|
|
19
|
+
* let a = new Timer; // creates a timer that is not set
|
|
20
|
+
* a.set(3); // sets the timer to 3 seconds
|
|
21
|
+
*
|
|
22
|
+
* let b = new Timer(1); // creates a timer with 1 second left
|
|
23
|
+
* b.unset(); // unset the timer
|
|
24
|
+
*/
|
|
25
|
+
class Timer
|
|
26
|
+
{
|
|
27
|
+
/** Create a timer object set time passed in
|
|
28
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
29
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
30
|
+
constructor(timeLeft, useRealTime=false)
|
|
31
|
+
{
|
|
32
|
+
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
33
|
+
this.useRealTime = useRealTime;
|
|
34
|
+
const globalTime = this.getGlobalTime();
|
|
35
|
+
/** @type {number|undefined} */
|
|
36
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
37
|
+
/** @type {number|undefined} */
|
|
38
|
+
this.setTime = timeLeft;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Set the timer with seconds passed in
|
|
42
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
43
|
+
set(timeLeft=0)
|
|
44
|
+
{
|
|
45
|
+
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
46
|
+
const globalTime = this.getGlobalTime();
|
|
47
|
+
this.time = globalTime + timeLeft;
|
|
48
|
+
this.setTime = timeLeft;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Set if the timer should keep running even when the game is paused
|
|
52
|
+
* @param {boolean} [useRealTime] */
|
|
53
|
+
setUseRealTime(useRealTime=true)
|
|
54
|
+
{
|
|
55
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
56
|
+
this.useRealTime = useRealTime;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Unset the timer */
|
|
60
|
+
unset() { this.time = undefined; }
|
|
61
|
+
|
|
62
|
+
/** Returns true if set
|
|
63
|
+
* @return {boolean} */
|
|
64
|
+
isSet() { return this.time !== undefined; }
|
|
65
|
+
|
|
66
|
+
/** Returns true if set and has not elapsed
|
|
67
|
+
* @return {boolean} */
|
|
68
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
69
|
+
|
|
70
|
+
/** Returns true if set and elapsed
|
|
71
|
+
* @return {boolean} */
|
|
72
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
73
|
+
|
|
74
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
75
|
+
* @return {number} */
|
|
76
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
77
|
+
|
|
78
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
79
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
80
|
+
* @return {number} */
|
|
81
|
+
getPercent()
|
|
82
|
+
{
|
|
83
|
+
if (!this.isSet()) return 0;
|
|
84
|
+
if (!(this.setTime > 0)) return 1;
|
|
85
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the time this timer was set to, returns 0 if not set
|
|
89
|
+
* @return {number} */
|
|
90
|
+
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
91
|
+
|
|
92
|
+
/** Get the current global time this timer is based on
|
|
93
|
+
* @return {number} */
|
|
94
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
95
|
+
|
|
96
|
+
/** Returns this timer expressed as a string
|
|
97
|
+
* @return {string} */
|
|
98
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
99
|
+
|
|
100
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
101
|
+
* @return {number} */
|
|
102
|
+
valueOf() { return this.get(); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
+
|
|
107
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
108
|
+
* @param {number} t - time in seconds
|
|
109
|
+
* @return {string}
|
|
110
|
+
* @memberof Utilities */
|
|
111
|
+
function formatTime(t)
|
|
112
|
+
{
|
|
113
|
+
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
114
|
+
t = abs(t)|0;
|
|
115
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
119
|
+
* @param {string} url - URL of JSON file
|
|
120
|
+
* @return {Promise<object>}
|
|
121
|
+
* @memberof Utilities */
|
|
122
|
+
async function fetchJSON(url)
|
|
123
|
+
{
|
|
124
|
+
const response = await fetch(url);
|
|
125
|
+
if (!response.ok)
|
|
126
|
+
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
127
|
+
const json = await response.json();
|
|
128
|
+
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
129
|
+
return json;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
+
|
|
134
|
+
/** Save a text file to disk
|
|
135
|
+
* @param {string} text
|
|
136
|
+
* @param {string} [filename]
|
|
137
|
+
* @param {string} [type]
|
|
138
|
+
* @memberof Utilities */
|
|
139
|
+
function saveText(text, filename='text', type='text/plain')
|
|
140
|
+
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
141
|
+
|
|
142
|
+
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
|
+
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
+
* @param {number} width - In pixels
|
|
145
|
+
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
|
+
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
147
|
+
* @return {OffscreenCanvasRenderingContext2D}
|
|
148
|
+
* @memberof Utilities */
|
|
149
|
+
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
|
+
{
|
|
151
|
+
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
+
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Save a canvas to disk
|
|
156
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
|
+
* @param {string} [filename]
|
|
158
|
+
* @param {string} [type]
|
|
159
|
+
* @memberof Utilities */
|
|
160
|
+
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
|
+
{
|
|
162
|
+
if (canvas instanceof OffscreenCanvas)
|
|
163
|
+
{
|
|
164
|
+
// copy to temporary canvas and save
|
|
165
|
+
const saveCanvas = document.createElement('canvas');
|
|
166
|
+
saveCanvas.width = canvas.width;
|
|
167
|
+
saveCanvas.height = canvas.height;
|
|
168
|
+
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
169
|
+
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
saveDataURL(canvas.toDataURL(type), filename);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Save a data url to disk
|
|
176
|
+
* @param {string} url
|
|
177
|
+
* @param {string} [filename]
|
|
178
|
+
* @param {number} [revokeTime] - how long before revoking the url
|
|
179
|
+
* @memberof Utilities */
|
|
180
|
+
function saveDataURL(url, filename='download', revokeTime)
|
|
181
|
+
{
|
|
182
|
+
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
183
|
+
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
184
|
+
|
|
185
|
+
// create link for saving screenshots
|
|
186
|
+
const link = document.createElement('a');
|
|
187
|
+
link.download = filename;
|
|
188
|
+
link.href = url;
|
|
189
|
+
link.click();
|
|
190
|
+
if (revokeTime !== undefined)
|
|
191
|
+
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Share content using the native share dialog if available
|
|
195
|
+
* @param {string} title - title of the share
|
|
196
|
+
* @param {string} url - url to share
|
|
197
|
+
* @param {Function} [callback] - Called when share is complete
|
|
198
|
+
* @memberof Utilities */
|
|
199
|
+
function shareURL(title, url, callback)
|
|
200
|
+
{
|
|
201
|
+
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
202
|
+
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
203
|
+
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
207
|
+
|
|
208
|
+
/** Read save data from local storage
|
|
209
|
+
* - The result has the type of defaultSaveData, or any when there is none
|
|
210
|
+
* @template {Object<string, any>} [T=any]
|
|
211
|
+
* @param {string} saveName - unique name for the game/save
|
|
212
|
+
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
213
|
+
* @return {T}
|
|
214
|
+
* @memberof Utilities */
|
|
215
|
+
function readSaveData(saveName, defaultSaveData)
|
|
216
|
+
{
|
|
217
|
+
ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
218
|
+
ASSERT(defaultSaveData === undefined ||
|
|
219
|
+
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
220
|
+
'readSaveData: default must be an object - the result is ' +
|
|
221
|
+
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
222
|
+
'Use readSaveData(key, {best:0}).best');
|
|
223
|
+
|
|
224
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
225
|
+
// iframes) and corrupt JSON in stored data
|
|
226
|
+
let loadedData = {};
|
|
227
|
+
try
|
|
228
|
+
{
|
|
229
|
+
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
230
|
+
if (data)
|
|
231
|
+
{
|
|
232
|
+
try { loadedData = JSON.parse(data); }
|
|
233
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
237
|
+
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Write save data to local storage
|
|
241
|
+
* @param {string} saveName - unique name for the game/save
|
|
242
|
+
* @param {object} saveData - object containing data to be saved
|
|
243
|
+
* @memberof Utilities */
|
|
244
|
+
function writeSaveData(saveName, saveData)
|
|
245
|
+
{
|
|
246
|
+
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
247
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
+
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
253
|
+
|
|
254
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
255
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
256
|
+
function noiseHash(i)
|
|
257
|
+
{
|
|
258
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
259
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
260
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
261
|
+
h ^= h >>> 16;
|
|
262
|
+
return (h >>> 0) / 2**32;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
266
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
267
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
268
|
+
* @param {number} x
|
|
269
|
+
* @return {number}
|
|
270
|
+
* @memberof Utilities */
|
|
271
|
+
function noise1D(x)
|
|
272
|
+
{
|
|
273
|
+
const i = floor(x);
|
|
274
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
278
|
+
* @param {number} x
|
|
279
|
+
* @param {number} y
|
|
280
|
+
* @return {number}
|
|
281
|
+
* @memberof Utilities */
|
|
282
|
+
function noise2D(x, y)
|
|
283
|
+
{
|
|
284
|
+
const ix = floor(x), iy = floor(y);
|
|
285
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
+
// large prime decorrelates neighboring rows
|
|
287
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
288
|
+
return lerp(
|
|
289
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
291
|
+
fy);
|
|
292
292
|
}
|
package/src/engineWebGL.js
CHANGED
|
@@ -770,6 +770,9 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
770
770
|
glDraw(x, y, sizeX, sizeY, angle, 0, 0, 0, 0, 0, rgba);
|
|
771
771
|
}
|
|
772
772
|
|
|
773
|
+
// the list and the vectors glDrawPointsTransform writes its points into
|
|
774
|
+
const glPointsOut = [], glPointsPool = [];
|
|
775
|
+
|
|
773
776
|
/** Transform and add a polygon to the gl draw list
|
|
774
777
|
* @param {Array<Vector2>} points - Array of Vector2 points
|
|
775
778
|
* @param {number} rgba - Color of the polygon as a 32-bit integer
|
|
@@ -782,15 +785,21 @@ function glDrawUntextured(x, y, sizeX, sizeY, angle, rgba)
|
|
|
782
785
|
* @memberof WebGL */
|
|
783
786
|
function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
784
787
|
{
|
|
785
|
-
|
|
788
|
+
// written into vectors kept for this, a circle would make one for each of its sides at every draw; they are
|
|
789
|
+
// read into the batch before this returns
|
|
790
|
+
const pointsOut = glPointsOut, pool = glPointsPool, count = points.length;
|
|
791
|
+
pointsOut.length = count;
|
|
786
792
|
const sa = sin(-angle);
|
|
787
793
|
const ca = cos(-angle);
|
|
788
|
-
for (
|
|
794
|
+
for (let i = 0; i < count; ++i)
|
|
789
795
|
{
|
|
790
796
|
// transform the point
|
|
797
|
+
const p = points[i], out = pool[i] ||= vec2();
|
|
791
798
|
const px = p.x*sx;
|
|
792
799
|
const py = p.y*sy;
|
|
793
|
-
|
|
800
|
+
out.x = x + ca*px - sa*py;
|
|
801
|
+
out.y = y + sa*px + ca*py;
|
|
802
|
+
pointsOut[i] = out;
|
|
794
803
|
}
|
|
795
804
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
796
805
|
glDrawPoints(drawPoints, rgba);
|