littlejsengine 1.22.0 → 1.23.1

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.
@@ -593,45 +593,50 @@ function tweenUpdate(gameDelta, realDelta)
593
593
  const list = tweenUpdateList.length ? [] : tweenUpdateList, pass = ++tweenUpdatePass;
594
594
  for (const t of tweenActive)
595
595
  list.push(t);
596
- for (let i = list.length; i--;)
596
+ // the list is let go however the walk ends: a callback that throws must not leave it held, or every
597
+ // update after would take it for an update still going and make a list of its own
598
+ try
597
599
  {
598
- const t = list[i];
599
- // stopped, or started again by a callback this update, or during an update a callback ran inside it, which
600
- // counts on from this one
601
- if (!t.active || t.activePass >= pass) continue;
602
- let dt;
603
- if (enginePath)
600
+ for (let i = list.length; i--;)
604
601
  {
605
- // a paused tween keeps count too, so it does not jump when resumed
606
- dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
607
- t.lastTime = time;
608
- t.lastTimeReal = timeReal;
609
- }
610
- else
611
- dt = t.useRealTime ? realDelta : gameDelta;
612
- if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
613
- if (t.paused || dt <= 0) continue;
602
+ const t = list[i];
603
+ // stopped, or started again by a callback this update, or during an update a callback ran inside it, which
604
+ // counts on from this one
605
+ if (!t.active || t.activePass >= pass) continue;
606
+ let dt;
607
+ if (enginePath)
608
+ {
609
+ // a paused tween keeps count too, so it does not jump when resumed
610
+ dt = t.useRealTime ? timeReal - t.lastTimeReal : time - t.lastTime;
611
+ t.lastTime = time;
612
+ t.lastTimeReal = timeReal;
613
+ }
614
+ else
615
+ dt = t.useRealTime ? realDelta : gameDelta;
616
+ if (t.target?.destroyed) { t.stop(); continue; } // its object is gone, paused or not
617
+ if (t.paused || dt <= 0) continue;
614
618
 
615
- t.life -= dt;
616
- if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
617
- {
618
- t.callback(t.interp(t.life));
619
- }
620
- else
621
- {
622
- // Completion: fire end value, remove from active, start the next iteration
623
- // of a loop or pingPong, or when there is none it has completed, fire onComplete
624
- t.callback(t.interp(0));
625
- if (!t.active || t.activePass >= pass)
626
- continue; // stopped or restarted by its own callback, the run it was on ends without completing
627
- tweenDeactivate(t);
628
- const next = t.thenCallback;
629
- t.thenCallback = undefined;
630
- if (!(next && next()) && t.onComplete)
631
- t.onComplete();
619
+ t.life -= dt;
620
+ if (t.life > 1e-9) // the engine's deltas add up a rounding error short of the duration
621
+ {
622
+ t.callback(t.interp(t.life));
623
+ }
624
+ else
625
+ {
626
+ // Completion: fire end value, remove from active, start the next iteration
627
+ // of a loop or pingPong, or when there is none it has completed, fire onComplete
628
+ t.callback(t.interp(0));
629
+ if (!t.active || t.activePass >= pass)
630
+ continue; // stopped or restarted by its own callback, the run it was on ends without completing
631
+ tweenDeactivate(t);
632
+ const next = t.thenCallback;
633
+ t.thenCallback = undefined;
634
+ if (!(next && next()) && t.onComplete)
635
+ t.onComplete();
636
+ }
632
637
  }
633
638
  }
634
- list.length = 0;
639
+ finally { list.length = 0; }
635
640
  }
636
641
 
637
642
  /** Stop every active tween, ending loops too, without calling their then-callbacks.
@@ -615,8 +615,7 @@ class UISystemPlugin
615
615
  }
616
616
  }
617
617
 
618
- /** Destroy and remove all objects
619
- * @memberof UISystem */
618
+ /** Destroy and remove all objects */
620
619
  destroyObjects()
621
620
  {
622
621
  for (const o of this.uiObjects)
@@ -1137,8 +1136,10 @@ class UIObject
1137
1136
  return void inputClearKey(0, 0, false, true, false);
1138
1137
  }
