@linear_non/stellar-kit 1.1.6 → 1.1.8

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.
@@ -4,6 +4,7 @@ import { emitter, EVENTS } from "../events"
4
4
  export default class FontLoader {
5
5
  static type = "fonts"
6
6
  static events = { PROGRESS: EVENTS.APP_FONTS_PROGRESS, LOADED: EVENTS.APP_FONTS_LOADED }
7
+ static getCount = (opts = {}) => opts.fonts?.length ?? 0
7
8
  constructor({ fonts = [] }) {
8
9
  // fonts = [{ family: "Inter", url: "/fonts/Inter.woff2" }, ...]
9
10
 
@@ -1,4 +1,4 @@
1
- // ImageLoader.js
1
+ // classes/ImageLoader.js
2
2
  import { emitter, EVENTS } from "../events"
3
3
 
4
4
  export default class ImageLoader {
@@ -7,97 +7,159 @@ export default class ImageLoader {
7
7
  PROGRESS: EVENTS.APP_IMAGES_PROGRESS,
8
8
  LOADED: EVENTS.APP_IMAGES_LOADED,
9
9
  }
10
+ static getCount = (opts = {}) => opts.urls?.length ?? opts.total ?? 0
10
11
 
11
- constructor({ origin, fileName, extension, total, urls = [] }) {
12
+ constructor({ origin, fileName, extension, total, urls = [], useWorker = true, cache = null }) {
12
13
  this.origin = origin
13
14
  this.fileName = fileName
14
15
  this.extension = extension
15
16
  this.total = total
16
17
  this.urls = urls
18
+ this.useWorker = useWorker
19
+ this.cache = cache
17
20
 
18
21
  this.images = []
19
22
  this.loaded = 0
20
23
  this.isLoaded = false
24
+
25
+ this._results = new Map() // url -> HTMLImageElement
26
+ this._objectURLs = [] // for revoke
21
27
  }
22
28
 
23
29
  async load() {
24
- const urls = this.urls.length
30
+ const raw = this.urls.length
25
31
  ? this.urls
26
32
  : Array.from({ length: this.total }, (_, i) => `${this.origin}/${this.fileName}${i}.${this.extension}`)
27
33
 
34
+ const base =
35
+ typeof document !== "undefined"
36
+ ? document.baseURI
37
+ : typeof location !== "undefined"
38
+ ? location.href
39
+ : ""
40
+ const urls = raw.map(u => {
41
+ try {
42
+ return new URL(u, base).href
43
+ } catch {
44
+ return u
45
+ }
46
+ })
47
+
48
+ console.log(urls)
49
+
50
+ // cache-aware filter
51
+ const toLoad = this.cache ? urls.filter(u => !this.cache.has(u)) : urls
28
52
  this.total = urls.length
53
+ this.loaded = this.total - toLoad.length // count already-cached as loaded
29
54
 
30
- const promises = urls.map((url, i) => this.loadSingle(url, i))
31
- await Promise.all(promises)
55
+ // (optional) emit initial progress if cache pre-fills
56
+ if (this.loaded > 0) {
57
+ emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
58
+ loaded: this.loaded,
59
+ total: this.total,
60
+ percent: Math.round((this.loaded / this.total) * 100),
61
+ })
62
+ }
32
63
 
64
+ await Promise.all(toLoad.map(url => this._loadOne(url)))
65
+
66
+ // build ordered result (use cache first, then freshly loaded)
67
+ this.images = urls.map(u => this.cache?.get(u) || this._results.get(u) || null)
33
68
  this.isLoaded = true
34
69
  emitter.emit(EVENTS.APP_IMAGES_LOADED, this.images)
35
-
36
70
  return this.images
37
71
  }
38
72
 
39
- // --- inline worker (classic) ---
40
- createWorker() {
73
+ _createInlineWorker() {
41
74
  const code = `
42
75
  self.onmessage = async (e) => {
43
- const { url, index } = e.data;
76
+ const { url } = e.data;
44
77
  try {
45
78
  const res = await fetch(url);
46
79
  const blob = await res.blob();
47
- self.postMessage({ url, blob, index });
80
+ self.postMessage({ url, blob });
48
81
  } catch (err) {
49
- self.postMessage({ url, error: String(err), index });
82
+ self.postMessage({ url, error: String(err) });
50
83
  }
51
84
  };`
52
85
  const blob = new Blob([code], { type: "application/javascript" })
53
- const workerUrl = URL.createObjectURL(blob)
54
- const w = new Worker(workerUrl) // classic worker (no imports)
55
- // (optional) URL.revokeObjectURL(workerUrl) after creation; leaving as-is for safety
56
- return w
86
+ return new Worker(URL.createObjectURL(blob))
57
87
  }
58
88
 
59
- loadSingle(url, index) {
60
- return new Promise(resolve => {
61
- const worker = this.createWorker()
62
- worker.postMessage({ url, index })
63
-
64
- worker.addEventListener("message", e => {
65
- const { blob, index, error } = e.data
66
- this.loaded++
67
-
68
- if (!error && blob) {
69
- const img = new Image()
70
- img.src = URL.createObjectURL(blob)
71
- img.onload = () => {
72
- this.images[index] = img
73
- emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
74
- loaded: this.loaded,
75
- total: this.total,
76
- percent: Math.round((this.loaded / this.total) * 100),
77
- })
78
- resolve(img)
79
- worker.terminate()
89
+ async _loadOne(url) {
90
+ // worker path
91
+ if (this.useWorker && typeof Worker !== "undefined") {
92
+ return new Promise(resolve => {
93
+ const w = this._createInlineWorker()
94
+ w.onmessage = async e => {
95
+ const { blob, error } = e.data
96
+ if (error || !blob) {
97
+ this._bumpProgress()
98
+ w.terminate()
99
+ return resolve(null)
80
100
  }
81
- img.onerror = () => {
82
- emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
83
- loaded: this.loaded,
84
- total: this.total,
85
- percent: Math.round((this.loaded / this.total) * 100),
86
- })
87
- resolve(null)
88
- worker.terminate()
89
- }
90
- } else {
91
- // count progress even on error
92
- emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
93
- loaded: this.loaded,
94
- total: this.total,
95
- percent: Math.round((this.loaded / this.total) * 100),
96
- })
97
- resolve(null)
98
- worker.terminate()
101
+ const img = await this._blobToImage(blob)
102
+ this._store(url, img)
103
+ this._bumpProgress()
104
+ w.terminate()
105
+ resolve(img)
99
106
  }
107
+ w.postMessage({ url })
100
108
  })
109
+ }
110
+
111
+ // fallback path (no worker)
112
+ try {
113
+ const res = await fetch(url)
114
+ const blob = await res.blob()
115
+ const img = await this._blobToImage(blob)
116
+ this._store(url, img)
117
+ this._bumpProgress()
118
+ return img
119
+ } catch {
120
+ this._bumpProgress()
121
+ return null
122
+ }
123
+ }
124
+
125
+ async _blobToImage(blob) {
126
+ if (!blob || !blob.type?.startsWith?.("image/")) return null
127
+ return new Promise(resolve => {
128
+ const url = URL.createObjectURL(blob)
129
+ this._objectURLs.push(url)
130
+ const img = new Image()
131
+ img.src = url
132
+ img.onload = async () => {
133
+ if (img.decode) {
134
+ try {
135
+ await img.decode()
136
+ } catch {}
137
+ }
138
+ resolve(img)
139
+ }
140
+ img.onerror = () => resolve(null)
141
+ })
142
+ }
143
+
144
+ _store(url, img) {
145
+ if (!img) return
146
+ this._results.set(url, img)
147
+ if (this.cache) this.cache.set(url, img)
148
+ }
149
+
150
+ _bumpProgress() {
151
+ this.loaded++
152
+ emitter.emit(EVENTS.APP_IMAGES_PROGRESS, {
153
+ loaded: this.loaded,
154
+ total: this.total,
155
+ percent: Math.round((this.loaded / this.total) * 100),
101
156
  })
102
157
  }
158
+
159
+ destroy() {
160
+ this._objectURLs.forEach(u => URL.revokeObjectURL(u))
161
+ this._objectURLs.length = 0
162
+ this.images = []
163
+ this._results.clear()
164
+ }
103
165
  }
@@ -12,7 +12,7 @@ export default class MasterLoader {
12
12
  add(loaderClass, options) {
13
13
  const loader = new loaderClass(options)
14
14
  this.loaders.push({ loaderClass, loader })
15
- this.total += loader.total || 0
15
+ this.total += loaderClass.getCount?.(options) ?? loader.total ?? 0
16
16
  return loader
17
17
  }
18
18
 
package/events/Raf.js CHANGED
@@ -106,16 +106,31 @@ export default class Raf {
106
106
  })
107
107
  }
108
108
 
109
+ stop = () => {
110
+ gsap.ticker.remove(this.tick)
111
+ }
112
+
113
+ resume = () => {
114
+ this.scroll.current = this.scroll.target
115
+ this.scroll.rounded = this.scroll.target
116
+ gsap.ticker.add(this.tick)
117
+ ScrollTrigger.update()
118
+ }
119
+
109
120
  on() {
110
121
  gsap.ticker.add(this.tick)
111
122
  emitter.on(EVENTS.APP_SCROLL, this.onScroll)
112
123
  emitter.on(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
124
+ window.addEventListener("pagehide", () => this.stop)
125
+ window.addEventListener("pageshow", () => this.resume)
113
126
  }
114
127
 
115
128
  off() {
116
129
  gsap.ticker.remove(this.tick)
117
130
  emitter.off(EVENTS.APP_SCROLL, this.onScroll)
118
131
  emitter.off(EVENTS.APP_MOUSEMOVE, this.onMouseMove)
132
+ window.removeEventListener("pagehide", () => this.stop)
133
+ window.removeEventListener("pageshow", () => this.resume)
119
134
  }
120
135
 
121
136
  destroy() {
package/kitStore.js CHANGED
@@ -7,6 +7,9 @@ export const sizes = {
7
7
  m: 390, // mobile width
8
8
  }
9
9
 
10
+ // in case you want to "cache" your assets
11
+ export const assets = {}
12
+
10
13
  export const flags = {
11
14
  isFocus: false,
12
15
  isSmooth: false,
@@ -34,4 +37,5 @@ export default {
34
37
  mouse,
35
38
  flags,
36
39
  pageContent: null, // This should be set to the main content element
40
+ assets,
37
41
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@linear_non/stellar-kit",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Stellar frontend core for Non-Linear Studio projects.",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
8
  "./utils": "./utils/index.js",
9
9
  "./classes": "./classes/index.js",
10
- "./workers": "./workers/*",
11
10
  "./events": "./events/index.js",
12
11
  "./gsap": "./libraries/gsap/index.js"
13
12
  },
@@ -23,7 +22,6 @@
23
22
  "utils/",
24
23
  "libraries/",
25
24
  "libraries/gsap/",
26
- "workers/",
27
25
  "kitStore.js",
28
26
  "index.js"
29
27
  ],
@@ -1,10 +0,0 @@
1
- self.addEventListener("message", async e => {
2
- const { url, index } = e.data
3
- try {
4
- const res = await fetch(url)
5
- const blob = await res.blob()
6
- self.postMessage({ url, blob, index })
7
- } catch (err) {
8
- console.error("Image worker error:", err)
9
- }
10
- })