@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.
- package/LICENSE +53 -2
- package/README.md +54 -12
- package/components/AgentStatus/AgentStatus.css +26 -11
- package/components/Card/Card.css +9 -0
- package/components/Card/Card.d.ts +6 -0
- package/components/Card/Card.js +2 -1
- package/components/ChatMessage/ChatMessage.css +6 -0
- package/components/ChatMessage/ChatMessage.d.ts +7 -1
- package/components/ChatMessage/ChatMessage.js +2 -0
- package/components/Composer/Composer.css +23 -6
- package/components/Composer/Composer.d.ts +3 -0
- package/components/Composer/Composer.js +12 -1
- package/components/Prose/Prose.css +18 -1
- package/components/Reasoning/Reasoning.css +19 -4
- package/components/ShaderField/ShaderField.css +24 -0
- package/components/ShaderField/ShaderField.d.ts +68 -0
- package/components/ShaderField/ShaderField.js +54 -0
- package/components/ShaderField/field.glsl.d.ts +33 -0
- package/components/ShaderField/field.glsl.js +177 -0
- package/components/ShaderField/useShaderField.d.ts +86 -0
- package/components/ShaderField/useShaderField.js +395 -0
- package/components/Timeline/Timeline.css +9 -1
- package/components/registry.json +8 -0
- package/components/registry.json.d.ts +8 -0
- package/components/registry.json.js +1 -1
- package/index.d.ts +1 -0
- package/index.js +8 -0
- package/package.json +5 -1
- package/tokens/registry.json +2 -0
- package/tokens/registry.json.d.ts +2 -0
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +1 -1
- package/tokens/tokens-light.css +3 -2
- package/tokens/tokens-motion.css +2 -0
- 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;
|