@hkdigital/lib-sveltekit 0.1.21 → 0.1.22

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.
@@ -1,78 +1,50 @@
1
- /**
2
- * @typedef {object} SourceConfig
3
- * // property ...
4
- */
5
- /**
6
- * @typedef {object} ImageSource
7
- * @property {string} label
8
- * @property {ImageLoader} imageLoader
9
- * @property {ImageMeta} [imageMeta]
10
- */
11
1
  export default class ImageScene {
12
- state: string;
13
- loaded: boolean;
14
- destroy(): void;
15
2
  /**
16
- * Add image source
17
- * - Uses an ImageLoader instance to load image data from network
3
+ * Define an image to be managed by this scene
18
4
  *
19
- * @param {object} _
20
- * @param {string} _.label
21
- * @param {ImageMeta|ImageMeta[]} _.imageMeta
5
+ * @param {object} params
6
+ * @param {string} params.label - Unique identifier for the image
7
+ * @param {ImageMeta|ImageMeta[]} params.imageMeta - Image metadata (single or variants)
22
8
  */
23
9
  defineImage({ label, imageMeta }: {
24
10
  label: string;
25
11
  imageMeta: ImageMeta | ImageMeta[];
26
12
  }): void;
27
13
  /**
28
- * Start loading all image sources
14
+ * Start loading all defined images
29
15
  */
30
16
  load(): void;
31
17
  /**
32
- * Get image scene loading progress
18
+ * Unload all images and free resources
33
19
  */
34
- get progress(): {
35
- totalBytesLoaded: number;
36
- totalSize: number;
37
- sourcesLoaded: number;
38
- numberOfSources: number;
39
- };
20
+ unload(): void;
40
21
  /**
41
- * Get an image loader
22
+ * Get the image loader for a specific label
42
23
  *
43
- * @param {string} label
44
- *
45
- * @returns {ImageLoader}
24
+ * @param {string} label - Image identifier
25
+ * @returns {ImageLoader|null} The image loader or null if not found
46
26
  */
47
- getImageLoader(label: string): ImageLoader;
27
+ getImageLoader(label: string): ImageLoader | null;
48
28
  /**
49
- * Get object URL that can be used as src parameter of an HTML image
50
- *
51
- * @param {string} label
52
- *
53
- * @returns {ImageMeta}
29
+ * Check if the scene is currently loading
54
30
  */
55
- getImageMeta(label: string): ImageMeta;
31
+ get loading(): boolean;
56
32
  /**
57
- * Get object URL that can be used as src parameter of an HTML image
58
- *
59
- * @param {string} label
60
- *
61
- * @note the objectURL should be revoked when no longer used
62
- *
63
- * @returns {string}
33
+ * Check if all images in the scene are loaded
64
34
  */
65
- getObjectURL(label: string): string;
35
+ get loaded(): boolean;
36
+ /**
37
+ * Get the current loading progress
38
+ */
39
+ get progress(): {
40
+ bytesLoaded: number;
41
+ size: number;
42
+ loaded: boolean;
43
+ };
66
44
  #private;
67
45
  }
68
- export type ImageMeta = import("./typedef.js").ImageMeta;
69
46
  /**
70
- * // property ...
47
+ * A simple class to preload and manage images for a scene
71
48
  */
72
- export type SourceConfig = object;
73
- export type ImageSource = {
74
- label: string;
75
- imageLoader: ImageLoader;
76
- imageMeta?: ImageMeta;
77
- };
78
- import ImageLoader from './ImageLoader.svelte.js';
49
+ export type ImageMeta = import("../../../config/typedef.js").ImageMeta;
50
+ import { ImageLoader } from './index.js';
@@ -1,253 +1,120 @@
1
- /** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
2
-
3
- import * as expect from '../../../util/expect/index.js';
4
-
5
- import {
6
- LoadingStateMachine,
7
- STATE_INITIAL,
8
- STATE_LOADING,
9
- STATE_UNLOADING,
10
- STATE_LOADED,
11
- STATE_CANCELLED,
12
- STATE_ERROR,
13
- LOAD,
14
- // CANCEL,
15
- ERROR,
16
- LOADED,
17
- UNLOAD,
18
- INITIAL
19
- } from '../loading-state-machine/index.js';
20
-
21
- import ImageLoader from './ImageLoader.svelte.js';
22
-
23
- /**
24
- * @typedef {object} SourceConfig
25
- * // property ...
26
- */
27
-
28
1
  /**
29
- * @typedef {object} ImageSource
30
- * @property {string} label
31
- * @property {ImageLoader} imageLoader
32
- * @property {ImageMeta} [imageMeta]
2
+ * A simple class to preload and manage images for a scene
3
+ *
4
+ * @typedef {import('../../../config/typedef.js').ImageMeta} ImageMeta
33
5
  */
6
+ import { ImageLoader } from './index.js';
34
7
 
