@edrlab/thorium-web 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ThPreferencesAdapter-DrZ5_6Dv.d.mts → ThPreferencesAdapter-D0rzsGRl.d.mts} +50 -13
- package/dist/{ThSettingsWrapper-8Kx0SnH4.d.mts → ThSettingsWrapper-BXuRgdqp.d.mts} +42 -4
- package/dist/{actions-D2CHvCHu.d.mts → actions-BLAr0oaM.d.mts} +16 -4
- package/dist/{actionsReducer-kc-S130w.d.mts → actionsReducer-BhG1wicI.d.mts} +37 -12
- package/dist/chunk-3GDQP6AS.mjs +14 -0
- package/dist/chunk-3GDQP6AS.mjs.map +1 -0
- package/dist/{chunk-72XCX5TD.mjs → chunk-3LDWKC5N.mjs} +13 -8
- package/dist/chunk-3LDWKC5N.mjs.map +1 -0
- package/dist/{chunk-NYZBHYW2.mjs → chunk-4ODYHZKD.mjs} +343 -38
- package/dist/chunk-4ODYHZKD.mjs.map +1 -0
- package/dist/{chunk-QPE574OW.mjs → chunk-4TVEDX7L.mjs} +23 -32
- package/dist/chunk-4TVEDX7L.mjs.map +1 -0
- package/dist/chunk-7CGMWOZN.mjs +20 -0
- package/dist/chunk-7CGMWOZN.mjs.map +1 -0
- package/dist/{chunk-7NEQAW7J.mjs → chunk-C236BQQB.mjs} +656 -917
- package/dist/chunk-C236BQQB.mjs.map +1 -0
- package/dist/{chunk-PXAUQJEU.mjs → chunk-D7MFLHXV.mjs} +2267 -1599
- package/dist/chunk-D7MFLHXV.mjs.map +1 -0
- package/dist/{chunk-47AIIJFO.mjs → chunk-ITDBOMY5.mjs} +3 -3
- package/dist/{chunk-47AIIJFO.mjs.map → chunk-ITDBOMY5.mjs.map} +1 -1
- package/dist/{chunk-XVSFXHYB.mjs → chunk-L4XGZAZ5.mjs} +23 -20
- package/dist/chunk-L4XGZAZ5.mjs.map +1 -0
- package/dist/chunk-OWHP7ONM.mjs +134 -0
- package/dist/chunk-OWHP7ONM.mjs.map +1 -0
- package/dist/{chunk-P4V3LA5R.mjs → chunk-RRVLWDT3.mjs} +10 -7
- package/dist/chunk-RRVLWDT3.mjs.map +1 -0
- package/dist/chunk-T2E6MRVP.mjs +862 -0
- package/dist/chunk-T2E6MRVP.mjs.map +1 -0
- package/dist/chunk-TEZB4ULX.mjs +57 -0
- package/dist/chunk-TEZB4ULX.mjs.map +1 -0
- package/dist/chunk-UCTMVCW7.mjs +833 -0
- package/dist/chunk-UCTMVCW7.mjs.map +1 -0
- package/dist/chunk-WECWPYZB.mjs +1950 -0
- package/dist/chunk-WECWPYZB.mjs.map +1 -0
- package/dist/{chunk-4VHEHMJN.mjs → chunk-XBZWGRDM.mjs} +228 -94
- package/dist/chunk-XBZWGRDM.mjs.map +1 -0
- package/dist/{chunk-K3K7TUWM.mjs → chunk-YZ3KCMKY.mjs} +237 -83
- package/dist/chunk-YZ3KCMKY.mjs.map +1 -0
- package/dist/components/Audio/index.css +1858 -0
- package/dist/components/Audio/index.css.map +1 -0
- package/dist/components/Audio/index.d.mts +103 -0
- package/dist/components/Audio/index.mjs +23 -0
- package/dist/components/Audio/index.mjs.map +1 -0
- package/dist/components/Epub/index.css +365 -9
- package/dist/components/Epub/index.css.map +1 -1
- package/dist/components/Epub/index.d.mts +17 -19
- package/dist/components/Epub/index.mjs +15 -10
- package/dist/components/Misc/index.css +5 -2
- package/dist/components/Misc/index.css.map +1 -1
- package/dist/components/Misc/index.mjs +4 -132
- package/dist/components/Misc/index.mjs.map +1 -1
- package/dist/components/Reader/index.css +1022 -183
- package/dist/components/Reader/index.css.map +1 -1
- package/dist/components/Reader/index.d.mts +16 -16
- package/dist/components/Reader/index.mjs +121 -22
- package/dist/components/Reader/index.mjs.map +1 -1
- package/dist/components/WebPub/index.css +365 -9
- package/dist/components/WebPub/index.css.map +1 -1
- package/dist/components/WebPub/index.d.mts +16 -16
- package/dist/components/WebPub/index.mjs +15 -10
- package/dist/core/Components/index.d.mts +64 -15
- package/dist/core/Components/index.mjs +2 -1
- package/dist/core/Helpers/index.d.mts +2 -2
- package/dist/core/Helpers/index.mjs +4 -2
- package/dist/core/Hooks/index.d.mts +7 -8
- package/dist/core/Hooks/index.mjs +3 -1
- package/dist/i18n/index.mjs +4 -5
- package/dist/lib/index.d.mts +159 -15
- package/dist/lib/index.mjs +4 -2
- package/dist/lib-M3PPQDJJ.mjs +6548 -0
- package/dist/lib-M3PPQDJJ.mjs.map +1 -0
- package/dist/locales/en/thorium-web.json +22 -0
- package/dist/next-lib/index.mjs +2 -0
- package/dist/next-lib/index.mjs.map +1 -1
- package/dist/preferences/index.d.mts +111 -13
- package/dist/preferences/index.mjs +6 -3
- package/dist/{settingsReducer-C1wwCAMv.d.mts → settingsReducer-Bu1zeveu.d.mts} +1 -1
- package/dist/{ui-CamWuqOo.d.mts → ui-nBv8gfr0.d.mts} +20 -1
- package/dist/useAudioNavigator-C5aW4-eT.d.mts +133 -0
- package/dist/{useContrast-D6sjPjxy.d.mts → useContrast-2t429O9O.d.mts} +16 -8
- package/dist/usePreferences-VaBf46eP.d.mts +230 -0
- package/dist/useReaderTransitions-JDzlBFsu.d.mts +530 -0
- package/dist/{useTimeline-DyMx_aWY.d.mts → useTimeline-DCZ1qoCO.d.mts} +4 -2
- package/package.json +15 -11
- package/dist/chunk-4VHEHMJN.mjs.map +0 -1
- package/dist/chunk-72XCX5TD.mjs.map +0 -1
- package/dist/chunk-7NEQAW7J.mjs.map +0 -1
- package/dist/chunk-K3K7TUWM.mjs.map +0 -1
- package/dist/chunk-NYZBHYW2.mjs.map +0 -1
- package/dist/chunk-P4V3LA5R.mjs.map +0 -1
- package/dist/chunk-PXAUQJEU.mjs.map +0 -1
- package/dist/chunk-QPE574OW.mjs.map +0 -1
- package/dist/chunk-XVSFXHYB.mjs.map +0 -1
- package/dist/useEpubNavigator-CwHJfoiV.d.mts +0 -42
- package/dist/usePreferences-BXFJbval.d.mts +0 -43
- package/dist/useReaderTransitions-guT-eA-Q.d.mts +0 -365
- package/dist/useWebPubNavigator-CuSNQKMw.d.mts +0 -39
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
import { propsToCSSVars } from './chunk-7CGMWOZN.mjs';
|
|
2
|
+
import { useBreakpoints, useForcedColors, useMonochrome, useReducedMotion, useReducedTransparency } from './chunk-GFSLVQIG.mjs';
|
|
3
|
+
import { defaultAudioPreferencesContextValue, devContentProtectionConfig, ThAudioPreferencesContext, defaultPreferencesContextValue, ThPreferencesContext, defaultTextSettingsMain, defaultTextSettingsSubpanel, defaultSpacingSettingsMain, defaultSpacingSettingsSubpanel, defaultSpacingPresetsOrder } from './chunk-C236BQQB.mjs';
|
|
4
|
+
import { useColorScheme, useContrast } from './chunk-NQ2ZSGCX.mjs';
|
|
5
|
+
import { useMemo, useState, useCallback, useEffect, useContext, useRef } from 'react';
|
|
6
|
+
import { jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
// src/preferences/helpers/buildThemeObject.ts
|
|
9
|
+
var buildThemeObject = ({
|
|
10
|
+
theme,
|
|
11
|
+
themeKeys,
|
|
12
|
+
systemThemes,
|
|
13
|
+
colorScheme
|
|
14
|
+
}) => {
|
|
15
|
+
if (!theme) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
if (theme === "auto" && colorScheme && systemThemes) {
|
|
19
|
+
theme = colorScheme === "dark" /* dark */ ? systemThemes.dark : systemThemes.light;
|
|
20
|
+
}
|
|
21
|
+
let themeProps = {};
|
|
22
|
+
const themeToken = themeKeys[theme];
|
|
23
|
+
if (themeToken) {
|
|
24
|
+
themeProps = {
|
|
25
|
+
backgroundColor: themeToken.background,
|
|
26
|
+
textColor: themeToken.text,
|
|
27
|
+
linkColor: themeToken.link,
|
|
28
|
+
selectionBackgroundColor: themeToken.select,
|
|
29
|
+
selectionTextColor: themeToken.onSelect,
|
|
30
|
+
visitedColor: themeToken.visited
|
|
31
|
+
};
|
|
32
|
+
} else {
|
|
33
|
+
console.warn(`Theme key "${String(theme)}" not found in themeKeys.`);
|
|
34
|
+
themeProps = {
|
|
35
|
+
backgroundColor: null,
|
|
36
|
+
textColor: null,
|
|
37
|
+
linkColor: null,
|
|
38
|
+
selectionBackgroundColor: null,
|
|
39
|
+
selectionTextColor: null,
|
|
40
|
+
visitedColor: null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return themeProps;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/preferences/helpers/fontPref/bunnyFonts.ts
|
|
47
|
+
var DEFAULT_FALLBACK = "sans-serif";
|
|
48
|
+
var createDefinitionsFromBunnyFonts = (params) => {
|
|
49
|
+
const { cssUrl, options } = params;
|
|
50
|
+
const { fallbacks, order, labels } = options || {};
|
|
51
|
+
const processedUrl = cssUrl.includes("@import") ? cssUrl.match(/@import\s+url\(['"]?([^'")]+)['"]?\)/i)?.[1] || cssUrl : cssUrl.includes("href=") ? cssUrl.match(/href=["']([^"']+)["']/)?.[1] || cssUrl : cssUrl;
|
|
52
|
+
const url = new URL(processedUrl);
|
|
53
|
+
if (!url.hostname.includes("fonts.bunny.net")) {
|
|
54
|
+
throw new Error("Invalid Bunny Fonts URL");
|
|
55
|
+
}
|
|
56
|
+
const familyParam = url.searchParams.get("family");
|
|
57
|
+
if (!familyParam) {
|
|
58
|
+
throw new Error("No family parameter found in Bunny Fonts URL");
|
|
59
|
+
}
|
|
60
|
+
const fontEntries = familyParam.split("|").map((familyStr) => {
|
|
61
|
+
const [familyName, weightsStr = ""] = familyStr.split(":");
|
|
62
|
+
if (!familyName) {
|
|
63
|
+
throw new Error(`Invalid font family format: ${familyStr}`);
|
|
64
|
+
}
|
|
65
|
+
const weightStyles = /* @__PURE__ */ new Map();
|
|
66
|
+
if (weightsStr) {
|
|
67
|
+
weightsStr.split(",").forEach((weightStr) => {
|
|
68
|
+
const isItalic = weightStr.endsWith("i");
|
|
69
|
+
const weightValue = parseInt(isItalic ? weightStr.slice(0, -1) : weightStr, 10);
|
|
70
|
+
if (!isNaN(weightValue)) {
|
|
71
|
+
if (!weightStyles.has(weightValue)) {
|
|
72
|
+
weightStyles.set(weightValue, /* @__PURE__ */ new Set());
|
|
73
|
+
}
|
|
74
|
+
weightStyles.get(weightValue)?.add(isItalic ? "italic" : "normal");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const weights = Array.from(weightStyles.keys()).sort((a, b) => a - b);
|
|
79
|
+
const hasItalic = Array.from(weightStyles.values()).some((styles2) => styles2.has("italic"));
|
|
80
|
+
const styles = hasItalic ? ["normal", "italic"] : ["normal"];
|
|
81
|
+
const fontId = familyName;
|
|
82
|
+
const familyDisplayName = familyName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
83
|
+
return [
|
|
84
|
+
fontId,
|
|
85
|
+
{
|
|
86
|
+
id: fontId,
|
|
87
|
+
name: familyDisplayName,
|
|
88
|
+
...labels?.[fontId] && { label: labels[fontId] },
|
|
89
|
+
source: {
|
|
90
|
+
type: "custom",
|
|
91
|
+
provider: "bunny"
|
|
92
|
+
},
|
|
93
|
+
spec: {
|
|
94
|
+
family: familyDisplayName,
|
|
95
|
+
fallbacks: fallbacks?.[fontId] || [DEFAULT_FALLBACK],
|
|
96
|
+
weights: {
|
|
97
|
+
type: "static",
|
|
98
|
+
values: weights.length ? weights : [400]
|
|
99
|
+
},
|
|
100
|
+
styles
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
});
|
|
105
|
+
const result = Object.fromEntries(fontEntries);
|
|
106
|
+
if (order && order.length > 0) {
|
|
107
|
+
const orderedResult = {};
|
|
108
|
+
order.forEach((fontId) => {
|
|
109
|
+
if (result[fontId]) {
|
|
110
|
+
orderedResult[fontId] = result[fontId];
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
Object.entries(result).forEach(([fontId, definition]) => {
|
|
114
|
+
if (!orderedResult[fontId]) {
|
|
115
|
+
orderedResult[fontId] = definition;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return orderedResult;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
};
|
|
122
|
+
var ThDirectionSetter = ({
|
|
123
|
+
direction,
|
|
124
|
+
children
|
|
125
|
+
}) => {
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (direction) document.documentElement.dir = direction;
|
|
128
|
+
}, [direction]);
|
|
129
|
+
return children;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/preferences/adapters/ThAudioMemoryPreferencesAdapter.ts
|
|
133
|
+
var ThAudioMemoryPreferencesAdapter = class {
|
|
134
|
+
currentPreferences;
|
|
135
|
+
listeners = /* @__PURE__ */ new Set();
|
|
136
|
+
constructor(initialPreferences) {
|
|
137
|
+
this.currentPreferences = { ...initialPreferences };
|
|
138
|
+
}
|
|
139
|
+
getPreferences() {
|
|
140
|
+
return { ...this.currentPreferences };
|
|
141
|
+
}
|
|
142
|
+
setPreferences(prefs) {
|
|
143
|
+
this.currentPreferences = { ...prefs };
|
|
144
|
+
this.notifyListeners(this.currentPreferences);
|
|
145
|
+
}
|
|
146
|
+
subscribe(listener) {
|
|
147
|
+
this.listeners.add(listener);
|
|
148
|
+
}
|
|
149
|
+
unsubscribe(listener) {
|
|
150
|
+
this.listeners.delete(listener);
|
|
151
|
+
}
|
|
152
|
+
notifyListeners(prefs) {
|
|
153
|
+
this.listeners.forEach((listener) => listener({ ...prefs }));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function ThAudioPreferencesProvider({
|
|
157
|
+
adapter,
|
|
158
|
+
initialPreferences,
|
|
159
|
+
devMode,
|
|
160
|
+
children
|
|
161
|
+
}) {
|
|
162
|
+
const effectiveAdapter = useMemo(() => {
|
|
163
|
+
let fallback = defaultAudioPreferencesContextValue.preferences;
|
|
164
|
+
if (devMode && !initialPreferences) {
|
|
165
|
+
fallback = { ...fallback, contentProtection: devContentProtectionConfig };
|
|
166
|
+
}
|
|
167
|
+
return adapter || new ThAudioMemoryPreferencesAdapter(
|
|
168
|
+
initialPreferences || fallback
|
|
169
|
+
);
|
|
170
|
+
}, [adapter, initialPreferences, devMode]);
|
|
171
|
+
const [preferences, setPreferences] = useState(
|
|
172
|
+
(() => {
|
|
173
|
+
let fallback = defaultAudioPreferencesContextValue.preferences;
|
|
174
|
+
if (devMode && !initialPreferences) {
|
|
175
|
+
fallback = { ...fallback, contentProtection: devContentProtectionConfig };
|
|
176
|
+
}
|
|
177
|
+
return initialPreferences || fallback;
|
|
178
|
+
})()
|
|
179
|
+
);
|
|
180
|
+
const handlePreferenceChange = useCallback((newPrefs) => {
|
|
181
|
+
setPreferences(
|
|
182
|
+
(prev) => JSON.stringify(prev) === JSON.stringify(newPrefs) ? prev : newPrefs
|
|
183
|
+
);
|
|
184
|
+
}, []);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
effectiveAdapter.subscribe(handlePreferenceChange);
|
|
187
|
+
return () => {
|
|
188
|
+
effectiveAdapter.unsubscribe(handlePreferenceChange);
|
|
189
|
+
};
|
|
190
|
+
}, [effectiveAdapter, handlePreferenceChange]);
|
|
191
|
+
const contextValue = useMemo(() => ({
|
|
192
|
+
preferences,
|
|
193
|
+
updatePreferences: (newPrefs) => {
|
|
194
|
+
effectiveAdapter.setPreferences(newPrefs);
|
|
195
|
+
}
|
|
196
|
+
}), [preferences, effectiveAdapter]);
|
|
197
|
+
return /* @__PURE__ */ jsx(ThAudioPreferencesContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(ThDirectionSetter, { direction: preferences.direction, children }) });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/preferences/adapters/ThMemoryPreferencesAdapter.ts
|
|
201
|
+
var ThMemoryPreferencesAdapter = class {
|
|
202
|
+
currentPreferences;
|
|
203
|
+
listeners = /* @__PURE__ */ new Set();
|
|
204
|
+
constructor(initialPreferences) {
|
|
205
|
+
this.currentPreferences = { ...initialPreferences };
|
|
206
|
+
}
|
|
207
|
+
getPreferences() {
|
|
208
|
+
return { ...this.currentPreferences };
|
|
209
|
+
}
|
|
210
|
+
setPreferences(prefs) {
|
|
211
|
+
this.currentPreferences = { ...prefs };
|
|
212
|
+
this.notifyListeners(this.currentPreferences);
|
|
213
|
+
}
|
|
214
|
+
subscribe(listener) {
|
|
215
|
+
this.listeners.add(listener);
|
|
216
|
+
}
|
|
217
|
+
unsubscribe(listener) {
|
|
218
|
+
this.listeners.delete(listener);
|
|
219
|
+
}
|
|
220
|
+
notifyListeners(prefs) {
|
|
221
|
+
this.listeners.forEach((listener) => listener({ ...prefs }));
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
function ThPreferencesProvider({
|
|
225
|
+
adapter,
|
|
226
|
+
initialPreferences,
|
|
227
|
+
devMode,
|
|
228
|
+
children
|
|
229
|
+
}) {
|
|
230
|
+
const effectiveAdapter = useMemo(() => {
|
|
231
|
+
let fallbackPreferences = defaultPreferencesContextValue.preferences;
|
|
232
|
+
if (devMode && !initialPreferences) {
|
|
233
|
+
fallbackPreferences = {
|
|
234
|
+
...fallbackPreferences,
|
|
235
|
+
contentProtection: devContentProtectionConfig
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return adapter || new ThMemoryPreferencesAdapter(
|
|
239
|
+
initialPreferences || fallbackPreferences
|
|
240
|
+
);
|
|
241
|
+
}, [adapter, initialPreferences, devMode]);
|
|
242
|
+
const [preferences, setPreferences] = useState(
|
|
243
|
+
(() => {
|
|
244
|
+
let fallbackPreferences = defaultPreferencesContextValue.preferences;
|
|
245
|
+
if (devMode && !initialPreferences) {
|
|
246
|
+
fallbackPreferences = {
|
|
247
|
+
...fallbackPreferences,
|
|
248
|
+
contentProtection: devContentProtectionConfig
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return initialPreferences || fallbackPreferences;
|
|
252
|
+
})()
|
|
253
|
+
);
|
|
254
|
+
const handlePreferenceChange = useCallback((newPrefs) => {
|
|
255
|
+
setPreferences((prev) => {
|
|
256
|
+
return JSON.stringify(prev) === JSON.stringify(newPrefs) ? prev : newPrefs;
|
|
257
|
+
});
|
|
258
|
+
}, []);
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
effectiveAdapter.subscribe(handlePreferenceChange);
|
|
261
|
+
return () => {
|
|
262
|
+
effectiveAdapter.unsubscribe(handlePreferenceChange);
|
|
263
|
+
};
|
|
264
|
+
}, [effectiveAdapter, handlePreferenceChange]);
|
|
265
|
+
const contextValue = useMemo(() => ({
|
|
266
|
+
preferences,
|
|
267
|
+
updatePreferences: (newPrefs) => {
|
|
268
|
+
effectiveAdapter.setPreferences(newPrefs);
|
|
269
|
+
}
|
|
270
|
+
}), [preferences, effectiveAdapter]);
|
|
271
|
+
return /* @__PURE__ */ jsx(ThPreferencesContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(ThDirectionSetter, { direction: preferences.direction, children }) });
|
|
272
|
+
}
|
|
273
|
+
var useActionsPreferences = () => {
|
|
274
|
+
const audioCtx = useContext(ThAudioPreferencesContext);
|
|
275
|
+
const readerCtx = useContext(ThPreferencesContext);
|
|
276
|
+
if (audioCtx) {
|
|
277
|
+
return {
|
|
278
|
+
docking: audioCtx.preferences.docking,
|
|
279
|
+
actionsKeys: {
|
|
280
|
+
...audioCtx.preferences.actions.primary.keys,
|
|
281
|
+
...audioCtx.preferences.actions.secondary.keys
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (readerCtx) {
|
|
286
|
+
return {
|
|
287
|
+
docking: readerCtx.preferences.docking,
|
|
288
|
+
actionsKeys: readerCtx.preferences.actions.keys
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
throw new Error("useActionsPreferences must be used within a ThPreferencesProvider or ThAudioPreferencesProvider");
|
|
292
|
+
};
|
|
293
|
+
var useAudioActionsPreferences = () => {
|
|
294
|
+
const audioCtx = useContext(ThAudioPreferencesContext);
|
|
295
|
+
if (!audioCtx) {
|
|
296
|
+
throw new Error("useAudioActionsPreferences must be used within a ThAudioPreferencesProvider");
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
docking: audioCtx.preferences.docking,
|
|
300
|
+
primaryActionsKeys: audioCtx.preferences.actions.primary.keys,
|
|
301
|
+
secondaryActionsKeys: audioCtx.preferences.actions.secondary.keys
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
function useAudioPreferences() {
|
|
305
|
+
const context = useContext(ThAudioPreferencesContext);
|
|
306
|
+
if (!context) {
|
|
307
|
+
throw new Error("useAudioPreferences must be used within a ThAudioPreferencesProvider");
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
preferences: context.preferences,
|
|
311
|
+
updatePreferences: context.updatePreferences
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/preferences/services/createBunnyFontResources.ts
|
|
316
|
+
var buildBunnyFontsUrl = ({
|
|
317
|
+
family,
|
|
318
|
+
weights,
|
|
319
|
+
styles = ["normal"]
|
|
320
|
+
}) => {
|
|
321
|
+
if (weights.type !== "static") {
|
|
322
|
+
throw new Error("Bunny Fonts only supports static fonts");
|
|
323
|
+
}
|
|
324
|
+
const weightValues = weights.values;
|
|
325
|
+
const variants = /* @__PURE__ */ new Set();
|
|
326
|
+
for (const weight of weightValues) {
|
|
327
|
+
variants.add(weight.toString());
|
|
328
|
+
if (styles.includes("italic")) {
|
|
329
|
+
variants.add(`${weight}i`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const variantList = Array.from(variants).sort();
|
|
333
|
+
const familyParam = family.replace(/ /g, "-").toLowerCase();
|
|
334
|
+
const variantParam = variantList.join(",");
|
|
335
|
+
return `https://fonts.bunny.net/css?family=${familyParam}:${variantParam}`;
|
|
336
|
+
};
|
|
337
|
+
var createBunnyFontResources = (font) => {
|
|
338
|
+
if (font.source.type !== "custom" || font.source.provider !== "bunny" || font.spec.weights.type !== "static") {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
const { family, weights, styles } = font.spec;
|
|
342
|
+
const url = buildBunnyFontsUrl({
|
|
343
|
+
family,
|
|
344
|
+
weights,
|
|
345
|
+
styles
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
as: "link",
|
|
349
|
+
rel: "stylesheet",
|
|
350
|
+
url
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// src/preferences/services/createGoogleFontResources.ts
|
|
355
|
+
var buildGoogleFontsV2Url = ({
|
|
356
|
+
family,
|
|
357
|
+
weights,
|
|
358
|
+
styles = ["normal"],
|
|
359
|
+
widths,
|
|
360
|
+
display = "block",
|
|
361
|
+
text
|
|
362
|
+
}) => {
|
|
363
|
+
if (text) {
|
|
364
|
+
return `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}&text=${encodeURIComponent(text)}`;
|
|
365
|
+
}
|
|
366
|
+
const hasItalic = styles.includes("italic");
|
|
367
|
+
const hasWidth = !!widths;
|
|
368
|
+
const weightValues = weights.type === "static" ? weights.values.join(",") : `${weights.min}..${weights.max}`;
|
|
369
|
+
const widthValues = hasWidth && widths ? `${widths.min}..${widths.max}` : void 0;
|
|
370
|
+
const familyParam = family.replace(/ /g, "+");
|
|
371
|
+
let axesParam;
|
|
372
|
+
if (hasItalic && hasWidth) {
|
|
373
|
+
const variants = [
|
|
374
|
+
`0,${widthValues},${weightValues}`,
|
|
375
|
+
// normal
|
|
376
|
+
`1,${widthValues},${weightValues}`
|
|
377
|
+
// italic
|
|
378
|
+
];
|
|
379
|
+
axesParam = `:ital,wdth,wght@${variants.join(";")}`;
|
|
380
|
+
} else if (hasItalic) {
|
|
381
|
+
const variants = [
|
|
382
|
+
`0,${weightValues}`,
|
|
383
|
+
// normal
|
|
384
|
+
`1,${weightValues}`
|
|
385
|
+
// italic
|
|
386
|
+
];
|
|
387
|
+
axesParam = `:ital,wght@${variants.join(";")}`;
|
|
388
|
+
} else if (hasWidth) {
|
|
389
|
+
axesParam = `:wdth,wght@${widthValues},${weightValues}`;
|
|
390
|
+
} else {
|
|
391
|
+
axesParam = `:wght@${weightValues}`;
|
|
392
|
+
}
|
|
393
|
+
const displayParam = display ? `&display=${display}` : "";
|
|
394
|
+
return `https://fonts.googleapis.com/css2?family=${familyParam}${axesParam}${displayParam}`;
|
|
395
|
+
};
|
|
396
|
+
var createGoogleFontResources = (font, text) => {
|
|
397
|
+
if (font.source.type !== "custom" || font.source.provider !== "google") {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const { family, weights, display, styles, widths } = font.spec;
|
|
401
|
+
const url = buildGoogleFontsV2Url({
|
|
402
|
+
family,
|
|
403
|
+
weights,
|
|
404
|
+
display,
|
|
405
|
+
styles,
|
|
406
|
+
widths,
|
|
407
|
+
text
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
as: "link",
|
|
411
|
+
rel: "stylesheet",
|
|
412
|
+
url
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/preferences/services/createLocalFontResources.ts
|
|
417
|
+
var getFontFormat = (path) => {
|
|
418
|
+
const ext = path.split(".").pop()?.toLowerCase();
|
|
419
|
+
switch (ext) {
|
|
420
|
+
case "woff":
|
|
421
|
+
return "woff";
|
|
422
|
+
case "woff2":
|
|
423
|
+
return "woff2";
|
|
424
|
+
case "ttf":
|
|
425
|
+
return "truetype";
|
|
426
|
+
case "otf":
|
|
427
|
+
return "opentype";
|
|
428
|
+
case "eot":
|
|
429
|
+
return "embedded-opentype";
|
|
430
|
+
case "svg":
|
|
431
|
+
return "svg";
|
|
432
|
+
default:
|
|
433
|
+
return "woff2";
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
var createLocalFontResources = (font) => {
|
|
437
|
+
if (font.source.type !== "custom" || font.source.provider !== "local") {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const { family, weights, display, widths } = font.spec;
|
|
441
|
+
const fontFiles = font.source.files || [];
|
|
442
|
+
const cssContent = fontFiles.map((fontFile) => {
|
|
443
|
+
const format = getFontFormat(fontFile.path);
|
|
444
|
+
const fontUrl = new URL(fontFile.path, window.location.origin).toString();
|
|
445
|
+
const isVariable = font.source.type === "custom" && font.source.provider === "local" && "variant" in font.source && font.source.variant === "variable";
|
|
446
|
+
const rules = [
|
|
447
|
+
`@font-face {`,
|
|
448
|
+
` font-family: "${family}";`,
|
|
449
|
+
` src: url("${fontUrl}") format("${format}");`
|
|
450
|
+
];
|
|
451
|
+
if (isVariable && weights.type === "variable") {
|
|
452
|
+
rules.push(` font-weight: ${weights.min} ${weights.max};`);
|
|
453
|
+
} else if ("weight" in fontFile) {
|
|
454
|
+
rules.push(` font-weight: ${fontFile.weight};`);
|
|
455
|
+
}
|
|
456
|
+
if ("style" in fontFile) {
|
|
457
|
+
rules.push(` font-style: ${fontFile.style};`);
|
|
458
|
+
}
|
|
459
|
+
if (isVariable && widths) {
|
|
460
|
+
rules.push(` font-stretch: ${widths.min}% ${widths.max}%;`);
|
|
461
|
+
}
|
|
462
|
+
if (display) {
|
|
463
|
+
rules.push(` font-display: ${display};`);
|
|
464
|
+
} else {
|
|
465
|
+
rules.push(` font-display: block;`);
|
|
466
|
+
}
|
|
467
|
+
return rules.join("\n") + "\n}";
|
|
468
|
+
}).filter(Boolean).join("\n\n");
|
|
469
|
+
const blob = new Blob([cssContent], { type: "text/css" });
|
|
470
|
+
return {
|
|
471
|
+
as: "link",
|
|
472
|
+
rel: "stylesheet",
|
|
473
|
+
blob
|
|
474
|
+
};
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// src/preferences/services/fonts.ts
|
|
478
|
+
var createFontService = (fontFamilyPref) => {
|
|
479
|
+
const allSupportedLanguages = [];
|
|
480
|
+
const parsedFonts = /* @__PURE__ */ new Map();
|
|
481
|
+
const bunnyFonts = /* @__PURE__ */ new Map();
|
|
482
|
+
const googleFonts = /* @__PURE__ */ new Map();
|
|
483
|
+
const localFonts = /* @__PURE__ */ new Map();
|
|
484
|
+
const resolveFontLanguage = (bcp47Tag, direction = "ltr") => {
|
|
485
|
+
if (!bcp47Tag) return "default";
|
|
486
|
+
if (allSupportedLanguages.includes(bcp47Tag)) {
|
|
487
|
+
return bcp47Tag;
|
|
488
|
+
}
|
|
489
|
+
const parts = bcp47Tag.split(/[-_]/);
|
|
490
|
+
const language = parts[0].toLowerCase();
|
|
491
|
+
const scriptOrRegion = parts[1]?.toLowerCase();
|
|
492
|
+
if (scriptOrRegion) {
|
|
493
|
+
const langScriptOrRegion = `${language}-${scriptOrRegion}`;
|
|
494
|
+
if (allSupportedLanguages.includes(langScriptOrRegion)) {
|
|
495
|
+
return langScriptOrRegion;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (language === "ja" && !scriptOrRegion) {
|
|
499
|
+
if (direction === "rtl" && allSupportedLanguages.includes("ja-v")) {
|
|
500
|
+
return "ja-v";
|
|
501
|
+
}
|
|
502
|
+
if (allSupportedLanguages.includes("ja")) {
|
|
503
|
+
return "ja";
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const shouldFilter = language === "mn" && (scriptOrRegion === "mong" || scriptOrRegion === "cyrl") || language === "zh" && (scriptOrRegion === "hant" || scriptOrRegion === "tw" || scriptOrRegion === "hk");
|
|
507
|
+
if (!shouldFilter && allSupportedLanguages.includes(language)) {
|
|
508
|
+
return language;
|
|
509
|
+
}
|
|
510
|
+
return "default";
|
|
511
|
+
};
|
|
512
|
+
Object.entries(fontFamilyPref).forEach(([collectionName, collectionData]) => {
|
|
513
|
+
const fontCollection = "fonts" in collectionData ? collectionData.fonts : collectionData;
|
|
514
|
+
if ("supportedLanguages" in collectionData) {
|
|
515
|
+
const reducedLanguages = collectionData.supportedLanguages.map((lang) => {
|
|
516
|
+
const parts = lang.split(/[-_]/);
|
|
517
|
+
const language = parts[0].toLowerCase();
|
|
518
|
+
const scriptOrRegion = parts[1]?.toLowerCase();
|
|
519
|
+
return scriptOrRegion ? `${language}-${scriptOrRegion}` : language;
|
|
520
|
+
});
|
|
521
|
+
allSupportedLanguages.push(...reducedLanguages);
|
|
522
|
+
}
|
|
523
|
+
bunnyFonts.set(collectionName, []);
|
|
524
|
+
googleFonts.set(collectionName, []);
|
|
525
|
+
localFonts.set(collectionName, []);
|
|
526
|
+
const collectionBunnyFonts = bunnyFonts.get(collectionName);
|
|
527
|
+
const collectionGoogleFonts = googleFonts.get(collectionName);
|
|
528
|
+
const collectionLocalFonts = localFonts.get(collectionName);
|
|
529
|
+
Object.entries(fontCollection).forEach(([id, font]) => {
|
|
530
|
+
const fontFamily = font.spec.family;
|
|
531
|
+
let fontStack = fontFamily;
|
|
532
|
+
if (font.source.type === "custom") {
|
|
533
|
+
switch (font.source.provider) {
|
|
534
|
+
case "bunny":
|
|
535
|
+
collectionBunnyFonts.push(font);
|
|
536
|
+
break;
|
|
537
|
+
case "google":
|
|
538
|
+
collectionGoogleFonts.push(font);
|
|
539
|
+
break;
|
|
540
|
+
case "local":
|
|
541
|
+
collectionLocalFonts.push(font);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const wrapIfNeeded = (name) => {
|
|
546
|
+
const trimmed = name.trim();
|
|
547
|
+
if (!trimmed) return "";
|
|
548
|
+
if (trimmed.includes(" ") && !/^['"].*['"]$/.test(trimmed)) {
|
|
549
|
+
return `"${trimmed}"`;
|
|
550
|
+
}
|
|
551
|
+
return trimmed;
|
|
552
|
+
};
|
|
553
|
+
const wrappedFontFamily = wrapIfNeeded(fontFamily);
|
|
554
|
+
if (font.spec.fallbacks?.length) {
|
|
555
|
+
const uniqueFallbacks = [...new Set(
|
|
556
|
+
font.spec.fallbacks.filter((fallback) => fallback.toLowerCase() !== fontFamily.toLowerCase()).map(wrapIfNeeded)
|
|
557
|
+
)];
|
|
558
|
+
if (uniqueFallbacks.length > 0) {
|
|
559
|
+
fontStack = [wrappedFontFamily, ...uniqueFallbacks].join(", ");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
parsedFonts.set(id, {
|
|
563
|
+
fontStack: fontStack || wrappedFontFamily,
|
|
564
|
+
fontFamily: wrappedFontFamily,
|
|
565
|
+
weights: font.spec.weights || null,
|
|
566
|
+
widths: font.spec.widths || null
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
const defaultBunnyFonts = bunnyFonts.get("default") || [];
|
|
571
|
+
const defaultGoogleFonts = googleFonts.get("default") || [];
|
|
572
|
+
const defaultLocalFonts = localFonts.get("default") || [];
|
|
573
|
+
const processFonts = (bunnyFontsList, googleFontsList, localFontsList, optimize = false) => {
|
|
574
|
+
const result = {
|
|
575
|
+
allowedDomains: [],
|
|
576
|
+
prepend: [],
|
|
577
|
+
append: []
|
|
578
|
+
};
|
|
579
|
+
const bunnyResources = bunnyFontsList.map((font) => createBunnyFontResources(font)).filter((resource) => resource !== null);
|
|
580
|
+
if (bunnyResources.length > 0) {
|
|
581
|
+
result.allowedDomains.push(
|
|
582
|
+
"https://fonts.bunny.net"
|
|
583
|
+
);
|
|
584
|
+
result.prepend.push(
|
|
585
|
+
{
|
|
586
|
+
as: "link",
|
|
587
|
+
rel: "preconnect",
|
|
588
|
+
url: "https://fonts.bunny.net"
|
|
589
|
+
}
|
|
590
|
+
);
|
|
591
|
+
result.append.push(...bunnyResources);
|
|
592
|
+
}
|
|
593
|
+
const googleResources = googleFontsList.map((font) => createGoogleFontResources(font, optimize ? font.name : void 0)).filter((resource) => resource !== null);
|
|
594
|
+
if (googleResources.length > 0) {
|
|
595
|
+
result.allowedDomains.push(
|
|
596
|
+
"https://fonts.googleapis.com",
|
|
597
|
+
"https://fonts.gstatic.com"
|
|
598
|
+
);
|
|
599
|
+
result.prepend.push(
|
|
600
|
+
{
|
|
601
|
+
as: "link",
|
|
602
|
+
rel: "preconnect",
|
|
603
|
+
url: "https://fonts.googleapis.com"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
as: "link",
|
|
607
|
+
rel: "preconnect",
|
|
608
|
+
url: "https://fonts.gstatic.com",
|
|
609
|
+
attributes: { crossOrigin: "anonymous" }
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
result.append.push(...googleResources);
|
|
613
|
+
}
|
|
614
|
+
const localResources = localFontsList.map(createLocalFontResources).filter((resource) => resource !== null);
|
|
615
|
+
if (localResources.length > 0) {
|
|
616
|
+
result.allowedDomains.push(window.location.origin);
|
|
617
|
+
result.append.push(...localResources);
|
|
618
|
+
}
|
|
619
|
+
return result.append.length > 0 ? result : null;
|
|
620
|
+
};
|
|
621
|
+
const getInjectables = (options, optimize = false) => {
|
|
622
|
+
if (options && "key" in options) {
|
|
623
|
+
const { key } = options;
|
|
624
|
+
if (!key || !(key in fontFamilyPref)) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return processFonts(bunnyFonts.get(key) || [], googleFonts.get(key) || [], localFonts.get(key) || [], optimize);
|
|
628
|
+
}
|
|
629
|
+
if (options && "language" in options) {
|
|
630
|
+
const { language: publicationLanguage } = options;
|
|
631
|
+
for (const [collectionName, collectionData] of Object.entries(fontFamilyPref)) {
|
|
632
|
+
if (collectionName === "default") continue;
|
|
633
|
+
const supportedLangs = "supportedLanguages" in collectionData ? collectionData.supportedLanguages : null;
|
|
634
|
+
if (supportedLangs && Array.isArray(supportedLangs) && publicationLanguage && supportedLangs.includes(publicationLanguage)) {
|
|
635
|
+
return processFonts(bunnyFonts.get(collectionName) || [], googleFonts.get(collectionName) || [], localFonts.get(collectionName) || [], optimize);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return processFonts(defaultBunnyFonts, defaultGoogleFonts, defaultLocalFonts, optimize);
|
|
640
|
+
};
|
|
641
|
+
const getFontMetadata = (fontId) => {
|
|
642
|
+
const parsed = parsedFonts.get(fontId);
|
|
643
|
+
return parsed || { fontStack: null, fontFamily: null, weights: null, widths: null };
|
|
644
|
+
};
|
|
645
|
+
const getFontCollection = (options) => {
|
|
646
|
+
if (options && "key" in options) {
|
|
647
|
+
const { key } = options;
|
|
648
|
+
if (!key || !(key in fontFamilyPref)) {
|
|
649
|
+
return fontFamilyPref.default;
|
|
650
|
+
}
|
|
651
|
+
if (key === "default") {
|
|
652
|
+
return fontFamilyPref.default;
|
|
653
|
+
}
|
|
654
|
+
const prefRecord = fontFamilyPref;
|
|
655
|
+
const collection = prefRecord[key];
|
|
656
|
+
if (collection && "fonts" in collection) {
|
|
657
|
+
return collection.fonts;
|
|
658
|
+
}
|
|
659
|
+
return fontFamilyPref.default;
|
|
660
|
+
}
|
|
661
|
+
if (options && "language" in options) {
|
|
662
|
+
const { language: publicationLanguage } = options;
|
|
663
|
+
for (const [collectionName, collectionData] of Object.entries(fontFamilyPref)) {
|
|
664
|
+
if (collectionName === "default") continue;
|
|
665
|
+
const collection = "fonts" in collectionData ? collectionData : { fonts: collectionData };
|
|
666
|
+
const supportedLangs = "supportedLanguages" in collection ? collection.supportedLanguages : null;
|
|
667
|
+
if (supportedLangs?.includes(publicationLanguage)) {
|
|
668
|
+
return collection.fonts;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return fontFamilyPref.default;
|
|
672
|
+
}
|
|
673
|
+
return fontFamilyPref.default;
|
|
674
|
+
};
|
|
675
|
+
return {
|
|
676
|
+
getInjectables,
|
|
677
|
+
getFontMetadata,
|
|
678
|
+
getFontCollection,
|
|
679
|
+
resolveFontLanguage
|
|
680
|
+
};
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/preferences/hooks/usePreferences.ts
|
|
684
|
+
function usePreferences() {
|
|
685
|
+
const context = useContext(ThPreferencesContext);
|
|
686
|
+
if (!context) {
|
|
687
|
+
throw new Error("usePreferences must be used within a ThPreferencesProvider");
|
|
688
|
+
}
|
|
689
|
+
const fontService = createFontService(context.preferences.settings.keys.fontFamily);
|
|
690
|
+
return {
|
|
691
|
+
preferences: context.preferences,
|
|
692
|
+
updatePreferences: context.updatePreferences,
|
|
693
|
+
getFontInjectables: (options, optimize) => {
|
|
694
|
+
return fontService.getInjectables(options, optimize);
|
|
695
|
+
},
|
|
696
|
+
getFontsList: (options) => {
|
|
697
|
+
return fontService.getFontCollection(options);
|
|
698
|
+
},
|
|
699
|
+
getFontMetadata: (fontId) => {
|
|
700
|
+
return fontService.getFontMetadata(fontId);
|
|
701
|
+
},
|
|
702
|
+
resolveFontLanguage: (bcp47Tag, direction) => {
|
|
703
|
+
return fontService.resolveFontLanguage(bcp47Tag, direction);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/preferences/hooks/usePreferenceKeys.ts
|
|
709
|
+
var usePreferenceKeys = () => {
|
|
710
|
+
const { preferences } = usePreferences();
|
|
711
|
+
const reflowActionKeys = preferences.actions.reflowOrder;
|
|
712
|
+
const fxlActionKeys = preferences.actions.fxlOrder;
|
|
713
|
+
const webPubActionKeys = preferences.actions.webPubOrder;
|
|
714
|
+
const reflowThemeKeys = preferences.theming.themes.reflowOrder;
|
|
715
|
+
const fxlThemeKeys = preferences.theming.themes.fxlOrder;
|
|
716
|
+
const reflowSettingsKeys = preferences.settings.reflowOrder;
|
|
717
|
+
const fxlSettingsKeys = preferences.settings.fxlOrder;
|
|
718
|
+
const webPubSettingsKeys = preferences.settings.webPubOrder;
|
|
719
|
+
const mainTextSettingsKeys = preferences.settings.text?.main ?? defaultTextSettingsMain;
|
|
720
|
+
const subPanelTextSettingsKeys = preferences.settings.text?.subPanel ?? defaultTextSettingsSubpanel;
|
|
721
|
+
const mainSpacingSettingsKeys = preferences.settings.spacing?.main ?? defaultSpacingSettingsMain;
|
|
722
|
+
const subPanelSpacingSettingsKeys = preferences.settings.spacing?.subPanel ?? defaultSpacingSettingsSubpanel;
|
|
723
|
+
const reflowSpacingPresetKeys = preferences.settings.spacing?.presets?.reflowOrder ?? defaultSpacingPresetsOrder;
|
|
724
|
+
const fxlSpacingPresetKeys = [];
|
|
725
|
+
const webPubSpacingPresetKeys = preferences.settings.spacing?.presets?.webPubOrder ?? defaultSpacingPresetsOrder;
|
|
726
|
+
return {
|
|
727
|
+
reflowActionKeys,
|
|
728
|
+
fxlActionKeys,
|
|
729
|
+
webPubActionKeys,
|
|
730
|
+
reflowThemeKeys,
|
|
731
|
+
fxlThemeKeys,
|
|
732
|
+
reflowSettingsKeys,
|
|
733
|
+
fxlSettingsKeys,
|
|
734
|
+
webPubSettingsKeys,
|
|
735
|
+
mainTextSettingsKeys,
|
|
736
|
+
subPanelTextSettingsKeys,
|
|
737
|
+
mainSpacingSettingsKeys,
|
|
738
|
+
subPanelSpacingSettingsKeys,
|
|
739
|
+
reflowSpacingPresetKeys,
|
|
740
|
+
fxlSpacingPresetKeys,
|
|
741
|
+
webPubSpacingPresetKeys
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// src/core/Helpers/prefixString.ts
|
|
746
|
+
var PREFIXES = {
|
|
747
|
+
short: "th",
|
|
748
|
+
full: "thorium_web"
|
|
749
|
+
};
|
|
750
|
+
var prefixString = (str, variant = "short") => {
|
|
751
|
+
return `${PREFIXES[variant]}-${str}`;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// node_modules/.pnpm/colorthief@3.3.1_sharp@0.34.5/node_modules/colorthief/dist/index.js
|
|
755
|
+
var __defProp = Object.defineProperty;
|
|
756
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
757
|
+
var __esm = (fn, res) => function __init() {
|
|
758
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
759
|
+
};
|
|
760
|
+
var __export = (target, all) => {
|
|
761
|
+
for (var name in all)
|
|
762
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
763
|
+
};
|
|
764
|
+
function srgbToLinear(c) {
|
|
765
|
+
const s = c / 255;
|
|
766
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
767
|
+
}
|
|
768
|
+
function linearToSrgb(c) {
|
|
769
|
+
const s = c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
770
|
+
return Math.round(Math.max(0, Math.min(255, s * 255)));
|
|
771
|
+
}
|
|
772
|
+
function rgbToOklch(r, g, b) {
|
|
773
|
+
const lr = srgbToLinear(r);
|
|
774
|
+
const lg = srgbToLinear(g);
|
|
775
|
+
const lb = srgbToLinear(b);
|
|
776
|
+
const l_ = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
|
|
777
|
+
const m_ = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
|
|
778
|
+
const s_ = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
|
|
779
|
+
const l3 = Math.cbrt(l_);
|
|
780
|
+
const m3 = Math.cbrt(m_);
|
|
781
|
+
const s3 = Math.cbrt(s_);
|
|
782
|
+
const L = 0.2104542553 * l3 + 0.793617785 * m3 - 0.0040720468 * s3;
|
|
783
|
+
const a = 1.9779984951 * l3 - 2.428592205 * m3 + 0.4505937099 * s3;
|
|
784
|
+
const bLab = 0.0259040371 * l3 + 0.7827717662 * m3 - 0.808675766 * s3;
|
|
785
|
+
const C = Math.sqrt(a * a + bLab * bLab);
|
|
786
|
+
let H = Math.atan2(bLab, a) * (180 / Math.PI);
|
|
787
|
+
if (H < 0) H += 360;
|
|
788
|
+
return { l: L, c: C, h: H };
|
|
789
|
+
}
|
|
790
|
+
function oklchToRgb(l, c, h) {
|
|
791
|
+
const hRad = h * (Math.PI / 180);
|
|
792
|
+
const a = c * Math.cos(hRad);
|
|
793
|
+
const bLab = c * Math.sin(hRad);
|
|
794
|
+
const l3 = l + 0.3963377774 * a + 0.2158037573 * bLab;
|
|
795
|
+
const m3 = l - 0.1055613458 * a - 0.0638541728 * bLab;
|
|
796
|
+
const s3 = l - 0.0894841775 * a - 1.291485548 * bLab;
|
|
797
|
+
const l_ = l3 * l3 * l3;
|
|
798
|
+
const m_ = m3 * m3 * m3;
|
|
799
|
+
const s_ = s3 * s3 * s3;
|
|
800
|
+
const lr = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
|
|
801
|
+
const lg = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
|
|
802
|
+
const lb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_;
|
|
803
|
+
return [linearToSrgb(lr), linearToSrgb(lg), linearToSrgb(lb)];
|
|
804
|
+
}
|
|
805
|
+
function pixelsRgbToOklchScaled(pixels) {
|
|
806
|
+
const out = new Array(pixels.length);
|
|
807
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
808
|
+
const [r, g, b] = pixels[i];
|
|
809
|
+
const { l, c, h } = rgbToOklch(r, g, b);
|
|
810
|
+
out[i] = [
|
|
811
|
+
Math.round(l * 255),
|
|
812
|
+
Math.round(c / 0.4 * 255),
|
|
813
|
+
Math.round(h / 360 * 255)
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
function paletteOklchScaledToRgb(colors) {
|
|
819
|
+
return colors.map(({ color: [ls, cs, hs], population }) => {
|
|
820
|
+
const l = ls / 255;
|
|
821
|
+
const c = cs / 255 * 0.4;
|
|
822
|
+
const h = hs / 255 * 360;
|
|
823
|
+
return { color: oklchToRgb(l, c, h), population };
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
var init_color_space = __esm({
|
|
827
|
+
"src/color-space.ts"() {
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
function rgbToHsl(r, g, b) {
|
|
831
|
+
const r1 = r / 255;
|
|
832
|
+
const g1 = g / 255;
|
|
833
|
+
const b1 = b / 255;
|
|
834
|
+
const max = Math.max(r1, g1, b1);
|
|
835
|
+
const min = Math.min(r1, g1, b1);
|
|
836
|
+
const l = (max + min) / 2;
|
|
837
|
+
let h = 0;
|
|
838
|
+
let s = 0;
|
|
839
|
+
if (max !== min) {
|
|
840
|
+
const d = max - min;
|
|
841
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
842
|
+
if (max === r1) {
|
|
843
|
+
h = ((g1 - b1) / d + (g1 < b1 ? 6 : 0)) / 6;
|
|
844
|
+
} else if (max === g1) {
|
|
845
|
+
h = ((b1 - r1) / d + 2) / 6;
|
|
846
|
+
} else {
|
|
847
|
+
h = ((r1 - g1) / d + 4) / 6;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
h: Math.round(h * 360),
|
|
852
|
+
s: Math.round(s * 100),
|
|
853
|
+
l: Math.round(l * 100)
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function relativeLuminance(r, g, b) {
|
|
857
|
+
const toLinear = (c) => {
|
|
858
|
+
const s = c / 255;
|
|
859
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
860
|
+
};
|
|
861
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
862
|
+
}
|
|
863
|
+
function contrastRatio(l1, l2) {
|
|
864
|
+
const lighter = Math.max(l1, l2);
|
|
865
|
+
const darker = Math.min(l1, l2);
|
|
866
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
867
|
+
}
|
|
868
|
+
function createColor(r, g, b, population, proportion = 0) {
|
|
869
|
+
return new ColorImpl(r, g, b, population, proportion);
|
|
870
|
+
}
|
|
871
|
+
var ColorImpl;
|
|
872
|
+
var init_color = __esm({
|
|
873
|
+
"src/color.ts"() {
|
|
874
|
+
init_color_space();
|
|
875
|
+
ColorImpl = class {
|
|
876
|
+
constructor(r, g, b, population, proportion) {
|
|
877
|
+
this._r = r;
|
|
878
|
+
this._g = g;
|
|
879
|
+
this._b = b;
|
|
880
|
+
this.population = population;
|
|
881
|
+
this.proportion = proportion;
|
|
882
|
+
}
|
|
883
|
+
rgb() {
|
|
884
|
+
return { r: this._r, g: this._g, b: this._b };
|
|
885
|
+
}
|
|
886
|
+
hex() {
|
|
887
|
+
const toHex = (n) => n.toString(16).padStart(2, "0");
|
|
888
|
+
return `#${toHex(this._r)}${toHex(this._g)}${toHex(this._b)}`;
|
|
889
|
+
}
|
|
890
|
+
hsl() {
|
|
891
|
+
if (!this._hsl) {
|
|
892
|
+
this._hsl = rgbToHsl(this._r, this._g, this._b);
|
|
893
|
+
}
|
|
894
|
+
return this._hsl;
|
|
895
|
+
}
|
|
896
|
+
oklch() {
|
|
897
|
+
if (!this._oklch) {
|
|
898
|
+
this._oklch = rgbToOklch(this._r, this._g, this._b);
|
|
899
|
+
}
|
|
900
|
+
return this._oklch;
|
|
901
|
+
}
|
|
902
|
+
css(format = "rgb") {
|
|
903
|
+
switch (format) {
|
|
904
|
+
case "hsl": {
|
|
905
|
+
const { h, s, l } = this.hsl();
|
|
906
|
+
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
907
|
+
}
|
|
908
|
+
case "oklch": {
|
|
909
|
+
const { l, c, h } = this.oklch();
|
|
910
|
+
return `oklch(${l.toFixed(3)} ${c.toFixed(3)} ${h.toFixed(1)})`;
|
|
911
|
+
}
|
|
912
|
+
case "rgb":
|
|
913
|
+
default:
|
|
914
|
+
return `rgb(${this._r}, ${this._g}, ${this._b})`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
array() {
|
|
918
|
+
return [this._r, this._g, this._b];
|
|
919
|
+
}
|
|
920
|
+
toString() {
|
|
921
|
+
return this.hex();
|
|
922
|
+
}
|
|
923
|
+
get textColor() {
|
|
924
|
+
return this.isDark ? "#ffffff" : "#000000";
|
|
925
|
+
}
|
|
926
|
+
get luminance() {
|
|
927
|
+
if (this._luminance === void 0) {
|
|
928
|
+
this._luminance = relativeLuminance(this._r, this._g, this._b);
|
|
929
|
+
}
|
|
930
|
+
return this._luminance;
|
|
931
|
+
}
|
|
932
|
+
get isDark() {
|
|
933
|
+
return this.luminance <= 0.179;
|
|
934
|
+
}
|
|
935
|
+
get isLight() {
|
|
936
|
+
return !this.isDark;
|
|
937
|
+
}
|
|
938
|
+
get contrast() {
|
|
939
|
+
if (!this._contrast) {
|
|
940
|
+
const lum = this.luminance;
|
|
941
|
+
const white = contrastRatio(lum, 1);
|
|
942
|
+
const black = contrastRatio(lum, 0);
|
|
943
|
+
const foreground = this.isDark ? createColor(255, 255, 255, 0, 0) : createColor(0, 0, 0, 0, 0);
|
|
944
|
+
this._contrast = {
|
|
945
|
+
white: Math.round(white * 100) / 100,
|
|
946
|
+
black: Math.round(black * 100) / 100,
|
|
947
|
+
foreground
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return this._contrast;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
var pipeline_exports = {};
|
|
956
|
+
__export(pipeline_exports, {
|
|
957
|
+
computeFallbackColor: () => computeFallbackColor,
|
|
958
|
+
createPixelArray: () => createPixelArray,
|
|
959
|
+
extractPalette: () => extractPalette,
|
|
960
|
+
validateOptions: () => validateOptions
|
|
961
|
+
});
|
|
962
|
+
function validateOptions(options) {
|
|
963
|
+
let { colorCount, quality } = options;
|
|
964
|
+
if (typeof colorCount === "undefined" || !Number.isInteger(colorCount)) {
|
|
965
|
+
colorCount = 10;
|
|
966
|
+
} else if (colorCount === 1) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
"colorCount should be between 2 and 20. To get one color, call getColor() instead of getPalette()"
|
|
969
|
+
);
|
|
970
|
+
} else {
|
|
971
|
+
colorCount = Math.max(colorCount, 2);
|
|
972
|
+
colorCount = Math.min(colorCount, 20);
|
|
973
|
+
}
|
|
974
|
+
if (typeof quality === "undefined" || !Number.isInteger(quality) || quality < 1) {
|
|
975
|
+
quality = 10;
|
|
976
|
+
}
|
|
977
|
+
const ignoreWhite = options.ignoreWhite !== void 0 ? !!options.ignoreWhite : true;
|
|
978
|
+
const whiteThreshold = typeof options.whiteThreshold === "number" ? options.whiteThreshold : 250;
|
|
979
|
+
const alphaThreshold = typeof options.alphaThreshold === "number" ? options.alphaThreshold : 125;
|
|
980
|
+
const minSaturation = typeof options.minSaturation === "number" ? Math.max(0, Math.min(1, options.minSaturation)) : 0;
|
|
981
|
+
const colorSpace = options.colorSpace ?? "oklch";
|
|
982
|
+
return {
|
|
983
|
+
colorCount,
|
|
984
|
+
quality,
|
|
985
|
+
ignoreWhite,
|
|
986
|
+
whiteThreshold,
|
|
987
|
+
alphaThreshold,
|
|
988
|
+
minSaturation,
|
|
989
|
+
colorSpace
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
function createPixelArray(data, pixelCount, quality, filterOptions) {
|
|
993
|
+
const {
|
|
994
|
+
ignoreWhite = true,
|
|
995
|
+
whiteThreshold = 250,
|
|
996
|
+
alphaThreshold = 125,
|
|
997
|
+
minSaturation = 0
|
|
998
|
+
} = filterOptions;
|
|
999
|
+
const pixelArray = [];
|
|
1000
|
+
for (let i = 0; i < pixelCount; i += quality) {
|
|
1001
|
+
const offset = i * 4;
|
|
1002
|
+
const r = data[offset];
|
|
1003
|
+
const g = data[offset + 1];
|
|
1004
|
+
const b = data[offset + 2];
|
|
1005
|
+
const a = data[offset + 3];
|
|
1006
|
+
if (a !== void 0 && a < alphaThreshold) continue;
|
|
1007
|
+
if (ignoreWhite && r > whiteThreshold && g > whiteThreshold && b > whiteThreshold)
|
|
1008
|
+
continue;
|
|
1009
|
+
if (minSaturation > 0) {
|
|
1010
|
+
const max = Math.max(r, g, b);
|
|
1011
|
+
if (max === 0 || (max - Math.min(r, g, b)) / max < minSaturation)
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
pixelArray.push([r, g, b]);
|
|
1015
|
+
}
|
|
1016
|
+
return pixelArray;
|
|
1017
|
+
}
|
|
1018
|
+
function computeFallbackColor(data, pixelCount, quality) {
|
|
1019
|
+
let rTotal = 0;
|
|
1020
|
+
let gTotal = 0;
|
|
1021
|
+
let bTotal = 0;
|
|
1022
|
+
let count = 0;
|
|
1023
|
+
for (let i = 0; i < pixelCount; i += quality) {
|
|
1024
|
+
const offset = i * 4;
|
|
1025
|
+
rTotal += data[offset];
|
|
1026
|
+
gTotal += data[offset + 1];
|
|
1027
|
+
bTotal += data[offset + 2];
|
|
1028
|
+
count++;
|
|
1029
|
+
}
|
|
1030
|
+
if (count === 0) return null;
|
|
1031
|
+
return [
|
|
1032
|
+
Math.round(rTotal / count),
|
|
1033
|
+
Math.round(gTotal / count),
|
|
1034
|
+
Math.round(bTotal / count)
|
|
1035
|
+
];
|
|
1036
|
+
}
|
|
1037
|
+
function extractPalette(data, width, height, opts, quantizer) {
|
|
1038
|
+
const pixelCount = width * height;
|
|
1039
|
+
const filterOptions = {
|
|
1040
|
+
ignoreWhite: opts.ignoreWhite,
|
|
1041
|
+
whiteThreshold: opts.whiteThreshold,
|
|
1042
|
+
alphaThreshold: opts.alphaThreshold,
|
|
1043
|
+
minSaturation: opts.minSaturation
|
|
1044
|
+
};
|
|
1045
|
+
let pixelArray = createPixelArray(data, pixelCount, opts.quality, filterOptions);
|
|
1046
|
+
if (pixelArray.length === 0) {
|
|
1047
|
+
pixelArray = createPixelArray(data, pixelCount, opts.quality, {
|
|
1048
|
+
...filterOptions,
|
|
1049
|
+
ignoreWhite: false
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (pixelArray.length === 0) {
|
|
1053
|
+
pixelArray = createPixelArray(data, pixelCount, opts.quality, {
|
|
1054
|
+
...filterOptions,
|
|
1055
|
+
ignoreWhite: false,
|
|
1056
|
+
alphaThreshold: 0
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
let quantized;
|
|
1060
|
+
if (opts.colorSpace === "oklch") {
|
|
1061
|
+
const scaled = pixelsRgbToOklchScaled(pixelArray);
|
|
1062
|
+
quantized = paletteOklchScaledToRgb(
|
|
1063
|
+
quantizer.quantize(scaled, opts.colorCount)
|
|
1064
|
+
);
|
|
1065
|
+
} else {
|
|
1066
|
+
quantized = quantizer.quantize(pixelArray, opts.colorCount);
|
|
1067
|
+
}
|
|
1068
|
+
if (quantized.length > 0) {
|
|
1069
|
+
const totalPopulation = quantized.reduce((sum, q) => sum + q.population, 0);
|
|
1070
|
+
return quantized.map(
|
|
1071
|
+
({ color: [r, g, b], population }) => createColor(r, g, b, population, totalPopulation > 0 ? population / totalPopulation : 0)
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
const fallback = computeFallbackColor(data, pixelCount, opts.quality);
|
|
1075
|
+
return fallback ? [createColor(fallback[0], fallback[1], fallback[2], 1, 1)] : null;
|
|
1076
|
+
}
|
|
1077
|
+
var init_pipeline = __esm({
|
|
1078
|
+
"src/pipeline.ts"() {
|
|
1079
|
+
init_color();
|
|
1080
|
+
init_color_space();
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
var browser_exports = {};
|
|
1084
|
+
__export(browser_exports, {
|
|
1085
|
+
BrowserPixelLoader: () => BrowserPixelLoader
|
|
1086
|
+
});
|
|
1087
|
+
var BrowserPixelLoader;
|
|
1088
|
+
var init_browser = __esm({
|
|
1089
|
+
"src/loaders/browser.ts"() {
|
|
1090
|
+
BrowserPixelLoader = class {
|
|
1091
|
+
async load(source) {
|
|
1092
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
1093
|
+
return this.loadFromImage(source);
|
|
1094
|
+
}
|
|
1095
|
+
if (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) {
|
|
1096
|
+
return this.loadFromCanvas(source);
|
|
1097
|
+
}
|
|
1098
|
+
if (typeof ImageData !== "undefined" && source instanceof ImageData) {
|
|
1099
|
+
return {
|
|
1100
|
+
data: source.data,
|
|
1101
|
+
width: source.width,
|
|
1102
|
+
height: source.height
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof HTMLVideoElement !== "undefined" && source instanceof HTMLVideoElement) {
|
|
1106
|
+
return this.loadFromVideo(source);
|
|
1107
|
+
}
|
|
1108
|
+
if (typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
|
|
1109
|
+
return this.loadFromImageBitmap(source);
|
|
1110
|
+
}
|
|
1111
|
+
if (typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas) {
|
|
1112
|
+
return this.loadFromOffscreenCanvas(source);
|
|
1113
|
+
}
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
"Unsupported source type. Expected HTMLImageElement, HTMLCanvasElement, HTMLVideoElement, ImageData, ImageBitmap, or OffscreenCanvas."
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
loadFromImage(img) {
|
|
1119
|
+
if (!img.complete) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
'Image has not finished loading. Wait for the "load" event before calling getColor/getPalette.'
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (!img.naturalWidth) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
"Image has no dimensions. It may not have loaded successfully."
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const canvas = document.createElement("canvas");
|
|
1130
|
+
const ctx = canvas.getContext("2d");
|
|
1131
|
+
const width = canvas.width = img.naturalWidth;
|
|
1132
|
+
const height = canvas.height = img.naturalHeight;
|
|
1133
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1134
|
+
try {
|
|
1135
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1136
|
+
return { data: imageData.data, width, height };
|
|
1137
|
+
} catch (e) {
|
|
1138
|
+
if (e instanceof DOMException && e.name === "SecurityError") {
|
|
1139
|
+
const err = new Error(
|
|
1140
|
+
'Image is tainted by cross-origin data. Add crossorigin="anonymous" to the <img> tag and ensure the server sends appropriate CORS headers.'
|
|
1141
|
+
);
|
|
1142
|
+
err.cause = e;
|
|
1143
|
+
throw err;
|
|
1144
|
+
}
|
|
1145
|
+
throw e;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
loadFromCanvas(canvas) {
|
|
1149
|
+
const ctx = canvas.getContext("2d");
|
|
1150
|
+
const { width, height } = canvas;
|
|
1151
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1152
|
+
return { data: imageData.data, width, height };
|
|
1153
|
+
}
|
|
1154
|
+
loadFromVideo(video) {
|
|
1155
|
+
if (video.readyState < 2) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
'Video is not ready. Wait for the "loadeddata" or "canplay" event before calling getColor/getPalette.'
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const width = video.videoWidth;
|
|
1161
|
+
const height = video.videoHeight;
|
|
1162
|
+
if (!width || !height) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
"Video has no dimensions. It may not have loaded successfully."
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const canvas = document.createElement("canvas");
|
|
1168
|
+
const ctx = canvas.getContext("2d");
|
|
1169
|
+
canvas.width = width;
|
|
1170
|
+
canvas.height = height;
|
|
1171
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
1172
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1173
|
+
return { data: imageData.data, width, height };
|
|
1174
|
+
}
|
|
1175
|
+
loadFromOffscreenCanvas(canvas) {
|
|
1176
|
+
const ctx = canvas.getContext("2d");
|
|
1177
|
+
if (!ctx) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
"Could not get 2D context from OffscreenCanvas."
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const { width, height } = canvas;
|
|
1183
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1184
|
+
return { data: imageData.data, width, height };
|
|
1185
|
+
}
|
|
1186
|
+
loadFromImageBitmap(bitmap) {
|
|
1187
|
+
const canvas = document.createElement("canvas");
|
|
1188
|
+
const ctx = canvas.getContext("2d");
|
|
1189
|
+
canvas.width = bitmap.width;
|
|
1190
|
+
canvas.height = bitmap.height;
|
|
1191
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1192
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
1193
|
+
return { data: imageData.data, width: bitmap.width, height: bitmap.height };
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
var node_exports = {};
|
|
1199
|
+
__export(node_exports, {
|
|
1200
|
+
NodePixelLoader: () => NodePixelLoader,
|
|
1201
|
+
createNodeLoader: () => createNodeLoader
|
|
1202
|
+
});
|
|
1203
|
+
function createNodeLoader(options) {
|
|
1204
|
+
return new NodePixelLoader(options);
|
|
1205
|
+
}
|
|
1206
|
+
var NodePixelLoader;
|
|
1207
|
+
var WORKER_SOURCE;
|
|
1208
|
+
var manager_exports = {};
|
|
1209
|
+
__export(manager_exports, {
|
|
1210
|
+
extractInWorker: () => extractInWorker,
|
|
1211
|
+
isWorkerSupported: () => isWorkerSupported,
|
|
1212
|
+
terminateWorker: () => terminateWorker
|
|
1213
|
+
});
|
|
1214
|
+
function isWorkerSupported() {
|
|
1215
|
+
return typeof Worker !== "undefined";
|
|
1216
|
+
}
|
|
1217
|
+
function getOrCreateWorker() {
|
|
1218
|
+
if (worker) return worker;
|
|
1219
|
+
if (!isWorkerSupported()) {
|
|
1220
|
+
throw new Error("Web Workers are not supported in this environment.");
|
|
1221
|
+
}
|
|
1222
|
+
blobUrl = URL.createObjectURL(
|
|
1223
|
+
new Blob([WORKER_SOURCE], { type: "application/javascript" })
|
|
1224
|
+
);
|
|
1225
|
+
worker = new Worker(blobUrl);
|
|
1226
|
+
worker.onmessage = (e) => {
|
|
1227
|
+
const { id, palette, error } = e.data;
|
|
1228
|
+
const entry = pending.get(id);
|
|
1229
|
+
if (!entry) return;
|
|
1230
|
+
pending.delete(id);
|
|
1231
|
+
if (error) {
|
|
1232
|
+
entry.reject(new Error(error));
|
|
1233
|
+
} else {
|
|
1234
|
+
const raw = palette;
|
|
1235
|
+
const totalPopulation = raw.reduce((sum, q) => sum + q.population, 0);
|
|
1236
|
+
const colors = raw.map(({ color: [r, g, b], population }) => createColor(r, g, b, population, totalPopulation > 0 ? population / totalPopulation : 0));
|
|
1237
|
+
entry.resolve(colors);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
worker.onerror = (e) => {
|
|
1241
|
+
for (const [, entry] of pending) {
|
|
1242
|
+
entry.reject(new Error(e.message));
|
|
1243
|
+
}
|
|
1244
|
+
pending.clear();
|
|
1245
|
+
};
|
|
1246
|
+
return worker;
|
|
1247
|
+
}
|
|
1248
|
+
function extractInWorker(pixels, maxColors, signal) {
|
|
1249
|
+
return new Promise((resolve, reject) => {
|
|
1250
|
+
if (signal?.aborted) {
|
|
1251
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const id = nextId++;
|
|
1255
|
+
pending.set(id, { resolve, reject });
|
|
1256
|
+
const onAbort = () => {
|
|
1257
|
+
pending.delete(id);
|
|
1258
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
1259
|
+
};
|
|
1260
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1261
|
+
try {
|
|
1262
|
+
const w = getOrCreateWorker();
|
|
1263
|
+
w.postMessage({ id, pixels, maxColors });
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
pending.delete(id);
|
|
1266
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1267
|
+
reject(err);
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
function terminateWorker() {
|
|
1272
|
+
if (worker) {
|
|
1273
|
+
worker.terminate();
|
|
1274
|
+
worker = null;
|
|
1275
|
+
}
|
|
1276
|
+
if (blobUrl) {
|
|
1277
|
+
URL.revokeObjectURL(blobUrl);
|
|
1278
|
+
blobUrl = null;
|
|
1279
|
+
}
|
|
1280
|
+
for (const [, entry] of pending) {
|
|
1281
|
+
entry.reject(new Error("Worker terminated"));
|
|
1282
|
+
}
|
|
1283
|
+
pending.clear();
|
|
1284
|
+
}
|
|
1285
|
+
var worker;
|
|
1286
|
+
var blobUrl;
|
|
1287
|
+
var nextId;
|
|
1288
|
+
var pending;
|
|
1289
|
+
init_pipeline();
|
|
1290
|
+
init_pipeline();
|
|
1291
|
+
init_color();
|
|
1292
|
+
createColor(255, 255, 255, 0);
|
|
1293
|
+
createColor(0, 0, 0, 0);
|
|
1294
|
+
var SIGBITS = 5;
|
|
1295
|
+
var RSHIFT = 8 - SIGBITS;
|
|
1296
|
+
var MAX_ITERATIONS = 1e3;
|
|
1297
|
+
var FRACT_BY_POPULATIONS = 0.75;
|
|
1298
|
+
var HISTO_SIZE = 1 << 3 * SIGBITS;
|
|
1299
|
+
function getColorIndex(r, g, b) {
|
|
1300
|
+
return (r << 2 * SIGBITS) + (g << SIGBITS) + b;
|
|
1301
|
+
}
|
|
1302
|
+
var VBox = class _VBox {
|
|
1303
|
+
constructor(r1, r2, g1, g2, b1, b2, histo) {
|
|
1304
|
+
this.r1 = r1;
|
|
1305
|
+
this.r2 = r2;
|
|
1306
|
+
this.g1 = g1;
|
|
1307
|
+
this.g2 = g2;
|
|
1308
|
+
this.b1 = b1;
|
|
1309
|
+
this.b2 = b2;
|
|
1310
|
+
this.histo = histo;
|
|
1311
|
+
}
|
|
1312
|
+
volume(force = false) {
|
|
1313
|
+
if (this._volume === void 0 || force) {
|
|
1314
|
+
this._volume = (this.r2 - this.r1 + 1) * (this.g2 - this.g1 + 1) * (this.b2 - this.b1 + 1);
|
|
1315
|
+
}
|
|
1316
|
+
return this._volume;
|
|
1317
|
+
}
|
|
1318
|
+
count(force = false) {
|
|
1319
|
+
if (this._count === void 0 || force) {
|
|
1320
|
+
let npix = 0;
|
|
1321
|
+
for (let i = this.r1; i <= this.r2; i++) {
|
|
1322
|
+
for (let j = this.g1; j <= this.g2; j++) {
|
|
1323
|
+
for (let k = this.b1; k <= this.b2; k++) {
|
|
1324
|
+
npix += this.histo[getColorIndex(i, j, k)] || 0;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
this._count = npix;
|
|
1329
|
+
}
|
|
1330
|
+
return this._count;
|
|
1331
|
+
}
|
|
1332
|
+
copy() {
|
|
1333
|
+
return new _VBox(this.r1, this.r2, this.g1, this.g2, this.b1, this.b2, this.histo);
|
|
1334
|
+
}
|
|
1335
|
+
avg(force = false) {
|
|
1336
|
+
if (this._avg === void 0 || force) {
|
|
1337
|
+
const mult = 1 << RSHIFT;
|
|
1338
|
+
if (this.r1 === this.r2 && this.g1 === this.g2 && this.b1 === this.b2) {
|
|
1339
|
+
this._avg = [
|
|
1340
|
+
this.r1 << RSHIFT,
|
|
1341
|
+
this.g1 << RSHIFT,
|
|
1342
|
+
this.b1 << RSHIFT
|
|
1343
|
+
];
|
|
1344
|
+
} else {
|
|
1345
|
+
let ntot = 0;
|
|
1346
|
+
let rsum = 0;
|
|
1347
|
+
let gsum = 0;
|
|
1348
|
+
let bsum = 0;
|
|
1349
|
+
for (let i = this.r1; i <= this.r2; i++) {
|
|
1350
|
+
for (let j = this.g1; j <= this.g2; j++) {
|
|
1351
|
+
for (let k = this.b1; k <= this.b2; k++) {
|
|
1352
|
+
const hval = this.histo[getColorIndex(i, j, k)] || 0;
|
|
1353
|
+
ntot += hval;
|
|
1354
|
+
rsum += hval * (i + 0.5) * mult;
|
|
1355
|
+
gsum += hval * (j + 0.5) * mult;
|
|
1356
|
+
bsum += hval * (k + 0.5) * mult;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (ntot) {
|
|
1361
|
+
this._avg = [
|
|
1362
|
+
~~(rsum / ntot),
|
|
1363
|
+
~~(gsum / ntot),
|
|
1364
|
+
~~(bsum / ntot)
|
|
1365
|
+
];
|
|
1366
|
+
} else {
|
|
1367
|
+
this._avg = [
|
|
1368
|
+
~~(mult * (this.r1 + this.r2 + 1) / 2),
|
|
1369
|
+
~~(mult * (this.g1 + this.g2 + 1) / 2),
|
|
1370
|
+
~~(mult * (this.b1 + this.b2 + 1) / 2)
|
|
1371
|
+
];
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return this._avg;
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var PQueue = class {
|
|
1379
|
+
constructor(comparator) {
|
|
1380
|
+
this.comparator = comparator;
|
|
1381
|
+
this.contents = [];
|
|
1382
|
+
this.sorted = false;
|
|
1383
|
+
}
|
|
1384
|
+
sort() {
|
|
1385
|
+
this.contents.sort(this.comparator);
|
|
1386
|
+
this.sorted = true;
|
|
1387
|
+
}
|
|
1388
|
+
push(item) {
|
|
1389
|
+
this.contents.push(item);
|
|
1390
|
+
this.sorted = false;
|
|
1391
|
+
}
|
|
1392
|
+
peek(index) {
|
|
1393
|
+
if (!this.sorted) this.sort();
|
|
1394
|
+
return this.contents[index ?? this.contents.length - 1];
|
|
1395
|
+
}
|
|
1396
|
+
pop() {
|
|
1397
|
+
if (!this.sorted) this.sort();
|
|
1398
|
+
return this.contents.pop();
|
|
1399
|
+
}
|
|
1400
|
+
size() {
|
|
1401
|
+
return this.contents.length;
|
|
1402
|
+
}
|
|
1403
|
+
map(fn) {
|
|
1404
|
+
return this.contents.map(fn);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
function getHisto(pixels) {
|
|
1408
|
+
const histo = new Uint32Array(HISTO_SIZE);
|
|
1409
|
+
for (const pixel of pixels) {
|
|
1410
|
+
const rval = pixel[0] >> RSHIFT;
|
|
1411
|
+
const gval = pixel[1] >> RSHIFT;
|
|
1412
|
+
const bval = pixel[2] >> RSHIFT;
|
|
1413
|
+
histo[getColorIndex(rval, gval, bval)]++;
|
|
1414
|
+
}
|
|
1415
|
+
return histo;
|
|
1416
|
+
}
|
|
1417
|
+
function vboxFromPixels(pixels, histo) {
|
|
1418
|
+
let rmin = 1e6;
|
|
1419
|
+
let rmax = 0;
|
|
1420
|
+
let gmin = 1e6;
|
|
1421
|
+
let gmax = 0;
|
|
1422
|
+
let bmin = 1e6;
|
|
1423
|
+
let bmax = 0;
|
|
1424
|
+
for (const pixel of pixels) {
|
|
1425
|
+
const rval = pixel[0] >> RSHIFT;
|
|
1426
|
+
const gval = pixel[1] >> RSHIFT;
|
|
1427
|
+
const bval = pixel[2] >> RSHIFT;
|
|
1428
|
+
if (rval < rmin) rmin = rval;
|
|
1429
|
+
else if (rval > rmax) rmax = rval;
|
|
1430
|
+
if (gval < gmin) gmin = gval;
|
|
1431
|
+
else if (gval > gmax) gmax = gval;
|
|
1432
|
+
if (bval < bmin) bmin = bval;
|
|
1433
|
+
else if (bval > bmax) bmax = bval;
|
|
1434
|
+
}
|
|
1435
|
+
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
|
1436
|
+
}
|
|
1437
|
+
function medianCutApply(histo, vbox) {
|
|
1438
|
+
if (!vbox.count()) return void 0;
|
|
1439
|
+
if (vbox.count() === 1) return [vbox.copy(), null];
|
|
1440
|
+
const rw = vbox.r2 - vbox.r1 + 1;
|
|
1441
|
+
const gw = vbox.g2 - vbox.g1 + 1;
|
|
1442
|
+
const bw = vbox.b2 - vbox.b1 + 1;
|
|
1443
|
+
const maxw = Math.max(rw, gw, bw);
|
|
1444
|
+
let total = 0;
|
|
1445
|
+
const partialsum = [];
|
|
1446
|
+
const lookaheadsum = [];
|
|
1447
|
+
if (maxw === rw) {
|
|
1448
|
+
for (let i = vbox.r1; i <= vbox.r2; i++) {
|
|
1449
|
+
let sum = 0;
|
|
1450
|
+
for (let j = vbox.g1; j <= vbox.g2; j++) {
|
|
1451
|
+
for (let k = vbox.b1; k <= vbox.b2; k++) {
|
|
1452
|
+
sum += histo[getColorIndex(i, j, k)] || 0;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
total += sum;
|
|
1456
|
+
partialsum[i] = total;
|
|
1457
|
+
}
|
|
1458
|
+
} else if (maxw === gw) {
|
|
1459
|
+
for (let i = vbox.g1; i <= vbox.g2; i++) {
|
|
1460
|
+
let sum = 0;
|
|
1461
|
+
for (let j = vbox.r1; j <= vbox.r2; j++) {
|
|
1462
|
+
for (let k = vbox.b1; k <= vbox.b2; k++) {
|
|
1463
|
+
sum += histo[getColorIndex(j, i, k)] || 0;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
total += sum;
|
|
1467
|
+
partialsum[i] = total;
|
|
1468
|
+
}
|
|
1469
|
+
} else {
|
|
1470
|
+
for (let i = vbox.b1; i <= vbox.b2; i++) {
|
|
1471
|
+
let sum = 0;
|
|
1472
|
+
for (let j = vbox.r1; j <= vbox.r2; j++) {
|
|
1473
|
+
for (let k = vbox.g1; k <= vbox.g2; k++) {
|
|
1474
|
+
sum += histo[getColorIndex(j, k, i)] || 0;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
total += sum;
|
|
1478
|
+
partialsum[i] = total;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
partialsum.forEach((d, i) => {
|
|
1482
|
+
lookaheadsum[i] = total - d;
|
|
1483
|
+
});
|
|
1484
|
+
function doCut(color) {
|
|
1485
|
+
const dim1 = color + "1";
|
|
1486
|
+
const dim2 = color + "2";
|
|
1487
|
+
for (let i = vbox[dim1]; i <= vbox[dim2]; i++) {
|
|
1488
|
+
if (partialsum[i] > total / 2) {
|
|
1489
|
+
const vbox1 = vbox.copy();
|
|
1490
|
+
const vbox2 = vbox.copy();
|
|
1491
|
+
const left = i - vbox[dim1];
|
|
1492
|
+
const right = vbox[dim2] - i;
|
|
1493
|
+
let d2;
|
|
1494
|
+
if (left <= right) {
|
|
1495
|
+
d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
|
|
1496
|
+
} else {
|
|
1497
|
+
d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
|
|
1498
|
+
}
|
|
1499
|
+
while (!partialsum[d2]) d2++;
|
|
1500
|
+
let count2 = lookaheadsum[d2];
|
|
1501
|
+
while (!count2 && partialsum[d2 - 1]) count2 = lookaheadsum[--d2];
|
|
1502
|
+
vbox1[dim2] = d2;
|
|
1503
|
+
vbox2[dim1] = vbox1[dim2] + 1;
|
|
1504
|
+
return [vbox1, vbox2];
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return void 0;
|
|
1508
|
+
}
|
|
1509
|
+
if (maxw === rw) return doCut("r");
|
|
1510
|
+
if (maxw === gw) return doCut("g");
|
|
1511
|
+
return doCut("b");
|
|
1512
|
+
}
|
|
1513
|
+
function iterate(pq, target, histo) {
|
|
1514
|
+
let ncolors = pq.size();
|
|
1515
|
+
let niters = 0;
|
|
1516
|
+
while (niters < MAX_ITERATIONS) {
|
|
1517
|
+
if (ncolors >= target) return;
|
|
1518
|
+
niters++;
|
|
1519
|
+
const vbox = pq.pop();
|
|
1520
|
+
if (!vbox.count()) {
|
|
1521
|
+
pq.push(vbox);
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const result = medianCutApply(histo, vbox);
|
|
1525
|
+
if (!result || !result[0]) return;
|
|
1526
|
+
pq.push(result[0]);
|
|
1527
|
+
if (result[1]) {
|
|
1528
|
+
pq.push(result[1]);
|
|
1529
|
+
ncolors++;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
function quantize(pixels, maxColors) {
|
|
1534
|
+
if (!pixels.length || maxColors < 2 || maxColors > 256) return [];
|
|
1535
|
+
const seenColors = /* @__PURE__ */ new Set();
|
|
1536
|
+
const uniqueColors = [];
|
|
1537
|
+
for (const color of pixels) {
|
|
1538
|
+
const key = color.join(",");
|
|
1539
|
+
if (!seenColors.has(key)) {
|
|
1540
|
+
seenColors.add(key);
|
|
1541
|
+
uniqueColors.push(color);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (uniqueColors.length <= maxColors) {
|
|
1545
|
+
const countMap = /* @__PURE__ */ new Map();
|
|
1546
|
+
for (const color of pixels) {
|
|
1547
|
+
const key = color.join(",");
|
|
1548
|
+
countMap.set(key, (countMap.get(key) || 0) + 1);
|
|
1549
|
+
}
|
|
1550
|
+
return uniqueColors.map((color) => ({
|
|
1551
|
+
color,
|
|
1552
|
+
population: countMap.get(color.join(","))
|
|
1553
|
+
}));
|
|
1554
|
+
}
|
|
1555
|
+
const histo = getHisto(pixels);
|
|
1556
|
+
const vbox = vboxFromPixels(pixels, histo);
|
|
1557
|
+
const pq = new PQueue((a, b) => a.count() - b.count());
|
|
1558
|
+
pq.push(vbox);
|
|
1559
|
+
iterate(pq, FRACT_BY_POPULATIONS * maxColors, histo);
|
|
1560
|
+
const pq2 = new PQueue((a, b) => a.count() * a.volume() - b.count() * b.volume());
|
|
1561
|
+
while (pq.size()) {
|
|
1562
|
+
pq2.push(pq.pop());
|
|
1563
|
+
}
|
|
1564
|
+
iterate(pq2, maxColors, histo);
|
|
1565
|
+
const results = [];
|
|
1566
|
+
while (pq2.size()) {
|
|
1567
|
+
const box = pq2.pop();
|
|
1568
|
+
results.push({
|
|
1569
|
+
color: box.avg(),
|
|
1570
|
+
population: box.count()
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
return results;
|
|
1574
|
+
}
|
|
1575
|
+
var MmcqQuantizer = class {
|
|
1576
|
+
async init() {
|
|
1577
|
+
}
|
|
1578
|
+
quantize(pixels, maxColors) {
|
|
1579
|
+
return quantize(pixels, maxColors);
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
init_browser();
|
|
1583
|
+
init_pipeline();
|
|
1584
|
+
new BrowserPixelLoader();
|
|
1585
|
+
var defaultQuantizer = new MmcqQuantizer();
|
|
1586
|
+
function getColorSync(source, options) {
|
|
1587
|
+
const palette = getPaletteSync(source, { colorCount: 5, ...options });
|
|
1588
|
+
return palette ? palette[0] : null;
|
|
1589
|
+
}
|
|
1590
|
+
function getPaletteSync(source, options) {
|
|
1591
|
+
const opts = validateOptions(options ?? {});
|
|
1592
|
+
const quantizer = options?.quantizer ?? defaultQuantizer;
|
|
1593
|
+
const pixels = loadPixelsSync(source);
|
|
1594
|
+
return extractPalette(
|
|
1595
|
+
pixels.data,
|
|
1596
|
+
pixels.width,
|
|
1597
|
+
pixels.height,
|
|
1598
|
+
opts,
|
|
1599
|
+
quantizer
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
function loadPixelsSync(source) {
|
|
1603
|
+
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
|
|
1604
|
+
return loadFromImage(source);
|
|
1605
|
+
}
|
|
1606
|
+
if (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) {
|
|
1607
|
+
return loadFromCanvas(source);
|
|
1608
|
+
}
|
|
1609
|
+
if (typeof ImageData !== "undefined" && source instanceof ImageData) {
|
|
1610
|
+
return { data: source.data, width: source.width, height: source.height };
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof HTMLVideoElement !== "undefined" && source instanceof HTMLVideoElement) {
|
|
1613
|
+
return loadFromVideo(source);
|
|
1614
|
+
}
|
|
1615
|
+
if (typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
|
|
1616
|
+
return loadFromImageBitmap(source);
|
|
1617
|
+
}
|
|
1618
|
+
if (typeof OffscreenCanvas !== "undefined" && source instanceof OffscreenCanvas) {
|
|
1619
|
+
return loadFromOffscreenCanvas(source);
|
|
1620
|
+
}
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
"Unsupported source type. Expected HTMLImageElement, HTMLCanvasElement, HTMLVideoElement, ImageData, ImageBitmap, or OffscreenCanvas."
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
function loadFromImage(img) {
|
|
1626
|
+
if (!img.complete) {
|
|
1627
|
+
throw new Error(
|
|
1628
|
+
'Image has not finished loading. Wait for the "load" event before calling getColorSync/getPaletteSync.'
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
if (!img.naturalWidth) {
|
|
1632
|
+
throw new Error(
|
|
1633
|
+
"Image has no dimensions. It may not have loaded successfully."
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
const canvas = document.createElement("canvas");
|
|
1637
|
+
const ctx = canvas.getContext("2d");
|
|
1638
|
+
const width = canvas.width = img.naturalWidth;
|
|
1639
|
+
const height = canvas.height = img.naturalHeight;
|
|
1640
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
1641
|
+
try {
|
|
1642
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1643
|
+
return { data: imageData.data, width, height };
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
if (e instanceof DOMException && e.name === "SecurityError") {
|
|
1646
|
+
const err = new Error(
|
|
1647
|
+
'Image is tainted by cross-origin data. Add crossorigin="anonymous" to the <img> tag and ensure the server sends appropriate CORS headers.'
|
|
1648
|
+
);
|
|
1649
|
+
err.cause = e;
|
|
1650
|
+
throw err;
|
|
1651
|
+
}
|
|
1652
|
+
throw e;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function loadFromCanvas(canvas) {
|
|
1656
|
+
const ctx = canvas.getContext("2d");
|
|
1657
|
+
const { width, height } = canvas;
|
|
1658
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1659
|
+
return { data: imageData.data, width, height };
|
|
1660
|
+
}
|
|
1661
|
+
function loadFromVideo(video) {
|
|
1662
|
+
if (video.readyState < 2) {
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
'Video is not ready. Wait for the "loadeddata" or "canplay" event before calling getColorSync/getPaletteSync.'
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
const width = video.videoWidth;
|
|
1668
|
+
const height = video.videoHeight;
|
|
1669
|
+
if (!width || !height) {
|
|
1670
|
+
throw new Error(
|
|
1671
|
+
"Video has no dimensions. It may not have loaded successfully."
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
const canvas = document.createElement("canvas");
|
|
1675
|
+
const ctx = canvas.getContext("2d");
|
|
1676
|
+
canvas.width = width;
|
|
1677
|
+
canvas.height = height;
|
|
1678
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
1679
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1680
|
+
return { data: imageData.data, width, height };
|
|
1681
|
+
}
|
|
1682
|
+
function loadFromOffscreenCanvas(canvas) {
|
|
1683
|
+
const ctx = canvas.getContext("2d");
|
|
1684
|
+
if (!ctx) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
"Could not get 2D context from OffscreenCanvas."
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const { width, height } = canvas;
|
|
1690
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
1691
|
+
return { data: imageData.data, width, height };
|
|
1692
|
+
}
|
|
1693
|
+
function loadFromImageBitmap(bitmap) {
|
|
1694
|
+
const canvas = document.createElement("canvas");
|
|
1695
|
+
const ctx = canvas.getContext("2d");
|
|
1696
|
+
canvas.width = bitmap.width;
|
|
1697
|
+
canvas.height = bitmap.height;
|
|
1698
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
1699
|
+
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
1700
|
+
return { data: imageData.data, width: bitmap.width, height: bitmap.height };
|
|
1701
|
+
}
|
|
1702
|
+
init_color();
|
|
1703
|
+
|
|
1704
|
+
// src/preferences/helpers/getDominantColor.ts
|
|
1705
|
+
var getDominantColor = (img) => {
|
|
1706
|
+
return new Promise((resolve, reject) => {
|
|
1707
|
+
if (!img.complete || img.naturalWidth === 0) {
|
|
1708
|
+
reject(new Error("Image not loaded"));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const color = getColorSync(img);
|
|
1713
|
+
if (color) {
|
|
1714
|
+
const hex = color.hex();
|
|
1715
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(hex)) {
|
|
1716
|
+
reject(new Error(`Invalid color extracted: ${hex}`));
|
|
1717
|
+
} else {
|
|
1718
|
+
resolve(hex);
|
|
1719
|
+
}
|
|
1720
|
+
} else {
|
|
1721
|
+
reject(new Error("No color extracted"));
|
|
1722
|
+
}
|
|
1723
|
+
} catch (error) {
|
|
1724
|
+
reject(new Error(`Color extraction failed: ${error.message}`));
|
|
1725
|
+
}
|
|
1726
|
+
});
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
// src/preferences/helpers/themeGeneration.ts
|
|
1730
|
+
var hexToHsl = (hex) => {
|
|
1731
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
1732
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
1733
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
1734
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
1735
|
+
const l = (max + min) / 2;
|
|
1736
|
+
if (max === min) return [0, 0, l];
|
|
1737
|
+
const d = max - min;
|
|
1738
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1739
|
+
const h = max === r ? ((g - b) / d + (g < b ? 6 : 0)) / 6 : max === g ? ((b - r) / d + 2) / 6 : ((r - g) / d + 4) / 6;
|
|
1740
|
+
return [h, s, l];
|
|
1741
|
+
};
|
|
1742
|
+
var hslToHex = (h, s, l) => {
|
|
1743
|
+
const a = s * Math.min(l, 1 - l);
|
|
1744
|
+
const f = (n) => {
|
|
1745
|
+
const k = (n + h * 12) % 12;
|
|
1746
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
1747
|
+
return Math.round(255 * color).toString(16).padStart(2, "0");
|
|
1748
|
+
};
|
|
1749
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
1750
|
+
};
|
|
1751
|
+
var luminance = (hex) => {
|
|
1752
|
+
const toLinear = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
1753
|
+
const r = toLinear(parseInt(hex.slice(1, 3), 16) / 255);
|
|
1754
|
+
const g = toLinear(parseInt(hex.slice(3, 5), 16) / 255);
|
|
1755
|
+
const b = toLinear(parseInt(hex.slice(5, 7), 16) / 255);
|
|
1756
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1757
|
+
};
|
|
1758
|
+
var contrastRatio2 = (a, b) => {
|
|
1759
|
+
const l1 = luminance(a), l2 = luminance(b);
|
|
1760
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
1761
|
+
};
|
|
1762
|
+
var isLightColor = (hexColor) => luminance(hexColor) > 0.179;
|
|
1763
|
+
var shadeColor = (hex, isLight) => {
|
|
1764
|
+
const [h, s] = hexToHsl(hex);
|
|
1765
|
+
return isLight ? hslToHex(h, Math.min(s, 0.25), 0.93) : hslToHex(h, Math.min(s, 0.3), 0.13);
|
|
1766
|
+
};
|
|
1767
|
+
var generateThemeFromColor = (color) => {
|
|
1768
|
+
const [h, s] = hexToHsl(color);
|
|
1769
|
+
const isLight = isLightColor(color);
|
|
1770
|
+
const background = shadeColor(color, isLight);
|
|
1771
|
+
const text = contrastRatio2(background, "#ffffff") >= contrastRatio2(background, "#000000") ? "#ffffff" : "#000000";
|
|
1772
|
+
const subdue = isLight ? hslToHex(h, Math.min(s, 0.15), 0.55) : hslToHex(h, Math.min(s, 0.15), 0.55);
|
|
1773
|
+
const hover = isLight ? hslToHex(h, Math.min(s, 0.2), 0.83) : hslToHex(h, Math.min(s, 0.2), 0.22);
|
|
1774
|
+
const elevate = `0px 0px 2px ${isLight ? hslToHex(h, 0.1, 0.7) : hslToHex(h, 0.1, 0.08)}`;
|
|
1775
|
+
const link = isLight ? hslToHex(h, 0.7, 0.35) : hslToHex(h, 0.8, 0.72);
|
|
1776
|
+
const visited = isLight ? hslToHex(h, 0.5, 0.28) : hslToHex(h, 0.55, 0.6);
|
|
1777
|
+
const select = hslToHex(h, 0.6, isLight ? 0.8 : 0.35);
|
|
1778
|
+
const focus = hslToHex(h, 0.9, isLight ? 0.4 : 0.65);
|
|
1779
|
+
return {
|
|
1780
|
+
background,
|
|
1781
|
+
text,
|
|
1782
|
+
link,
|
|
1783
|
+
visited,
|
|
1784
|
+
subdue,
|
|
1785
|
+
disable: subdue,
|
|
1786
|
+
hover,
|
|
1787
|
+
onHover: text,
|
|
1788
|
+
select,
|
|
1789
|
+
onSelect: "inherit",
|
|
1790
|
+
focus,
|
|
1791
|
+
elevate,
|
|
1792
|
+
immerse: isLight ? "0.6" : "0.4"
|
|
1793
|
+
};
|
|
1794
|
+
};
|
|
1795
|
+
var extractThemeFromImage = async (imageUrl) => {
|
|
1796
|
+
const img = new Image();
|
|
1797
|
+
img.crossOrigin = "anonymous";
|
|
1798
|
+
await new Promise((resolve, reject) => {
|
|
1799
|
+
img.onload = () => {
|
|
1800
|
+
if (img.naturalWidth === 0) {
|
|
1801
|
+
reject(new Error("Image loaded but has no content (blocked or corrupt)"));
|
|
1802
|
+
} else {
|
|
1803
|
+
resolve();
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
img.onerror = () => reject(new Error(`Failed to load image: ${imageUrl}`));
|
|
1807
|
+
img.src = imageUrl;
|
|
1808
|
+
});
|
|
1809
|
+
const dominantColor = await getDominantColor(img);
|
|
1810
|
+
return generateThemeFromColor(dominantColor);
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
// src/helpers/proxyUrl.ts
|
|
1814
|
+
var proxyUrl = (url) => {
|
|
1815
|
+
if (!url) return void 0;
|
|
1816
|
+
try {
|
|
1817
|
+
const parsed = new URL(url);
|
|
1818
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
1819
|
+
return `/api/proxy?url=${encodeURIComponent(url)}`;
|
|
1820
|
+
}
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
return url;
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// src/preferences/hooks/useTheming.ts
|
|
1827
|
+
var useTheming = ({
|
|
1828
|
+
theme,
|
|
1829
|
+
systemKeys,
|
|
1830
|
+
themeKeys,
|
|
1831
|
+
breakpointsMap,
|
|
1832
|
+
initProps,
|
|
1833
|
+
coverUrl,
|
|
1834
|
+
autoThemeSource,
|
|
1835
|
+
onBreakpointChange,
|
|
1836
|
+
onColorSchemeChange,
|
|
1837
|
+
onContrastChange,
|
|
1838
|
+
onForcedColorsChange,
|
|
1839
|
+
onMonochromeChange,
|
|
1840
|
+
onReducedMotionChange,
|
|
1841
|
+
onReducedTransparencyChange,
|
|
1842
|
+
onCoverThemeGenerated
|
|
1843
|
+
}) => {
|
|
1844
|
+
const [coverThemeTokens, setCoverThemeTokens] = useState(null);
|
|
1845
|
+
const [coverThemeFailed, setCoverThemeFailed] = useState(false);
|
|
1846
|
+
const breakpoints = useBreakpoints(breakpointsMap, onBreakpointChange);
|
|
1847
|
+
const colorScheme = useColorScheme(onColorSchemeChange);
|
|
1848
|
+
const colorSchemeRef = useRef(colorScheme);
|
|
1849
|
+
const contrast = useContrast(onContrastChange);
|
|
1850
|
+
const forcedColors = useForcedColors(onForcedColorsChange);
|
|
1851
|
+
const monochrome = useMonochrome(onMonochromeChange);
|
|
1852
|
+
const reducedMotion = useReducedMotion(onReducedMotionChange);
|
|
1853
|
+
const reducedTransparency = useReducedTransparency(onReducedTransparencyChange);
|
|
1854
|
+
useEffect(() => {
|
|
1855
|
+
if (autoThemeSource === "cover" && coverUrl && !coverThemeTokens) {
|
|
1856
|
+
const extractTheme = async () => {
|
|
1857
|
+
try {
|
|
1858
|
+
const themeTokens = await extractThemeFromImage(proxyUrl(coverUrl) ?? coverUrl);
|
|
1859
|
+
setCoverThemeTokens(themeTokens);
|
|
1860
|
+
onCoverThemeGenerated?.(themeTokens);
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
console.warn("Failed to extract cover theme:", error);
|
|
1863
|
+
setCoverThemeFailed(true);
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
extractTheme();
|
|
1867
|
+
}
|
|
1868
|
+
}, [autoThemeSource, coverUrl, coverThemeTokens, onCoverThemeGenerated]);
|
|
1869
|
+
const updateThemeColorMetaTag = useCallback((color) => {
|
|
1870
|
+
if (typeof document === "undefined") return;
|
|
1871
|
+
let metaTag = document.querySelector("meta[name='theme-color']");
|
|
1872
|
+
if (!metaTag) {
|
|
1873
|
+
metaTag = document.createElement("meta");
|
|
1874
|
+
metaTag.setAttribute("name", "theme-color");
|
|
1875
|
+
document.head.appendChild(metaTag);
|
|
1876
|
+
}
|
|
1877
|
+
metaTag.setAttribute("content", color);
|
|
1878
|
+
}, []);
|
|
1879
|
+
const initThemingCustomProps = useCallback(() => {
|
|
1880
|
+
for (let p in initProps) {
|
|
1881
|
+
document.documentElement.style.setProperty(p, initProps[p]);
|
|
1882
|
+
}
|
|
1883
|
+
}, [initProps]);
|
|
1884
|
+
const inferThemeAuto = useCallback(() => {
|
|
1885
|
+
if (autoThemeSource === "cover") {
|
|
1886
|
+
if (coverThemeTokens) return "cover";
|
|
1887
|
+
if (!coverThemeFailed) return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
return colorSchemeRef.current === "dark" /* dark */ ? systemKeys?.dark : systemKeys?.light;
|
|
1890
|
+
}, [systemKeys, autoThemeSource, coverThemeTokens, coverThemeFailed]);
|
|
1891
|
+
const setThemeCustomProps = useCallback((t) => {
|
|
1892
|
+
if (!t) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (t === "auto") {
|
|
1896
|
+
const autoTheme = inferThemeAuto();
|
|
1897
|
+
if (!autoTheme) {
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
t = autoTheme;
|
|
1901
|
+
}
|
|
1902
|
+
let themeTokens;
|
|
1903
|
+
if (t === "cover" && coverThemeTokens) {
|
|
1904
|
+
themeTokens = coverThemeTokens;
|
|
1905
|
+
} else {
|
|
1906
|
+
themeTokens = themeKeys[t];
|
|
1907
|
+
}
|
|
1908
|
+
if (!themeTokens) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const props = propsToCSSVars(themeTokens, { prefix: prefixString("theme") });
|
|
1912
|
+
for (let p in props) {
|
|
1913
|
+
document.documentElement.style.setProperty(p, props[p]);
|
|
1914
|
+
}
|
|
1915
|
+
updateThemeColorMetaTag(themeTokens.background);
|
|
1916
|
+
}, [inferThemeAuto, updateThemeColorMetaTag, themeKeys, coverThemeTokens]);
|
|
1917
|
+
useEffect(() => {
|
|
1918
|
+
initThemingCustomProps();
|
|
1919
|
+
}, [initThemingCustomProps]);
|
|
1920
|
+
useEffect(() => {
|
|
1921
|
+
colorSchemeRef.current = colorScheme;
|
|
1922
|
+
setThemeCustomProps(theme);
|
|
1923
|
+
}, [setThemeCustomProps, theme, colorScheme]);
|
|
1924
|
+
useEffect(() => {
|
|
1925
|
+
if (!coverThemeTokens || theme !== "auto") return;
|
|
1926
|
+
const props = propsToCSSVars(coverThemeTokens, { prefix: prefixString("theme") });
|
|
1927
|
+
for (let p in props) {
|
|
1928
|
+
document.documentElement.style.setProperty(p, props[p]);
|
|
1929
|
+
}
|
|
1930
|
+
updateThemeColorMetaTag(coverThemeTokens.background);
|
|
1931
|
+
}, [coverThemeTokens, theme, updateThemeColorMetaTag]);
|
|
1932
|
+
const themeResolved = autoThemeSource !== "cover" || !coverUrl || !!coverThemeTokens || coverThemeFailed;
|
|
1933
|
+
return {
|
|
1934
|
+
inferThemeAuto,
|
|
1935
|
+
theme,
|
|
1936
|
+
breakpoints,
|
|
1937
|
+
colorScheme,
|
|
1938
|
+
contrast,
|
|
1939
|
+
forcedColors,
|
|
1940
|
+
monochrome,
|
|
1941
|
+
reducedMotion,
|
|
1942
|
+
reducedTransparency,
|
|
1943
|
+
coverThemeTokens,
|
|
1944
|
+
themeResolved
|
|
1945
|
+
};
|
|
1946
|
+
};
|
|
1947
|
+
|
|
1948
|
+
export { ThAudioMemoryPreferencesAdapter, ThAudioPreferencesProvider, ThMemoryPreferencesAdapter, ThPreferencesProvider, buildThemeObject, createDefinitionsFromBunnyFonts, prefixString, proxyUrl, useActionsPreferences, useAudioActionsPreferences, useAudioPreferences, usePreferenceKeys, usePreferences, useTheming };
|
|
1949
|
+
//# sourceMappingURL=chunk-WECWPYZB.mjs.map
|
|
1950
|
+
//# sourceMappingURL=chunk-WECWPYZB.mjs.map
|