@konce-pt/backdrop 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/backdrop",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Animated backdrop core for Koncept UI — canvas renderers plus the maths behind two effects: a density-ramped grid of squares and a flowing current of light with an OKLCH colour ramp. Framework-free TypeScript; the colour maths lives in @konce-pt/color.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",
@@ -37,7 +37,7 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "@konce-pt/color": "0.9.0"
40
+ "@konce-pt/color": "0.10.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "typescript": "~6.0.3"
package/dist/color.d.ts DELETED
@@ -1,101 +0,0 @@
1
- /**
2
- * Barwy wstęgi. Mieszanie idzie po współrzędnych biegunowych OKLab (czyli OKLCH), nie po
3
- * składowych sRGB ani nawet nie po prostokątnym OKLab.
4
- *
5
- * Interpolacja sRGB prowadzi mieszankę przez błoto, bo miesza wartości po gammie. Prostokątny
6
- * OKLab naprawia jasność, ale dla odcieni leżących naprzeciwko siebie — a takie są w rampie
7
- * wstęgi — i tak przecina oś achromatyczną: w połowie przejścia z niebieskiego w żółty zostaje
8
- * szarość dokładnie tam, gdzie wstęga świeci najmocniej. Dopiero rozbicie na jasność, nasycenie
9
- * i kąt odcienia pozwala obejść koło dookoła z zachowanym nasyceniem.
10
- *
11
- * Konwersje wg modelu OKLab Björna Ottossona.
12
- */
13
- /** Kolor w sRGB, składowe 0–255. */
14
- export interface KptRgb {
15
- r: number;
16
- g: number;
17
- b: number;
18
- }
19
- /** Kolor w OKLab: jasność `l` oraz para składowych chromatycznych. */
20
- export interface KptOklab {
21
- l: number;
22
- a: number;
23
- b: number;
24
- }
25
- /**
26
- * Zapis heksadecymalny (`#rgb` albo `#rrggbb`, z krzyżykiem lub bez) na składowe.
27
- * Zwraca `null` na wszystkim innym — kolor przychodzi z wejścia komponentu albo z konfiguratora,
28
- * więc śmieci są normalnym przypadkiem, a nie wyjątkiem.
29
- */
30
- export declare function parseHexColor(hex: string): KptRgb | null;
31
- /**
32
- * Dowolny zapis koloru CSS na składowe — także `oklch()`, `color-mix()` czy nazwa własna.
33
- *
34
- * Tokeny biblioteki są zapisane w OKLCH, a barwy wstęgi trzeba wymieszać, nie tylko podać dalej.
35
- * Zamiast pisać własny parser każdej składni CSS, malujemy kolor na canvasie 1×1 i odczytujemy
36
- * piksel: przeglądarka rozumie każdy zapis, który sama akceptuje, i nigdy nie rozejdzie się
37
- * z tym, co widać na stronie.
38
- *
39
- * Zapis nieprawidłowy daje `null`, tak samo jak środowisko bez DOM. `fillStyle` odrzuca taką
40
- * wartość po cichu, zostawiając poprzednią, więc sam odczyt piksela nie odróżniłby śmiecia od
41
- * legalnej czerni — stąd dwa różne wartowniki: przy wartości nieprawidłowej każdy z nich zostaje
42
- * na swoim miejscu i oba przebiegi dają inny wynik. Getter zwraca kolor już zserializowany,
43
- * więc bramka nie potrzebuje odczytu piksela.
44
- */
45
- export declare function readCssColor(value: string): KptRgb | null;
46
- /** Składowe na `#rrggbb`. */
47
- export declare function toHexColor(rgb: KptRgb): string;
48
- export declare function srgbToOklab(rgb: KptRgb): KptOklab;
49
- export declare function oklabToSrgb(lab: KptOklab): KptRgb;
50
- /**
51
- * Obrót odcienia o zadany kąt (stopnie) przy zachowanej jasności i nasyceniu.
52
- *
53
- * Stąd biorą się domyślne barwy wstęgi: paleta biblioteki jest neutralna, więc zamiast sięgać
54
- * po tokeny statusowe (`info`, `success`) do celów dekoracyjnych, dwie towarzyszące barwy
55
- * wyprowadzamy z koloru wiodącego. Wstęga zostaje w kolorach aplikacji niezależnie od motywu.
56
- */
57
- export declare function shiftHue(rgb: KptRgb, degrees: number): KptRgb;
58
- /**
59
- * Kolor na zamkniętej rampie `colors` w miejscu `position`.
60
- *
61
- * Rampa jest cykliczna: ostatni przystanek wraca do pierwszego, bo barwy przesuwają się wzdłuż
62
- * wstęgi bez końca i szew w środku kadru byłby widoczny. `position` poza zakresem 0–1 zawija się,
63
- * więc wywołujący nie musi pilnować rosnącego czasu.
64
- *
65
- * Odcień idzie krótszym łukiem koła. Przystanek bez nasycenia przejmuje odcień sąsiada — inaczej
66
- * przejście do szarości i z powrotem szarpnęłoby barwą, bo `atan2` na zerowym nasyceniu zwraca
67
- * kąt przypadkowy.
68
- */
69
- export declare function mixOklch(colors: readonly KptRgb[], position: number): KptRgb;
70
- /**
71
- * Jasność przeniesiona na powierzchnię bieżącego motywu z zachowaniem odległości od niej.
72
- *
73
- * Barwa zapisana na biel odsuwa się od niej o `d`; na ciemnej powierzchni ma się odsunąć o to samo
74
- * `d`, tyle że w drugą stronę — bo tam właśnie jest miejsce. Stąd czerń na bieli staje się bielą
75
- * na ciemnym tle, a biel staje się dokładnie powierzchnią. Barwa w połowie skali zostaje mniej
76
- * więcej tam, gdzie była, i dalej odcina się od tła.
77
- *
78
- * Na jasnej powierzchni wychodzi tożsamość: `1 - (1 - L)` to `L`.
79
- */
80
- export declare function adaptLightness(lightness: number, surface: number): number;
81
- /**
82
- * Barwa przeniesiona na zadaną powierzchnię. Rusza wyłącznie jasność — odcień i nasycenie zostają,
83
- * więc barwa marki nie zmienia się w inną barwę, tylko dobiera jasność do tła.
84
- */
85
- export declare function adaptToSurface(rgb: KptRgb, surface: KptRgb): KptRgb;
86
- /**
87
- * Jasność względna wg WCAG (0 = czerń, 1 = biel).
88
- *
89
- * To nie to samo co jasność OKLab: OKLab opisuje, jak jasno barwa *wygląda*, a ta liczba mówi,
90
- * ile z niej wychodzi światła — i tylko na niej stoi rachunek kontrastu, którym rozstrzygamy,
91
- * czy na danym tle czytelniejszy jest napis ciemny czy jasny.
92
- */
93
- export declare function relativeLuminance(rgb: KptRgb): number;
94
- /**
95
- * Czy na tej powierzchni czytelniejsza jest treść ciemna.
96
- *
97
- * Sekcja z własnym tłem przestaje być powierzchnią strony, więc `--kpt-color-on-surface` opisuje
98
- * już nie to tło, tylko sąsiada. Bez tego rachunku treść w sekcji z ciemnym tłem w motywie jasnym
99
- * wychodzi czarna na czarnym.
100
- */
101
- export declare function prefersDarkContent(surface: KptRgb): boolean;
package/dist/color.js DELETED
@@ -1,228 +0,0 @@
1
- /**
2
- * Barwy wstęgi. Mieszanie idzie po współrzędnych biegunowych OKLab (czyli OKLCH), nie po
3
- * składowych sRGB ani nawet nie po prostokątnym OKLab.
4
- *
5
- * Interpolacja sRGB prowadzi mieszankę przez błoto, bo miesza wartości po gammie. Prostokątny
6
- * OKLab naprawia jasność, ale dla odcieni leżących naprzeciwko siebie — a takie są w rampie
7
- * wstęgi — i tak przecina oś achromatyczną: w połowie przejścia z niebieskiego w żółty zostaje
8
- * szarość dokładnie tam, gdzie wstęga świeci najmocniej. Dopiero rozbicie na jasność, nasycenie
9
- * i kąt odcienia pozwala obejść koło dookoła z zachowanym nasyceniem.
10
- *
11
- * Konwersje wg modelu OKLab Björna Ottossona.
12
- */
13
- const TAU = Math.PI * 2;
14
- const clamp255 = (value) => (value < 0 ? 0 : value > 255 ? 255 : Math.round(value));
15
- /** Gamma sRGB → liniowo. */
16
- const toLinear = (channel) => {
17
- const value = channel / 255;
18
- return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
19
- };
20
- /** Liniowo → gamma sRGB. */
21
- const toGamma = (value) => {
22
- const encoded = value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055;
23
- return clamp255(encoded * 255);
24
- };
25
- /**
26
- * Zapis heksadecymalny (`#rgb` albo `#rrggbb`, z krzyżykiem lub bez) na składowe.
27
- * Zwraca `null` na wszystkim innym — kolor przychodzi z wejścia komponentu albo z konfiguratora,
28
- * więc śmieci są normalnym przypadkiem, a nie wyjątkiem.
29
- */
30
- export function parseHexColor(hex) {
31
- const value = hex.trim().replace(/^#/, '');
32
- const full = value.length === 3 ? value.replace(/./g, (char) => char + char) : value;
33
- if (!/^[0-9a-f]{6}$/i.test(full))
34
- return null;
35
- return {
36
- r: Number.parseInt(full.slice(0, 2), 16),
37
- g: Number.parseInt(full.slice(2, 4), 16),
38
- b: Number.parseInt(full.slice(4, 6), 16),
39
- };
40
- }
41
- /** Leniwy kontekst 1×1 — służy wyłącznie do pytania przeglądarki o znaczenie zapisu koloru. */
42
- let probe;
43
- /**
44
- * Dowolny zapis koloru CSS na składowe — także `oklch()`, `color-mix()` czy nazwa własna.
45
- *
46
- * Tokeny biblioteki są zapisane w OKLCH, a barwy wstęgi trzeba wymieszać, nie tylko podać dalej.
47
- * Zamiast pisać własny parser każdej składni CSS, malujemy kolor na canvasie 1×1 i odczytujemy
48
- * piksel: przeglądarka rozumie każdy zapis, który sama akceptuje, i nigdy nie rozejdzie się
49
- * z tym, co widać na stronie.
50
- *
51
- * Zapis nieprawidłowy daje `null`, tak samo jak środowisko bez DOM. `fillStyle` odrzuca taką
52
- * wartość po cichu, zostawiając poprzednią, więc sam odczyt piksela nie odróżniłby śmiecia od
53
- * legalnej czerni — stąd dwa różne wartowniki: przy wartości nieprawidłowej każdy z nich zostaje
54
- * na swoim miejscu i oba przebiegi dają inny wynik. Getter zwraca kolor już zserializowany,
55
- * więc bramka nie potrzebuje odczytu piksela.
56
- */
57
- export function readCssColor(value) {
58
- const direct = parseHexColor(value);
59
- if (direct)
60
- return direct;
61
- if (typeof document === 'undefined')
62
- return null;
63
- if (probe === undefined) {
64
- const canvas = document.createElement('canvas');
65
- canvas.width = 1;
66
- canvas.height = 1;
67
- probe = canvas.getContext('2d', { willReadFrequently: true });
68
- }
69
- if (!probe)
70
- return null;
71
- probe.fillStyle = '#000000';
72
- probe.fillStyle = value;
73
- const first = probe.fillStyle;
74
- probe.fillStyle = '#ffffff';
75
- probe.fillStyle = value;
76
- if (probe.fillStyle !== first)
77
- return null;
78
- // Sonda musi zaczynać od przezroczystości: barwa z kanałem alfa nałożyłaby się na poprzedni
79
- // odczyt i wyszłaby mieszanka dwóch niezwiązanych ze sobą wywołań.
80
- probe.clearRect(0, 0, 1, 1);
81
- probe.fillRect(0, 0, 1, 1);
82
- const [r, g, b] = probe.getImageData(0, 0, 1, 1).data;
83
- return { r, g, b };
84
- }
85
- /** Składowe na `#rrggbb`. */
86
- export function toHexColor(rgb) {
87
- const part = (value) => clamp255(value).toString(16).padStart(2, '0');
88
- return `#${part(rgb.r)}${part(rgb.g)}${part(rgb.b)}`;
89
- }
90
- export function srgbToOklab(rgb) {
91
- const r = toLinear(rgb.r);
92
- const g = toLinear(rgb.g);
93
- const b = toLinear(rgb.b);
94
- const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
95
- const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
96
- const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
97
- return {
98
- l: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
99
- a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
100
- b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
101
- };
102
- }
103
- export function oklabToSrgb(lab) {
104
- const l = (lab.l + 0.3963377774 * lab.a + 0.2158037573 * lab.b) ** 3;
105
- const m = (lab.l - 0.1055613458 * lab.a - 0.0638541728 * lab.b) ** 3;
106
- const s = (lab.l - 0.0894841775 * lab.a - 1.291485548 * lab.b) ** 3;
107
- return {
108
- r: toGamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
109
- g: toGamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
110
- b: toGamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
111
- };
112
- }
113
- /**
114
- * Obrót odcienia o zadany kąt (stopnie) przy zachowanej jasności i nasyceniu.
115
- *
116
- * Stąd biorą się domyślne barwy wstęgi: paleta biblioteki jest neutralna, więc zamiast sięgać
117
- * po tokeny statusowe (`info`, `success`) do celów dekoracyjnych, dwie towarzyszące barwy
118
- * wyprowadzamy z koloru wiodącego. Wstęga zostaje w kolorach aplikacji niezależnie od motywu.
119
- */
120
- export function shiftHue(rgb, degrees) {
121
- const lab = srgbToOklab(rgb);
122
- const chroma = Math.hypot(lab.a, lab.b);
123
- const hue = Math.atan2(lab.b, lab.a) + (degrees / 360) * TAU;
124
- return oklabToSrgb({ l: lab.l, a: Math.cos(hue) * chroma, b: Math.sin(hue) * chroma });
125
- }
126
- /** Poniżej tego nasycenia odcień przestaje cokolwiek znaczyć — szarość nie ma kąta. */
127
- const ACHROMATIC = 1e-4;
128
- /**
129
- * Kolor na zamkniętej rampie `colors` w miejscu `position`.
130
- *
131
- * Rampa jest cykliczna: ostatni przystanek wraca do pierwszego, bo barwy przesuwają się wzdłuż
132
- * wstęgi bez końca i szew w środku kadru byłby widoczny. `position` poza zakresem 0–1 zawija się,
133
- * więc wywołujący nie musi pilnować rosnącego czasu.
134
- *
135
- * Odcień idzie krótszym łukiem koła. Przystanek bez nasycenia przejmuje odcień sąsiada — inaczej
136
- * przejście do szarości i z powrotem szarpnęłoby barwą, bo `atan2` na zerowym nasyceniu zwraca
137
- * kąt przypadkowy.
138
- */
139
- export function mixOklch(colors, position) {
140
- if (colors.length === 0)
141
- return { r: 0, g: 0, b: 0 };
142
- if (colors.length === 1)
143
- return colors[0];
144
- const wrapped = position - Math.floor(position);
145
- const scaled = wrapped * colors.length;
146
- const index = Math.min(Math.floor(scaled), colors.length - 1);
147
- const blend = scaled - index;
148
- const from = srgbToOklab(colors[index]);
149
- const to = srgbToOklab(colors[(index + 1) % colors.length]);
150
- const fromChroma = Math.hypot(from.a, from.b);
151
- const toChroma = Math.hypot(to.a, to.b);
152
- const chroma = fromChroma + (toChroma - fromChroma) * blend;
153
- let hue;
154
- if (fromChroma < ACHROMATIC && toChroma < ACHROMATIC) {
155
- hue = 0;
156
- }
157
- else if (fromChroma < ACHROMATIC) {
158
- hue = Math.atan2(to.b, to.a);
159
- }
160
- else if (toChroma < ACHROMATIC) {
161
- hue = Math.atan2(from.b, from.a);
162
- }
163
- else {
164
- const start = Math.atan2(from.b, from.a);
165
- const end = Math.atan2(to.b, to.a);
166
- // Krótszy łuk: różnicę sprowadzamy do (-π, π], zamiast iść dookoła przez pół koła.
167
- const delta = ((end - start + Math.PI * 3) % TAU) - Math.PI;
168
- hue = start + delta * blend;
169
- }
170
- return oklabToSrgb({
171
- l: from.l + (to.l - from.l) * blend,
172
- a: Math.cos(hue) * chroma,
173
- b: Math.sin(hue) * chroma,
174
- });
175
- }
176
- /**
177
- * Jasność powierzchni, względem której zapisano barwę. Biblioteka trzyma motyw jasny w `:root`,
178
- * a ciemny jako nadpisanie, więc wartość wpisana do szablonu jest z założenia dobrana do jasnego —
179
- * i to jest jedyne założenie, jakie robi adaptacja.
180
- */
181
- const REFERENCE_SURFACE = 1;
182
- /** Powyżej tej jasności powierzchnię uznajemy za jasną, poniżej za ciemną. */
183
- const SURFACE_MIDPOINT = 0.5;
184
- const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);
185
- /**
186
- * Jasność przeniesiona na powierzchnię bieżącego motywu z zachowaniem odległości od niej.
187
- *
188
- * Barwa zapisana na biel odsuwa się od niej o `d`; na ciemnej powierzchni ma się odsunąć o to samo
189
- * `d`, tyle że w drugą stronę — bo tam właśnie jest miejsce. Stąd czerń na bieli staje się bielą
190
- * na ciemnym tle, a biel staje się dokładnie powierzchnią. Barwa w połowie skali zostaje mniej
191
- * więcej tam, gdzie była, i dalej odcina się od tła.
192
- *
193
- * Na jasnej powierzchni wychodzi tożsamość: `1 - (1 - L)` to `L`.
194
- */
195
- export function adaptLightness(lightness, surface) {
196
- const distance = Math.abs(REFERENCE_SURFACE - lightness);
197
- return clamp01(surface > SURFACE_MIDPOINT ? REFERENCE_SURFACE - distance : surface + distance);
198
- }
199
- /**
200
- * Barwa przeniesiona na zadaną powierzchnię. Rusza wyłącznie jasność — odcień i nasycenie zostają,
201
- * więc barwa marki nie zmienia się w inną barwę, tylko dobiera jasność do tła.
202
- */
203
- export function adaptToSurface(rgb, surface) {
204
- const lab = srgbToOklab(rgb);
205
- return oklabToSrgb({ ...lab, l: adaptLightness(lab.l, srgbToOklab(surface).l) });
206
- }
207
- /**
208
- * Jasność względna wg WCAG (0 = czerń, 1 = biel).
209
- *
210
- * To nie to samo co jasność OKLab: OKLab opisuje, jak jasno barwa *wygląda*, a ta liczba mówi,
211
- * ile z niej wychodzi światła — i tylko na niej stoi rachunek kontrastu, którym rozstrzygamy,
212
- * czy na danym tle czytelniejszy jest napis ciemny czy jasny.
213
- */
214
- export function relativeLuminance(rgb) {
215
- return 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b);
216
- }
217
- /** Próg WCAG na wybór treści ciemnej albo jasnej — powyżej niego ciemna wygrywa kontrastem. */
218
- const CONTENT_THRESHOLD = 0.179;
219
- /**
220
- * Czy na tej powierzchni czytelniejsza jest treść ciemna.
221
- *
222
- * Sekcja z własnym tłem przestaje być powierzchnią strony, więc `--kpt-color-on-surface` opisuje
223
- * już nie to tło, tylko sąsiada. Bez tego rachunku treść w sekcji z ciemnym tłem w motywie jasnym
224
- * wychodzi czarna na czarnym.
225
- */
226
- export function prefersDarkContent(surface) {
227
- return relativeLuminance(surface) > CONTENT_THRESHOLD;
228
- }