35
8
  export default class ImageScene {
36
- #state = new LoadingStateMachine();
37
-
38
- // @note this exported state is set by $effect's
39
- state = $state(STATE_INITIAL);
40
-
41
- // @note this exported state is set by $effect's
42
- loaded = $derived.by(() => {
43
- return this.state === STATE_LOADED;
44
- });
45
-
46
- /** @type {ImageSource[]} */
47
- #imageSources = $state([]);
48
-
49
- #progress = $derived.by(() => {
50
- // console.log('update progress');
51
-
52
- let totalSize = 0;
53
- let totalBytesLoaded = 0;
54
- let sourcesLoaded = 0;
55
-
56
- const sources = this.#imageSources;
57
- const numberOfSources = sources.length;
58
-
59
- for (let j = 0; j < numberOfSources; j++) {
60
- const source = sources[j];
61
- const { imageLoader } = source;
62
-
63
- const { bytesLoaded, size, loaded } = imageLoader.progress;
64
-
65
- totalSize += size;
66
- totalBytesLoaded += bytesLoaded;
67
-
68
- if (loaded) {
69
- sourcesLoaded++;
70
- }
71
- } // end for
72
-
73
- return {
74
- totalBytesLoaded,
75
- totalSize,
76
- sourcesLoaded,
77
- numberOfSources
78
- };
79
- });
9
+ /** @type {Map<string, ImageLoader>} */
10
+ #loaders = $state.raw(new Map());
80
11
 
81
- /**
82
- * Construct ImageScene
83
- */
84
- constructor() {
85
- const state = this.#state;
86
-
87
- $effect(() => {
88
- if (state.current === STATE_LOADING) {
89
- // console.log(
90
- // 'progress',
91
- // JSON.stringify($state.snapshot(this.#progress))
92
- // );
93
-
94
- const { sourcesLoaded, numberOfSources } = this.#progress;
95
-
96
- if (sourcesLoaded === numberOfSources) {
97
- // console.log(`All [${numberOfSources}] sources loaded`);
98
- this.#state.send(LOADED);
99
- }
100
- }
101
- });
12
+ /** @type {boolean} */
13
+ #loading = $state(false);
102
14
 
103
- $effect(() => {
104
- switch (state.current) {
105
- case STATE_LOADING:
106
- {
107
- // console.log('ImageScene:loading');
108
- this.#startLoading();
109
- }
110
- break;
111
-
112
- case STATE_UNLOADING:
113
- {
114
- // console.log('ImageScene:unloading');
115
- // this.#startUnLoading();
116
- }
117
- break;
118
-
119
- case STATE_LOADED:
120
- {
121
- // console.log('ImageScene:loaded');
122
- // TODO
123
- // this.#abortLoading = null;
124
- }
125
- break;
126
-
127
- case STATE_CANCELLED:
128
- {
129
- // console.log('ImageScene:cancelled');
130
- // TODO
131
- }
132
- break;
133
-
134
- case STATE_ERROR:
135
- {
136
- console.log('ImageScene:error', state.error);
137
- }
138
- break;
139
- } // end switch
140
-
141
- this.state = state.current;
142
- });
143
- }
15
+ /** @type {boolean} */
16
+ #loaded = $state(false);
144
17
 
145
- destroy() {
146
- // TODO: disconnect all image sources?
147
- // TODO: Unload ImageLoaders?
148
- }
18
+ /** @type {{ bytesLoaded: number, size: number, loaded: boolean }} */
19
+ #progress = $state({ bytesLoaded: 0, size: 0, loaded: false });
149
20
 
150
21
  /**
151
- * Add image source
152
- * - Uses an ImageLoader instance to load image data from network
22
+ * Define an image to be managed by this scene
153
23
  *
154
- * @param {object} _
155
- * @param {string} _.label
156
- * @param {ImageMeta|ImageMeta[]} _.imageMeta
24
+ * @param {object} params
25
+ * @param {string} params.label - Unique identifier for the image
26
+ * @param {ImageMeta|ImageMeta[]} params.imageMeta - Image metadata (single or variants)
157
27
  */
158
28
  defineImage({ label, imageMeta }) {
159
- expect.notEmptyString(label);
160
-
161
- // expect.notEmptyString(url);
162
-
163
- const imageLoader = new ImageLoader({ imageMeta });
164
-
165
- this.#imageSources.push({ label, imageLoader, imageMeta });
29
+ // Create loader for this image
30
+ const loader = new ImageLoader({ imageMeta });
31
+ this.#loaders.set(label, loader);
166
32
  }
167
33
 
168
34
  /**
169
- * Start loading all image sources
35
+ * Start loading all defined images
170
36
  */
