@webmate-studio/builder 0.2.136 → 0.2.139
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/package.json +1 -1
- package/src/design-tokens-v2.js +388 -59
package/package.json
CHANGED
package/src/design-tokens-v2.js
CHANGED
|
@@ -8,79 +8,118 @@
|
|
|
8
8
|
* Dokumentation: webmate-studio/docs/DESIGN-TOKEN-SCHEMA-V2.md
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
// ─── Farbskala-Generierung
|
|
11
|
+
// ─── OKLCH-Farbskala-Generierung ────────────────────────────────────────────
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Preset-Kurven für die Chroma-Verteilung über die 12 Stufen.
|
|
15
|
+
* stdDev = Breite der Gaußkurve (wie weit Sättigung von der Base ausstrahlt)
|
|
16
|
+
* chromaScale = globaler Sättigungsmultiplikator
|
|
17
|
+
*/
|
|
18
|
+
export const COLOR_SCALE_PRESETS = {
|
|
19
|
+
vivid: { label: 'Leuchtend', stdDev: 40, chromaScale: 1.15 },
|
|
20
|
+
natural: { label: 'Natürlich', stdDev: 30, chromaScale: 1.0 },
|
|
21
|
+
soft: { label: 'Pastellig', stdDev: 16, chromaScale: 0.75 },
|
|
22
|
+
muted: { label: 'Gedämpft', stdDev: 22, chromaScale: 0.55 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generiert eine 12-Stufen-Farbskala aus einer Basisfarbe im OKLCH-Farbraum.
|
|
15
27
|
* Stufe 9 = Basisfarbe. Stufen 1-8 heller, 10-12 dunkler.
|
|
16
28
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
29
|
+
* Verwendet OKLCH für perceptuell gleichmäßige Helligkeitsverteilung —
|
|
30
|
+
* garantiert monoton abnehmende Helligkeit von Stufe 1 bis 12.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} baseHex - Basisfarbe als Hex-String
|
|
33
|
+
* @param {object} [curve] - Optionale Kurvenparameter: { preset: 'natural' } oder { preset: 'custom', stdDev, chromaScale }
|
|
19
34
|
*/
|
|
20
|
-
export function generateColorScale(baseHex) {
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
35
|
+
export function generateColorScale(baseHex, curve) {
|
|
36
|
+
const base = hexToOklch(baseHex);
|
|
37
|
+
if (!base) return null;
|
|
38
|
+
|
|
39
|
+
// Kurvenparameter bestimmen
|
|
40
|
+
const presetName = curve?.preset || 'natural';
|
|
41
|
+
const preset = COLOR_SCALE_PRESETS[presetName];
|
|
42
|
+
const stdDev = curve?.preset === 'custom' ? (curve.stdDev ?? 20) : (preset?.stdDev ?? 20);
|
|
43
|
+
const chromaScale = curve?.preset === 'custom' ? (curve.chromaScale ?? 0.85) : (preset?.chromaScale ?? 0.85);
|
|
44
|
+
|
|
45
|
+
// Neutral-Erkennung: Basis mit sehr niedriger Chroma (reines Grau)
|
|
46
|
+
const isPureNeutral = base.c < 0.008;
|
|
47
|
+
|
|
48
|
+
// Lightness-Rampe adaptiv an Base-Lightness
|
|
49
|
+
const L = buildLightnessRamp(base.l);
|
|
50
|
+
|
|
51
|
+
// Wurde die Base-Lightness geclampt? (extrem helle/dunkle Farben)
|
|
52
|
+
const baseClamped = Math.abs(L[8] - base.l) > 0.01;
|
|
53
|
+
|
|
54
|
+
const scale = {};
|
|
55
|
+
for (let i = 0; i < 12; i++) {
|
|
56
|
+
const step = i + 1;
|
|
57
|
+
if (step === 9 && !baseClamped) {
|
|
58
|
+
// Step 9 = exakte Base-Farbe (wenn nicht geclampt)
|
|
59
|
+
scale[step] = baseHex;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
23
62
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
63
|
+
// Chroma über Gaußkurve berechnen
|
|
64
|
+
let c;
|
|
65
|
+
if (isPureNeutral) {
|
|
66
|
+
// Reines Grau: minimale Chroma, Step 1 wird weiß
|
|
67
|
+
c = base.c * 0.3;
|
|
68
|
+
} else if (step === 9) {
|
|
69
|
+
// Geclampt: volle Base-Chroma bei korrigierter Lightness
|
|
70
|
+
c = base.c;
|
|
71
|
+
} else {
|
|
72
|
+
c = computeChroma(L[i], L[8], base.c, stdDev, chromaScale);
|
|
73
|
+
}
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Helle Stufen: leichter Hue-Shift für natürlichere Farbverläufe
|
|
33
|
-
const hShift = isNeutral ? 0 : -5;
|
|
75
|
+
const mapped = gamutMapOklch(L[i], c, base.h);
|
|
76
|
+
scale[step] = oklchToHex(mapped.l, mapped.c, mapped.h);
|
|
77
|
+
}
|
|
34
78
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
6: hslToHex(h - 2, Math.min(s * 0.63 * sFactor, isNeutral ? 5 : 52), 77),
|
|
42
|
-
7: hslToHex(h - 1, Math.min(s * 0.6 * sFactor, isNeutral ? 5 : 50), 68),
|
|
43
|
-
8: hslToHex(h, Math.min(s * 0.62 * sFactor, isNeutral ? 6 : 50), 53),
|
|
44
|
-
9: baseHex,
|
|
45
|
-
10: hslToHex(h + 2, Math.min(s * 1.3 * sFactor, isNeutral ? 8 : 100), Math.max(l - 7, 15)),
|
|
46
|
-
11: hslToHex(h + 1, Math.min(s * 1.3 * sFactor, isNeutral ? 6 : 100), Math.max(l - 11, 20)),
|
|
47
|
-
12: hslToHex(h - 5, Math.min(s * 0.55 * sFactor, isNeutral ? 4 : 45), isNeutral ? 13 : Math.max(l * 0.5, 15)),
|
|
48
|
-
};
|
|
79
|
+
// Reines Neutral Step 1 = reines Weiß
|
|
80
|
+
if (isPureNeutral) {
|
|
81
|
+
scale[1] = '#ffffff';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return scale;
|
|
49
85
|
}
|
|
50
86
|
|
|
51
87
|
/**
|
|
52
|
-
* Generiert eine invertierte 12-Stufen-Skala für Dark Mode.
|
|
88
|
+
* Generiert eine invertierte 12-Stufen-Skala für Dark Mode im OKLCH-Farbraum.
|
|
53
89
|
* Stufe 1 = dunkelster Wert, Stufe 12 = hellster.
|
|
54
90
|
*/
|
|
55
|
-
export function generateDarkColorScale(baseHex) {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
91
|
+
export function generateDarkColorScale(baseHex, curve) {
|
|
92
|
+
const base = hexToOklch(baseHex);
|
|
93
|
+
if (!base) return null;
|
|
94
|
+
|
|
95
|
+
const presetName = curve?.preset || 'natural';
|
|
96
|
+
const preset = COLOR_SCALE_PRESETS[presetName];
|
|
97
|
+
const stdDev = curve?.preset === 'custom' ? (curve.stdDev ?? 20) : (preset?.stdDev ?? 20);
|
|
98
|
+
const chromaScale = curve?.preset === 'custom' ? (curve.chromaScale ?? 0.85) : (preset?.chromaScale ?? 0.85);
|
|
99
|
+
|
|
100
|
+
const isPureNeutral = base.c < 0.008;
|
|
101
|
+
|
|
102
|
+
// Dark Mode: Base etwas heller für bessere Sichtbarkeit
|
|
103
|
+
const darkBaseL = Math.min(base.l + 0.08, 0.65);
|
|
104
|
+
|
|
105
|
+
// Lightness-Rampe adaptiv (invertiert für Dark Mode)
|
|
106
|
+
const L = buildDarkLightnessRamp(darkBaseL);
|
|
107
|
+
|
|
108
|
+
const scale = {};
|
|
109
|
+
for (let i = 0; i < 12; i++) {
|
|
110
|
+
const step = i + 1;
|
|
111
|
+
let c;
|
|
112
|
+
if (isPureNeutral) {
|
|
113
|
+
c = base.c * 0.3;
|
|
114
|
+
} else {
|
|
115
|
+
c = computeChroma(L[i], L[8], base.c, stdDev, chromaScale);
|
|
116
|
+
}
|
|
66
117
|
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
const mapped = gamutMapOklch(L[i], c, base.h);
|
|
119
|
+
scale[step] = oklchToHex(mapped.l, mapped.c, mapped.h);
|
|
120
|
+
}
|
|
69
121
|
|
|
70
|
-
return
|
|
71
|
-
1: hslToHex(h, Math.min(s * 0.35 * sFactor, isNeutral ? 2 : 15), 8),
|
|
72
|
-
2: hslToHex(h, Math.min(s * 0.4 * sFactor, isNeutral ? 3 : 18), 12),
|
|
73
|
-
3: hslToHex(h, Math.min(s * 0.5 * sFactor, isNeutral ? 4 : 22), 18),
|
|
74
|
-
4: hslToHex(h, Math.min(s * 0.55 * sFactor, isNeutral ? 4 : 28), 24),
|
|
75
|
-
5: hslToHex(h, Math.min(s * 0.6 * sFactor, isNeutral ? 5 : 32), 30),
|
|
76
|
-
6: hslToHex(h - 2, Math.min(s * 0.63 * sFactor, isNeutral ? 5 : 40), 38),
|
|
77
|
-
7: hslToHex(h - 1, Math.min(s * 0.7 * sFactor, isNeutral ? 5 : 48), 48),
|
|
78
|
-
8: hslToHex(h, Math.min(s * 0.8 * sFactor, isNeutral ? 6 : 55), 58),
|
|
79
|
-
9: darkBase,
|
|
80
|
-
10: hslToHex(h + 1, Math.min(s * 0.85 * sFactor, isNeutral ? 6 : 65), Math.min(l + 20, 75)),
|
|
81
|
-
11: hslToHex(h + 2, Math.min(s * 0.6 * sFactor, isNeutral ? 5 : 50), Math.min(l + 35, 85)),
|
|
82
|
-
12: hslToHex(h - 3, Math.min(s * 0.35 * sFactor, isNeutral ? 3 : 30), Math.min(l + 50, 95)),
|
|
83
|
-
};
|
|
122
|
+
return scale;
|
|
84
123
|
}
|
|
85
124
|
|
|
86
125
|
/**
|
|
@@ -171,6 +210,286 @@ function hslToHex(h, s, l) {
|
|
|
171
210
|
return rgbToHex((r + m) * 255, (g + m) * 255, (b + m) * 255);
|
|
172
211
|
}
|
|
173
212
|
|
|
213
|
+
// ─── OKLCH-Hilfsfunktionen ──────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Baut eine strikt monoton fallende Lightness-Rampe für 12 Stufen.
|
|
217
|
+
* Steps 1-8 werden zwischen topL und baseL verteilt (Ease-Out-Kurve).
|
|
218
|
+
* Steps 10-12 werden zwischen baseL und bottomL verteilt.
|
|
219
|
+
* Step 9 = baseL (exakt).
|
|
220
|
+
*
|
|
221
|
+
* Funktioniert auch bei extremen Base-Werten (sehr hell/dunkel):
|
|
222
|
+
* - Die Rampe wird auf den verfügbaren Bereich komprimiert
|
|
223
|
+
* - Monotonie ist durch die Konstruktion garantiert
|
|
224
|
+
*/
|
|
225
|
+
function buildLightnessRamp(baseL) {
|
|
226
|
+
const topL = 0.985; // Step 1: fast Weiß
|
|
227
|
+
const bottomL = 0.25; // Step 12: dunkelster Wert
|
|
228
|
+
|
|
229
|
+
// Base clampen: min. 0.30 damit 3 dunkle Stufen Platz haben,
|
|
230
|
+
// max. 0.80 damit 8 helle Stufen genug Abstand in sRGB haben
|
|
231
|
+
const safeBaseL = Math.max(0.30, Math.min(0.80, baseL));
|
|
232
|
+
|
|
233
|
+
// Steps 1-8: Von topL nach safeBaseL + Lücke
|
|
234
|
+
// Die Lücke stellt sicher, dass Step 8 immer heller als Step 9 ist
|
|
235
|
+
const gap = Math.max(0.02, (topL - safeBaseL) * 0.05);
|
|
236
|
+
const step8Target = safeBaseL + gap;
|
|
237
|
+
const range = topL - step8Target;
|
|
238
|
+
|
|
239
|
+
// Feste Verteilungs-Gewichte: Steps 1-2 bleiben nahe Weiß,
|
|
240
|
+
// dann beschleunigter Abstieg zur Base. Inspiriert von Radix' Lightness-Verteilung.
|
|
241
|
+
const weights = [0.0, 0.04, 0.10, 0.20, 0.34, 0.50, 0.70, 0.92];
|
|
242
|
+
const lightSteps = weights.map(w => topL - w * range);
|
|
243
|
+
|
|
244
|
+
// Steps 10-12: nach unten von Base weg
|
|
245
|
+
const darkRange = safeBaseL - bottomL;
|
|
246
|
+
const step10L = safeBaseL - darkRange * 0.12;
|
|
247
|
+
const step11L = safeBaseL - darkRange * 0.55;
|
|
248
|
+
const step12L = bottomL;
|
|
249
|
+
|
|
250
|
+
const ramp = [
|
|
251
|
+
...lightSteps, // Steps 1-8
|
|
252
|
+
safeBaseL, // Step 9
|
|
253
|
+
step10L, // Step 10
|
|
254
|
+
step11L, // Step 11
|
|
255
|
+
step12L, // Step 12
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// Finale Sicherheit: strikt monoton fallend
|
|
259
|
+
for (let i = 1; i < ramp.length; i++) {
|
|
260
|
+
if (ramp[i] >= ramp[i - 1]) {
|
|
261
|
+
ramp[i] = ramp[i - 1] - 0.003;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return ramp;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Baut eine strikt monoton steigende Lightness-Rampe für Dark Mode.
|
|
270
|
+
* Steps 1-8: dunkel → darkBaseL, Steps 10-12: darkBaseL → hell.
|
|
271
|
+
*/
|
|
272
|
+
function buildDarkLightnessRamp(darkBaseL) {
|
|
273
|
+
const bottomL = 0.14;
|
|
274
|
+
const topL = 0.94;
|
|
275
|
+
|
|
276
|
+
const safeBaseL = Math.max(0.22, Math.min(0.85, darkBaseL));
|
|
277
|
+
|
|
278
|
+
const gap = Math.max(0.02, (safeBaseL - bottomL) * 0.05);
|
|
279
|
+
const step8Target = safeBaseL - gap;
|
|
280
|
+
const range = step8Target - bottomL;
|
|
281
|
+
|
|
282
|
+
const weights = [0.0, 0.04, 0.10, 0.20, 0.34, 0.50, 0.70, 0.92];
|
|
283
|
+
const darkSteps = weights.map(w => bottomL + w * range);
|
|
284
|
+
|
|
285
|
+
const lightRange = topL - safeBaseL;
|
|
286
|
+
const step10L = safeBaseL + lightRange * 0.15;
|
|
287
|
+
const step11L = safeBaseL + lightRange * 0.55;
|
|
288
|
+
const step12L = topL;
|
|
289
|
+
|
|
290
|
+
const ramp = [
|
|
291
|
+
...darkSteps,
|
|
292
|
+
safeBaseL,
|
|
293
|
+
step10L,
|
|
294
|
+
step11L,
|
|
295
|
+
step12L,
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
for (let i = 1; i < ramp.length; i++) {
|
|
299
|
+
if (ramp[i] <= ramp[i - 1]) {
|
|
300
|
+
ramp[i] = ramp[i - 1] + 0.003;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return ramp;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Chroma-Berechnung für eine Stufe: Gaußkurve × Lightness-Begrenzung.
|
|
309
|
+
*
|
|
310
|
+
* Die Gaußkurve bestimmt den gewünschten Chroma-Anteil basierend auf der
|
|
311
|
+
* Distanz zur Base-Lightness. Zusätzlich wird die Chroma durch einen
|
|
312
|
+
* Lightness-Faktor begrenzt: Je näher an Weiß (L=1) oder Schwarz (L=0),
|
|
313
|
+
* desto weniger Chroma ist physisch/visuell sinnvoll.
|
|
314
|
+
*
|
|
315
|
+
* Das sorgt dafür, dass Steps 1-2 fast weiß und Steps 11-12 fast schwarz
|
|
316
|
+
* sind — wie bei Radix Colors.
|
|
317
|
+
*/
|
|
318
|
+
function computeChroma(stepL, baseL, baseC, stdDev, chromaScale) {
|
|
319
|
+
// Gaußkurve: Peak bei baseL, flachere Flanken als vorher
|
|
320
|
+
const diff = baseL - stepL;
|
|
321
|
+
const gaussian = Math.exp((-12 / stdDev) * diff * diff);
|
|
322
|
+
|
|
323
|
+
// Lightness-Begrenzung: Asymmetrisch
|
|
324
|
+
// Helle Seite (L > 0.85): Chroma fällt steil auf 0 → Steps 1-2 bleiben fast weiß
|
|
325
|
+
// Dunkle Seite (L < 0.3): Chroma fällt moderat → Steps 11-12 behalten Farbton
|
|
326
|
+
// Mitte (0.3-0.85): Nahezu volle Chroma möglich
|
|
327
|
+
let lightnessLimit;
|
|
328
|
+
if (stepL > 0.85) {
|
|
329
|
+
// Steep falloff for near-white steps
|
|
330
|
+
const t = (stepL - 0.85) / 0.15; // 0 at L=0.85, 1 at L=1.0
|
|
331
|
+
lightnessLimit = 1.0 - t * t; // quadratic drop
|
|
332
|
+
} else if (stepL < 0.3) {
|
|
333
|
+
// Moderate falloff for near-black steps
|
|
334
|
+
const t = stepL / 0.3; // 0 at L=0, 1 at L=0.3
|
|
335
|
+
lightnessLimit = t * t; // quadratic rise
|
|
336
|
+
} else {
|
|
337
|
+
lightnessLimit = 1.0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return baseC * gaussian * lightnessLimit * chromaScale;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Hex → OKLCH Konvertierung.
|
|
346
|
+
* Pfad: Hex → sRGB → Linear RGB → OKLab → OKLCH
|
|
347
|
+
*/
|
|
348
|
+
function hexToOklch(hex) {
|
|
349
|
+
const rgb = hexToRgb(hex);
|
|
350
|
+
if (!rgb) return null;
|
|
351
|
+
|
|
352
|
+
// sRGB → Linear RGB (Gamma-Dekodierung)
|
|
353
|
+
const lr = srgbToLinear(rgb.r / 255);
|
|
354
|
+
const lg = srgbToLinear(rgb.g / 255);
|
|
355
|
+
const lb = srgbToLinear(rgb.b / 255);
|
|
356
|
+
|
|
357
|
+
// Linear RGB → OKLab
|
|
358
|
+
const lab = linearRgbToOklab(lr, lg, lb);
|
|
359
|
+
|
|
360
|
+
// OKLab → OKLCH
|
|
361
|
+
const c = Math.sqrt(lab.a * lab.a + lab.b * lab.b);
|
|
362
|
+
let h = Math.atan2(lab.b, lab.a) * 180 / Math.PI;
|
|
363
|
+
if (h < 0) h += 360;
|
|
364
|
+
|
|
365
|
+
return { l: lab.l, c, h };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* OKLCH → Hex Konvertierung.
|
|
370
|
+
* Pfad: OKLCH → OKLab → Linear RGB → sRGB → Hex
|
|
371
|
+
*/
|
|
372
|
+
function oklchToHex(l, c, h) {
|
|
373
|
+
// OKLCH → OKLab
|
|
374
|
+
const hRad = h * Math.PI / 180;
|
|
375
|
+
const a = c * Math.cos(hRad);
|
|
376
|
+
const b = c * Math.sin(hRad);
|
|
377
|
+
|
|
378
|
+
// OKLab → Linear RGB
|
|
379
|
+
const lin = oklabToLinearRgb(l, a, b);
|
|
380
|
+
|
|
381
|
+
// Linear RGB → sRGB (Gamma-Kodierung)
|
|
382
|
+
const sr = Math.round(linearToSrgb(lin.r) * 255);
|
|
383
|
+
const sg = Math.round(linearToSrgb(lin.g) * 255);
|
|
384
|
+
const sb = Math.round(linearToSrgb(lin.b) * 255);
|
|
385
|
+
|
|
386
|
+
return rgbToHex(
|
|
387
|
+
Math.max(0, Math.min(255, sr)),
|
|
388
|
+
Math.max(0, Math.min(255, sg)),
|
|
389
|
+
Math.max(0, Math.min(255, sb))
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Gamut-Mapping: Reduziert Chroma bis die Farbe in sRGB darstellbar ist.
|
|
395
|
+
* Behält Lightness und Hue bei.
|
|
396
|
+
*/
|
|
397
|
+
function gamutMapOklch(l, c, h) {
|
|
398
|
+
// Triviale Fälle
|
|
399
|
+
if (c <= 0) return { l, c: 0, h };
|
|
400
|
+
if (isInSrgbGamut(l, c, h)) return { l, c, h };
|
|
401
|
+
|
|
402
|
+
// Binary Search: Chroma reduzieren
|
|
403
|
+
let low = 0;
|
|
404
|
+
let high = c;
|
|
405
|
+
for (let i = 0; i < 20; i++) {
|
|
406
|
+
const mid = (low + high) / 2;
|
|
407
|
+
if (isInSrgbGamut(l, mid, h)) {
|
|
408
|
+
low = mid;
|
|
409
|
+
} else {
|
|
410
|
+
high = mid;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return { l, c: low, h };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Prüft ob eine OKLCH-Farbe innerhalb des sRGB-Gamuts liegt.
|
|
418
|
+
*/
|
|
419
|
+
function isInSrgbGamut(l, c, h) {
|
|
420
|
+
const hRad = h * Math.PI / 180;
|
|
421
|
+
const a = c * Math.cos(hRad);
|
|
422
|
+
const b = c * Math.sin(hRad);
|
|
423
|
+
const lin = oklabToLinearRgb(l, a, b);
|
|
424
|
+
const eps = -0.001; // Kleine Toleranz für Rundungsfehler
|
|
425
|
+
return lin.r >= eps && lin.r <= 1.001 &&
|
|
426
|
+
lin.g >= eps && lin.g <= 1.001 &&
|
|
427
|
+
lin.b >= eps && lin.b <= 1.001;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Linear RGB → OKLab (Bjorn Ottosson, 2020)
|
|
432
|
+
*/
|
|
433
|
+
function linearRgbToOklab(r, g, b) {
|
|
434
|
+
// Linear RGB → LMS (via M1 Matrix)
|
|
435
|
+
const l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
436
|
+
const m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
437
|
+
const s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
438
|
+
|
|
439
|
+
// Kubikwurzel
|
|
440
|
+
const l = Math.cbrt(l_);
|
|
441
|
+
const m = Math.cbrt(m_);
|
|
442
|
+
const s = Math.cbrt(s_);
|
|
443
|
+
|
|
444
|
+
// LMS → OKLab (via M2 Matrix)
|
|
445
|
+
return {
|
|
446
|
+
l: 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
|
|
447
|
+
a: 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
|
|
448
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* OKLab → Linear RGB (Inverse von linearRgbToOklab)
|
|
454
|
+
*/
|
|
455
|
+
function oklabToLinearRgb(L, a, b) {
|
|
456
|
+
// OKLab → LMS (inverse M2)
|
|
457
|
+
const l = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
458
|
+
const m = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
459
|
+
const s = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
460
|
+
|
|
461
|
+
// LMS kubisch
|
|
462
|
+
const l3 = l * l * l;
|
|
463
|
+
const m3 = m * m * m;
|
|
464
|
+
const s3 = s * s * s;
|
|
465
|
+
|
|
466
|
+
// LMS → Linear RGB (inverse M1)
|
|
467
|
+
return {
|
|
468
|
+
r: 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3,
|
|
469
|
+
g: -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3,
|
|
470
|
+
b: -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* sRGB Gamma-Dekodierung (sRGB → Linear)
|
|
476
|
+
*/
|
|
477
|
+
function srgbToLinear(c) {
|
|
478
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* sRGB Gamma-Kodierung (Linear → sRGB)
|
|
483
|
+
*/
|
|
484
|
+
function linearToSrgb(c) {
|
|
485
|
+
if (c <= 0) return 0;
|
|
486
|
+
if (c >= 1) return 1;
|
|
487
|
+
return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
// ─── Legacy Farb-Hilfsfunktionen (HSL, Luminanz) ───────────────────────────
|
|
492
|
+
|
|
174
493
|
function relativeLuminance(r, g, b) {
|
|
175
494
|
const [rs, gs, bs] = [r, g, b].map(c => {
|
|
176
495
|
c = c / 255;
|
|
@@ -193,6 +512,8 @@ function contrastRatio(l1, l2) {
|
|
|
193
512
|
* Brand-Welten (primary, secondary, accent) + Status-Welten (success, warning, error, info) + Neutral.
|
|
194
513
|
*/
|
|
195
514
|
export const COLOR_WORLDS = ['primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info', 'neutral'];
|
|
515
|
+
export const BRAND_COLOR_WORLDS = ['primary', 'secondary', 'accent', 'neutral'];
|
|
516
|
+
export const STATUS_COLOR_WORLDS = ['success', 'warning', 'error', 'info'];
|
|
196
517
|
export const SEMANTIC_COLOR_WORLDS = ['primary', 'secondary', 'accent', 'success', 'warning', 'error', 'info'];
|
|
197
518
|
|
|
198
519
|
/**
|
|
@@ -302,34 +623,42 @@ export const defaultDesignTokensV2 = {
|
|
|
302
623
|
colors: {
|
|
303
624
|
primary: {
|
|
304
625
|
base: '#1E40AF',
|
|
626
|
+
curve: { preset: 'natural' },
|
|
305
627
|
scale: generateColorScale('#1E40AF')
|
|
306
628
|
},
|
|
307
629
|
secondary: {
|
|
308
630
|
base: '#6366F1',
|
|
631
|
+
curve: { preset: 'natural' },
|
|
309
632
|
scale: generateColorScale('#6366F1')
|
|
310
633
|
},
|
|
311
634
|
accent: {
|
|
312
635
|
base: '#8B5CF6',
|
|
636
|
+
curve: { preset: 'natural' },
|
|
313
637
|
scale: generateColorScale('#8B5CF6')
|
|
314
638
|
},
|
|
315
639
|
success: {
|
|
316
640
|
base: '#16A34A',
|
|
641
|
+
curve: { preset: 'natural' },
|
|
317
642
|
scale: generateColorScale('#16A34A')
|
|
318
643
|
},
|
|
319
644
|
warning: {
|
|
320
645
|
base: '#D97706',
|
|
646
|
+
curve: { preset: 'natural' },
|
|
321
647
|
scale: generateColorScale('#D97706')
|
|
322
648
|
},
|
|
323
649
|
error: {
|
|
324
650
|
base: '#DC2626',
|
|
651
|
+
curve: { preset: 'natural' },
|
|
325
652
|
scale: generateColorScale('#DC2626')
|
|
326
653
|
},
|
|
327
654
|
info: {
|
|
328
655
|
base: '#2563EB',
|
|
656
|
+
curve: { preset: 'natural' },
|
|
329
657
|
scale: generateColorScale('#2563EB')
|
|
330
658
|
},
|
|
331
659
|
neutral: {
|
|
332
|
-
base: '#6B7280',
|
|
660
|
+
base: '#6B7280',
|
|
661
|
+
curve: { preset: 'natural' },
|
|
333
662
|
scale: generateColorScale('#6B7280')
|
|
334
663
|
}
|
|
335
664
|
},
|