LittleZhu001 commited on
Commit
4c28838
·
verified ·
1 Parent(s): 599718a

上传站点辅助脚本

Browse files
Files changed (1) hide show
  1. scripts/color-palettes.js +274 -0
scripts/color-palettes.js ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global color palettes generator and watcher
2
+ // - Observes CSS variable --primary-color and theme changes
3
+ // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
+ // - Exposes results as CSS variables on :root
5
+ // - Supports variable color counts per palette via CSS vars
6
+ // - Dispatches a 'palettes:updated' CustomEvent after each update
7
+
8
+ (() => {
9
+ const MODE = { cssRoot: document.documentElement };
10
+
11
+ const getCssVar = (name) => {
12
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
+ };
14
+ const getIntFromCssVar = (name, fallback) => {
15
+ const raw = getCssVar(name);
16
+ if (!raw) return fallback;
17
+ const v = parseInt(String(raw), 10);
18
+ if (Number.isNaN(v)) return fallback;
19
+ return v;
20
+ };
21
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
+
23
+ // Color math (OKLab/OKLCH)
24
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
+ const rgbToOklab = (r, g, b) => {
27
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
+ return { L, a, b: b2 };
35
+ };
36
+ const oklabToRgb = (L, a, b) => {
37
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
+ const l = l_ * l_ * l_;
41
+ const m = m_ * m_ * m_;
42
+ const s = s_ * s_ * s_;
43
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
+ return { r, g, b: b3 };
47
+ };
48
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
+ const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a * a + b * b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
50
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
+ const toHex = ({ r, g, b }) => {
53
+ const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255);
54
+ const h = (n) => n.toString(16).padStart(2, '0');
55
+ return `#${h(R)}${h(G)}${h(B)}`.toUpperCase();
56
+ };
57
+ const oklchToHexSafe = (L, C, h) => { let c = C; for (let i = 0; i < 12; i++) { const { a, b } = oklchToOklab(L, c, h); const rgb = oklabToRgb(L, a, b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c - 0.02); } return toHex(oklabToRgb(L, 0, 0)); };
58
+ const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1]) / 255, g: Number(m[2]) / 255, b: Number(m[3]) / 255 }; } catch { return null; } };
59
+
60
+ // Get primary color in OKLCH format to preserve precision
61
+ const getPrimaryOKLCH = () => {
62
+ const css = getCssVar('--primary-color');
63
+ if (!css) return null;
64
+
65
+ // For OKLCH colors, return the exact values without conversion
66
+ if (css.includes('oklch')) {
67
+ const oklchMatch = css.match(/oklch\(([^)]+)\)/);
68
+ if (oklchMatch) {
69
+ const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim()));
70
+ if (values.length >= 3) {
71
+ const [L, C, h] = values;
72
+ return { L, C, h };
73
+ }
74
+ }
75
+ }
76
+
77
+ // For non-OKLCH colors, convert to OKLCH for consistency
78
+ const rgb = parseCssColorToRgb(css);
79
+ if (rgb) {
80
+ const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b);
81
+ const { C, h } = oklabToOklch(L, a, b);
82
+ return { L, C, h };
83
+ }
84
+ return null;
85
+ };
86
+
87
+ // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH
88
+ const getPrimaryHex = () => {
89
+ const oklch = getPrimaryOKLCH();
90
+ if (!oklch) return null;
91
+
92
+ const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h);
93
+ const rgb = oklabToRgb(oklch.L, a, b);
94
+ return toHex(rgb);
95
+ };
96
+ // No count management via CSS anymore; counts are passed directly to the API
97
+
98
+ const generators = {
99
+ categorical: (baseOKLCH, count) => {
100
+ const { L, C, h } = baseOKLCH;
101
+ const L0 = Math.min(0.85, Math.max(0.4, L));
102
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
103
+ const total = Math.max(1, Math.min(12, count || 8));
104
+ const hueStep = 360 / total;
105
+ const results = [];
106
+ for (let i = 0; i < total; i++) {
107
+ const hDeg = (h + i * hueStep) % 360;
108
+ const lVar = ((i % 3) - 1) * 0.04;
109
+ results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg));
110
+ }
111
+ return results;
112
+ },
113
+ sequential: (baseOKLCH, count) => {
114
+ const { L, C, h } = baseOKLCH;
115
+ const total = Math.max(1, Math.min(12, count || 8));
116
+ const startL = Math.max(0.25, L - 0.18);
117
+ const endL = Math.min(0.92, L + 0.18);
118
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
119
+ const out = [];
120
+ for (let i = 0; i < total; i++) {
121
+ const t = total === 1 ? 0 : i / (total - 1);
122
+ const lNow = startL * (1 - t) + endL * t;
123
+ const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2));
124
+ out.push(oklchToHexSafe(lNow, cNow, h));
125
+ }
126
+ return out;
127
+ },
128
+ diverging: (baseOKLCH, count) => {
129
+ const { L, C, h } = baseOKLCH;
130
+ const total = Math.max(1, Math.min(12, count || 8));
131
+
132
+ // Left endpoint: EXACT primary color (no darkening)
133
+ const leftLab = oklchToOklab(L, C, h);
134
+ // Right endpoint: complement with same L and similar C (clamped safe)
135
+ const compH = (h + 180) % 360;
136
+ const cSafe = Math.min(0.35, Math.max(0.08, C));
137
+ const rightLab = oklchToOklab(L, cSafe, compH);
138
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
139
+
140
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
141
+ const lerp = (a, b, t) => a + (b - a) * t;
142
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
143
+
144
+ const out = [];
145
+ if (total % 2 === 1) {
146
+ const nSide = (total - 1) >> 1; // items on each side
147
+ // Left side: include left endpoint exactly at index 0
148
+ for (let i = 0; i < nSide; i++) {
149
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
150
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
151
+ const tt = t * 0.9; // keep some distance from pure white before center
152
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
153
+ }
154
+ // Center
155
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
156
+ // Right side: start near white and end EXACTLY at rightLab
157
+ for (let i = 0; i < nSide; i++) {
158
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
159
+ const tt = Math.max(0.1, t); // avoid starting at pure white
160
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
161
+ }
162
+ // Ensure first and last are exact endpoints
163
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
164
+ } else {
165
+ const nSide = total >> 1;
166
+ // Left half including left endpoint, approaching white but not reaching it
167
+ for (let i = 0; i < nSide; i++) {
168
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
169
+ const tt = t * 0.9;
170
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
171
+ }
172
+ // Right half: mirror from near white to exact right endpoint
173
+ for (let i = 0; i < nSide; i++) {
174
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
175
+ const tt = Math.max(0.1, t);
176
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
177
+ }
178
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
179
+ }
180
+ return out;
181
+ }
182
+ };
183
+
184
+ let lastSignature = '';
185
+
186
+ const updatePalettes = () => {
187
+ const primaryOKLCH = getPrimaryOKLCH();
188
+ const primaryHex = getPrimaryHex();
189
+ const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`;
190
+ if (signature === lastSignature) return;
191
+ lastSignature = signature;
192
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { }
193
+ };
194
+
195
+ const bootstrap = () => {
196
+ // Initial setup - only run once on page load
197
+ updatePalettes();
198
+
199
+ // Observer will handle all subsequent changes
200
+ const mo = new MutationObserver(() => updatePalettes());
201
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
202
+
203
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
204
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
205
+ const cssRoot = document.documentElement;
206
+ const getCssVar = (name) => {
207
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
208
+ };
209
+ const resolveCssToRgb01 = (css) => {
210
+ const rgb = parseCssColorToRgb(css);
211
+ if (!rgb) return null;
212
+ return rgb; // already 0..1
213
+ };
214
+ const mixRgb01 = (a, b, t) => ({ r: a.r * (1 - t) + b.r * t, g: a.g * (1 - t) + b.g * t, b: a.b * (1 - t) + b.b * t });
215
+ const relLum = (rgb) => {
216
+ const f = (u) => srgbToLinear(u);
217
+ return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b);
218
+ };
219
+ const contrast = (fg, bg) => {
220
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2);
221
+ return (a + 0.05) / (b + 0.05);
222
+ };
223
+ try {
224
+ const bg = resolveCssToRgb01(bgCss);
225
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
226
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
227
+ const candidates = candidatesCss
228
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
229
+ .filter(x => !!x.rgb);
230
+ // Pick the max contrast
231
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
232
+ for (let i = 1; i < candidates.length; i++) {
233
+ const cr = contrast(candidates[i].rgb, bg);
234
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
235
+ }
236
+ // Optional softening via blend factor (0..1), blending towards muted color
237
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
238
+ let finalRgb = best.rgb;
239
+ if (blend > 0) {
240
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
241
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
242
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
243
+ }
244
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
245
+ const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40 * haloStrength})` : `rgba(0,0,0,${0.30 + 0.30 * haloStrength})`;
246
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
247
+ } catch {
248
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
249
+ }
250
+ };
251
+ window.ColorPalettes = {
252
+ refresh: updatePalettes,
253
+ notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } },
254
+ getPrimary: () => getPrimaryHex(),
255
+ getPrimaryOKLCH: () => getPrimaryOKLCH(),
256
+ getColors: (key, count = 6) => {
257
+ const primaryOKLCH = getPrimaryOKLCH();
258
+ if (!primaryOKLCH) return [];
259
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
260
+ if (key === 'categorical') return generators.categorical(primaryOKLCH, total);
261
+ if (key === 'sequential') return generators.sequential(primaryOKLCH, total);
262
+ if (key === 'diverging') return generators.diverging(primaryOKLCH, total);
263
+ return [];
264
+ },
265
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
266
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
267
+ };
268
+ };
269
+
270
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
271
+ else bootstrap();
272
+ })();
273
+
274
+