171
37
  load() {
172
- this.#state.send(LOAD);
38
+ if (this.#loading || this.#loaded) return;
173
39
 
174
- // FIXME: in unit test when moved to startloading it hangs!
40
+ this.#loading = true;
175
41
 
176
- for (const { imageLoader } of this.#imageSources) {
177
- imageLoader.load();
42
+ // Start loading all images
43
+ for (const loader of this.#loaders.values()) {
44
+ loader.load();
178
45
  }
179
- }
180
46
 
181
- async #startLoading() {
182
- // console.log('#startLoading');
183
- // FIXME: in unit test when moved to startloading it hangs!
184
- // for (const { audioLoader } of this.#memorySources) {
185
- // audioLoader.load();
186
- // }
187
- }
47
+ // Track overall loading progress
48
+ $effect(() => {
49
+ if (this.#loaders.size === 0) return;
188
50
 
189
- /**
190
- * Get Image source
191
- *
192
- * @param {string} label
193
- *
194
- * @returns {ImageSource}
195
- */
196
- #getImageSource(label) {
197
- for (const source of this.#imageSources) {
198
- if (label === source.label) {
199
- return source;
51
+ let totalBytesLoaded = 0;
52
+ let totalSize = 0;
53
+ let allLoaded = true;
54
+
55
+ for (const loader of this.#loaders.values()) {
56
+ const progress = loader.progress;
57
+
58
+ totalBytesLoaded += progress.bytesLoaded;
59
+ totalSize += progress.size || 0;
60
+
61
+ if (!progress.loaded) {
62
+ allLoaded = false;
63
+ }
200
64
  }
201
- }
202
65
 
203
- throw new Error(`Source [${label}] has not been defined`);
66
+ this.#progress = {
67
+ bytesLoaded: totalBytesLoaded,
68
+ size: totalSize,
69
+ loaded: allLoaded
70
+ };
71
+
72
+ if (allLoaded && this.#loading) {
73
+ this.#loaded = true;
74
+ }
75
+ });
204
76
  }
205
77
 
206
78
  /**
207
- * Get image scene loading progress
79
+ * Unload all images and free resources
208
80
  */
209
- get progress() {
210
- return this.#progress;
81
+ unload() {
82
+ for (const loader of this.#loaders.values()) {
83
+ loader.unload();
84
+ }
85
+
86
+ this.#loading = false;
87
+ this.#loaded = false;
211
88
  }
212
89
 
213
90
  /**
214
- * Get an image loader
215
- *
216
- * @param {string} label
91
+ * Get the image loader for a specific label
217
92
  *
218
- * @returns {ImageLoader}
93
+ * @param {string} label - Image identifier
94
+ * @returns {ImageLoader|null} The image loader or null if not found
219
95
  */
220
96
  getImageLoader(label) {
221
- const source = this.#getImageSource(label);
222
-
223
- return source.imageLoader;
97
+ return this.#loaders.get(label) || null;
224
98
  }
225
99
 
226
100
  /**
227
- * Get object URL that can be used as src parameter of an HTML image
228
- *
229
- * @param {string} label
230
- *
231
- * @returns {ImageMeta}
101
+ * Check if the scene is currently loading
232
102
  */
233
- getImageMeta(label) {
234
- const source = this.#getImageSource(label);
235
-
236
- return source.imageMeta;
103
+ get loading() {
104
+ return this.#loading;
237
105
  }
238
106
 
239
107
  /**
240
- * Get object URL that can be used as src parameter of an HTML image
241
- *
242
- * @param {string} label
243
- *
244
- * @note the objectURL should be revoked when no longer used
245
- *
246
- * @returns {string}
108
+ * Check if all images in the scene are loaded
247
109
  */
248
- getObjectURL(label) {
249
- const source = this.#getImageSource(label);
110
+ get loaded() {
111
+ return this.#loaded;
112
+ }
250
113
 
251
- return source.imageLoader.getObjectURL();
114
+ /**
115
+ * Get the current loading progress
116
+ */
117
+ get progress() {
118
+ return this.#progress;
252
119
  }
253
120
  }
@@ -0,0 +1,253 @@
1
+ /** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
2
+
3
+ import * as expect from '$lib/util/expect/index.js';
4
+
5
+ import {
6
+ LoadingStateMachine,
7
+ STATE_INITIAL,
8
+ STATE_LOADING,
9
+ STATE_UNLOADING,
10
+ STATE_LOADED,
11
+ STATE_CANCELLED,
12
+ STATE_ERROR,
13
+ LOAD,
14
+ // CANCEL,
15
+ ERROR,
16
+ LOADED,
17
+ UNLOAD,
18
+ INITIAL
19
+ } from '$lib/classes/svelte/loading-state-machine/index.js';
20
+
21
+ import ImageLoader from '$lib/classes/svelte/image/ImageLoader.svelte.js';
22
+
23
+ /**
24
+ * @typedef {object} SourceConfig
25
+ * // property ...
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} ImageSource
30
+ * @property {string} label
31
+ * @property {ImageLoader} imageLoader
32
+ * @property {ImageMeta} [imageMeta]
33
+ */
34
+
35
+ export default class ImageScene {
36
+ #state = new LoadingStateMachine();
37
+
38
+ // @note this exported state is set by $effect's
39
+ state = $state(STATE_INITIAL);
40
+
41
+ // @note this exported state is set by $effect's
42
+ loaded = $derived.by(() => {
43
+ return this.state === STATE_LOADED;
44
+ });
45
+
46
+ /** @type {ImageSource[]} */
47
+ #imageSources = $state([]);
48
+
49
+ #progress = $derived.by(() => {
50
+ // console.log('update progress');
51
+
52
+ let totalSize = 0;
53
+ let totalBytesLoaded = 0;
54
+ let sourcesLoaded = 0;
55
+
56
+ const sources = this.#imageSources;
57
+ const numberOfSources = sources.length;
58
+
59
+ for (let j = 0; j < numberOfSources; j++) {
60
+ const source = sources[j];
61
+ const { imageLoader } = source;
62
+
63
+ const { bytesLoaded, size, loaded } = imageLoader.progress;
64
+
65
+ totalSize += size;
66
+ totalBytesLoaded += bytesLoaded;
67
+
68
+ if (loaded) {
69
+ sourcesLoaded++;
70
+ }
71
+ } // end for
72
+
73
+ return {
74
+ totalBytesLoaded,
75
+ totalSize,
76
+ sourcesLoaded,
77
+ numberOfSources
78
+ };
79
+ });
80
+
81
+ /**
82
+ * Construct ImageScene
83
+ */
84
+ constructor() {
85
+ const state = this.#state;
86
+
87
+ $effect(() => {
88
+ if (state.current === STATE_LOADING) {
89
+ // console.log(
90
+ // 'progress',
91
+ // JSON.stringify($state.snapshot(this.#progress))
92
+ // );
93
+
94
+ const { sourcesLoaded, numberOfSources } = this.#progress;
95
+
96
+ if (sourcesLoaded === numberOfSources) {
97
+ // console.log(`All [${numberOfSources}] sources loaded`);
98
+ this.#state.send(LOADED);
99
+ }
100
+ }
101
+ });
102
+
103
+ $effect(() => {
104
+ switch (state.current) {
105
+ case STATE_LOADING:
106
+ {
107
+ // console.log('ImageScene:loading');
108
+ this.#startLoading();
109
+ }
110
+ break;
111
+
112
+ case STATE_UNLOADING:
113
+ {
114
+ // console.log('ImageScene:unloading');
115
+ // this.#startUnLoading();
116
+ }
117
+ break;
118
+
119
+ case STATE_LOADED:
120
+ {
121
+ // console.log('ImageScene:loaded');
122
+ // TODO
123
+ // this.#abortLoading = null;
124
+ }
125
+ break;
126
+
127
+ case STATE_CANCELLED:
128
+ {
129
+ // console.log('ImageScene:cancelled');
130
+ // TODO
131
+ }
132
+ break;
133
+
134
+ case STATE_ERROR:
135
+ {
136
+ console.log('ImageScene:error', state.error);
137
+ }
138
+ break;
139
+ } // end switch
140
+
141
+ this.state = state.current;
142
+ });
143
+ }
144
+
145
+ destroy() {
146
+ // TODO: disconnect all image sources?
147
+ // TODO: Unload ImageLoaders?
148
+ }
149
+
150
+ /**
151
+ * Add image source
152
+ * - Uses an ImageLoader instance to load image data from network
153
+ *
154
+ * @param {object} _
155
+ * @param {string} _.label
156
+ * @param {ImageMeta|ImageMeta[]} _.imageMeta
157
+ */
158
+ defineImage({ label, imageMeta }) {
159
+ expect.notEmptyString(label);
160
+
161
+ // expect.notEmptyString(url);
162
+
163
+ const imageLoader = new ImageLoader({ imageMeta });
164
+
165
+ this.#imageSources.push({ label, imageLoader, imageMeta });
166
+ }
167
+
168
+ /**
169
+ * Start loading all image sources
170
+ */
171
+ load() {
172
+ this.#state.send(LOAD);
173
+
174
+ // FIXME: in unit test when moved to startloading it hangs!
175
+
176
+ for (const { imageLoader } of this.#imageSources) {
177
+ imageLoader.load();
178
+ }
179
+ }
180
+
181
+ async #startLoading() {
182
+ // console.log('#startLoading');
183
+ // FIXME: in unit test when moved to startloading it hangs!
184
+ // for (const { audioLoader } of this.#memorySources) {
185
+ // audioLoader.load();
186
+ // }
187
+ }
188
+
189
+ /**
190
+ * Get Image source
191
+ *
192
+ * @param {string} label
193
+ *
194
+ * @returns {ImageSource}
195
+ */
196
+ #getImageSource(label) {
197
+ for (const source of this.#imageSources) {
198
+ if (label === source.label) {
199
+ return source;
200
+ }
201
+ }
202
+
203
+ throw new Error(`Source [${label}] has not been defined`);
204
+ }
205
+
206
+ /**
207
+ * Get image scene loading progress
208
+ */
209
+ get progress() {
210
+ return this.#progress;
211
+ }
212
+
213
+ /**
214
+ * Get an image loader
215
+ *
216
+ * @param {string} label
217
+ *
218
+ * @returns {ImageLoader}
219
+ */
220
+ getImageLoader(label) {
221
+ const source = this.#getImageSource(label);
222
+
223
+ return source.imageLoader;
224
+ }
225
+
226
+ /**
227
+ * Get object URL that can be used as src parameter of an HTML image
228
+ *
229
+ * @param {string} label
230
+ *
231
+ * @returns {ImageMeta}
232
+ */
233
+ getImageMeta(label) {
234
+ const source = this.#getImageSource(label);
235
+
236
+ return source.imageMeta;
237
+ }
238
+
239
+ /**
240
+ * Get object URL that can be used as src parameter of an HTML image
241
+ *
242
+ * @param {string} label
243
+ *
244
+ * @note the objectURL should be revoked when no longer used
245
+ *
246
+ * @returns {string}
247
+ */
248
+ getObjectURL(label) {
249
+ const source = this.#getImageSource(label);
250
+
251
+ return source.imageLoader.getObjectURL();
252
+ }
253
+ }
@@ -1,8 +1,5 @@
1
1
  export default class ImageVariantsLoader {
2
- /**
3
- * @param {ImageMeta[]} imagesMeta
4
- */
5
- constructor(imagesMeta: ImageMeta[], { devicePixelRatio }?: {
2
+ constructor(imagesMeta: any, { devicePixelRatio }?: {
6
3
  devicePixelRatio?: number;
7
4
  });
8
5
  /**
@@ -28,7 +25,11 @@ export default class ImageVariantsLoader {
28
25
  * @returns {string|null}
29
26
  */
30
27
  getObjectURL(): string | null;
31
- get progress(): import("../network-loader/typedef.js").LoadingProgress;
28
+ get progress(): {
29
+ bytesLoaded: any;
30
+ size: any;
31
+ loaded: boolean;
32
+ };
32
33
  /**
33
34
  * Get optimal image variant
34
35
  *
@@ -1,11 +1,7 @@
1
1
  /** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
2
2
 
3
- // import * as expect from '../../../util/expect/index.js';
4
-
5
3
  import { calculateEffectiveWidth } from '../../../util/image/index.js';
6
-
7
4
  import { untrack } from 'svelte';
8
-
9
5
  import ImageLoader from './ImageLoader.svelte.js';
10
6
 
11
7
  export default class ImageVariantsLoader {
@@ -21,22 +17,32 @@ export default class ImageVariantsLoader {
21
17
  /** @type {ImageLoader|null} */
22
18
  #imageLoader = $state(null);
23
19
 
24
- #progress = $derived.by(() => {
25
- if (this.#imageLoader) {
26
- return this.#imageLoader.progress;
27
- } else {
28
- return { bytesLoaded: 0, size: 0, loaded: false };
29
- }
30
- });
20
+ /** @type {boolean} */
21
+ #isObjectUrlCreated = $state(false);
31
22
 
32
- #loaded = $derived.by(() => this.#progress?.loaded || false);
23
+ /** @type {boolean} */
24
+ #variantLoaded = $state(false);
25
+
26
+ /** @type {Object} */
27
+ #baseProgress = $state({ bytesLoaded: 0, size: 0, loaded: false });
33
28
 
34
- /**
35
- * @param {ImageMeta[]} imagesMeta
36
- */
37
29
  constructor(imagesMeta, { devicePixelRatio = 1 } = {}) {
38
30
  this.#devicePixelRatio = devicePixelRatio ?? 1;
39
31
  this.#imagesMeta = [...imagesMeta].sort((a, b) => a.width - b.width);
32
+
33
+ // Track the imageLoader's progress
34
+ $effect(() => {
35
+ if (this.#imageLoader) {
36
+ // Store the base progress from the loader
37
+ this.#baseProgress = this.#imageLoader.progress;
38
+
39
+ // When the base image is loaded, we can say variant is loaded
40
+ // if an object URL has been created
41
+ if (this.#baseProgress.loaded && this.#isObjectUrlCreated) {
42
+ this.#variantLoaded = true;
43
+ }
44
+ }
45
+ });
40
46
  }
41
47
 
42
48
  /**
@@ -67,7 +73,11 @@ export default class ImageVariantsLoader {
67
73
  ) {
68
74
  this.#imageVariant = newVariant;
69
75
 
70
- // Create and start loader here directly when variant changes
76
+ // Reset our loaded flags when changing variants
77
+ this.#isObjectUrlCreated = false;
78
+ this.#variantLoaded = false;
79
+
80
+ // Clean up and create a new loader
71
81
  if (this.#imageLoader?.initial) {
72
82
  this.#imageLoader.unload();
73
83
  }
@@ -81,7 +91,7 @@ export default class ImageVariantsLoader {
81
91
  }
82
92
 
83
93
  get loaded() {
84
- return this.#loaded;
94
+ return this.#variantLoaded;
85
95
  }
86
96
 
87
97
  get variant() {
@@ -96,31 +106,38 @@ export default class ImageVariantsLoader {
96
106
  * @returns {string|null}
97
107
  */
98
108
  getObjectURL() {
99
- // Example usage:
100
- //
101
- // $effect(() => {
102
- // if (variantsLoader.loaded) {
103
- // // @ts-ignore
104
- // imageUrl = variantsLoader.getObjectURL();
105
- // }
106
-
107
- // return () => {
108
- // if (imageUrl) {
109
- // URL.revokeObjectURL(imageUrl);
110
- // imageUrl = null;
111
- // }
112
- // };
113
- // });
114
-
115
- const blob = this.#imageLoader?.getBlob();
116
-
117
- const url = blob ? URL.createObjectURL(blob) : null;
109
+ if (!this.#imageLoader) {
110
+ return null;
111
+ }
112
+
113
+ const blob = this.#imageLoader.getBlob();
114
+
115
+ if (!blob) {
116
+ return null;
117
+ }
118
+
119
+ // Get the URL
120
+ const url = URL.createObjectURL(blob);
121
+
122
+ // Mark that we've successfully created an object URL
123
+ this.#isObjectUrlCreated = true;
124
+
125
+ // If the underlying loader is also loaded, we can consider
126
+ // the whole variant loaded
127
+ if (this.#baseProgress.loaded) {
128
+ this.#variantLoaded = true;
129
+ }
118
130
 
119
131
  return url;
120
132
  }
121
133
 
122
134
  get progress() {
123
- return this.#progress;
135
+ // Only return loaded:true in the progress when we're fully loaded
136
+ return {
137
+ bytesLoaded: this.#baseProgress.bytesLoaded,
138
+ size: this.#baseProgress.size,
139
+ loaded: this.#variantLoaded
140
+ };
124
141
  }
125
142
 
126
143
  /**
@@ -141,7 +158,6 @@ export default class ImageVariantsLoader {
141
158
  const imagesMeta = this.#imagesMeta;
142
159
 
143
160
  // Find the smallest image that's larger than our required width
144
-
145
161
  const optimal = imagesMeta.find(
146
162
  (current) => current.width >= requiredWidth
147
163
  );
@@ -149,4 +165,4 @@ export default class ImageVariantsLoader {
149
165
  // Fall back to the largest image if nothing is big enough
150
166
  return optimal || imagesMeta[imagesMeta.length - 1];
151
167
  }
152
- } // end class
168
+ }
@@ -0,0 +1,184 @@
1
+ /** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
2
+
3
+ // import * as expect from '$lib/util/expect/index.js';
4
+
5
+ import { calculateEffectiveWidth } from '$lib/util/image/index.js';
6
+
7
+ import { untrack } from 'svelte';
8
+
9
+ import ImageLoader from './ImageLoader.svelte.js';
10
+
11
+ export default class ImageVariantsLoader {
12
+ /** @type {number} */
13
+ #devicePixelRatio;
14
+
15
+ /** @type {ImageMeta[]} */
16
+ #imagesMeta;
17
+
18
+ /** @type {ImageMeta|null} */
19
+ #imageVariant = $state(null);
20
+
21
+ /** @type {ImageLoader|null} */
22
+ #imageLoader = $state(null);
23
+
24
+ /** @type {boolean} */
25
+ #manuallyCheckedLoaded = $state(false);
26
+
27
+ // Create a custom progress object that we control fully
28
+ #customProgress = $state({
29
+ bytesLoaded: 0,
30
+ size: 0,
31
+ loaded: false
32
+ });
33
+
34
+ #progress = $derived.by(() => {
35
+ return this.#customProgress;
36
+ });
37
+
38
+ // Derive loaded state from our custom progress
39
+ #loaded = $derived.by(() => this.#customProgress.loaded);
40
+
41
+ /**
42
+ * @param {ImageMeta[]} imagesMeta
43
+ */
44
+ constructor(imagesMeta, { devicePixelRatio = 1 } = {}) {
45
+ this.#devicePixelRatio = devicePixelRatio ?? 1;
46
+ this.#imagesMeta = [...imagesMeta].sort((a, b) => a.width - b.width);
47
+
48
+ // Track when the imageLoader's progress changes
49
+ $effect(() => {
50
+ if (this.#imageLoader) {
51
+ const loaderProgress = this.#imageLoader.progress;
52
+
53
+ // Update our custom progress with the loader's values,
54
+ // but maintain our own loaded state
55
+ this.#customProgress = {
56
+ bytesLoaded: loaderProgress.bytesLoaded,
57
+ size: loaderProgress.size,
58
+ loaded: this.#manuallyCheckedLoaded && loaderProgress.loaded
59
+ };
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Set new optimal image variant or keep current
66
+ *
67
+ * @param {object} params
68
+ * @param {number} [params.containerWidth] Container width
69
+ * @param {number} [params.containerHeight] Container height
70
+ * @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
71
+ */
72
+ updateOptimalImageMeta({ containerWidth, containerHeight, fit = 'contain' }) {
73
+ const baseImage = this.#imagesMeta[0];
74
+ const imageAspectRatio = baseImage.width / baseImage.height;
75
+
76
+ const effectiveWidth = calculateEffectiveWidth({
77
+ containerWidth,
78
+ containerHeight,
79
+ imageAspectRatio,
80
+ fit
81
+ });
82
+
83
+ const newVariant = this.getOptimalImageMeta(effectiveWidth);
84
+
85
+ if (
86
+ !newVariant ||
87
+ !this.#imageVariant ||
88
+ newVariant.width > this.#imageVariant.width
89
+ ) {
90
+ this.#imageVariant = newVariant;
91
+
92
+ // Reset loaded state when changing variants
93
+ this.#manuallyCheckedLoaded = false;
94
+ this.#customProgress = {
95
+ bytesLoaded: 0,
96
+ size: 0,
97
+ loaded: false
98
+ };
99
+
100
+ // Create and start loader here directly when variant changes
101
+ if (this.#imageLoader?.initial) {
102
+ this.#imageLoader.unload();
103
+ }
104
+
105
+ this.#imageLoader = new ImageLoader({
106
+ imageMeta: newVariant
107
+ });
108
+
109
+ this.#imageLoader.load();
110
+ }
111
+ }
112
+
113
+ get loaded() {
114
+ return this.#loaded;
115
+ }
116
+
117
+ get variant() {
118
+ return this.#imageVariant;
119
+ }
120
+
121
+ /**
122
+ * Get object URL that can be used as src parameter of an HTML image
123
+ *
124
+ * @note the objectURL should be revoked when no longer used
125
+ *
126
+ * @returns {string|null}
127
+ */
128
+ getObjectURL() {
129
+ // First check if the loader is actually loaded
130
+ if (!this.#imageLoader?.loaded) {
131
+ return null;
132
+ }
133
+
134
+ const blob = this.#imageLoader.getBlob();
135
+
136
+ if (!blob) {
137
+ return null;
138
+ }
139
+
140
+ // Successfully got a blob, so we're definitely loaded
141
+ if (!this.#manuallyCheckedLoaded) {
142
+ this.#manuallyCheckedLoaded = true;
143
+
144
+ // Update our custom progress to indicate we're loaded
145
+ this.#customProgress = {
146
+ bytesLoaded: this.#customProgress.bytesLoaded,
147
+ size: this.#customProgress.size,
148
+ loaded: true
149
+ };
150
+ }
151
+
152
+ return URL.createObjectURL(blob);
153
+ }
154
+
155
+ get progress() {
156
+ return this.#progress;
157
+ }
158
+
159
+ /**
160
+ * Get optimal image variant
161
+ *
162
+ * @param {number} containerWidth
163
+ *
164
+ * @returns {ImageMeta|null}
165
+ */
166
+ getOptimalImageMeta(containerWidth) {
167
+ if (!containerWidth) {
168
+ return null;
169
+ }
170
+
171
+ // Calculate the required width (container * DPR)
172
+ const requiredWidth = containerWidth * this.#devicePixelRatio;
173
+
174
+ const imagesMeta = this.#imagesMeta;
175
+
176
+ // Find the smallest image that's larger than our required width
177
+ const optimal = imagesMeta.find(
178
+ (current) => current.width >= requiredWidth
179
+ );
180
+
181
+ // Fall back to the largest image if nothing is big enough
182
+ return optimal || imagesMeta[imagesMeta.length - 1];
183
+ }
184
+ } // end class
@@ -1,2 +1,3 @@
1
1
  export * as observe from "./observe/index.js";
