@hkdigital/lib-core 0.4.27 → 0.4.28
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/network/loaders/audio/AudioScene.svelte.d.ts +31 -28
- package/dist/network/loaders/audio/AudioScene.svelte.js +36 -166
- package/dist/network/loaders/audio/README.md +195 -0
- package/dist/network/loaders/base/SceneBase.svelte.d.ts +65 -0
- package/dist/network/loaders/base/SceneBase.svelte.js +283 -0
- package/dist/network/loaders/image/ImageScene.svelte.d.ts +12 -24
- package/dist/network/loaders/image/ImageScene.svelte.js +17 -155
- package/package.json +1 -1
|
@@ -8,36 +8,23 @@
|
|
|
8
8
|
* @property {AudioLoader} audioLoader
|
|
9
9
|
* @property {SourceConfig} [config]
|
|
10
10
|
*/
|
|
11
|
-
export default class AudioScene {
|
|
12
|
-
state: string;
|
|
13
|
-
loaded: boolean;
|
|
11
|
+
export default class AudioScene extends SceneBase {
|
|
14
12
|
muted: boolean;
|
|
15
13
|
targetGain: number;
|
|
16
14
|
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
totalBytesLoaded: number;
|
|
21
|
-
totalSize: number;
|
|
22
|
-
sourcesLoaded: number;
|
|
23
|
-
numberOfSources: number;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Get audio scene abort progress
|
|
27
|
-
*/
|
|
28
|
-
get abortProgress(): {
|
|
29
|
-
sourcesAborted: number;
|
|
30
|
-
numberOfSources: number;
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Start loading all audio sources
|
|
15
|
+
* Get the array of memory sources managed by this scene
|
|
16
|
+
*
|
|
17
|
+
* @returns {MemorySource[]}
|
|
34
18
|
*/
|
|
35
|
-
|
|
19
|
+
get sources(): MemorySource[];
|
|
36
20
|
/**
|
|
37
|
-
*
|
|
21
|
+
* Extract the audio loader from a source object
|
|
22
|
+
*
|
|
23
|
+
* @param {MemorySource} source
|
|
24
|
+
*
|
|
25
|
+
* @returns {AudioLoader}
|
|
38
26
|
*/
|
|
39
|
-
|
|
40
|
-
destroy(): void;
|
|
27
|
+
getLoaderFromSource(source: MemorySource): AudioLoader;
|
|
41
28
|
/**
|
|
42
29
|
* Add in-memory audio source
|
|
43
30
|
* - Uses an AudioLoader instance to load audio data from network
|
|
@@ -67,18 +54,33 @@ export default class AudioScene {
|
|
|
67
54
|
*/
|
|
68
55
|
setAudioContext(audioContext?: AudioContext): void;
|
|
69
56
|
/**
|
|
70
|
-
* Set target gain
|
|
57
|
+
* Set target gain (volume level) for all audio in this scene
|
|
58
|
+
* - Currently applies immediately, but "target" allows for future
|
|
59
|
+
* smooth transitions using Web Audio API's gain scheduling
|
|
60
|
+
* - Range: 0.0 (silence) to 1.0 (original) to 1.0+ (amplified)
|
|
71
61
|
*
|
|
72
|
-
* @param {number} value
|
|
62
|
+
* @param {number} value - Target gain value (0.0 to 1.0+)
|
|
73
63
|
*/
|
|
74
64
|
setTargetGain(value: number): void;
|
|
75
65
|
/**
|
|
76
|
-
* Get the
|
|
66
|
+
* Get the current target gain (volume level)
|
|
77
67
|
*
|
|
78
|
-
* @returns {number} value
|
|
68
|
+
* @returns {number} Target gain value (0.0 to 1.0+)
|
|
79
69
|
*/
|
|
80
70
|
getTargetGain(): number;
|
|
71
|
+
/**
|
|
72
|
+
* Mute all audio in this scene
|
|
73
|
+
* - Saves current volume level for restoration
|
|
74
|
+
* - Sets target gain to 0 (silence)
|
|
75
|
+
* - Safe to call multiple times
|
|
76
|
+
*/
|
|
81
77
|
mute(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Unmute all audio in this scene
|
|
80
|
+
* - Restores volume level from before muting
|
|
81
|
+
* - Safe to call multiple times
|
|
82
|
+
* - No effect if scene is not currently muted
|
|
83
|
+
*/
|
|
82
84
|
unmute(): void;
|
|
83
85
|
#private;
|
|
84
86
|
}
|
|
@@ -91,4 +93,5 @@ export type MemorySource = {
|
|
|
91
93
|
audioLoader: AudioLoader;
|
|
92
94
|
config?: SourceConfig;
|
|
93
95
|
};
|
|
96
|
+
import SceneBase from '../base/SceneBase.svelte.js';
|
|
94
97
|
import AudioLoader from './AudioLoader.svelte.js';
|
|
@@ -1,20 +1,6 @@
|
|
|
1
1
|
import * as expect from '../../../util/expect.js';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
STATE_INITIAL,
|
|
7
|
-
STATE_LOADING,
|
|
8
|
-
STATE_LOADED,
|
|
9
|
-
STATE_ABORTING,
|
|
10
|
-
STATE_ABORTED,
|
|
11
|
-
STATE_ERROR,
|
|
12
|
-
LOAD,
|
|
13
|
-
LOADED,
|
|
14
|
-
ABORT,
|
|
15
|
-
ABORTED
|
|
16
|
-
} from '../../../state/machines.js';
|
|
17
|
-
|
|
3
|
+
import SceneBase from '../base/SceneBase.svelte.js';
|
|
18
4
|
import AudioLoader from './AudioLoader.svelte.js';
|
|
19
5
|
|
|
20
6
|
/**
|
|
@@ -29,15 +15,7 @@ import AudioLoader from './AudioLoader.svelte.js';
|
|
|
29
15
|
* @property {SourceConfig} [config]
|
|
30
16
|
*/
|
|
31
17
|
|
|
32
|
-
export default class AudioScene {
|
|
33
|
-
#state = new LoadingStateMachine();
|
|
34
|
-
|
|
35
|
-
// @note this exported state is set by onenter
|
|
36
|
-
state = $state(STATE_INITIAL);
|
|
37
|
-
|
|
38
|
-
loaded = $derived.by(() => {
|
|
39
|
-
return this.state === STATE_LOADED;
|
|
40
|
-
});
|
|
18
|
+
export default class AudioScene extends SceneBase {
|
|
41
19
|
|
|
42
20
|
#targetGain = $state(1);
|
|
43
21
|
|
|
@@ -56,143 +34,35 @@ export default class AudioScene {
|
|
|
56
34
|
/** @type {MemorySource[]} */
|
|
57
35
|
#memorySources = $state([]);
|
|
58
36
|
|
|
59
|
-
#progress = $derived.by(() => {
|
|
60
|
-
// console.log('update progress');
|
|
61
|
-
|
|
62
|
-
let totalSize = 0;
|
|
63
|
-
let totalBytesLoaded = 0;
|
|
64
|
-
let sourcesLoaded = 0;
|
|
65
|
-
|
|
66
|
-
const sources = this.#memorySources;
|
|
67
|
-
const numberOfSources = sources.length;
|
|
68
|
-
|
|
69
|
-
for (let j = 0; j < numberOfSources; j++) {
|
|
70
|
-
const source = sources[j];
|
|
71
|
-
const { audioLoader } = source;
|
|
72
|
-
|
|
73
|
-
const { bytesLoaded, size, loaded } = audioLoader.progress;
|
|
74
|
-
|
|
75
|
-
totalSize += size;
|
|
76
|
-
totalBytesLoaded += bytesLoaded;
|
|
77
|
-
|
|
78
|
-
if (loaded) {
|
|
79
|
-
sourcesLoaded++;
|
|
80
|
-
}
|
|
81
|
-
} // end for
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
totalBytesLoaded,
|
|
85
|
-
totalSize,
|
|
86
|
-
sourcesLoaded,
|
|
87
|
-
numberOfSources
|
|
88
|
-
};
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
#abortProgress = $derived.by(() => {
|
|
92
|
-
let sourcesAborted = 0;
|
|
93
|
-
const sources = this.#memorySources;
|
|
94
|
-
const numberOfSources = sources.length;
|
|
95
|
-
|
|
96
|
-
for (let j = 0; j < numberOfSources; j++) {
|
|
97
|
-
const source = sources[j];
|
|
98
|
-
const { audioLoader } = source;
|
|
99
|
-
const loaderState = audioLoader.state;
|
|
100
|
-
|
|
101
|
-
if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
|
|
102
|
-
sourcesAborted++;
|
|
103
|
-
}
|
|
104
|
-
} // end for
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
sourcesAborted,
|
|
108
|
-
numberOfSources
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
37
|
|
|
112
38
|
/**
|
|
113
39
|
* Construct AudioScene
|
|
114
40
|
*/
|
|
115
41
|
constructor() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
$effect(() => {
|
|
119
|
-
if (state.current === STATE_LOADING) {
|
|
120
|
-
// console.log(
|
|
121
|
-
// 'progress',
|
|
122
|
-
// JSON.stringify($state.snapshot(this.#progress))
|
|
123
|
-
// );
|
|
124
|
-
|
|
125
|
-
const { sourcesLoaded, numberOfSources } = this.#progress;
|
|
126
|
-
|
|
127
|
-
if (sourcesLoaded === numberOfSources) {
|
|
128
|
-
// console.debug(`AudioScene: ${numberOfSources} sources loaded`);
|
|
129
|
-
this.#state.send(LOADED);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
$effect(() => {
|
|
135
|
-
if (state.current === STATE_ABORTING) {
|
|
136
|
-
const { sourcesAborted, numberOfSources } = this.#abortProgress;
|
|
137
|
-
|
|
138
|
-
if (sourcesAborted === numberOfSources) {
|
|
139
|
-
// console.debug(`AudioScene: ${numberOfSources} sources aborted`);
|
|
140
|
-
this.#state.send(ABORTED);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
state.onenter = ( currentState ) => {
|
|
146
|
-
// console.log('onenter', currentState );
|
|
147
|
-
|
|
148
|
-
if(currentState === STATE_LOADING )
|
|
149
|
-
{
|
|
150
|
-
// console.log('AudioScene:loading');
|
|
151
|
-
this.#startLoading();
|
|
152
|
-
}
|
|
153
|
-
else if(currentState === STATE_ABORTING )
|
|
154
|
-
{
|
|
155
|
-
// console.log('AudioScene:aborting');
|
|
156
|
-
this.#startAbort();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
this.state = state.current;
|
|
160
|
-
};
|
|
42
|
+
super();
|
|
161
43
|
}
|
|
162
44
|
|
|
163
|
-
/* ====
|
|
45
|
+
/* ==== SceneBase implementation */
|
|
164
46
|
|
|
165
47
|
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return this.#progress;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get audio scene abort progress
|
|
174
|
-
*/
|
|
175
|
-
get abortProgress() {
|
|
176
|
-
return this.#abortProgress;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Start loading all audio sources
|
|
48
|
+
* Get the array of memory sources managed by this scene
|
|
49
|
+
*
|
|
50
|
+
* @returns {MemorySource[]}
|
|
181
51
|
*/
|
|
182
|
-
|
|
183
|
-
this.#
|
|
52
|
+
get sources() {
|
|
53
|
+
return this.#memorySources;
|
|
184
54
|
}
|
|
185
55
|
|
|
186
56
|
/**
|
|
187
|
-
*
|
|
57
|
+
* Extract the audio loader from a source object
|
|
58
|
+
*
|
|
59
|
+
* @param {MemorySource} source
|
|
60
|
+
*
|
|
61
|
+
* @returns {AudioLoader}
|
|
188
62
|
*/
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
destroy() {
|
|
194
|
-
// TODO: disconnect all audio sources?
|
|
195
|
-
// TODO: Unload AUdioLoaders?
|
|
63
|
+
// eslint-disable-next-line no-unused-vars
|
|
64
|
+
getLoaderFromSource(source) {
|
|
65
|
+
return source.audioLoader;
|
|
196
66
|
}
|
|
197
67
|
|
|
198
68
|
/* ==== Source definitions */
|
|
@@ -260,28 +130,16 @@ export default class AudioScene {
|
|
|
260
130
|
this.#audioContext = audioContext;
|
|
261
131
|
}
|
|
262
132
|
|
|
263
|
-
async #startLoading() {
|
|
264
|
-
// console.log('#startLoading');
|
|
265
|
-
|
|
266
|
-
for (const { audioLoader } of this.#memorySources) {
|
|
267
|
-
audioLoader.load();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
#startAbort() {
|
|
272
|
-
// console.log('#startAbort');
|
|
273
|
-
|
|
274
|
-
for (const { audioLoader } of this.#memorySources) {
|
|
275
|
-
audioLoader.abort();
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
133
|
|
|
279
134
|
/* ==== Audio specific */
|
|
280
135
|
|
|
281
136
|
/**
|
|
282
|
-
* Set target gain
|
|
137
|
+
* Set target gain (volume level) for all audio in this scene
|
|
138
|
+
* - Currently applies immediately, but "target" allows for future
|
|
139
|
+
* smooth transitions using Web Audio API's gain scheduling
|
|
140
|
+
* - Range: 0.0 (silence) to 1.0 (original) to 1.0+ (amplified)
|
|
283
141
|
*
|
|
284
|
-
* @param {number} value
|
|
142
|
+
* @param {number} value - Target gain value (0.0 to 1.0+)
|
|
285
143
|
*/
|
|
286
144
|
setTargetGain( value ) {
|
|
287
145
|
this.#targetGain = value;
|
|
@@ -291,15 +149,21 @@ export default class AudioScene {
|
|
|
291
149
|
}
|
|
292
150
|
|
|
293
151
|
/**
|
|
294
|
-
* Get the
|
|
152
|
+
* Get the current target gain (volume level)
|
|
295
153
|
*
|
|
296
|
-
* @returns {number} value
|
|
154
|
+
* @returns {number} Target gain value (0.0 to 1.0+)
|
|
297
155
|
*/
|
|
298
156
|
getTargetGain()
|
|
299
157
|
{
|
|
300
158
|
return this.#targetGain;
|
|
301
159
|
}
|
|
302
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Mute all audio in this scene
|
|
163
|
+
* - Saves current volume level for restoration
|
|
164
|
+
* - Sets target gain to 0 (silence)
|
|
165
|
+
* - Safe to call multiple times
|
|
166
|
+
*/
|
|
303
167
|
mute() {
|
|
304
168
|
if( this.muted )
|
|
305
169
|
{
|
|
@@ -310,6 +174,12 @@ export default class AudioScene {
|
|
|
310
174
|
this.setTargetGain(0);
|
|
311
175
|
}
|
|
312
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Unmute all audio in this scene
|
|
179
|
+
* - Restores volume level from before muting
|
|
180
|
+
* - Safe to call multiple times
|
|
181
|
+
* - No effect if scene is not currently muted
|
|
182
|
+
*/
|
|
313
183
|
unmute() {
|
|
314
184
|
if( !this.muted )
|
|
315
185
|
{
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Audio Loaders
|
|
2
|
+
|
|
3
|
+
Audio loading and scene management for web applications using the Web Audio API.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The audio loading system consists of two main components:
|
|
8
|
+
|
|
9
|
+
- **AudioLoader** - Loads individual audio files from network sources
|
|
10
|
+
- **AudioScene** - Manages collections of audio sources with centralized playback control
|
|
11
|
+
|
|
12
|
+
## AudioScene
|
|
13
|
+
|
|
14
|
+
`AudioScene` extends `SceneBase` to provide audio-specific functionality including volume control, muting, and Web Audio API integration.
|
|
15
|
+
|
|
16
|
+
### Key Features
|
|
17
|
+
|
|
18
|
+
- **Scene-wide volume control** via gain nodes
|
|
19
|
+
- **Mute/unmute functionality** with volume restoration
|
|
20
|
+
- **Web Audio API integration** for precise audio control
|
|
21
|
+
- **Multiple audio source management** with unified loading states
|
|
22
|
+
- **Audio context management** with automatic creation or custom injection
|
|
23
|
+
|
|
24
|
+
### Audio Context Usage
|
|
25
|
+
|
|
26
|
+
AudioScene uses the Web Audio API's `AudioContext` for advanced audio processing:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
const audioScene = new AudioScene();
|
|
30
|
+
|
|
31
|
+
// Option 1: Use automatic context creation (default)
|
|
32
|
+
audioScene.load(); // Creates AudioContext internally
|
|
33
|
+
|
|
34
|
+
// Option 2: Provide your own context for integration
|
|
35
|
+
const customContext = new AudioContext();
|
|
36
|
+
audioScene.setAudioContext(customContext);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Volume Control (Target Gain)
|
|
40
|
+
|
|
41
|
+
The scene uses a "target gain" system based on Web Audio API's gain scheduling:
|
|
42
|
+
|
|
43
|
+
- **Target Gain**: The desired volume level that the audio system transitions towards
|
|
44
|
+
- `0.0` = silence (muted)
|
|
45
|
+
- `1.0` = original volume
|
|
46
|
+
- `> 1.0` = amplified (use carefully to avoid distortion)
|
|
47
|
+
|
|
48
|
+
**Why "Target"?**
|
|
49
|
+
The Web Audio API allows smooth transitions between gain values over time. When you set a target gain, the audio system can gradually transition from the current level to the target level, preventing audio pops and clicks.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// Set immediate volume change to 50%
|
|
53
|
+
audioScene.setTargetGain(0.5);
|
|
54
|
+
|
|
55
|
+
// Future: Could support smooth transitions
|
|
56
|
+
// gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.3);
|
|
57
|
+
|
|
58
|
+
// Get current target volume
|
|
59
|
+
const volume = audioScene.getTargetGain(); // 0.5
|
|
60
|
+
|
|
61
|
+
// Mute (remembers previous volume)
|
|
62
|
+
audioScene.mute();
|
|
63
|
+
|
|
64
|
+
// Restore previous volume
|
|
65
|
+
audioScene.unmute();
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The current implementation sets gain immediately, but the "target" naming prepares for potential smooth volume transitions in future versions.
|
|
69
|
+
|
|
70
|
+
### Audio Source Management
|
|
71
|
+
|
|
72
|
+
Define audio sources before loading:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// Add audio sources
|
|
76
|
+
audioScene.defineMemorySource({
|
|
77
|
+
label: 'background-music',
|
|
78
|
+
url: '/audio/bg-music.mp3'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
audioScene.defineMemorySource({
|
|
82
|
+
label: 'sound-effect',
|
|
83
|
+
url: '/audio/beep.wav'
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Load all sources
|
|
87
|
+
audioScene.load();
|
|
88
|
+
|
|
89
|
+
// Wait for loading completion
|
|
90
|
+
await waitForState(() => audioScene.loaded);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Playback
|
|
94
|
+
|
|
95
|
+
Get playable audio sources after loading:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// Get a source node for one-time playback
|
|
99
|
+
const sourceNode = await audioScene.getSourceNode('sound-effect');
|
|
100
|
+
|
|
101
|
+
// Play immediately
|
|
102
|
+
sourceNode.start();
|
|
103
|
+
|
|
104
|
+
// Play with delay
|
|
105
|
+
sourceNode.start(audioContext.currentTime + 0.5);
|
|
106
|
+
|
|
107
|
+
// Cleanup is automatic when playback ends
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Web Audio API Architecture
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
AudioSources → GainNode → AudioContext.destination → Speakers
|
|
114
|
+
↑
|
|
115
|
+
Volume Control
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each audio source connects through a gain node that provides scene-wide volume control before reaching the audio output.
|
|
119
|
+
|
|
120
|
+
### State Management
|
|
121
|
+
|
|
122
|
+
AudioScene inherits state management from SceneBase:
|
|
123
|
+
|
|
124
|
+
- `STATE_INITIAL` - Scene created, sources defined
|
|
125
|
+
- `STATE_LOADING` - Audio files downloading
|
|
126
|
+
- `STATE_LOADED` - All sources ready for playback
|
|
127
|
+
- `STATE_ABORTING` - Canceling downloads
|
|
128
|
+
- `STATE_ABORTED` - Downloads canceled
|
|
129
|
+
|
|
130
|
+
### Preloading with Progress and Abort
|
|
131
|
+
|
|
132
|
+
AudioScene supports convenient preloading with automatic progress tracking:
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Basic preload - returns promise and abort function
|
|
136
|
+
const { promise, abort } = audioScene.preload();
|
|
137
|
+
|
|
138
|
+
// Preload with options
|
|
139
|
+
const { promise, abort } = audioScene.preload({
|
|
140
|
+
timeoutMs: 5000, // Timeout after 5 seconds
|
|
141
|
+
onProgress: (progress) => {
|
|
142
|
+
console.log(`Loading: ${progress.sourcesLoaded}/${progress.numberOfSources}`);
|
|
143
|
+
console.log(`Bytes: ${progress.totalBytesLoaded}/${progress.totalSize}`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Wait for completion
|
|
148
|
+
try {
|
|
149
|
+
const loadedScene = await promise;
|
|
150
|
+
console.log('All audio loaded successfully!');
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Preload failed:', error.message);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Can abort anytime
|
|
156
|
+
document.getElementById('cancelBtn').onclick = () => abort();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Preload Features:**
|
|
160
|
+
- **Promise-based**: Returns `{ promise, abort }` object
|
|
161
|
+
- **Progress tracking**: Optional callback with loading progress
|
|
162
|
+
- **Timeout support**: Configurable timeout with automatic abort
|
|
163
|
+
- **Manual abort**: Call `abort()` function to cancel loading
|
|
164
|
+
- **Error handling**: Promise rejects on timeout, abort, or loading errors
|
|
165
|
+
|
|
166
|
+
### Progress Tracking
|
|
167
|
+
|
|
168
|
+
Monitor loading progress across all sources:
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
const progress = audioScene.progress;
|
|
172
|
+
console.log(`${progress.sourcesLoaded}/${progress.numberOfSources} loaded`);
|
|
173
|
+
console.log(`${progress.totalBytesLoaded}/${progress.totalSize} bytes`);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Best Practices
|
|
177
|
+
|
|
178
|
+
1. **Audio Context Lifecycle**: Create AudioContext in response to user interaction to avoid browser autoplay restrictions
|
|
179
|
+
|
|
180
|
+
2. **Memory Management**: Audio sources are loaded into memory - consider file sizes for mobile devices
|
|
181
|
+
|
|
182
|
+
3. **Source Node Usage**: Each `getSourceNode()` call creates a new playable instance - don't reuse source nodes
|
|
183
|
+
|
|
184
|
+
4. **Volume Levels**: Keep target gain ≤ 1.0 to avoid audio clipping and distortion
|
|
185
|
+
|
|
186
|
+
5. **Error Handling**: Check scene state before attempting playback
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
if (audioScene.loaded) {
|
|
190
|
+
const source = await audioScene.getSourceNode('audio-label');
|
|
191
|
+
source.start();
|
|
192
|
+
} else {
|
|
193
|
+
console.warn('Audio scene not ready for playback');
|
|
194
|
+
}
|
|
195
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for scene loaders that manage collections of media sources
|
|
3
|
+
*/
|
|
4
|
+
export default class SceneBase {
|
|
5
|
+
state: string;
|
|
6
|
+
loaded: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Get the array of sources managed by this scene
|
|
9
|
+
*
|
|
10
|
+
* @returns {Array} Array of source objects
|
|
11
|
+
*/
|
|
12
|
+
get sources(): any[];
|
|
13
|
+
/**
|
|
14
|
+
* Extract the loader from a source object
|
|
15
|
+
*
|
|
16
|
+
* @param {*} source
|
|
17
|
+
*
|
|
18
|
+
* @returns {*} Loader object with progress and state properties
|
|
19
|
+
*/
|
|
20
|
+
getLoaderFromSource(source: any): any;
|
|
21
|
+
/**
|
|
22
|
+
* Get scene loading progress
|
|
23
|
+
*/
|
|
24
|
+
get progress(): {
|
|
25
|
+
totalBytesLoaded: number;
|
|
26
|
+
totalSize: number;
|
|
27
|
+
sourcesLoaded: number;
|
|
28
|
+
numberOfSources: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get scene abort progress
|
|
32
|
+
*/
|
|
33
|
+
get abortProgress(): {
|
|
34
|
+
sourcesAborted: number;
|
|
35
|
+
numberOfSources: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Start loading all sources
|
|
39
|
+
*/
|
|
40
|
+
load(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Abort loading all sources
|
|
43
|
+
*/
|
|
44
|
+
abort(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Preload all sources with progress tracking and abort capability
|
|
47
|
+
* - Starts loading and waits for completion
|
|
48
|
+
* - Supports timeout and progress callbacks
|
|
49
|
+
* - Returns object with promise and abort function
|
|
50
|
+
*
|
|
51
|
+
* @param {object} [options]
|
|
52
|
+
* @param {number} [options.timeoutMs=10000] - Timeout in milliseconds
|
|
53
|
+
* @param {Function} [options.onProgress] - Progress callback function
|
|
54
|
+
*
|
|
55
|
+
* @returns {object} Object with promise and abort function
|
|
56
|
+
* @returns {Promise<SceneBase>} returns.promise - Promise that resolves when loaded
|
|
57
|
+
* @returns {Function} returns.abort - Function to abort preloading
|
|
58
|
+
*/
|
|
59
|
+
preload({ timeoutMs, onProgress }?: {
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
onProgress?: Function;
|
|
62
|
+
}): object;
|
|
63
|
+
destroy(): void;
|
|
64
|
+
#private;
|
|
65
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { LoadingStateMachine } from '../../../state/machines.js';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
STATE_INITIAL,
|
|
5
|
+
STATE_LOADING,
|
|
6
|
+
STATE_LOADED,
|
|
7
|
+
STATE_ABORTING,
|
|
8
|
+
STATE_ABORTED,
|
|
9
|
+
STATE_ERROR,
|
|
10
|
+
LOAD,
|
|
11
|
+
LOADED,
|
|
12
|
+
ABORT,
|
|
13
|
+
ABORTED
|
|
14
|
+
} from '../../../state/machines.js';
|
|
15
|
+
|
|
16
|
+
import { waitForState } from '../../../util/svelte.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base class for scene loaders that manage collections of media sources
|
|
20
|
+
*/
|
|
21
|
+
export default class SceneBase {
|
|
22
|
+
#state = new LoadingStateMachine();
|
|
23
|
+
|
|
24
|
+
// @note this exported state is set by onenter
|
|
25
|
+
state = $state(STATE_INITIAL);
|
|
26
|
+
|
|
27
|
+
loaded = $derived.by(() => {
|
|
28
|
+
return this.state === STATE_LOADED;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
#progress = $derived.by(() => {
|
|
32
|
+
let totalSize = 0;
|
|
33
|
+
let totalBytesLoaded = 0;
|
|
34
|
+
let sourcesLoaded = 0;
|
|
35
|
+
|
|
36
|
+
const sources = this.sources;
|
|
37
|
+
const numberOfSources = sources.length;
|
|
38
|
+
|
|
39
|
+
for (let j = 0; j < numberOfSources; j++) {
|
|
40
|
+
const source = sources[j];
|
|
41
|
+
const loader = this.getLoaderFromSource(source);
|
|
42
|
+
|
|
43
|
+
const { bytesLoaded, size, loaded } = loader.progress;
|
|
44
|
+
|
|
45
|
+
totalSize += size;
|
|
46
|
+
totalBytesLoaded += bytesLoaded;
|
|
47
|
+
|
|
48
|
+
if (loaded) {
|
|
49
|
+
sourcesLoaded++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
totalBytesLoaded,
|
|
55
|
+
totalSize,
|
|
56
|
+
sourcesLoaded,
|
|
57
|
+
numberOfSources
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
#abortProgress = $derived.by(() => {
|
|
62
|
+
let sourcesAborted = 0;
|
|
63
|
+
const sources = this.sources;
|
|
64
|
+
const numberOfSources = sources.length;
|
|
65
|
+
|
|
66
|
+
for (let j = 0; j < numberOfSources; j++) {
|
|
67
|
+
const source = sources[j];
|
|
68
|
+
const loader = this.getLoaderFromSource(source);
|
|
69
|
+
const loaderState = loader.state;
|
|
70
|
+
|
|
71
|
+
if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
|
|
72
|
+
sourcesAborted++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
sourcesAborted,
|
|
78
|
+
numberOfSources
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Construct SceneBase
|
|
84
|
+
*/
|
|
85
|
+
constructor() {
|
|
86
|
+
const state = this.#state;
|
|
87
|
+
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (state.current === STATE_LOADING) {
|
|
90
|
+
const { sourcesLoaded, numberOfSources } = this.#progress;
|
|
91
|
+
|
|
92
|
+
if (sourcesLoaded === numberOfSources) {
|
|
93
|
+
this.#state.send(LOADED);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (state.current === STATE_ABORTING) {
|
|
100
|
+
const { sourcesAborted, numberOfSources } = this.#abortProgress;
|
|
101
|
+
|
|
102
|
+
if (sourcesAborted === numberOfSources) {
|
|
103
|
+
this.#state.send(ABORTED);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
state.onenter = (currentState) => {
|
|
109
|
+
if (currentState === STATE_LOADING) {
|
|
110
|
+
this.#startLoading();
|
|
111
|
+
} else if (currentState === STATE_ABORTING) {
|
|
112
|
+
this.#startAbort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.state = currentState;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ==== Abstract methods - must be implemented by subclasses */
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the array of sources managed by this scene
|
|
123
|
+
*
|
|
124
|
+
* @returns {Array} Array of source objects
|
|
125
|
+
*/
|
|
126
|
+
get sources() {
|
|
127
|
+
throw new Error('Subclass must implement sources getter');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Extract the loader from a source object
|
|
132
|
+
*
|
|
133
|
+
* @param {*} source
|
|
134
|
+
*
|
|
135
|
+
* @returns {*} Loader object with progress and state properties
|
|
136
|
+
*/
|
|
137
|
+
// eslint-disable-next-line no-unused-vars
|
|
138
|
+
getLoaderFromSource(source) {
|
|
139
|
+
throw new Error('Subclass must implement getLoaderFromSource method');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ==== Common loader interface */
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get scene loading progress
|
|
146
|
+
*/
|
|
147
|
+
get progress() {
|
|
148
|
+
return this.#progress;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get scene abort progress
|
|
153
|
+
*/
|
|
154
|
+
get abortProgress() {
|
|
155
|
+
return this.#abortProgress;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Start loading all sources
|
|
160
|
+
*/
|
|
161
|
+
load() {
|
|
162
|
+
this.#state.send(LOAD);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Abort loading all sources
|
|
167
|
+
*/
|
|
168
|
+
abort() {
|
|
169
|
+
this.#state.send(ABORT);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Preload all sources with progress tracking and abort capability
|
|
174
|
+
* - Starts loading and waits for completion
|
|
175
|
+
* - Supports timeout and progress callbacks
|
|
176
|
+
* - Returns object with promise and abort function
|
|
177
|
+
*
|
|
178
|
+
* @param {object} [options]
|
|
179
|
+
* @param {number} [options.timeoutMs=10000] - Timeout in milliseconds
|
|
180
|
+
* @param {Function} [options.onProgress] - Progress callback function
|
|
181
|
+
*
|
|
182
|
+
* @returns {object} Object with promise and abort function
|
|
183
|
+
* @returns {Promise<SceneBase>} returns.promise - Promise that resolves when loaded
|
|
184
|
+
* @returns {Function} returns.abort - Function to abort preloading
|
|
185
|
+
*/
|
|
186
|
+
preload({ timeoutMs = 10000, onProgress } = {}) {
|
|
187
|
+
let timeoutId = null;
|
|
188
|
+
let progressIntervalId = null;
|
|
189
|
+
let isAborted = false;
|
|
190
|
+
|
|
191
|
+
const abort = () => {
|
|
192
|
+
if (isAborted) return;
|
|
193
|
+
isAborted = true;
|
|
194
|
+
|
|
195
|
+
if (timeoutId) {
|
|
196
|
+
clearTimeout(timeoutId);
|
|
197
|
+
timeoutId = null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (progressIntervalId) {
|
|
201
|
+
clearInterval(progressIntervalId);
|
|
202
|
+
progressIntervalId = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.abort();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const promise = new Promise((resolve, reject) => {
|
|
209
|
+
// Set up progress tracking with polling
|
|
210
|
+
if (onProgress) {
|
|
211
|
+
progressIntervalId = setInterval(() => {
|
|
212
|
+
if (!isAborted) {
|
|
213
|
+
onProgress(this.progress);
|
|
214
|
+
}
|
|
215
|
+
}, 50); // Poll every 50ms
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Set up timeout
|
|
219
|
+
if (timeoutMs > 0) {
|
|
220
|
+
timeoutId = setTimeout(() => {
|
|
221
|
+
abort();
|
|
222
|
+
reject(new Error(`Preload timed out after ${timeoutMs}ms`));
|
|
223
|
+
}, timeoutMs);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Start loading
|
|
227
|
+
this.load();
|
|
228
|
+
|
|
229
|
+
// Wait for completion with extended timeout
|
|
230
|
+
const waitTimeout = Math.max(timeoutMs + 1000, 2000);
|
|
231
|
+
waitForState(() => {
|
|
232
|
+
return this.loaded ||
|
|
233
|
+
this.state === STATE_ABORTED ||
|
|
234
|
+
this.state === STATE_ERROR;
|
|
235
|
+
}, waitTimeout)
|
|
236
|
+
.then(() => {
|
|
237
|
+
if (timeoutId) {
|
|
238
|
+
clearTimeout(timeoutId);
|
|
239
|
+
timeoutId = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (progressIntervalId) {
|
|
243
|
+
clearInterval(progressIntervalId);
|
|
244
|
+
progressIntervalId = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (isAborted || this.state === STATE_ABORTED) {
|
|
248
|
+
reject(new Error('Preload was aborted'));
|
|
249
|
+
} else if (this.state === STATE_ERROR) {
|
|
250
|
+
reject(new Error('Preload failed due to error'));
|
|
251
|
+
} else if (this.loaded) {
|
|
252
|
+
resolve(this);
|
|
253
|
+
} else {
|
|
254
|
+
reject(new Error(`Preload failed: unexpected state ${this.state}`));
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
.catch(reject);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return { promise, abort };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
destroy() {
|
|
264
|
+
// TODO: disconnect all sources?
|
|
265
|
+
// TODO: Unload loaders?
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ==== Internal methods */
|
|
269
|
+
|
|
270
|
+
#startLoading() {
|
|
271
|
+
for (const source of this.sources) {
|
|
272
|
+
const loader = this.getLoaderFromSource(source);
|
|
273
|
+
loader.load();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#startAbort() {
|
|
278
|
+
for (const source of this.sources) {
|
|
279
|
+
const loader = this.getLoaderFromSource(source);
|
|
280
|
+
loader.abort();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -7,34 +7,21 @@
|
|
|
7
7
|
* @property {string} label
|
|
8
8
|
* @property {ImageLoader} imageLoader
|
|
9
9
|
*/
|
|
10
|
-
export default class ImageScene {
|
|
11
|
-
state: string;
|
|
12
|
-
loaded: boolean;
|
|
10
|
+
export default class ImageScene extends SceneBase {
|
|
13
11
|
/**
|
|
14
|
-
* Get image
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
totalBytesLoaded: number;
|
|
18
|
-
totalSize: number;
|
|
19
|
-
sourcesLoaded: number;
|
|
20
|
-
numberOfSources: number;
|
|
21
|
-
};
|
|
22
|
-
/**
|
|
23
|
-
* Get image scene abort progress
|
|
24
|
-
*/
|
|
25
|
-
get abortProgress(): {
|
|
26
|
-
sourcesAborted: number;
|
|
27
|
-
numberOfSources: number;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Start loading all image sources
|
|
12
|
+
* Get the array of image sources managed by this scene
|
|
13
|
+
*
|
|
14
|
+
* @returns {ImageSceneSource[]}
|
|
31
15
|
*/
|
|
32
|
-
|
|
16
|
+
get sources(): ImageSceneSource[];
|
|
33
17
|
/**
|
|
34
|
-
*
|
|
18
|
+
* Extract the image loader from a source object
|
|
19
|
+
*
|
|
20
|
+
* @param {ImageSceneSource} source
|
|
21
|
+
*
|
|
22
|
+
* @returns {ImageLoader}
|
|
35
23
|
*/
|
|
36
|
-
|
|
37
|
-
destroy(): void;
|
|
24
|
+
getLoaderFromSource(source: ImageSceneSource): ImageLoader;
|
|
38
25
|
/**
|
|
39
26
|
* Add image source
|
|
40
27
|
* - Uses an ImageLoader instance to load image data from network
|
|
@@ -84,4 +71,5 @@ export type ImageSceneSource = {
|
|
|
84
71
|
label: string;
|
|
85
72
|
imageLoader: ImageLoader;
|
|
86
73
|
};
|
|
74
|
+
import SceneBase from '../base/SceneBase.svelte.js';
|
|
87
75
|
import ImageLoader from './ImageLoader.svelte.js';
|
|
@@ -2,21 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as expect from '../../../util/expect.js';
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
STATE_INITIAL,
|
|
9
|
-
STATE_LOADING,
|
|
10
|
-
STATE_LOADED,
|
|
11
|
-
STATE_ABORTING,
|
|
12
|
-
STATE_ABORTED,
|
|
13
|
-
STATE_ERROR,
|
|
14
|
-
LOAD,
|
|
15
|
-
LOADED,
|
|
16
|
-
ABORT,
|
|
17
|
-
ABORTED
|
|
18
|
-
} from '../../../state/machines.js';
|
|
19
|
-
|
|
5
|
+
import SceneBase from '../base/SceneBase.svelte.js';
|
|
20
6
|
import ImageLoader from './ImageLoader.svelte.js';
|
|
21
7
|
|
|
22
8
|
/**
|
|
@@ -30,152 +16,40 @@ import ImageLoader from './ImageLoader.svelte.js';
|
|
|
30
16
|
* @property {ImageLoader} imageLoader
|
|
31
17
|
*/
|
|
32
18
|
|
|
33
|
-
export default class ImageScene {
|
|
34
|
-
#state = new LoadingStateMachine();
|
|
35
|
-
|
|
36
|
-
// @note this exported state is set by onenter
|
|
37
|
-
state = $state(STATE_INITIAL);
|
|
38
|
-
|
|
39
|
-
loaded = $derived.by(() => {
|
|
40
|
-
return this.state === STATE_LOADED;
|
|
41
|
-
});
|
|
19
|
+
export default class ImageScene extends SceneBase {
|
|
42
20
|
|
|
43
21
|
/** @type {ImageSceneSource[]} */
|
|
44
22
|
#imageSources = $state([]);
|
|
45
23
|
|
|
46
|
-
#progress = $derived.by(() => {
|
|
47
|
-
// console.log('update progress');
|
|
48
|
-
|
|
49
|
-
let totalSize = 0;
|
|
50
|
-
let totalBytesLoaded = 0;
|
|
51
|
-
let sourcesLoaded = 0;
|
|
52
|
-
|
|
53
|
-
const sources = this.#imageSources;
|
|
54
|
-
const numberOfSources = sources.length;
|
|
55
|
-
|
|
56
|
-
for (let j = 0; j < numberOfSources; j++) {
|
|
57
|
-
const source = sources[j];
|
|
58
|
-
const { imageLoader } = source;
|
|
59
|
-
|
|
60
|
-
const { bytesLoaded, size, loaded } = imageLoader.progress;
|
|
61
|
-
|
|
62
|
-
totalSize += size;
|
|
63
|
-
totalBytesLoaded += bytesLoaded;
|
|
64
|
-
|
|
65
|
-
if (loaded) {
|
|
66
|
-
sourcesLoaded++;
|
|
67
|
-
}
|
|
68
|
-
} // end for
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
totalBytesLoaded,
|
|
72
|
-
totalSize,
|
|
73
|
-
sourcesLoaded,
|
|
74
|
-
numberOfSources
|
|
75
|
-
};
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
#abortProgress = $derived.by(() => {
|
|
79
|
-
let sourcesAborted = 0;
|
|
80
|
-
const sources = this.#imageSources;
|
|
81
|
-
const numberOfSources = sources.length;
|
|
82
|
-
|
|
83
|
-
for (let j = 0; j < numberOfSources; j++) {
|
|
84
|
-
const source = sources[j];
|
|
85
|
-
const { imageLoader } = source;
|
|
86
|
-
const loaderState = imageLoader.state;
|
|
87
|
-
|
|
88
|
-
if (loaderState === STATE_ABORTED || loaderState === STATE_ERROR) {
|
|
89
|
-
sourcesAborted++;
|
|
90
|
-
}
|
|
91
|
-
} // end for
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
sourcesAborted,
|
|
95
|
-
numberOfSources
|
|
96
|
-
};
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
#sourcesLoaded = $derived( this.#progress.sourcesLoaded );
|
|
100
|
-
#numberOfSources = $derived( this.#progress.numberOfSources );
|
|
101
24
|
|
|
102
25
|
/**
|
|
103
26
|
* Construct ImageScene
|
|
104
27
|
*/
|
|
105
28
|
constructor() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
$effect( () => {
|
|
109
|
-
if (state.current === STATE_LOADING) {
|
|
110
|
-
if (this.#sourcesLoaded === this.#numberOfSources) {
|
|
111
|
-
// console.log(`All [${this.#numberOfSources}] sources loaded`);
|
|
112
|
-
this.#state.send(LOADED);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} );
|
|
116
|
-
|
|
117
|
-
$effect(() => {
|
|
118
|
-
if (state.current === STATE_ABORTING) {
|
|
119
|
-
const { sourcesAborted, numberOfSources } = this.#abortProgress;
|
|
120
|
-
|
|
121
|
-
if (sourcesAborted === numberOfSources) {
|
|
122
|
-
// console.debug(`ImageScene: ${numberOfSources} sources aborted`);
|
|
123
|
-
this.#state.send(ABORTED);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
state.onenter = ( currentState ) => {
|
|
129
|
-
// console.log('onenter', currentState );
|
|
130
|
-
|
|
131
|
-
if(currentState === STATE_LOADING )
|
|
132
|
-
{
|
|
133
|
-
// console.log('ImageScene:loading');
|
|
134
|
-
this.#startLoading();
|
|
135
|
-
}
|
|
136
|
-
else if(currentState === STATE_ABORTING )
|
|
137
|
-
{
|
|
138
|
-
// console.log('ImageScene:aborting');
|
|
139
|
-
this.#startAbort();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this.state = currentState;
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/* ==== Common loader interface */
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get image scene loading progress
|
|
150
|
-
*/
|
|
151
|
-
get progress() {
|
|
152
|
-
return this.#progress;
|
|
29
|
+
super();
|
|
153
30
|
}
|
|
154
31
|
|
|
155
|
-
|
|
156
|
-
* Get image scene abort progress
|
|
157
|
-
*/
|
|
158
|
-
get abortProgress() {
|
|
159
|
-
return this.#abortProgress;
|
|
160
|
-
}
|
|
32
|
+
/* ==== SceneBase implementation */
|
|
161
33
|
|
|
162
34
|
/**
|
|
163
|
-
*
|
|
35
|
+
* Get the array of image sources managed by this scene
|
|
36
|
+
*
|
|
37
|
+
* @returns {ImageSceneSource[]}
|
|
164
38
|
*/
|
|
165
|
-
|
|
166
|
-
this.#
|
|
39
|
+
get sources() {
|
|
40
|
+
return this.#imageSources;
|
|
167
41
|
}
|
|
168
42
|
|
|
169
43
|
/**
|
|
170
|
-
*
|
|
44
|
+
* Extract the image loader from a source object
|
|
45
|
+
*
|
|
46
|
+
* @param {ImageSceneSource} source
|
|
47
|
+
*
|
|
48
|
+
* @returns {ImageLoader}
|
|
171
49
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
destroy() {
|
|
177
|
-
// TODO: disconnect all image sources?
|
|
178
|
-
// TODO: Unload ImageLoaders?
|
|
50
|
+
// eslint-disable-next-line no-unused-vars
|
|
51
|
+
getLoaderFromSource(source) {
|
|
52
|
+
return source.imageLoader;
|
|
179
53
|
}
|
|
180
54
|
|
|
181
55
|
/* ==== Source definitions */
|
|
@@ -241,18 +115,6 @@ export default class ImageScene {
|
|
|
241
115
|
return source.imageLoader.getObjectURL();
|
|
242
116
|
}
|
|
243
117
|
|
|
244
|
-
async #startLoading() {
|
|
245
|
-
for (const { imageLoader } of this.#imageSources) {
|
|
246
|
-
imageLoader.load();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
#startAbort() {
|
|
251
|
-
for (const { imageLoader } of this.#imageSources) {
|
|
252
|
-
imageLoader.abort();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
118
|
/* ==== Internals */
|
|
257
119
|
|
|
258
120
|
/**
|