@robr0/design-system 0.5.0 → 0.7.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.
Files changed (35) hide show
  1. package/LICENSE +53 -2
  2. package/README.md +54 -12
  3. package/components/AgentStatus/AgentStatus.css +26 -11
  4. package/components/Card/Card.css +9 -0
  5. package/components/Card/Card.d.ts +6 -0
  6. package/components/Card/Card.js +2 -1
  7. package/components/ChatMessage/ChatMessage.css +6 -0
  8. package/components/ChatMessage/ChatMessage.d.ts +7 -1
  9. package/components/ChatMessage/ChatMessage.js +2 -0
  10. package/components/Composer/Composer.css +23 -6
  11. package/components/Composer/Composer.d.ts +3 -0
  12. package/components/Composer/Composer.js +12 -1
  13. package/components/Prose/Prose.css +18 -1
  14. package/components/Reasoning/Reasoning.css +19 -4
  15. package/components/ShaderField/ShaderField.css +24 -0
  16. package/components/ShaderField/ShaderField.d.ts +68 -0
  17. package/components/ShaderField/ShaderField.js +54 -0
  18. package/components/ShaderField/field.glsl.d.ts +33 -0
  19. package/components/ShaderField/field.glsl.js +177 -0
  20. package/components/ShaderField/useShaderField.d.ts +86 -0
  21. package/components/ShaderField/useShaderField.js +395 -0
  22. package/components/Timeline/Timeline.css +9 -1
  23. package/components/registry.json +8 -0
  24. package/components/registry.json.d.ts +8 -0
  25. package/components/registry.json.js +1 -1
  26. package/index.d.ts +1 -0
  27. package/index.js +8 -0
  28. package/package.json +5 -1
  29. package/tokens/registry.json +2 -0
  30. package/tokens/registry.json.d.ts +2 -0
  31. package/tokens/registry.json.js +1 -1
  32. package/tokens/tokens-dark.css +1 -1
  33. package/tokens/tokens-light.css +3 -2
  34. package/tokens/tokens-motion.css +2 -0
  35. package/tokens/tokens-primitives.css +5 -0