2
2
  export * as stateContext from "./state-context/index.js";
3
+ export * as loading from "./loading/loading-tracker.svelte.js";
@@ -1,2 +1,4 @@
1
1
  export * as observe from './observe/index.js';
2
2
  export * as stateContext from './state-context/index.js';
3
+
4
+ export * as loading from './loading/loading-tracker.svelte.js';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Simple utility for tracking combined loading progress across multiple components
3
+ *
4
+ * @typedef {Object} LoadingProgress
5
+ * @property {number} bytesLoaded - Number of bytes loaded
6
+ * @property {number} size - Total size in bytes
7
+ * @property {boolean} loaded - Whether loading is complete
8
+ */
9
+ /**
10
+ * Creates a loading tracker that combines progress from multiple sources
11
+ *
12
+ * @returns {Object} Loading tracker instance
13
+ */
14
+ export function createTracker(): any;
15
+ /**
16
+ * Simple utility for tracking combined loading progress across multiple components
17
+ */
18
+ export type LoadingProgress = {
19
+ /**
20
+ * - Number of bytes loaded
21
+ */
22
+ bytesLoaded: number;
23
+ /**
24
+ * - Total size in bytes
25
+ */
26
+ size: number;
27
+ /**
28
+ * - Whether loading is complete
29
+ */
30
+ loaded: boolean;
31
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Simple utility for tracking combined loading progress across multiple components
3
+ *
4
+ * @typedef {Object} LoadingProgress
5
+ * @property {number} bytesLoaded - Number of bytes loaded
6
+ * @property {number} size - Total size in bytes
7
+ * @property {boolean} loaded - Whether loading is complete
8
+ */
9
+
10
+ /**
11
+ * Creates a loading tracker that combines progress from multiple sources
12
+ *
13
+ * @returns {Object} Loading tracker instance
14
+ */
15
+ export function createTracker() {
16
+ // Store progress for each item by ID
17
+ const items = new Map();
18
+
19
+ // Computed values
20
+ let totalBytesLoaded = 0;
21
+ let totalSize = 0;
22
+ let allLoaded = false;
23
+ let progressPercent = 0;
24
+
25
+ /**
26
+ * Update the totals based on current items
27
+ */
28
+ function updateTotals() {
29
+ let bytesLoaded = 0;
30
+ let size = 0;
31
+ let loaded = items.size > 0;
32
+
33
+ // Loop through all items and sum values
34
+ for (const progress of items.values()) {
35
+ bytesLoaded += progress.bytesLoaded || 0;
36
+ size += progress.size || 0;
37
+
38
+ if (!progress.loaded) {
39
+ loaded = false;
40
+ }
41
+ }
42
+
43
+ // Update state
44
+ totalBytesLoaded = bytesLoaded;
45
+ totalSize = size;
46
+ allLoaded = items.size > 0 ? loaded : false;
47
+ progressPercent = size > 0 ? Math.round((bytesLoaded / size) * 100) : 0;
48
+ }
49
+
50
+ /**
51
+ * Track progress for a component
52
+ *
53
+ * @param {LoadingProgress} progress - The progress update
54
+ * @param {string|Symbol} id - The component identifier
55
+ */
56
+ function track(progress, id) {
57
+ if (progress && id) {
58
+ items.set(id, progress);
59
+ updateTotals();
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Remove an item from tracking
65
+ *
66
+ * @param {string|Symbol} id - Item identifier to remove
67
+ */
68
+ function remove(id) {
69
+ if (items.has(id)) {
70
+ items.delete(id);
71
+ updateTotals();
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Reset the tracker, clearing all items
77
+ */
78
+ function reset() {
79
+ items.clear();
80
+ updateTotals();
81
+ }
82
+
83
+ // Return the public API
84
+ return {
85
+ track,
86
+ remove,
87
+ reset,
88
+
89
+ // Read-only properties
90
+ get state() {
91
+ return {
92
+ bytesLoaded: totalBytesLoaded,
93
+ size: totalSize,
94
+ loaded: allLoaded,
95
+ percent: progressPercent,
96
+ itemCount: items.size
97
+ };
98
+ },
99
+
100
+ get loaded() {
101
+ return allLoaded;
102
+ },
103
+
104
+ get percent() {
105
+ return progressPercent;
106
+ }
107
+ };
108
+ }
@@ -14,10 +14,11 @@
14
14
  * overflow?: string,
15
15
  * fit?: 'contain' | 'cover' | 'fill',
16
16
  * position?: string,
17
- * imageMeta: import('../../config/typedef.js').ImageMeta | import('../../config/typedef.js').ImageMeta[],
17
+ * imageMeta?: import('../../config/typedef.js').ImageMeta | import('../../config/typedef.js').ImageMeta[],
18
18
  * imageLoader?: import('../../classes/svelte/image/index.js').ImageLoader,
19
19
  * alt?: string,
20
- * onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress) => void,
20
+ * id?: string | Symbol,
21
+ * onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress, id?: string | Symbol) => void,
21
22
  * [attr: string]: any
22
23
  * }}
23
24
  */
@@ -42,6 +43,9 @@
42
43
  // Accessibility
43
44
  alt = '',
44
45
 
46
+ // Component identification
47
+ id = Symbol('ImageBox'),
48
+
45
49
  // Events
46
50
  onProgress,
47
51
 
@@ -49,9 +53,9 @@
49
53
  ...attrs
50
54
  } = $props();