1139
1138
  }
1139
+ // the object that was the active one going into this update: one pressed and let go inside a
1140
+ // frame is clicked on the next, with its release, not on both
1140
1141
  if (!uiSystem.activateOnPress)
1141
- if (!mouseDown && this.isActiveObject() && this.interactive)
1142
+ if (!mouseDown && isActive && this.isActiveObject() && this.interactive)
1142
1143
  this.click();
1143
1144
  if (this.destroyed) return;
1144
1145
  }
@@ -1881,7 +1882,10 @@ class UIVideo extends UIObject
1881
1882
  if (this.destroyed)
1882
1883
  return;
1883
1884
 
1885
+ // let go of the media too, a paused video keeps what it has loaded
1884
1886
  this.video.pause();
1887
+ this.video.removeAttribute?.('src');
1888
+ this.video.load?.();
1885
1889
  this.video.remove();
1886
1890
  super.destroy();
1887
1891
  }
package/plugins/zzfxm.js CHANGED
@@ -1,160 +1,162 @@
1
- /**
2
- * LittleJS ZzFXM Plugin
3
- * @namespace ZzFXM
4
- */
5
-
6
- 'use strict';
7
-
8
- /**
9
- * Music Object - Stores a zzfx music track for later use
10
- *
11
- * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
12
- * @extends Sound
13
- * @memberof ZzFXM
14
- * @example
15
- * // create some music
16
- * const music_example = new ZzFXMusic(
17
- * [
18
- * [ // instruments
19
- * [,0,400] // simple note
20
- * ],
21
- * [ // patterns
22
- * [ // pattern 1
23
- * [ // channel 0
24
- * 0, -1, // instrument 0, left speaker
25
- * 1, 0, 9, 1 // channel notes
26
- * ],
27
- * [ // channel 1
28
- * 0, 1, // instrument 0, right speaker
29
- * 0, 12, 17, -1 // channel notes
30
- * ]
31
- * ],
32
- * ],
33
- * [0, 0, 0, 0], // sequence, play pattern 0 four times
34
- * 90 // BPM
35
- * ]);
36
- *
37
- * // play the music on a loop
38
- * music_example.playMusic();
39
- */
40
- class ZzFXMusic extends Sound
41
- {
42
- /** Create a music object and cache the zzfx music samples for later use
43
- * @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
44
- */
45
- constructor(zzfxMusic)
46
- {
47
- super(undefined);
48
-
49
- if (!soundEnable || headlessMode) return;
50
- const [instruments, patterns, sequence, BPM] = zzfxMusic;
51
- super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
52
- this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
53
- this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
54
- }
55
- }
56
-
57
- ///////////////////////////////////////////////////////////////////////////////
58
- // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
59
-
60
- /** Generate samples for a ZzFM song with given parameters
61
- * @param {Array} instruments - Array of ZzFX sound parameters
62
- * @param {Array} patterns - Array of pattern data
63
- * @param {Array} sequence - Array of pattern indexes
64
- * @param {number} [BPM] - Playback speed of the song in BPM
65
- * @return {Array} - Left and right channel sample data
66
- * @memberof ZzFXM */
67
- function zzfxM(instruments, patterns, sequence, BPM = 125)
68
- {
69
- let i, j, k;
70
- let instrumentParameters;
71
- let note;
72
- let sample;
73
- let patternChannel;
74
- let notFirstBeat;
75
- let stop;
76
- let instrument;
77
- let attenuation;
78
- let outSampleOffset;
79
- let isSequenceEnd;
80
- let sampleOffset = 0;
81
- let nextSampleOffset;
82
- let sampleBuffer = [];
83
- let leftChannelBuffer = [];
84
- let rightChannelBuffer = [];
85
- let channelIndex = 0;
86
- let panning = 0;
87
- let hasMore = 1;
88
- let sampleCache = {};
89
- let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
90
-
91
- // for each channel in order until there are no more
92
- for (; hasMore; channelIndex++) {
93
-
94
- // reset current values
95
- sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
96
-
97
- // for each pattern in sequence
98
- sequence.forEach((patternIndex, sequenceIndex)=> {
99
- // get pattern for current channel, use empty 1 note pattern if none found
100
- patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
101
-
102
- // check if there are more channels
103
- hasMore |= patterns[patternIndex][channelIndex]&&1;
104
-
105
- // get next offset, use the length of first channel
106
- nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
107
- // for each beat in pattern, plus one extra if end of sequence
108
- isSequenceEnd = sequenceIndex === sequence.length - 1;
109
- for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
110
-
111
- // <channel-note>
112
- note = patternChannel[i];
113
-
114
- // stop if end, different instrument or new note
115
- stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
116
- instrument !== (patternChannel[0] || 0) || note | 0;
117
-
118
- // fill buffer with samples for previous beat, most cpu intensive part
119
- for (j = 0; j < beatLength && notFirstBeat;
120
-
121
- // fade off attenuation at end of beat if stopping note, prevents clicking
122
- j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
123
- ) {
124
- // copy sample to stereo buffers with panning
125
- sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
126
- leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
127
- rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
128
- }
129
-
130
- // set up for next note
131
- if (note) {
132
- // set attenuation
133
- attenuation = note % 1;
134
- panning = patternChannel[1] || 0;
135
- if (note |= 0) {
136
- // get cached sample
137
- sampleBuffer = sampleCache[
138
- [
139
- instrument = patternChannel[sampleOffset = 0] || 0,
140
- note
141
- ]
142
- ] = sampleCache[[instrument, note]] || (
143
- // add sample to cache
144
- instrumentParameters = [...instruments[instrument]],
145
- instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
146
-
147
- // allow negative values to stop notes
148
- note > 0 ? zzfxG(...instrumentParameters) : []
149
- );
150
- }
151
- }
152
- }
153
-
154
- // update the sample offset
155
- outSampleOffset = nextSampleOffset;
156
- });
157
- }
158
-
159
- return [leftChannelBuffer, rightChannelBuffer];
1
+ /**
2
+ * LittleJS ZzFXM Plugin
3
+ * - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
4
+ * https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
5
+ * @namespace ZzFXM
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ /**
11
+ * Music Object - Stores a zzfx music track for later use
12
+ *
13
+ * <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
14
+ * @extends Sound
15
+ * @memberof ZzFXM
16
+ * @example
17
+ * // create some music
18
+ * const music_example = new ZzFXMusic(
19
+ * [
20
+ * [ // instruments
21
+ * [,0,400] // simple note
22
+ * ],
23
+ * [ // patterns
24
+ * [ // pattern 1
25
+ * [ // channel 0
26
+ * 0, -1, // instrument 0, left speaker
27
+ * 1, 0, 9, 1 // channel notes
28
+ * ],
29
+ * [ // channel 1
30
+ * 0, 1, // instrument 0, right speaker
31
+ * 0, 12, 17, -1 // channel notes
32
+ * ]
33
+ * ],
34
+ * ],
35
+ * [0, 0, 0, 0], // sequence, play pattern 0 four times
36
+ * 90 // BPM
37
+ * ]);
38
+ *
39
+ * // play the music on a loop
40
+ * music_example.playMusic();
41
+ */
42
+ class ZzFXMusic extends Sound
43
+ {
44
+ /** Create a music object and cache the zzfx music samples for later use
45
+ * @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
46
+ */
47
+ constructor(zzfxMusic)
48
+ {
49
+ super(undefined);
50
+
51
+ if (!soundEnable || headlessMode) return;
52
+ const [instruments, patterns, sequence, BPM] = zzfxMusic;
53
+ super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
54
+ this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
55
+ this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
56
+ }
57
+ }
58
+
59
+ ///////////////////////////////////////////////////////////////////////////////
60
+ // ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
61
+
62
+ /** Generate samples for a ZzFM song with given parameters
63
+ * @param {Array} instruments - Array of ZzFX sound parameters
64
+ * @param {Array} patterns - Array of pattern data
65
+ * @param {Array} sequence - Array of pattern indexes
66
+ * @param {number} [BPM] - Playback speed of the song in BPM
67
+ * @return {Array} - Left and right channel sample data
68
+ * @memberof ZzFXM */
69
+ function zzfxM(instruments, patterns, sequence, BPM = 125)
70
+ {
71
+ let i, j, k;
72
+ let instrumentParameters;
73
+ let note;
74
+ let sample;
75
+ let patternChannel;
76
+ let notFirstBeat;
77
+ let stop;
78
+ let instrument;
79
+ let attenuation;
80
+ let outSampleOffset;
81
+ let isSequenceEnd;
82
+ let sampleOffset = 0;
83
+ let nextSampleOffset;
84
+ let sampleBuffer = [];
85
+ let leftChannelBuffer = [];
86
+ let rightChannelBuffer = [];
87
+ let channelIndex = 0;
88
+ let panning = 0;
89
+ let hasMore = 1;
90
+ let sampleCache = {};
91
+ let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
92
+
93
+ // for each channel in order until there are no more
94
+ for (; hasMore; channelIndex++) {
95
+
96
+ // reset current values
97
+ sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
98
+
99
+ // for each pattern in sequence
100
+ sequence.forEach((patternIndex, sequenceIndex)=> {
101
+ // get pattern for current channel, use empty 1 note pattern if none found
102
+ patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
103
+
104
+ // check if there are more channels
105
+ hasMore |= patterns[patternIndex][channelIndex]&&1;
106
+
107
+ // get next offset, use the length of first channel
108
+ nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
109
+ // for each beat in pattern, plus one extra if end of sequence
110
+ isSequenceEnd = sequenceIndex === sequence.length - 1;
111
+ for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
112
+
113
+ // <channel-note>
114
+ note = patternChannel[i];
115
+
116
+ // stop if end, different instrument or new note
117
+ stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
118
+ instrument !== (patternChannel[0] || 0) || note | 0;
119
+
120
+ // fill buffer with samples for previous beat, most cpu intensive part
121
+ for (j = 0; j < beatLength && notFirstBeat;
122
+
123
+ // fade off attenuation at end of beat if stopping note, prevents clicking
124
+ j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
125
+ ) {
126
+ // copy sample to stereo buffers with panning
127
+ sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
128
+ leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
129
+ rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
130
+ }
131
+
132
+ // set up for next note
133
+ if (note) {
134
+ // set attenuation
135
+ attenuation = note % 1;
136
+ panning = patternChannel[1] || 0;
137
+ if (note |= 0) {
138
+ // get cached sample
139
+ sampleBuffer = sampleCache[
140
+ [
141
+ instrument = patternChannel[sampleOffset = 0] || 0,
142
+ note
143
+ ]
144
+ ] = sampleCache[[instrument, note]] || (
145
+ // add sample to cache
146
+ instrumentParameters = [...instruments[instrument]],
147
+ instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
148
+
149
+ // allow negative values to stop notes
150
+ note > 0 ? zzfxG(...instrumentParameters) : []
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ // update the sample offset
157
+ outSampleOffset = nextSampleOffset;
158
+ });
159
+ }
160
+
161
+ return [leftChannelBuffer, rightChannelBuffer];
160
162
  }
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.22.0';
35
+ const engineVersion = '1.23.1';
36
36
 
37
37
  /** Frames per second to update
38
38
  * @type {number}
@@ -222,7 +222,13 @@ class Sound
222
222
  */
223
223
  constructor(asset, randomness, range, taper=soundDefaultTaper, onloadCallback)
224
224
  {
225
- if (!soundEnable || headlessMode) return;
225
+ if (!soundEnable || headlessMode)
226
+ {
227
+ // no sound is made: it counts as loaded, so a game that waits for its sounds goes on
228
+ this.loadedPercent = 1;
229
+ onloadCallback?.(this);
230
+ return;
231
+ }
226
232
  const rangeIsDefault = range === undefined;
227
233
  if (rangeIsDefault)
228
234
  range = soundDefaultRange;
@@ -233,10 +239,9 @@ class Sound
233
239
  ASSERT(isNumber(range), 'range must be a number');
234
240
  ASSERT(isNumber(taper), 'taper must be a number');
235
241
 
236
- /** @property {number} - World space max range of sound */
237
242
  this.range = range;
238
243
  /** @property {boolean} - No range was given, so it got soundDefaultRange; played in 3D such a sound is heard
239
- * to render3D.soundDefaultRange instead, which is further */
244
+ * to render3D.soundDefaultRange instead, which is further; setting range makes it false */
240
245
  this.rangeIsDefault = rangeIsDefault;
241
246
  /** @property {number} - At what percentage of range should it start tapering */
242
247
  this.taper = taper;
@@ -292,6 +297,15 @@ class Sound
292
297
  }
293
298
  }
294
299
 
300
+ /** World space max range of sound, 0 for no limit; a range that is set is the sound's own, in 3D too
301
+ * @type {number} */
302
+ get range() { return this.rangeValue; }
303
+ set range(range)
304
+ {
305
+ this.rangeValue = range;
306
+ this.rangeIsDefault = false;
307
+ }
308
+
295
309
  /** Sample data for each channel
296
310
  * Sounds keep their samples in an audio buffer, so reading this rebuilds
297
311
  * the arrays from it and caches them. The copies are safe to hold onto,
@@ -6,7 +6,7 @@
6
6
  * - Produces TypeScript definition file (.d.ts)
7
7
  * - Includes plugin files in builds
8
8
  * - Validates code and checks for errors
9
- * - Outputs to dist/ folder
9
+ * - Outputs to dist/ folder, which is left as it was when a step fails
10
10
  */
11
11
 
12
12
  import fs from 'node:fs';
@@ -19,7 +19,9 @@ const __dirname = dirname(__filename);
19
19
  const ROOT_DIR = join(__dirname, '..');
20
20
 
21
21
  const ENGINE_NAME = 'littlejs';
22
- const BUILD_FOLDER = join(ROOT_DIR, 'dist');
22
+ // built in a folder of its own and put in dist when all of it is there, so a step that fails leaves dist whole
23
+ const OUTPUT_FOLDER = join(ROOT_DIR, 'dist');
24
+ const BUILD_FOLDER = join(ROOT_DIR, 'dist.build');
23
25
  const SOURCE_FOLDER = join(ROOT_DIR, 'src');
24
26
  const PLUGIN_FOLDER = join(ROOT_DIR, 'plugins');
25
27
  const engineSourceFiles =
@@ -50,6 +52,7 @@ const enginePluginFiles =
50
52
  `${PLUGIN_FOLDER}/textureSheet.js`,
51
53
  `${PLUGIN_FOLDER}/tweenSystem.js`,
52
54
  `${PLUGIN_FOLDER}/sceneSystem.js`,
55
+ `${PLUGIN_FOLDER}/parallax.js`,
53
56
  `${PLUGIN_FOLDER}/pathFinder.js`,
54
57
  `${PLUGIN_FOLDER}/math3d.js`,
55
58
  `${PLUGIN_FOLDER}/render3d.js`,
@@ -103,6 +106,19 @@ catch (e) { handleError(e, 'Failed to create build folder!'); }
103
106
  // Build all versions
104
107
  await buildAll();
105
108
 
109
+ try
110
+ {
111
+ // every step is done: dist is given what was built, and loses what is no longer built
112
+ fs.mkdirSync(OUTPUT_FOLDER, { recursive: true });
113
+ const built = fs.readdirSync(BUILD_FOLDER);
114
+ for (const file of built)
115
+ fs.copyFileSync(join(BUILD_FOLDER, file), join(OUTPUT_FOLDER, file));
116
+ for (const file of fs.readdirSync(OUTPUT_FOLDER))
117
+ built.includes(file) || fs.rmSync(join(OUTPUT_FOLDER, file), { recursive: true, force: true });
118
+ fs.rmSync(BUILD_FOLDER, { recursive: true, force: true });
119
+ }
120
+ catch (e) { handleError(e, 'Failed to put the build in dist!'); }
121
+
106
122
  console.log(`Engine built in ${((Date.now() - startTime)/1e3).toFixed(2)} seconds! ✨`);
107
123
 
108
124
  ///////////////////////////////////////////////////////////////////////////////
package/src/engineDraw.js CHANGED
@@ -496,17 +496,29 @@ class SpriteAnimator
496
496
  * @return {SpriteAnimation} */
497
497
  get clip() { return this.clips[this.name]; }
498
498
 
499
+ /** The clip showing now, once a clip that has ended has called its onEnd: the callback may switch clips, and a
500
+ * read gives the clip it switched to, not the one that ended
501
+ * @return {SpriteAnimation}
502
+ * @ignore */
503
+ get settledClip()
504
+ {
505
+ // each clip's own isDone calls its onEnd once; a few switches in a row at most
506
+ for (let i = 4, clip; i-- && clip !== this.clip;)
507
+ (clip = this.clip).isDone;
508
+ return this.clip;
509
+ }
510
+
499
511
  /** The tile of the frame showing now
500
512
  * @return {TileInfo} */
501
- get tileInfo() { return this.clip.tileInfo; }
513
+ get tileInfo() { return this.settledClip.tileInfo; }
502
514
 
503
515
  /** The frame of the clip showing now
504
516
  * @return {number} */
505
- get frame() { return this.clip.frame; }
517
+ get frame() { return this.settledClip.frame; }
506
518
 
507
519
  /** True once the clip showing now is a play that has ended
508
520
  * @return {boolean} */
509
- get isDone() { return this.clip.isDone; }
521
+ get isDone() { return this.settledClip.isDone; }
510
522
  }
511
523
 
512
524
  ///////////////////////////////////////////////////////////////////////////////
@@ -931,7 +943,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
931
943
  // build regular polygon points
932
944
  const points = [];
933
945
  const sizeX = size.x/2, sizeY = size.y/2;
934
- for (let i=sides; i--;)
946
+ for (let i=sides; i-- > 0;) // a count that is not whole, or below zero, still ends
935
947
  {
936
948
  const a = (i/sides)*PI*2;
937
949
  points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
@@ -1042,7 +1054,7 @@ function drawEllipse(pos, size=vec2(1), color=WHITE, angle=0, lineWidth=0, lineC
1042
1054
  if (!ring)
1043
1055
  {
1044
1056
  const points = [];
1045
- for (let i=sides; i--;)
1057
+ for (let i=sides; i-- > 0;)
1046
1058
  {
1047
1059
  const a = (i/sides)*PI*2;
1048
1060
  points.push(vec2(sin(a), cos(a)));
@@ -1142,7 +1154,7 @@ function drawEllipseGradient(pos, size=vec2(1), colorInner=WHITE, colorOuter=CLE
1142
1154
  const startA = (offset%sides)/sides*PI*2;
1143
1155
  const points = [rim(startA)];
1144
1156
  const colors = [outerInt];
1145
- for (let i=sides; i--;)
1157
+ for (let i=sides; i-- > 0;)
1146
1158
  {
1147
1159
  const a = ((i+offset)%sides)/sides*PI*2;
1148
1160
  points.push(pos);
@@ -1565,7 +1577,8 @@ function isOnScreen(pos, size=0)
1565
1577
  function setAdditiveBlendMode(additive=true)
1566
1578
  {
1567
1579
  glAdditive = additive;
1568
- drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1580
+ if (drawContext) // none headless
1581
+ drawContext.globalCompositeOperation = additive ? 'lighter' : 'source-over';
1569
1582
  }
1570
1583
 
1571
1584
  /** Set the Shader that 2D draws use from now on, none for the engine's own
@@ -1688,9 +1701,12 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
1688
1701
  }
1689
1702
  else
1690
1703
  {
1691
- // copy to offscreen canvas
1692
- workReadCanvas.width = sWidth;
1693
- workReadCanvas.height = sHeight;
1704
+ // copy to offscreen canvas, sized again only when the size changes: setting a size remakes the canvas even
1705
+ // to the same one, and a game's tinted tiles are mostly of one size
1706
+ if (workReadCanvas.width !== sWidth || workReadCanvas.height !== sHeight)
1707
+ workReadCanvas.width = sWidth, workReadCanvas.height = sHeight;
1708
+ else
1709
+ workReadContext.clearRect(0, 0, sWidth, sHeight);
1694
1710
  workReadContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
1695
1711
 
1696
1712
  // tint image using offscreen work context