@@ -0,0 +1,54 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import React, { useRef, useMemo, useState, useEffect } from "react";
3
+ import { DEFAULT_SHADER_BLOBS, DEFAULT_SHADER_PARAMS, useShaderField } from "./useShaderField.js";
4
+ import "./ShaderField.css";
5
+ const WATCHDOG_MS = 1500;
6
+ const ShaderField = React.forwardRef(
7
+ ({
8
+ params,
9
+ blobs = DEFAULT_SHADER_BLOBS,
10
+ enabled = true,
11
+ onStatusChange,
12
+ className = "",
13
+ ...rest
14
+ }, ref) => {
15
+ const canvasRef = useRef(null);
16
+ React.useImperativeHandle(ref, () => canvasRef.current);
17
+ const fieldParams = useMemo(
18
+ () => ({ ...DEFAULT_SHADER_PARAMS, ...params, blobs }),
19
+ [params, blobs]
20
+ );
21
+ const { supported, active } = useShaderField(canvasRef, fieldParams, enabled);
22
+ const [gaveUp, setGaveUp] = useState(false);
23
+ useEffect(() => {
24
+ if (!enabled || active || !supported) return;
25
+ const timer = window.setTimeout(() => setGaveUp(true), WATCHDOG_MS);
26
+ return () => window.clearTimeout(timer);
27
+ }, [enabled, active, supported]);
28
+ const status = !enabled || !supported ? "unavailable" : active ? "active" : gaveUp ? "unavailable" : "pending";
29
+ const notify = useRef(onStatusChange);
30
+ notify.current = onStatusChange;
31
+ useEffect(() => {
32
+ notify.current?.(status);
33
+ }, [status]);
34
+ if (!enabled) return null;
35
+ const classes = ["ds-shader-field", className].filter(Boolean).join(" ");
36
+ return /* @__PURE__ */ jsx(
37
+ "canvas",
38
+ {
39
+ ...rest,
40
+ ref: canvasRef,
41
+ className: classes,
42
+ "data-status": status,
43
+ "aria-hidden": "true"
44
+ }
45
+ );
46
+ }
47
+ );
48
+ ShaderField.displayName = "ShaderField";
49
+ export {
50
+ DEFAULT_SHADER_BLOBS,
51
+ DEFAULT_SHADER_PARAMS,
52
+ ShaderField,
53
+ useShaderField
54
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * GLSL sources for ShaderField, the system's ambient background field.
3
+ *
4
+ * This file owns the shader and BLOB_COUNT only. The blob table and every
5
+ * tuneable parameter are data, passed in by the caller, so a look can be
6
+ * retuned without touching GLSL.
7
+ *
8
+ * The field is a sum of soft Gaussian sources. It computes the true Gaussian
9
+ * falloff a stack of blurred CSS discs only approximates, blends in linear
10
+ * space so midpoints keep their saturation, warps the field with fbm noise so
11
+ * it flows instead of translating rigidly, and dithers away the 8-bit banding
12
+ * these wide, soft ramps otherwise produce.
13
+ *
14
+ * Coordinate space is chosen to match a CSS blob layer exactly, so a fallback
15
+ * built from absolutely-positioned discs lines up with it: origin at the
16
+ * container centre (`left`/`top: 50%`), one unit = container width (discs are
17
+ * vw-sized, and CSS percentage margins — top included — resolve against the
18
+ * containing block's width). Centres are therefore constants that never need
19
+ * re-uploading on resize.
20
+ *
21
+ * Colours arrive already converted to linear RGB (the hook does the sRGB
22
+ * decode once per theme change); the shader only encodes back at the end.
23
+ */
24
+ /**
25
+ * Sizes the shader's uniform arrays, so it is a compile-time constant here
26
+ * rather than a length read from data. A caller may pass fewer blobs — unused
27
+ * slots are parked off-field — but never more; ShaderField's own default table
28
+ * fills it exactly, and scripts/validate-shader-background.mjs reads this
29
+ * number out of this file to hold the website's config to it.
30
+ */
31
+ export declare const BLOB_COUNT = 8;
32
+ export declare const vertexSource = "#version 300 es\n// Fullscreen triangle \u2014 no buffers, three vertices from gl_VertexID.\nvoid main() {\n vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
33
+ export declare const fragmentSource = "#version 300 es\nprecision highp float;\n\nuniform vec2 u_resolution; // drawing-buffer size in pixels\nuniform float u_time; // field time, seconds (speed applied JS-side)\nuniform vec3 u_color[8]; // linear RGB per blob\nuniform vec4 u_blob[8]; // xy = centre, z = sigma, w = emission weight\nuniform vec4 u_motion[8]; // xy = angular speeds, zw = phases\nuniform float u_intensity; // peak-alpha ceiling\nuniform float u_warp; // domain-warp strength\nuniform float u_scale; // noise frequency\nuniform float u_grain; // film-grain strength\nuniform float u_streak; // anisotropic stretch: 0 = blobs, 1 = light streams\nuniform vec2 u_mouse; // pointer in field coordinates (smoothed JS-side)\nuniform float u_mvel; // smoothed pointer speed, width units/s\nuniform float u_react; // cursor-reactivity strength\n\nout vec4 outColor;\n\n// Integer hash \u2014 WebGL2 has real uint ops, so no fract(sin()) precision\n// lottery on mobile GPUs.\nuint ihash(uvec2 p) {\n p = p * uvec2(73333u, 7777u) ^ (p.yx >> 3u);\n uint h = p.x ^ (p.y * 2654435761u);\n h ^= h >> 15u;\n h *= 2246822519u;\n h ^= h >> 13u;\n return h;\n}\n\nfloat rnd(ivec2 p) {\n return float(ihash(uvec2(p)) >> 8) * (1.0 / 16777216.0);\n}\n\nfloat vnoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);\n ivec2 c = ivec2(i);\n return mix(\n mix(rnd(c), rnd(c + ivec2(1, 0)), u.x),\n mix(rnd(c + ivec2(0, 1)), rnd(c + ivec2(1, 1)), u.x),\n u.y\n );\n}\n\n// 3 octaves; a 4th is invisible at this softness.\nfloat fbm(vec2 p) {\n float s = 0.0;\n float a = 0.5;\n for (int i = 0; i < 3; i++) {\n s += a * vnoise(p);\n p = p * 2.02 + vec2(17.0, 9.0);\n a *= 0.5;\n }\n return s;\n}\n\nvoid main() {\n // CSS coordinate space: origin at container centre, one unit = width,\n // y running downward.\n vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.x;\n p.y = -p.y;\n\n // Light-stream axis: a fixed diagonal. Distances across the axis are\n // magnified before the Gaussian, so every source stretches along it and\n // the field reads as rays instead of discs.\n float ca = cos(0.5);\n float sa = sin(0.5);\n mat2 toStreak = mat2(ca, -sa, sa, ca);\n float squeeze = 1.0 + u_streak * 4.0;\n\n // Domain warp: the field flows rather than drifts. The noise is sampled\n // in the same anisotropic space, so the flow lines follow the streams.\n float t = u_time;\n vec2 ps = toStreak * p;\n ps.y *= squeeze;\n vec2 q = vec2(\n fbm(ps * u_scale + vec2(0.0, t * 0.030)),\n fbm(ps * u_scale + vec2(5.2, 1.3) - t * 0.024)\n );\n vec2 w = p + u_warp * (q - 0.5);\n\n // Cursor wake: a smooth vortex around the pointer, amplified by speed.\n // The displacement is built from the *unnormalised* offset so it falls to\n // zero at the cursor itself. Normalising here (md/mr) made the direction\n // spin through 360 degrees at full strength across the centre pixel, which\n // rendered as a pinch \u2014 a cone converging to a point.\n vec2 md = p - u_mouse;\n float mr2 = dot(md, md);\n float minf = exp(-mr2 / (2.0 * 0.22 * 0.22));\n vec2 swirl = vec2(-md.y, md.x);\n // smoothstep, not min(): a hard clamp stepped visibly as speed crossed it.\n float stir = 0.25 + 1.3 * smoothstep(0.0, 0.8, u_mvel);\n w += u_react * minf * stir * (swirl * 0.9 + md * 0.35);\n\n // Accumulate true Gaussians in linear space. Positive weights emit;\n // negative weights absorb (shadows \u2014 the eclipse disc).\n vec3 acc = vec3(0.0);\n float cov = 0.0;\n float shade = 0.0;\n for (int i = 0; i < 8; i++) {\n vec2 c = u_blob[i].xy + 0.045 * vec2(\n cos(u_motion[i].x * t + u_motion[i].z),\n sin(u_motion[i].y * t + u_motion[i].w)\n );\n vec2 d = toStreak * (w - c);\n d.y *= squeeze;\n float s = u_blob[i].z;\n float g = exp(-dot(d, d) / (2.0 * s * s));\n float wgt = u_blob[i].w;\n if (wgt >= 0.0) {\n acc += u_color[i] * (g * wgt);\n cov += g * wgt;\n } else {\n shade += g * -wgt;\n }\n }\n\n // Glow trail: wider and softer than the swirl, and mostly velocity-driven,\n // so a resting cursor does not park a bright dot on the field.\n //\n // The wake brightens whatever colour it is passing over rather than adding\n // one of its own. Injecting u_color[0] meant the trail always wore the\n // first blob's token \u2014 gold in the site palette, violet in beam \u2014 which\n // read as a stray colour with no relationship to the field beneath it.\n // Weighting by the local hue keeps the wake colour-neutral: it reads as\n // light falling on the field, and it stays correct for any palette.\n vec3 local = acc / max(cov, 1e-4);\n float gfall = exp(-mr2 / (2.0 * 0.3 * 0.3));\n float glow = u_react * gfall * (0.12 + 0.5 * smoothstep(0.0, 0.8, u_mvel));\n acc += local * glow;\n cov += glow;\n\n vec3 lin = acc / max(cov, 1e-4);\n float lit = max(cov - shade, 0.0);\n float alpha = u_intensity * (1.0 - exp(-lit * 0.9));\n\n vec3 srgb = pow(max(lin, vec3(0.0)), vec3(1.0 / 2.2));\n\n // Film grain: animated hash noise. At u_grain 0 this degenerates to the\n // \u00B11/255 dither that kills banding, so the two share one lookup.\n uvec2 gp = uvec2(gl_FragCoord.xy) + uvec2(\n uint(fract(t * 7.31) * 1024.0) * 7919u,\n uint(fract(t * 3.17) * 1024.0) * 104729u\n );\n float gr = float(ihash(gp) & 255u) / 255.0 - 0.5;\n srgb += gr * (1.0 / 255.0 + u_grain * 0.35);\n // A small grain-driven alpha floor lets the sparkle read even where the\n // field itself is empty (the reference's grainy dark scene).\n float grainFloor = u_grain * 0.1 * (gr + 0.5);\n alpha = clamp(alpha * (1.0 + gr * u_grain * 0.5) + grainFloor, 0.0, 1.0);\n srgb = clamp(srgb, 0.0, 1.0);\n\n // Premultiplied output (context is created with premultipliedAlpha: true).\n outColor = vec4(srgb * alpha, alpha);\n}\n";
@@ -0,0 +1,177 @@
1
+ const BLOB_COUNT = 8;
2
+ const vertexSource = (
3
+ /* glsl */
4
+ `#version 300 es
5
+ // Fullscreen triangle — no buffers, three vertices from gl_VertexID.
6
+ void main() {
7
+ vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
8
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
9
+ }
10
+ `
11
+ );
12
+ const fragmentSource = (
13
+ /* glsl */
14
+ `#version 300 es
15
+ precision highp float;
16
+
17
+ uniform vec2 u_resolution; // drawing-buffer size in pixels
18
+ uniform float u_time; // field time, seconds (speed applied JS-side)
19
+ uniform vec3 u_color[${BLOB_COUNT}]; // linear RGB per blob
20
+ uniform vec4 u_blob[${BLOB_COUNT}]; // xy = centre, z = sigma, w = emission weight
21
+ uniform vec4 u_motion[${BLOB_COUNT}]; // xy = angular speeds, zw = phases
22
+ uniform float u_intensity; // peak-alpha ceiling
23
+ uniform float u_warp; // domain-warp strength
24
+ uniform float u_scale; // noise frequency
25
+ uniform float u_grain; // film-grain strength
26
+ uniform float u_streak; // anisotropic stretch: 0 = blobs, 1 = light streams
27
+ uniform vec2 u_mouse; // pointer in field coordinates (smoothed JS-side)
28
+ uniform float u_mvel; // smoothed pointer speed, width units/s
29
+ uniform float u_react; // cursor-reactivity strength
30
+
31
+ out vec4 outColor;
32
+
33
+ // Integer hash — WebGL2 has real uint ops, so no fract(sin()) precision
34
+ // lottery on mobile GPUs.
35
+ uint ihash(uvec2 p) {
36
+ p = p * uvec2(73333u, 7777u) ^ (p.yx >> 3u);
37
+ uint h = p.x ^ (p.y * 2654435761u);
38
+ h ^= h >> 15u;
39
+ h *= 2246822519u;
40
+ h ^= h >> 13u;
41
+ return h;
42
+ }
43
+
44
+ float rnd(ivec2 p) {
45
+ return float(ihash(uvec2(p)) >> 8) * (1.0 / 16777216.0);
46
+ }
47
+
48
+ float vnoise(vec2 p) {
49
+ vec2 i = floor(p);
50
+ vec2 f = fract(p);
51
+ vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
52
+ ivec2 c = ivec2(i);
53
+ return mix(
54
+ mix(rnd(c), rnd(c + ivec2(1, 0)), u.x),
55
+ mix(rnd(c + ivec2(0, 1)), rnd(c + ivec2(1, 1)), u.x),
56
+ u.y
57
+ );
58
+ }
59
+
60
+ // 3 octaves; a 4th is invisible at this softness.
61
+ float fbm(vec2 p) {
62
+ float s = 0.0;
63
+ float a = 0.5;
64
+ for (int i = 0; i < 3; i++) {
65
+ s += a * vnoise(p);
66
+ p = p * 2.02 + vec2(17.0, 9.0);
67
+ a *= 0.5;
68
+ }
69
+ return s;
70
+ }
71
+
72
+ void main() {
73
+ // CSS coordinate space: origin at container centre, one unit = width,
74
+ // y running downward.
75
+ vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution) / u_resolution.x;
76
+ p.y = -p.y;
77
+
78
+ // Light-stream axis: a fixed diagonal. Distances across the axis are
79
+ // magnified before the Gaussian, so every source stretches along it and
80
+ // the field reads as rays instead of discs.
81
+ float ca = cos(0.5);
82
+ float sa = sin(0.5);
83
+ mat2 toStreak = mat2(ca, -sa, sa, ca);
84
+ float squeeze = 1.0 + u_streak * 4.0;
85
+
86
+ // Domain warp: the field flows rather than drifts. The noise is sampled
87
+ // in the same anisotropic space, so the flow lines follow the streams.
88
+ float t = u_time;
89
+ vec2 ps = toStreak * p;
90
+ ps.y *= squeeze;
91
+ vec2 q = vec2(
92
+ fbm(ps * u_scale + vec2(0.0, t * 0.030)),
93
+ fbm(ps * u_scale + vec2(5.2, 1.3) - t * 0.024)
94
+ );
95
+ vec2 w = p + u_warp * (q - 0.5);
96
+
97
+ // Cursor wake: a smooth vortex around the pointer, amplified by speed.
98
+ // The displacement is built from the *unnormalised* offset so it falls to
99
+ // zero at the cursor itself. Normalising here (md/mr) made the direction
100
+ // spin through 360 degrees at full strength across the centre pixel, which
101
+ // rendered as a pinch — a cone converging to a point.
102
+ vec2 md = p - u_mouse;
103
+ float mr2 = dot(md, md);
104
+ float minf = exp(-mr2 / (2.0 * 0.22 * 0.22));
105
+ vec2 swirl = vec2(-md.y, md.x);
106
+ // smoothstep, not min(): a hard clamp stepped visibly as speed crossed it.
107
+ float stir = 0.25 + 1.3 * smoothstep(0.0, 0.8, u_mvel);
108
+ w += u_react * minf * stir * (swirl * 0.9 + md * 0.35);
109
+
110
+ // Accumulate true Gaussians in linear space. Positive weights emit;
111
+ // negative weights absorb (shadows — the eclipse disc).
112
+ vec3 acc = vec3(0.0);
113
+ float cov = 0.0;
114
+ float shade = 0.0;
115
+ for (int i = 0; i < ${BLOB_COUNT}; i++) {
116
+ vec2 c = u_blob[i].xy + 0.045 * vec2(
117
+ cos(u_motion[i].x * t + u_motion[i].z),
118
+ sin(u_motion[i].y * t + u_motion[i].w)
119
+ );
120
+ vec2 d = toStreak * (w - c);
121
+ d.y *= squeeze;
122
+ float s = u_blob[i].z;
123
+ float g = exp(-dot(d, d) / (2.0 * s * s));
124
+ float wgt = u_blob[i].w;
125
+ if (wgt >= 0.0) {
126
+ acc += u_color[i] * (g * wgt);
127
+ cov += g * wgt;
128
+ } else {
129
+ shade += g * -wgt;
130
+ }
131
+ }
132
+
133
+ // Glow trail: wider and softer than the swirl, and mostly velocity-driven,
134
+ // so a resting cursor does not park a bright dot on the field.
135
+ //
136
+ // The wake brightens whatever colour it is passing over rather than adding
137
+ // one of its own. Injecting u_color[0] meant the trail always wore the
138
+ // first blob's token — gold in the site palette, violet in beam — which
139
+ // read as a stray colour with no relationship to the field beneath it.
140
+ // Weighting by the local hue keeps the wake colour-neutral: it reads as
141
+ // light falling on the field, and it stays correct for any palette.
142
+ vec3 local = acc / max(cov, 1e-4);
143
+ float gfall = exp(-mr2 / (2.0 * 0.3 * 0.3));
144
+ float glow = u_react * gfall * (0.12 + 0.5 * smoothstep(0.0, 0.8, u_mvel));
145
+ acc += local * glow;
146
+ cov += glow;
147
+
148
+ vec3 lin = acc / max(cov, 1e-4);
149
+ float lit = max(cov - shade, 0.0);
150
+ float alpha = u_intensity * (1.0 - exp(-lit * 0.9));
151
+
152
+ vec3 srgb = pow(max(lin, vec3(0.0)), vec3(1.0 / 2.2));
153
+
154
+ // Film grain: animated hash noise. At u_grain 0 this degenerates to the
155
+ // ±1/255 dither that kills banding, so the two share one lookup.
156
+ uvec2 gp = uvec2(gl_FragCoord.xy) + uvec2(
157
+ uint(fract(t * 7.31) * 1024.0) * 7919u,
158
+ uint(fract(t * 3.17) * 1024.0) * 104729u
159
+ );
160
+ float gr = float(ihash(gp) & 255u) / 255.0 - 0.5;
161
+ srgb += gr * (1.0 / 255.0 + u_grain * 0.35);
162
+ // A small grain-driven alpha floor lets the sparkle read even where the
163
+ // field itself is empty (the reference's grainy dark scene).
164
+ float grainFloor = u_grain * 0.1 * (gr + 0.5);
165
+ alpha = clamp(alpha * (1.0 + gr * u_grain * 0.5) + grainFloor, 0.0, 1.0);
166
+ srgb = clamp(srgb, 0.0, 1.0);
167
+
168
+ // Premultiplied output (context is created with premultipliedAlpha: true).
169
+ outColor = vec4(srgb * alpha, alpha);
170
+ }
171
+ `
172
+ );
173
+ export {
174
+ BLOB_COUNT,
175
+ fragmentSource,
176
+ vertexSource
177
+ };
@@ -0,0 +1,86 @@
1
+ import { RefObject } from 'react';
2
+ /** The seven tuneable properties of the field. */
3
+ export interface ShaderParams {
4
+ /** Overall opacity of the field, 0.1–1. */
5
+ intensity: number;
6
+ /** Domain-warp strength: how much fbm noise bends the field, 0–0.5. */
7
+ warp: number;
8
+ /** Noise frequency. Higher values give finer, busier structure, 0.5–6. */
9
+ scale: number;
10
+ /** Drift-rate multiplier on every blob's period, 0–4. */
11
+ speed: number;
12
+ /** Dither/film-grain amplitude, 0–0.4. Also what kills 8-bit banding. */
13
+ grain: number;
14
+ /** Anisotropic stretch: 0 renders discs, 1 renders diagonal streams, 0–1. */
15
+ streak: number;
16
+ /**
17
+ * Cursor-wake strength, 0–1. Defaults to 0 — the interaction is built and
18
+ * dormant, not absent. Raising it turns on the swirl and glow trail, and
19
+ * with them the loop's step up from 30fps to 60fps while a wake is alive.
20
+ */
21
+ react: number;
22
+ }
23
+ /**
24
+ * One light source in the field.
25
+ *
26
+ * The coordinate space is the shader's (see field.glsl): origin at the
27
+ * container centre, one unit = container width, y running downward. Centres
28
+ * are constants — nothing here is recomputed on resize.
29
+ */
30
+ export interface ShaderBlob {
31
+ /**
32
+ * Semantic colour token, read from the canvas's computed style at runtime,
33
+ * so the field re-themes with `data-theme` and with any scoped custom-
34
+ * property override. A token nothing defines resolves to no colour — an
35
+ * invisible source, never an error.
36
+ */
37
+ token: string;
38
+ /** Disc diameter in container-width units. */
39
+ size: number;
40
+ /** Centre offset from the container centre, width units. */
41
+ cx: number;
42
+ /** Centre offset from the container centre, width units. */
43
+ cy: number;
44
+ /** Drift cycle in seconds, before `speed` is applied. */
45
+ period: number;
46
+ /** Phase offset, so sources never move in lockstep. */
47
+ phase: number;
48
+ /**
49
+ * Emission weight. Positive emits light; negative absorbs it, giving a
50
+ * shadow that occludes whatever shines behind it.
51
+ */
52
+ weight: number;
53
+ }
54
+ /**
55
+ * The shipped look: seven parameters tuned by eye, cursor wake off. Spread it
56
+ * and override the one or two you want rather than restating the set.
57
+ */
58
+ export declare const DEFAULT_SHADER_PARAMS: ShaderParams;
59
+ /**
60
+ * The default composition: eight sources on core accent tokens, filling
61
+ * BLOB_COUNT exactly.
62
+ *
63
+ * `--color-action-primary-bg` is deliberately absent. The action colour is
64
+ * reserved for CTAs and focus rings, and a full-viewport decorative field is
65
+ * precisely the use that would stop it meaning "click here" — so the last
66
+ * source takes `--color-core-ui-secondary` instead.
67
+ */
68
+ export declare const DEFAULT_SHADER_BLOBS: readonly ShaderBlob[];
69
+ /**
70
+ * Live tunables. Changing them never rebuilds the GL state — the render loop
71
+ * reads them through a ref, and a paused field redraws one frame.
72
+ */
73
+ export interface ShaderFieldParams extends ShaderParams {
74
+ /** The colour-source table; swap it to change composition live. */
75
+ blobs: readonly ShaderBlob[];
76
+ }
77
+ export interface ShaderFieldState {
78
+ /** False once getContext('webgl2') has returned null — the caller should
79
+ * leave its CSS fallback painted. */
80
+ supported: boolean;
81
+ /** True once the first frame has been drawn. */
82
+ active: boolean;
83
+ }
84
+ export declare function parseColor(value: string): [number, number, number] | null;
85
+ export declare function srgbToLinear(c: number): number;
86
+ export declare function useShaderField(canvasRef: RefObject<HTMLCanvasElement | null>, params: ShaderFieldParams, enabled?: boolean): ShaderFieldState;