51
55
 
52
- if (!imageMeta) {
53
- throw new Error('Missing [imageMeta]');
54
- }
56
+ // if (!imageMeta) {
57
+ // throw new Error('Missing [imageMeta]');
58
+ // }
55
59
 
56
60
  /** @type {HTMLDivElement|undefined} */
57
61
  let containerElem = $state();
@@ -87,11 +91,11 @@
87
91
 
88
92
  // Report progress from variants loader
89
93
  if (variantsLoader) {
90
- onProgress(variantsLoader.progress);
94
+ onProgress(variantsLoader.progress, id);
91
95
  }
92
96
  // Report progress from single image loader
93
97
  else if (imageLoader_) {
94
- onProgress(imageLoader_.progress);
98
+ onProgress(imageLoader_.progress, id);
95
99
  }
96
100
  });
97
101
 
@@ -12,10 +12,11 @@ type ImageBox = {
12
12
  overflow?: string;
13
13
  fit?: "fill" | "contain" | "cover";
14
14
  position?: string;
15
- imageMeta: ImageMeta | ImageMeta[];
15
+ imageMeta?: ImageMeta | ImageMeta[];
16
16
  imageLoader?: ImageLoader;
17
17
  alt?: string;
18
- onProgress?: (progress: LoadingProgress) => void;
18
+ id?: string | Symbol;
19
+ onProgress?: (progress: LoadingProgress, id?: string | Symbol) => void;
19
20
  }>): void;
20
21
  };
21
22
  declare const ImageBox: import("svelte").Component<{
@@ -29,8 +30,9 @@ declare const ImageBox: import("svelte").Component<{
29
30
  overflow?: string;
30
31
  fit?: "contain" | "cover" | "fill";
31
32
  position?: string;
32
- imageMeta: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
33
+ imageMeta?: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
33
34
  imageLoader?: import("../../classes/svelte/image/index.js").ImageLoader;
34
35
  alt?: string;
35
- onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress) => void;
36
+ id?: string | Symbol;
37
+ onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress, id?: string | Symbol) => void;
36
38
  }, {}, "">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"