@cueplusplus/ui 0.18.2 → 0.20.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/CHANGELOG.md +100 -0
- package/dist/brand/_motif-seed.js +20 -0
- package/dist/brand/gloss-object.d.ts +23 -0
- package/dist/brand/gloss-object.js +108 -0
- package/dist/brand/halftone.d.ts +23 -0
- package/dist/brand/halftone.js +54 -0
- package/dist/brand/index.d.ts +8 -1
- package/dist/brand/motif.d.ts +32 -0
- package/dist/brand/motif.js +71 -0
- package/dist/brand/particle-logo.d.ts +50 -0
- package/dist/brand/particle-logo.js +437 -0
- package/dist/brand/particle-mark.d.ts +29 -0
- package/dist/brand/particle-mark.js +137 -0
- package/dist/brand/ribbon.d.ts +20 -0
- package/dist/brand/ribbon.js +54 -0
- package/dist/brand/text-scramble.d.ts +42 -0
- package/dist/brand/text-scramble.js +160 -0
- package/dist/chat/delegation-card.js +2 -1
- package/dist/chat/message.js +12 -13
- package/dist/chrome/announcement-bar.d.ts +30 -0
- package/dist/chrome/announcement-bar.js +38 -0
- package/dist/chrome/app-bar.d.ts +4 -1
- package/dist/chrome/app-bar.js +7 -2
- package/dist/chrome/app-shell.js +8 -1
- package/dist/chrome/band.d.ts +3 -1
- package/dist/chrome/band.js +4 -3
- package/dist/chrome/hero.d.ts +61 -0
- package/dist/chrome/hero.js +108 -0
- package/dist/chrome/index.d.ts +6 -1
- package/dist/chrome/index.js +6 -1
- package/dist/chrome/keyword.d.ts +22 -0
- package/dist/chrome/keyword.js +28 -0
- package/dist/chrome/logo-strip.d.ts +23 -0
- package/dist/chrome/logo-strip.js +43 -0
- package/dist/chrome/page-shell.js +13 -1
- package/dist/chrome/stat-band.d.ts +32 -0
- package/dist/chrome/stat-band.js +39 -0
- package/dist/color/color-area.js +2 -1
- package/dist/dmx/channel-matrix.js +5 -3
- package/dist/dmx/dmx-strip.js +5 -3
- package/dist/elements/animated-number.js +4 -1
- package/dist/elements/replay.d.ts +1 -1
- package/dist/elements/replay.js +5 -2
- package/dist/elements/scroll-anchor.js +4 -1
- package/dist/forms/_chassis.d.ts +1 -1
- package/dist/forms/_chassis.js +2 -2
- package/dist/forms/env-var-input.js +2 -1
- package/dist/forms/file-upload.js +2 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +18 -2
- package/dist/instruments/data-table.js +3 -2
- package/dist/instruments/index.js +1 -1
- package/dist/instruments/stat.js +1 -1
- package/dist/instruments/table.js +1 -1
- package/dist/instruments/terminal-frame.js +5 -2
- package/dist/layout/grid.js +4 -2
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.js +2 -1
- package/dist/layout/preview.js +4 -188
- package/dist/layout/reveal.d.ts +44 -0
- package/dist/layout/reveal.js +106 -0
- package/dist/layout/sidebar.js +88 -52
- package/dist/lib/cn.js +23 -5
- package/dist/lib/particle-field-shaders.js +236 -0
- package/dist/lib/particle-field.js +210 -0
- package/dist/midi/spectrum-visualizer.js +2 -1
- package/dist/overlays/_surface.d.ts +2 -2
- package/dist/primitives/button.js +22 -3
- package/dist/primitives/chip.d.ts +1 -1
- package/dist/primitives/chip.js +63 -6
- package/dist/styles.css +26 -0
- package/dist/system/_scoped-pairing.js +18 -0
- package/dist/system/_stage-scope.js +199 -0
- package/dist/system/_style-check.js +1 -1
- package/dist/system/_style-chip.js +1 -1
- package/dist/system/_style-field-chassis.js +14 -1
- package/dist/system/_style-group-label.js +7 -3
- package/dist/system/_style-hero-title.js +41 -0
- package/dist/system/_style-kbd.js +1 -1
- package/dist/system/_style-label-ink.js +12 -5
- package/dist/system/_style-menu.js +1 -1
- package/dist/system/_style-page-title.js +1 -1
- package/dist/system/_style-segmented-control.js +1 -1
- package/dist/system/_style-skeleton.js +1 -1
- package/dist/system/_style-tab.js +1 -1
- package/dist/system/font-scope.d.ts +38 -0
- package/dist/system/font-scope.js +69 -0
- package/dist/system/frame-clock.d.ts +33 -0
- package/dist/system/frame-clock.js +55 -0
- package/dist/system/index.d.ts +4 -1
- package/dist/system/index.js +4 -1
- package/dist/system/portal.js +7 -1
- package/dist/system/still.d.ts +32 -0
- package/dist/system/still.js +60 -0
- package/manifest/components/alert-dialog.json +1 -1
- package/manifest/components/announcement-bar.json +82 -0
- package/manifest/components/app-bar.json +3 -2
- package/manifest/components/app-shell.json +3 -0
- package/manifest/components/autocomplete.json +2 -0
- package/manifest/components/band.json +2 -1
- package/manifest/components/bento.json +13 -13
- package/manifest/components/button.json +1 -0
- package/manifest/components/channel-matrix.json +1 -0
- package/manifest/components/chart-tooltip-content.json +1 -0
- package/manifest/components/checkbox-group.json +1 -1
- package/manifest/components/checkbox.json +1 -1
- package/manifest/components/chip.json +1 -0
- package/manifest/components/color-area.json +10 -0
- package/manifest/components/color-field.json +3 -0
- package/manifest/components/color-picker.json +3 -0
- package/manifest/components/combobox.json +2 -0
- package/manifest/components/command-palette.json +1 -0
- package/manifest/components/composer.json +3 -0
- package/manifest/components/context-menu.json +1 -1
- package/manifest/components/data-table-pagination.json +3 -0
- package/manifest/components/data-table-toolbar.json +3 -0
- package/manifest/components/date-field.json +3 -0
- package/manifest/components/date-picker.json +3 -0
- package/manifest/components/date-range-picker.json +3 -0
- package/manifest/components/delegation-card.json +6 -0
- package/manifest/components/density.json +6 -2
- package/manifest/components/dialog.json +1 -1
- package/manifest/components/dmx-bar.json +1 -1
- package/manifest/components/dmx-strip.json +1 -1
- package/manifest/components/drawer.json +1 -1
- package/manifest/components/dropdown-menu.json +1 -0
- package/manifest/components/env-var-input.json +3 -0
- package/manifest/components/file-upload.json +5 -0
- package/manifest/components/font-scope.json +82 -0
- package/manifest/components/footer.json +0 -1
- package/manifest/components/gloss-object.json +81 -0
- package/manifest/components/grid.json +6 -6
- package/manifest/components/halftone.json +79 -0
- package/manifest/components/hero.json +204 -0
- package/manifest/components/hover-card.json +1 -1
- package/manifest/components/input-group.json +3 -0
- package/manifest/components/input.json +3 -0
- package/manifest/components/kbd.json +1 -0
- package/manifest/components/keyword.json +67 -0
- package/manifest/components/ledger.json +5 -5
- package/manifest/components/logo-strip.json +93 -0
- package/manifest/components/markdown-text.json +3 -3
- package/manifest/components/menubar.json +1 -1
- package/manifest/components/message.json +8 -0
- package/manifest/components/motif.json +90 -0
- package/manifest/components/multi-select.json +2 -0
- package/manifest/components/musical-time-input.json +3 -0
- package/manifest/components/navigation-menu.json +1 -0
- package/manifest/components/number-field.json +3 -0
- package/manifest/components/otp-field.json +3 -0
- package/manifest/components/page-shell.json +5 -0
- package/manifest/components/particle-logo.json +140 -0
- package/manifest/components/particle-mark.json +75 -0
- package/manifest/components/password-input.json +3 -0
- package/manifest/components/popover.json +1 -1
- package/manifest/components/preview.json +10 -4
- package/manifest/components/radio-group.json +0 -1
- package/manifest/components/radio.json +0 -1
- package/manifest/components/reveal.json +106 -0
- package/manifest/components/ribbon.json +65 -0
- package/manifest/components/scrollable-tabs-list.json +1 -0
- package/manifest/components/scrub-input.json +3 -0
- package/manifest/components/search-input.json +3 -0
- package/manifest/components/segmented-control.json +1 -0
- package/manifest/components/select.json +3 -0
- package/manifest/components/sheet.json +1 -1
- package/manifest/components/sidebar.json +1 -0
- package/manifest/components/skeleton.json +1 -0
- package/manifest/components/spectrum-visualizer.json +10 -0
- package/manifest/components/stack.json +4 -4
- package/manifest/components/stat-band.json +68 -0
- package/manifest/components/stat.json +1 -1
- package/manifest/components/still-provider.json +60 -0
- package/manifest/components/switch.json +0 -1
- package/manifest/components/table.json +2 -2
- package/manifest/components/tabs.json +1 -0
- package/manifest/components/tags-input.json +2 -0
- package/manifest/components/text-scramble.json +90 -0
- package/manifest/components/textarea.json +3 -0
- package/manifest/components/theme-configurator.json +1 -0
- package/manifest/components/theme-provider.json +10 -4
- package/manifest/components/time-field.json +3 -0
- package/manifest/components/toast.json +1 -0
- package/manifest/components/tooltip.json +1 -1
- package/manifest/fixtures.json +18 -0
- package/manifest/manifest.json +368 -80
- package/manifest/tokens.json +93 -3
- package/package.json +6 -6
- package/dist/system/_paint-ground-pattern.js +0 -30
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { usePrefersReducedMotion } from "../instruments/_motion.js";
|
|
4
|
+
import { useStill } from "../system/still.js";
|
|
5
|
+
import { FAR, NEAR, TouchTrail, logoScale, modelView, normaliseColour, perspective, pickWaveColor, rasterise, sampleParticles, srgbToLinear, viewportAt } from "../lib/particle-field.js";
|
|
6
|
+
import { FRAGMENT_SHADER, VERTEX_SHADER } from "../lib/particle-field-shaders.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/brand/particle-logo.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The roles a wave sweeps through when no `waveColors` are given: the Look's own accent and
|
|
12
|
+
* signal colours, read off the element at run time, so the waves are the theme's.
|
|
13
|
+
*/
|
|
14
|
+
const ROLE_COLOURS = [
|
|
15
|
+
"--cue-accent",
|
|
16
|
+
"--cue-info",
|
|
17
|
+
"--cue-busy",
|
|
18
|
+
"--cue-ok",
|
|
19
|
+
"--cue-warn",
|
|
20
|
+
"--cue-danger",
|
|
21
|
+
"--cue-stream",
|
|
22
|
+
"--cue-fg"
|
|
23
|
+
];
|
|
24
|
+
/** The Look's role colours as linear-light triples, read through the element's own cascade. */
|
|
25
|
+
function rolePalette(element) {
|
|
26
|
+
const style = getComputedStyle(element);
|
|
27
|
+
return ROLE_COLOURS.map((name) => normaliseColour(style.getPropertyValue(name).trim())).filter((hex) => hex !== null).map(srgbToLinear);
|
|
28
|
+
}
|
|
29
|
+
function compile(gl, type, source) {
|
|
30
|
+
const shader = gl.createShader(type);
|
|
31
|
+
if (shader === null) return null;
|
|
32
|
+
gl.shaderSource(shader, source);
|
|
33
|
+
gl.compileShader(shader);
|
|
34
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
35
|
+
gl.deleteShader(shader);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return shader;
|
|
39
|
+
}
|
|
40
|
+
function build(gl, raster, sampled) {
|
|
41
|
+
const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);
|
|
42
|
+
const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
|
|
43
|
+
if (vertex === null || fragment === null) return null;
|
|
44
|
+
const program = gl.createProgram();
|
|
45
|
+
gl.attachShader(program, vertex);
|
|
46
|
+
gl.attachShader(program, fragment);
|
|
47
|
+
gl.linkProgram(program);
|
|
48
|
+
gl.deleteShader(vertex);
|
|
49
|
+
gl.deleteShader(fragment);
|
|
50
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
51
|
+
gl.deleteProgram(program);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const vao = gl.createVertexArray();
|
|
55
|
+
gl.bindVertexArray(vao);
|
|
56
|
+
const buffers = [];
|
|
57
|
+
const attribute = (name, data, size, type, divisor) => {
|
|
58
|
+
const location = gl.getAttribLocation(program, name);
|
|
59
|
+
const buffer = gl.createBuffer();
|
|
60
|
+
buffers.push(buffer);
|
|
61
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
62
|
+
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
|
|
63
|
+
if (location < 0) return;
|
|
64
|
+
gl.enableVertexAttribArray(location);
|
|
65
|
+
gl.vertexAttribPointer(location, size, type, false, 0, 0);
|
|
66
|
+
gl.vertexAttribDivisor(location, divisor);
|
|
67
|
+
};
|
|
68
|
+
attribute("position", new Float32Array([
|
|
69
|
+
-.5,
|
|
70
|
+
.5,
|
|
71
|
+
0,
|
|
72
|
+
.5,
|
|
73
|
+
.5,
|
|
74
|
+
0,
|
|
75
|
+
-.5,
|
|
76
|
+
-.5,
|
|
77
|
+
0,
|
|
78
|
+
.5,
|
|
79
|
+
-.5,
|
|
80
|
+
0
|
|
81
|
+
]), 3, gl.FLOAT, 0);
|
|
82
|
+
attribute("uv", new Float32Array([
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
1,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
1,
|
|
89
|
+
1,
|
|
90
|
+
1
|
|
91
|
+
]), 2, gl.FLOAT, 0);
|
|
92
|
+
attribute("pindex", sampled.indices, 1, gl.UNSIGNED_SHORT, 1);
|
|
93
|
+
attribute("offset", sampled.offsets, 3, gl.FLOAT, 1);
|
|
94
|
+
attribute("angle", sampled.angles, 1, gl.FLOAT, 1);
|
|
95
|
+
const elements = gl.createBuffer();
|
|
96
|
+
buffers.push(elements);
|
|
97
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elements);
|
|
98
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([
|
|
99
|
+
0,
|
|
100
|
+
2,
|
|
101
|
+
1,
|
|
102
|
+
2,
|
|
103
|
+
3,
|
|
104
|
+
1
|
|
105
|
+
]), gl.STATIC_DRAW);
|
|
106
|
+
gl.bindVertexArray(null);
|
|
107
|
+
const image = gl.createTexture();
|
|
108
|
+
gl.bindTexture(gl.TEXTURE_2D, image);
|
|
109
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
110
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, raster.width, raster.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(raster.data.buffer));
|
|
111
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
112
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
113
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
114
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
115
|
+
const touch = gl.createTexture();
|
|
116
|
+
gl.bindTexture(gl.TEXTURE_2D, touch);
|
|
117
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, TouchTrail.SIZE, TouchTrail.SIZE, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
118
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
|
|
119
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
120
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
121
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
122
|
+
const locations = /* @__PURE__ */ new Map();
|
|
123
|
+
const uniform = (name) => {
|
|
124
|
+
if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));
|
|
125
|
+
return locations.get(name);
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
program,
|
|
129
|
+
vao,
|
|
130
|
+
buffers,
|
|
131
|
+
image,
|
|
132
|
+
touch,
|
|
133
|
+
count: sampled.count,
|
|
134
|
+
uniform
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function release(gl, scene) {
|
|
138
|
+
for (const buffer of scene.buffers) gl.deleteBuffer(buffer);
|
|
139
|
+
gl.deleteVertexArray(scene.vao);
|
|
140
|
+
gl.deleteTexture(scene.image);
|
|
141
|
+
gl.deleteTexture(scene.touch);
|
|
142
|
+
gl.deleteProgram(scene.program);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A mark drawn as a field of particles that wiggle, flip colour in sweeping waves and scatter
|
|
146
|
+
* from the pointer — cueplusplus.com's hero logo, ported from three.js to raw WebGL2 with its
|
|
147
|
+
* shaders unchanged (spec 2026-10-04-cue-site-verbatim §4.1).
|
|
148
|
+
*
|
|
149
|
+
* The camera, the mesh scale, the textures, the blending and the wave and touch timings are the
|
|
150
|
+
* source's, copied: a 50° camera at z = 300, the mark 26% of the canvas's height, a wave every
|
|
151
|
+
* 4–8s that sweeps in 2s and fades to white over 3s, a pointer trail that ages over 80 rendered
|
|
152
|
+
* updates. It renders at about 30fps and costs nothing off screen or in a hidden tab.
|
|
153
|
+
*
|
|
154
|
+
* **Without WebGL2, or held still** (a thumbnail, or a reader who prefers reduced motion), it
|
|
155
|
+
* draws the source image itself, still, at the same fit — a picture of the mark rather than of
|
|
156
|
+
* the particles, which the docs say. A lost context draws the still until it is restored.
|
|
157
|
+
*
|
|
158
|
+
* Decorative: the canvas is hidden from assistive technology; name the mark on the element
|
|
159
|
+
* around it.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* <div role="img" aria-label="CUE++" className="h-[460px]">
|
|
163
|
+
* <ParticleLogo src="/logo.png" />
|
|
164
|
+
* </div>
|
|
165
|
+
*/
|
|
166
|
+
const ParticleLogo = React.forwardRef(function ParticleLogo({ src, fit = "logo", containFill = .85, particleSize = .5, randomSpread = 2, skipRatio = 36, waveColors, className, style, ...props }, ref) {
|
|
167
|
+
const held = useStill();
|
|
168
|
+
const reduced = usePrefersReducedMotion();
|
|
169
|
+
const still = held || reduced;
|
|
170
|
+
const canvas = React.useRef(null);
|
|
171
|
+
const [failed, setFailed] = React.useState(false);
|
|
172
|
+
const [lost, setLost] = React.useState(false);
|
|
173
|
+
const paletteKey = (waveColors ?? []).join(",");
|
|
174
|
+
const drawStill = still || failed;
|
|
175
|
+
React.useEffect(() => {
|
|
176
|
+
if (drawStill) return;
|
|
177
|
+
const element = canvas.current;
|
|
178
|
+
if (element === null) return;
|
|
179
|
+
const gl = typeof WebGL2RenderingContext === "undefined" ? null : element.getContext("webgl2", {
|
|
180
|
+
antialias: true,
|
|
181
|
+
alpha: true,
|
|
182
|
+
premultipliedAlpha: true
|
|
183
|
+
});
|
|
184
|
+
if (gl === null) {
|
|
185
|
+
setFailed(true);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const given = paletteKey === "" ? [] : paletteKey.split(",").map(srgbToLinear);
|
|
189
|
+
const varied = (colours) => colours.some((colour) => colour.join() !== colours[0].join());
|
|
190
|
+
let palette = varied(given) ? given : rolePalette(element);
|
|
191
|
+
if (palette.length === 0) palette = [[
|
|
192
|
+
1,
|
|
193
|
+
1,
|
|
194
|
+
1
|
|
195
|
+
]];
|
|
196
|
+
let disposed = false;
|
|
197
|
+
let contextLost = gl.isContextLost();
|
|
198
|
+
setLost(contextLost);
|
|
199
|
+
let scene = null;
|
|
200
|
+
let raster = null;
|
|
201
|
+
let raf = 0;
|
|
202
|
+
let visible = true;
|
|
203
|
+
const trail = new TouchTrail();
|
|
204
|
+
const touchCanvas = document.createElement("canvas");
|
|
205
|
+
touchCanvas.width = TouchTrail.SIZE;
|
|
206
|
+
touchCanvas.height = TouchTrail.SIZE;
|
|
207
|
+
const touchContext = touchCanvas.getContext("2d");
|
|
208
|
+
const pointer = {
|
|
209
|
+
x: 0,
|
|
210
|
+
y: 0
|
|
211
|
+
};
|
|
212
|
+
const onPointer = (event) => {
|
|
213
|
+
const box = element.getBoundingClientRect();
|
|
214
|
+
pointer.x = (event.clientX - box.left) / box.width * 2 - 1;
|
|
215
|
+
pointer.y = -((event.clientY - box.top) / box.height) * 2 + 1;
|
|
216
|
+
};
|
|
217
|
+
element.addEventListener("pointermove", onPointer);
|
|
218
|
+
const state = {
|
|
219
|
+
time: 0,
|
|
220
|
+
uTime: 0,
|
|
221
|
+
waving: false,
|
|
222
|
+
waveStart: 0,
|
|
223
|
+
waveDuration: 2,
|
|
224
|
+
nextWaveDelay: 5,
|
|
225
|
+
lastWaveEnd: -3,
|
|
226
|
+
direction: [1, 0],
|
|
227
|
+
position: 0,
|
|
228
|
+
waveTime: 0,
|
|
229
|
+
colour: palette[Math.floor(Math.random() * palette.length)],
|
|
230
|
+
previous: [
|
|
231
|
+
1,
|
|
232
|
+
1,
|
|
233
|
+
1
|
|
234
|
+
],
|
|
235
|
+
fade: 1
|
|
236
|
+
};
|
|
237
|
+
const resize = () => {
|
|
238
|
+
const dpr = Math.min(Math.max(window.devicePixelRatio || 1, 1), 2);
|
|
239
|
+
const width = Math.max(1, Math.round(element.clientWidth * dpr));
|
|
240
|
+
const height = Math.max(1, Math.round(element.clientHeight * dpr));
|
|
241
|
+
if (element.width !== width || element.height !== height) {
|
|
242
|
+
element.width = width;
|
|
243
|
+
element.height = height;
|
|
244
|
+
}
|
|
245
|
+
gl.viewport(0, 0, width, height);
|
|
246
|
+
return element.clientWidth / Math.max(1, element.clientHeight);
|
|
247
|
+
};
|
|
248
|
+
const frame = (delta) => {
|
|
249
|
+
if (scene === null || raster === null) return;
|
|
250
|
+
const s = state;
|
|
251
|
+
s.uTime += delta;
|
|
252
|
+
if (!s.waving) {
|
|
253
|
+
if (s.time - s.lastWaveEnd > s.nextWaveDelay) {
|
|
254
|
+
s.waving = true;
|
|
255
|
+
s.waveStart = s.time;
|
|
256
|
+
const angle = Math.random() * Math.PI * 2;
|
|
257
|
+
s.direction = [Math.cos(angle), Math.sin(angle)];
|
|
258
|
+
s.position = 0;
|
|
259
|
+
s.previous = [
|
|
260
|
+
s.colour[0] + (1 - s.colour[0]) * s.fade,
|
|
261
|
+
s.colour[1] + (1 - s.colour[1]) * s.fade,
|
|
262
|
+
s.colour[2] + (1 - s.colour[2]) * s.fade
|
|
263
|
+
];
|
|
264
|
+
s.colour = pickWaveColor(palette, s.colour);
|
|
265
|
+
s.nextWaveDelay = 4 + Math.random() * 4;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
const progress = (s.time - s.waveStart) / s.waveDuration;
|
|
269
|
+
s.position = Math.min(progress * 1.3, 1.3);
|
|
270
|
+
s.waveTime = Math.min(progress, 1);
|
|
271
|
+
s.fade = 0;
|
|
272
|
+
if (progress >= 1) {
|
|
273
|
+
s.waving = false;
|
|
274
|
+
s.lastWaveEnd = s.time;
|
|
275
|
+
s.waveTime = 1;
|
|
276
|
+
s.position = 1.3;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!s.waving) s.fade = Math.min((s.time - s.lastWaveEnd) / 3, 1);
|
|
280
|
+
trail.age();
|
|
281
|
+
trail.draw(touchContext);
|
|
282
|
+
const aspect = resize();
|
|
283
|
+
const scale = logoScale(fit, aspect, raster.width, raster.height, containFill);
|
|
284
|
+
const viewport = viewportAt(aspect);
|
|
285
|
+
const x = pointer.x * viewport.width * .5 / (raster.width * scale) + .5;
|
|
286
|
+
const y = pointer.y * viewport.height * .5 / (raster.height * scale) + .5;
|
|
287
|
+
if (x >= -.2 && x <= 1.2 && y >= -.2 && y <= 1.2) trail.add(Math.max(0, Math.min(1, x)), Math.max(0, Math.min(1, y)));
|
|
288
|
+
gl.clearColor(0, 0, 0, 0);
|
|
289
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
290
|
+
gl.disable(gl.DEPTH_TEST);
|
|
291
|
+
gl.enable(gl.BLEND);
|
|
292
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
293
|
+
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
294
|
+
gl.useProgram(scene.program);
|
|
295
|
+
gl.bindVertexArray(scene.vao);
|
|
296
|
+
gl.uniformMatrix4fv(scene.uniform("projectionMatrix"), false, perspective(50, aspect, NEAR, FAR));
|
|
297
|
+
gl.uniformMatrix4fv(scene.uniform("modelViewMatrix"), false, modelView(scale));
|
|
298
|
+
gl.uniform1f(scene.uniform("uTime"), s.uTime);
|
|
299
|
+
gl.uniform1f(scene.uniform("uRandom"), randomSpread);
|
|
300
|
+
gl.uniform1f(scene.uniform("uDepth"), .8);
|
|
301
|
+
gl.uniform1f(scene.uniform("uSize"), particleSize);
|
|
302
|
+
gl.uniform2f(scene.uniform("uTextureSize"), raster.width, raster.height);
|
|
303
|
+
gl.uniform2f(scene.uniform("uWaveDirection"), s.direction[0], s.direction[1]);
|
|
304
|
+
gl.uniform1f(scene.uniform("uWavePosition"), s.position);
|
|
305
|
+
gl.uniform1f(scene.uniform("uWaveTime"), s.waveTime);
|
|
306
|
+
gl.uniform3f(scene.uniform("uWaveColor"), ...s.colour);
|
|
307
|
+
gl.uniform3f(scene.uniform("uPrevColor"), ...s.previous);
|
|
308
|
+
gl.uniform1f(scene.uniform("uColorFade"), s.fade);
|
|
309
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
310
|
+
gl.bindTexture(gl.TEXTURE_2D, scene.image);
|
|
311
|
+
gl.uniform1i(scene.uniform("uTexture"), 0);
|
|
312
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
313
|
+
gl.bindTexture(gl.TEXTURE_2D, scene.touch);
|
|
314
|
+
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
315
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, touchCanvas);
|
|
316
|
+
gl.generateMipmap(gl.TEXTURE_2D);
|
|
317
|
+
gl.uniform1i(scene.uniform("uTouch"), 1);
|
|
318
|
+
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0, scene.count);
|
|
319
|
+
gl.bindVertexArray(null);
|
|
320
|
+
};
|
|
321
|
+
let last = performance.now();
|
|
322
|
+
let accumulated = 0;
|
|
323
|
+
const tick = () => {
|
|
324
|
+
const now = performance.now();
|
|
325
|
+
const delta = (now - last) / 1e3;
|
|
326
|
+
last = now;
|
|
327
|
+
if (!visible || document.hidden) {
|
|
328
|
+
raf = 0;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
raf = requestAnimationFrame(tick);
|
|
332
|
+
accumulated += delta;
|
|
333
|
+
state.time += delta;
|
|
334
|
+
if (accumulated < 1 / 30) return;
|
|
335
|
+
frame(accumulated);
|
|
336
|
+
accumulated = 0;
|
|
337
|
+
};
|
|
338
|
+
const start = () => {
|
|
339
|
+
if (raf !== 0 || disposed || contextLost || scene === null || !visible || document.hidden) return;
|
|
340
|
+
last = performance.now();
|
|
341
|
+
raf = requestAnimationFrame(tick);
|
|
342
|
+
};
|
|
343
|
+
const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver(([entry]) => {
|
|
344
|
+
visible = entry?.isIntersecting ?? true;
|
|
345
|
+
start();
|
|
346
|
+
});
|
|
347
|
+
observer?.observe(element);
|
|
348
|
+
const onVisibility = () => start();
|
|
349
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
350
|
+
const buildScene = () => {
|
|
351
|
+
if (disposed || contextLost || raster === null) return;
|
|
352
|
+
scene = build(gl, raster, sampleParticles(raster, skipRatio));
|
|
353
|
+
if (scene === null) {
|
|
354
|
+
setFailed(true);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
start();
|
|
358
|
+
};
|
|
359
|
+
const onLost = (event) => {
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
contextLost = true;
|
|
362
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
363
|
+
raf = 0;
|
|
364
|
+
scene = null;
|
|
365
|
+
setLost(true);
|
|
366
|
+
};
|
|
367
|
+
const onRestored = () => {
|
|
368
|
+
contextLost = false;
|
|
369
|
+
setLost(false);
|
|
370
|
+
buildScene();
|
|
371
|
+
};
|
|
372
|
+
element.addEventListener("webglcontextlost", onLost);
|
|
373
|
+
element.addEventListener("webglcontextrestored", onRestored);
|
|
374
|
+
const image = new Image();
|
|
375
|
+
image.crossOrigin = "anonymous";
|
|
376
|
+
image.onload = () => {
|
|
377
|
+
(image.decode?.() ?? Promise.resolve()).catch(() => {}).then(() => {
|
|
378
|
+
if (disposed) return;
|
|
379
|
+
raster = rasterise(image);
|
|
380
|
+
buildScene();
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
image.onerror = () => {
|
|
384
|
+
if (!disposed) setFailed(true);
|
|
385
|
+
};
|
|
386
|
+
image.src = src;
|
|
387
|
+
return () => {
|
|
388
|
+
disposed = true;
|
|
389
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
390
|
+
observer?.disconnect();
|
|
391
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
392
|
+
element.removeEventListener("pointermove", onPointer);
|
|
393
|
+
element.removeEventListener("webglcontextlost", onLost);
|
|
394
|
+
element.removeEventListener("webglcontextrestored", onRestored);
|
|
395
|
+
if (scene !== null && !contextLost) release(gl, scene);
|
|
396
|
+
if (!element.isConnected) gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
397
|
+
};
|
|
398
|
+
}, [
|
|
399
|
+
drawStill,
|
|
400
|
+
src,
|
|
401
|
+
fit,
|
|
402
|
+
containFill,
|
|
403
|
+
particleSize,
|
|
404
|
+
randomSpread,
|
|
405
|
+
skipRatio,
|
|
406
|
+
paletteKey
|
|
407
|
+
]);
|
|
408
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
409
|
+
ref,
|
|
410
|
+
"data-slot": "particle-logo",
|
|
411
|
+
className: cn("relative h-full w-full", className),
|
|
412
|
+
style,
|
|
413
|
+
...props,
|
|
414
|
+
children: [drawStill ? null : /* @__PURE__ */ jsx("canvas", {
|
|
415
|
+
ref: canvas,
|
|
416
|
+
"aria-hidden": "true",
|
|
417
|
+
"data-slot": "particle-logo-canvas",
|
|
418
|
+
className: "block h-full w-full",
|
|
419
|
+
style: {
|
|
420
|
+
background: "transparent",
|
|
421
|
+
touchAction: "none"
|
|
422
|
+
}
|
|
423
|
+
}), drawStill || lost ? /* @__PURE__ */ jsx("img", {
|
|
424
|
+
"data-slot": "particle-logo-still",
|
|
425
|
+
src,
|
|
426
|
+
alt: "",
|
|
427
|
+
"aria-hidden": "true",
|
|
428
|
+
className: "absolute inset-0 m-auto object-contain",
|
|
429
|
+
style: {
|
|
430
|
+
height: fit === "logo" ? "26%" : `${containFill * 100}%`,
|
|
431
|
+
width: "auto"
|
|
432
|
+
}
|
|
433
|
+
}) : null]
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
//#endregion
|
|
437
|
+
export { ParticleLogo };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/brand/particle-mark.d.ts
|
|
3
|
+
interface ParticleMarkProps extends React.ComponentPropsWithoutRef<"canvas"> {
|
|
4
|
+
/** The word the dots draw, short: a name or a mark. */
|
|
5
|
+
word: string;
|
|
6
|
+
/** Which scatter. Defaults to `1`. */
|
|
7
|
+
seed?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A word drawn as a field of dots that drift and settle — a studio site's mark
|
|
11
|
+
* (spec 2026-10-02 §3.4). The dots are the accent, read from the canvas's own
|
|
12
|
+
* computed style, so they follow the Look.
|
|
13
|
+
*
|
|
14
|
+
* **It costs nothing at rest.** It animates only while its element is displayed,
|
|
15
|
+
* on screen and in a visible tab, and while {@link useStill} is false (a still
|
|
16
|
+
* thumbnail, or a reader who prefers reduced motion). Otherwise it draws one
|
|
17
|
+
* still frame. That frame is redrawn whenever any theme axis changes anywhere above
|
|
18
|
+
* it, the system colour scheme flips, a face finishes loading, or it resizes, so a
|
|
19
|
+
* paused mark is never left in an old palette. Unmounting disconnects every
|
|
20
|
+
* observer and cancels every frame.
|
|
21
|
+
*
|
|
22
|
+
* Decorative: hidden from assistive technology.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <ParticleMark word="CUE" className="h-40 w-full" />
|
|
26
|
+
*/
|
|
27
|
+
declare function ParticleMark({ word, seed, ...canvasProps }: ParticleMarkProps): React.JSX.Element;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { ParticleMark, ParticleMarkProps };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { seeded } from "./_motif-seed.js";
|
|
3
|
+
import { usePrefersReducedMotion } from "../instruments/_motion.js";
|
|
4
|
+
import { useStill } from "../system/still.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
//#region src/brand/particle-mark.tsx
|
|
8
|
+
/** The six axes a motif's colours follow (spec 2026-10-02 §3.4). */
|
|
9
|
+
const AXES = [
|
|
10
|
+
"data-theme",
|
|
11
|
+
"data-font",
|
|
12
|
+
"data-mode",
|
|
13
|
+
"data-style",
|
|
14
|
+
"data-paint",
|
|
15
|
+
"data-density"
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* A word drawn as a field of dots that drift and settle — a studio site's mark
|
|
19
|
+
* (spec 2026-10-02 §3.4). The dots are the accent, read from the canvas's own
|
|
20
|
+
* computed style, so they follow the Look.
|
|
21
|
+
*
|
|
22
|
+
* **It costs nothing at rest.** It animates only while its element is displayed,
|
|
23
|
+
* on screen and in a visible tab, and while {@link useStill} is false (a still
|
|
24
|
+
* thumbnail, or a reader who prefers reduced motion). Otherwise it draws one
|
|
25
|
+
* still frame. That frame is redrawn whenever any theme axis changes anywhere above
|
|
26
|
+
* it, the system colour scheme flips, a face finishes loading, or it resizes, so a
|
|
27
|
+
* paused mark is never left in an old palette. Unmounting disconnects every
|
|
28
|
+
* observer and cancels every frame.
|
|
29
|
+
*
|
|
30
|
+
* Decorative: hidden from assistive technology.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* <ParticleMark word="CUE" className="h-40 w-full" />
|
|
34
|
+
*/
|
|
35
|
+
function ParticleMark({ word, seed = 1, ...canvasProps }) {
|
|
36
|
+
const ref = React.useRef(null);
|
|
37
|
+
const held = useStill();
|
|
38
|
+
const reduced = usePrefersReducedMotion();
|
|
39
|
+
const still = held || reduced;
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
const canvas = ref.current;
|
|
42
|
+
if (canvas === null) return;
|
|
43
|
+
const ctx = canvas.getContext("2d");
|
|
44
|
+
if (ctx === null) return;
|
|
45
|
+
let frame = 0;
|
|
46
|
+
let visible = false;
|
|
47
|
+
let points = [];
|
|
48
|
+
const layout = () => {
|
|
49
|
+
const rect = canvas.getBoundingClientRect();
|
|
50
|
+
const scale = window.devicePixelRatio || 1;
|
|
51
|
+
canvas.width = Math.max(1, Math.round(rect.width * scale));
|
|
52
|
+
canvas.height = Math.max(1, Math.round(rect.height * scale));
|
|
53
|
+
const off = document.createElement("canvas");
|
|
54
|
+
off.width = canvas.width;
|
|
55
|
+
off.height = canvas.height;
|
|
56
|
+
const o = off.getContext("2d");
|
|
57
|
+
if (o === null) return;
|
|
58
|
+
o.fillStyle = getComputedStyle(canvas).getPropertyValue("--cue-accent").trim() || "currentColor";
|
|
59
|
+
const family = getComputedStyle(canvas).fontFamily;
|
|
60
|
+
o.font = `700 100px ${family}`;
|
|
61
|
+
const advance = o.measureText(word).width / 100;
|
|
62
|
+
const size = Math.min(canvas.height * .62, advance > 0 ? canvas.width * .9 / advance : canvas.height * .62);
|
|
63
|
+
o.font = `700 ${Math.round(size)}px ${family}`;
|
|
64
|
+
o.textAlign = "center";
|
|
65
|
+
o.textBaseline = "middle";
|
|
66
|
+
o.fillText(word, canvas.width / 2, canvas.height / 2);
|
|
67
|
+
const data = o.getImageData(0, 0, off.width, off.height).data;
|
|
68
|
+
const rand = seeded(seed);
|
|
69
|
+
const step = Math.max(3, Math.round(4 * scale));
|
|
70
|
+
points = [];
|
|
71
|
+
for (let y = 0; y < off.height; y += step) for (let x = 0; x < off.width; x += step) if (data[(y * off.width + x) * 4 + 3] > 128) points.push({
|
|
72
|
+
x,
|
|
73
|
+
y,
|
|
74
|
+
dx: (rand() - .5) * step,
|
|
75
|
+
dy: (rand() - .5) * step
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const draw = (t) => {
|
|
79
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
80
|
+
ctx.fillStyle = getComputedStyle(canvas).getPropertyValue("--cue-accent").trim() || "currentColor";
|
|
81
|
+
const size = Math.max(1, (window.devicePixelRatio || 1) * 1.4);
|
|
82
|
+
const k = still ? 0 : Math.sin(t / 900);
|
|
83
|
+
for (const p of points) ctx.fillRect(p.x + p.dx * k, p.y + p.dy * k, size, size);
|
|
84
|
+
};
|
|
85
|
+
const rendered = () => typeof canvas.checkVisibility === "function" ? canvas.checkVisibility() : canvas.getClientRects().length > 0;
|
|
86
|
+
const shown = () => rendered() && visible && document.visibilityState === "visible";
|
|
87
|
+
const tick = (t) => {
|
|
88
|
+
draw(t);
|
|
89
|
+
frame = shown() && !still ? requestAnimationFrame(tick) : 0;
|
|
90
|
+
};
|
|
91
|
+
const refresh = () => {
|
|
92
|
+
cancelAnimationFrame(frame);
|
|
93
|
+
frame = 0;
|
|
94
|
+
layout();
|
|
95
|
+
draw(0);
|
|
96
|
+
if (shown() && !still) frame = requestAnimationFrame(tick);
|
|
97
|
+
};
|
|
98
|
+
const intersection = new IntersectionObserver(([entry]) => {
|
|
99
|
+
visible = entry?.isIntersecting ?? false;
|
|
100
|
+
refresh();
|
|
101
|
+
});
|
|
102
|
+
intersection.observe(canvas);
|
|
103
|
+
const resize = new ResizeObserver(refresh);
|
|
104
|
+
resize.observe(canvas);
|
|
105
|
+
const axes = new MutationObserver(refresh);
|
|
106
|
+
axes.observe(document.documentElement, {
|
|
107
|
+
subtree: true,
|
|
108
|
+
attributes: true,
|
|
109
|
+
attributeFilter: AXES
|
|
110
|
+
});
|
|
111
|
+
const scheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
112
|
+
scheme.addEventListener("change", refresh);
|
|
113
|
+
document.addEventListener("visibilitychange", refresh);
|
|
114
|
+
document.fonts?.addEventListener?.("loadingdone", refresh);
|
|
115
|
+
refresh();
|
|
116
|
+
return () => {
|
|
117
|
+
cancelAnimationFrame(frame);
|
|
118
|
+
intersection.disconnect();
|
|
119
|
+
resize.disconnect();
|
|
120
|
+
axes.disconnect();
|
|
121
|
+
scheme.removeEventListener("change", refresh);
|
|
122
|
+
document.removeEventListener("visibilitychange", refresh);
|
|
123
|
+
document.fonts?.removeEventListener?.("loadingdone", refresh);
|
|
124
|
+
};
|
|
125
|
+
}, [
|
|
126
|
+
word,
|
|
127
|
+
seed,
|
|
128
|
+
still
|
|
129
|
+
]);
|
|
130
|
+
return /* @__PURE__ */ jsx("canvas", {
|
|
131
|
+
ref,
|
|
132
|
+
"aria-hidden": "true",
|
|
133
|
+
...canvasProps
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { ParticleMark };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/brand/ribbon.d.ts
|
|
3
|
+
interface RibbonProps extends React.SVGProps<SVGSVGElement> {
|
|
4
|
+
/** Which ribbon: the same seed draws the same bands. Defaults to `1`. */
|
|
5
|
+
seed?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A flowing ribbon of layered bands in the theme's own inks — the payments-page
|
|
9
|
+
* motif (spec 2026-10-02 §3.4). Five bezier bands cross the frame on a diagonal,
|
|
10
|
+
* each a gradient from the accent to the foreground's mix, so it reads in every
|
|
11
|
+
* theme and both modes. Decorative: hidden from assistive technology.
|
|
12
|
+
*
|
|
13
|
+
* Static markup — no `"use client"`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <Ribbon seed={3} className="h-64 w-full" />
|
|
17
|
+
*/
|
|
18
|
+
declare function Ribbon({ seed, ...svgProps }: RibbonProps): React.JSX.Element;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { Ribbon, RibbonProps };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { r2, seeded } from "./_motif-seed.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/brand/ribbon.tsx
|
|
5
|
+
/**
|
|
6
|
+
* A flowing ribbon of layered bands in the theme's own inks — the payments-page
|
|
7
|
+
* motif (spec 2026-10-02 §3.4). Five bezier bands cross the frame on a diagonal,
|
|
8
|
+
* each a gradient from the accent to the foreground's mix, so it reads in every
|
|
9
|
+
* theme and both modes. Decorative: hidden from assistive technology.
|
|
10
|
+
*
|
|
11
|
+
* Static markup — no `"use client"`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* <Ribbon seed={3} className="h-64 w-full" />
|
|
15
|
+
*/
|
|
16
|
+
function Ribbon({ seed = 1, ...svgProps }) {
|
|
17
|
+
const id = React.useId().replace(/:/gu, "");
|
|
18
|
+
const rand = seeded(seed);
|
|
19
|
+
const bands = Array.from({ length: 5 }, (_, i) => {
|
|
20
|
+
const y0 = r2(40 + i * 14 + rand() * 10);
|
|
21
|
+
const c1 = r2(rand() * 60 - 10);
|
|
22
|
+
const c2 = r2(70 + rand() * 40);
|
|
23
|
+
const y1 = r2(i * 8 + rand() * 12);
|
|
24
|
+
const w = r2(10 + rand() * 8);
|
|
25
|
+
return `M0 ${y0} C 30 ${c1} 60 ${c2} 100 ${y1} L 100 ${r2(y1 + w)} C 60 ${r2(c2 + w)} 30 ${r2(c1 + w)} 0 ${r2(y0 + w)} Z`;
|
|
26
|
+
});
|
|
27
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
28
|
+
viewBox: "0 0 100 100",
|
|
29
|
+
preserveAspectRatio: "none",
|
|
30
|
+
"aria-hidden": "true",
|
|
31
|
+
focusable: "false",
|
|
32
|
+
...svgProps,
|
|
33
|
+
children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", {
|
|
34
|
+
id: `${id}-g`,
|
|
35
|
+
x1: "0",
|
|
36
|
+
y1: "0",
|
|
37
|
+
x2: "1",
|
|
38
|
+
y2: "1",
|
|
39
|
+
children: [/* @__PURE__ */ jsx("stop", {
|
|
40
|
+
offset: "0%",
|
|
41
|
+
stopColor: "var(--cue-accent)"
|
|
42
|
+
}), /* @__PURE__ */ jsx("stop", {
|
|
43
|
+
offset: "100%",
|
|
44
|
+
stopColor: "color-mix(in oklch, var(--cue-accent) 40%, var(--cue-fg))"
|
|
45
|
+
})]
|
|
46
|
+
}) }), bands.map((d, i) => /* @__PURE__ */ jsx("path", {
|
|
47
|
+
d,
|
|
48
|
+
fill: `url(#${id}-g)`,
|
|
49
|
+
opacity: r2(.35 + i * .12)
|
|
50
|
+
}, i))]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Ribbon };
|