fancy-ui-react 0.2.0 → 0.3.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/README.md +10 -1
- package/dist/components/button/Button.js +5 -1
- package/dist/components/datamosh-transition/DatamoshTransition.d.ts +76 -0
- package/dist/components/datamosh-transition/DatamoshTransition.js +368 -0
- package/dist/components/datamosh-transition/datamosh-transition-core.d.ts +180 -0
- package/dist/components/datamosh-transition/datamosh-transition-core.js +370 -0
- package/dist/components/datamosh-transition/index.d.ts +3 -0
- package/dist/components/fluid-cursor/FluidCursor.js +48 -18
- package/dist/components/fluid-cursor/fluid-shared.d.ts +18 -0
- package/dist/components/fluid-cursor/fluid-shared.js +7 -0
- package/dist/components/fluid-cursor/webgpu-engine.d.ts +6 -0
- package/dist/components/fluid-cursor/webgpu-engine.js +24 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/styles.css +75 -1
- package/package.json +1 -1
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
const DEFAULT_COLORS = [
|
|
3
|
+
"#ffffff",
|
|
4
|
+
"#3566ff",
|
|
5
|
+
"#ffcf00",
|
|
6
|
+
"#192aff",
|
|
7
|
+
"#d62036",
|
|
8
|
+
"#282142",
|
|
9
|
+
"#ec4978",
|
|
10
|
+
"#ff9900",
|
|
11
|
+
"#14101f",
|
|
12
|
+
"#00dd33",
|
|
13
|
+
"#00ef82"
|
|
14
|
+
];
|
|
15
|
+
const DEFAULT_COLUMNS = 11;
|
|
16
|
+
const DEFAULT_POWER = 1.65;
|
|
17
|
+
const DEFAULT_TILES = 15;
|
|
18
|
+
const CYCLE = 0.2;
|
|
19
|
+
const FALL_STAGGER = 6e-3;
|
|
20
|
+
const STRETCH = 8.5;
|
|
21
|
+
const BLEED = 0.012;
|
|
22
|
+
const COL_PHASE = -0.4;
|
|
23
|
+
const TAIL_SLOPE = 0.05;
|
|
24
|
+
const STRIP_LENGTH = 61;
|
|
25
|
+
function mulberry32(seed) {
|
|
26
|
+
let a = seed >>> 0;
|
|
27
|
+
return () => {
|
|
28
|
+
a = a + 1831565813 >>> 0;
|
|
29
|
+
let t = a;
|
|
30
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
31
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
32
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function luminance(color) {
|
|
36
|
+
const s = color.trim().toLowerCase();
|
|
37
|
+
let rgb = null;
|
|
38
|
+
const hex = s.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
|
|
39
|
+
if (hex) {
|
|
40
|
+
const h = hex[1];
|
|
41
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
42
|
+
rgb = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
43
|
+
} else {
|
|
44
|
+
const m = s.match(/^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
45
|
+
if (m) rgb = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
46
|
+
}
|
|
47
|
+
if (!rgb) return null;
|
|
48
|
+
const [r, g, b] = rgb.map((v) => {
|
|
49
|
+
const c = Math.min(255, v) / 255;
|
|
50
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
51
|
+
});
|
|
52
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
53
|
+
}
|
|
54
|
+
function paletteRoles(colors) {
|
|
55
|
+
const ranked = colors.map((c, i) => ({ i, l: luminance(c) ?? 0.5 })).sort((a, b) => a.l - b.l || a.i - b.i);
|
|
56
|
+
const light = ranked[ranked.length - 1].i;
|
|
57
|
+
const darkCount = colors.length >= 5 ? 2 : 1;
|
|
58
|
+
const darks = ranked.slice(0, darkCount).map((r) => r.i);
|
|
59
|
+
const hues = ranked.slice(darkCount, -1).map((r) => r.i);
|
|
60
|
+
return { light, darks, hues };
|
|
61
|
+
}
|
|
62
|
+
function pickDifferent(from, avoid, len) {
|
|
63
|
+
for (let i = 1; i <= len; i++) {
|
|
64
|
+
const c = (from + i) % len;
|
|
65
|
+
if (!avoid.includes(c)) return c;
|
|
66
|
+
}
|
|
67
|
+
return from;
|
|
68
|
+
}
|
|
69
|
+
function buildStrip(seed, colors, len = STRIP_LENGTH) {
|
|
70
|
+
const n = colors.length;
|
|
71
|
+
if (n < 2) return Array.from({ length: len }, () => 0);
|
|
72
|
+
const { light, darks, hues } = paletteRoles(colors);
|
|
73
|
+
const pool = hues.length ? hues : darks;
|
|
74
|
+
const rand = mulberry32(seed);
|
|
75
|
+
const strip = [];
|
|
76
|
+
let lastHue = -1;
|
|
77
|
+
for (let k = 0; k < len; k++) {
|
|
78
|
+
const r = rand();
|
|
79
|
+
let next;
|
|
80
|
+
if (r < 0.26) {
|
|
81
|
+
next = light;
|
|
82
|
+
} else if (r < 0.42) {
|
|
83
|
+
next = darks[Math.floor(rand() * darks.length)];
|
|
84
|
+
} else {
|
|
85
|
+
let h = pool[Math.floor(rand() * pool.length)];
|
|
86
|
+
if (h === lastHue && pool.length > 1) h = pool[(pool.indexOf(h) + 1) % pool.length];
|
|
87
|
+
lastHue = h;
|
|
88
|
+
next = h;
|
|
89
|
+
}
|
|
90
|
+
if (k > 0 && next === strip[k - 1]) next = pickDifferent(next, [strip[k - 1]], n);
|
|
91
|
+
strip.push(next);
|
|
92
|
+
}
|
|
93
|
+
if (len > 2 && strip[len - 1] === strip[0]) {
|
|
94
|
+
strip[len - 1] = pickDifferent(strip[len - 1], [strip[len - 2], strip[0]], n);
|
|
95
|
+
}
|
|
96
|
+
return strip;
|
|
97
|
+
}
|
|
98
|
+
function stripIndex(strip, id, column) {
|
|
99
|
+
const len = strip.length;
|
|
100
|
+
const s = id - column;
|
|
101
|
+
return strip[(s % len + len) % len];
|
|
102
|
+
}
|
|
103
|
+
function columnEdges(width, columns, power) {
|
|
104
|
+
const edges = [];
|
|
105
|
+
for (let i = 0; i <= columns; i++) {
|
|
106
|
+
edges.push(Math.round(width * Math.pow(i / columns, power)));
|
|
107
|
+
}
|
|
108
|
+
return edges;
|
|
109
|
+
}
|
|
110
|
+
function oddTiles(tiles) {
|
|
111
|
+
const t = Math.max(3, Math.round(Number.isFinite(tiles) ? tiles : DEFAULT_TILES));
|
|
112
|
+
return t % 2 === 0 ? t + 1 : t;
|
|
113
|
+
}
|
|
114
|
+
function tileEdge(k, tiles, stretch = STRETCH) {
|
|
115
|
+
const u = k / tiles;
|
|
116
|
+
if (u < 0) return u * TAIL_SLOPE;
|
|
117
|
+
if (u > 1) return 1 + (u - 1) * TAIL_SLOPE;
|
|
118
|
+
const a = Math.pow(u, stretch);
|
|
119
|
+
return a / (a + Math.pow(1 - u, stretch));
|
|
120
|
+
}
|
|
121
|
+
const SPRING_NORM = 1 - Math.exp(-1.6);
|
|
122
|
+
function springStep(p) {
|
|
123
|
+
const half = (t) => (1 - Math.exp(-1.6 * t)) / SPRING_NORM;
|
|
124
|
+
return p < 0.5 ? 0.5 * half(2 * p) : 1 - 0.5 * half(2 * (1 - p));
|
|
125
|
+
}
|
|
126
|
+
function columnFlow(elapsed, column, columns, speed = 1) {
|
|
127
|
+
const delay = (columns - 1 - column) * FALL_STAGGER;
|
|
128
|
+
const t = elapsed - delay;
|
|
129
|
+
const raw = t <= 0 ? 0 : t * speed / CYCLE;
|
|
130
|
+
const linear = raw + column * COL_PHASE;
|
|
131
|
+
const step = Math.floor(linear);
|
|
132
|
+
return step + springStep(linear - step);
|
|
133
|
+
}
|
|
134
|
+
function columnTiles(flow, tiles, height, bleed) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const base = -Math.floor(flow);
|
|
137
|
+
for (let n = tiles + 2; n >= -2; n--) {
|
|
138
|
+
const id = base + n;
|
|
139
|
+
const k = id + flow;
|
|
140
|
+
const top = Math.round(tileEdge(k, tiles) * height);
|
|
141
|
+
const bot = Math.round(tileEdge(k + 1, tiles) * height) + bleed;
|
|
142
|
+
if (bot <= top) continue;
|
|
143
|
+
out.push({ id, top, bot });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function createCoverage(columns, covered = false) {
|
|
148
|
+
const top = new Float32Array(columns);
|
|
149
|
+
const bot = new Float32Array(columns);
|
|
150
|
+
if (covered) bot.fill(1);
|
|
151
|
+
return { top, bot };
|
|
152
|
+
}
|
|
153
|
+
function columnDelays(columns, sweep, stagger, seed = 1) {
|
|
154
|
+
const mid = (columns - 1) / 2;
|
|
155
|
+
const rank = (i) => {
|
|
156
|
+
switch (sweep) {
|
|
157
|
+
case "left":
|
|
158
|
+
return i;
|
|
159
|
+
case "center":
|
|
160
|
+
return Math.abs(i - mid);
|
|
161
|
+
case "edges":
|
|
162
|
+
return mid - Math.abs(i - mid);
|
|
163
|
+
case "random":
|
|
164
|
+
return 0;
|
|
165
|
+
default:
|
|
166
|
+
return columns - 1 - i;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (sweep === "random") {
|
|
170
|
+
const order = Array.from({ length: columns }, (_, i) => i);
|
|
171
|
+
const rand = mulberry32(seed ^ 24301);
|
|
172
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
173
|
+
const j = Math.floor(rand() * (i + 1));
|
|
174
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
175
|
+
}
|
|
176
|
+
const delays = new Array(columns);
|
|
177
|
+
order.forEach((col, k) => delays[col] = k * stagger);
|
|
178
|
+
return delays;
|
|
179
|
+
}
|
|
180
|
+
return Array.from({ length: columns }, (_, i) => rank(i) * stagger);
|
|
181
|
+
}
|
|
182
|
+
function stepCoverage(cov, direction, phaseTime, dt, duration, delays) {
|
|
183
|
+
const n = cov.top.length;
|
|
184
|
+
const rate = duration > 0 ? dt / duration : Infinity;
|
|
185
|
+
let done = true;
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
const started = phaseTime >= (delays[i] ?? 0);
|
|
188
|
+
if (direction === "cover") {
|
|
189
|
+
if (cov.bot[i] - cov.top[i] <= 1e-6 && cov.bot[i] < 1) {
|
|
190
|
+
cov.top[i] = 0;
|
|
191
|
+
cov.bot[i] = 0;
|
|
192
|
+
}
|
|
193
|
+
if (started) {
|
|
194
|
+
cov.bot[i] = Math.min(1, cov.bot[i] + rate);
|
|
195
|
+
cov.top[i] = Math.max(0, cov.top[i] - rate);
|
|
196
|
+
}
|
|
197
|
+
if (cov.bot[i] < 1 || cov.top[i] > 0) done = false;
|
|
198
|
+
} else {
|
|
199
|
+
if (started) cov.top[i] = Math.min(cov.bot[i], cov.top[i] + rate);
|
|
200
|
+
if (cov.top[i] < cov.bot[i]) done = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return done;
|
|
204
|
+
}
|
|
205
|
+
function settleCoverage(cov, direction) {
|
|
206
|
+
if (direction === "cover") {
|
|
207
|
+
cov.top.fill(0);
|
|
208
|
+
cov.bot.fill(1);
|
|
209
|
+
} else {
|
|
210
|
+
cov.top.fill(0);
|
|
211
|
+
cov.bot.fill(0);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function easeEdge(a) {
|
|
215
|
+
const x = Math.min(1, Math.max(0, a));
|
|
216
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
217
|
+
}
|
|
218
|
+
function columnBand(effect, column, top, bot) {
|
|
219
|
+
const rise = effect === "rise" || effect === "interlace" && column % 2 === 1;
|
|
220
|
+
if (effect === "split") {
|
|
221
|
+
const half = Math.max(0, bot - top) / 2;
|
|
222
|
+
return { y0: 0.5 - half, y1: 0.5 + half, mirror: false };
|
|
223
|
+
}
|
|
224
|
+
if (rise) return { y0: 1 - bot, y1: 1 - top, mirror: true };
|
|
225
|
+
return { y0: top, y1: bot, mirror: false };
|
|
226
|
+
}
|
|
227
|
+
const DATAMOSH_PALETTES = {
|
|
228
|
+
broadcast: DEFAULT_COLORS,
|
|
229
|
+
thermal: [
|
|
230
|
+
"#fff4d6",
|
|
231
|
+
"#ffd23f",
|
|
232
|
+
"#ff9f1c",
|
|
233
|
+
"#ff5a1f",
|
|
234
|
+
"#e0282e",
|
|
235
|
+
"#b0125b",
|
|
236
|
+
"#6a0f63",
|
|
237
|
+
"#2a0a3d",
|
|
238
|
+
"#0d0614"
|
|
239
|
+
],
|
|
240
|
+
sunset: [
|
|
241
|
+
"#fff6d8",
|
|
242
|
+
"#fbc56b",
|
|
243
|
+
"#f59a3a",
|
|
244
|
+
"#e97a1d",
|
|
245
|
+
"#c85a14",
|
|
246
|
+
"#8a4a22",
|
|
247
|
+
"#5a3b25",
|
|
248
|
+
"#2d1c16",
|
|
249
|
+
"#140b08"
|
|
250
|
+
],
|
|
251
|
+
mono: ["#ffffff", "#e5e5e5", "#b5b5b5", "#8a8a8a", "#5c5c5c", "#2e2e2e", "#141414", "#050505"],
|
|
252
|
+
acid: [
|
|
253
|
+
"#f4ffe8",
|
|
254
|
+
"#e4ff1a",
|
|
255
|
+
"#00ff9c",
|
|
256
|
+
"#00e0ff",
|
|
257
|
+
"#3d5afe",
|
|
258
|
+
"#8c3dff",
|
|
259
|
+
"#ff2bd6",
|
|
260
|
+
"#1b0b36",
|
|
261
|
+
"#070707"
|
|
262
|
+
]
|
|
263
|
+
};
|
|
264
|
+
function resolvePalette(colors) {
|
|
265
|
+
if (typeof colors === "string") return [...DATAMOSH_PALETTES[colors] ?? DEFAULT_COLORS];
|
|
266
|
+
return colors && colors.length >= 2 ? [...colors] : [...DEFAULT_COLORS];
|
|
267
|
+
}
|
|
268
|
+
const MAX_GAIN = 1.6;
|
|
269
|
+
const SOURCE_SPAN = 0.6;
|
|
270
|
+
function sourceSlotY(slot, tiles) {
|
|
271
|
+
return 0.5 + ((slot + 0.5) / tiles - 0.5) * SOURCE_SPAN;
|
|
272
|
+
}
|
|
273
|
+
function sourceSlot(id, column, tiles) {
|
|
274
|
+
const s = id - column;
|
|
275
|
+
return (s % tiles + tiles) % tiles;
|
|
276
|
+
}
|
|
277
|
+
function coverFit(imgW, imgH, boxW, boxH) {
|
|
278
|
+
if (imgW <= 0 || imgH <= 0 || boxW <= 0 || boxH <= 0) return { sx: 0, sy: 0, sw: imgW, sh: imgH };
|
|
279
|
+
const scale = Math.max(boxW / imgW, boxH / imgH);
|
|
280
|
+
const sw = boxW / scale;
|
|
281
|
+
const sh = boxH / scale;
|
|
282
|
+
return { sx: (imgW - sw) / 2, sy: (imgH - sh) / 2, sw, sh };
|
|
283
|
+
}
|
|
284
|
+
function sampleSource(pixels, pw, ph, edges, tiles, boost = true) {
|
|
285
|
+
const at = (x, y) => {
|
|
286
|
+
const cx = Math.min(pw - 1, Math.max(0, x));
|
|
287
|
+
const cy = Math.min(ph - 1, Math.max(0, y));
|
|
288
|
+
return (cy * pw + cx) * 4;
|
|
289
|
+
};
|
|
290
|
+
const cells = [];
|
|
291
|
+
let lo = Infinity;
|
|
292
|
+
let hi = -Infinity;
|
|
293
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
294
|
+
const x = Math.round((edges[i] + edges[i + 1]) / 2 * (pw - 1));
|
|
295
|
+
const row = [];
|
|
296
|
+
for (let s = 0; s < tiles; s++) {
|
|
297
|
+
const y = Math.round(sourceSlotY(s, tiles) * (ph - 1));
|
|
298
|
+
let r = 0;
|
|
299
|
+
let g = 0;
|
|
300
|
+
let b = 0;
|
|
301
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
302
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
303
|
+
const k = at(x + dx, y + dy);
|
|
304
|
+
r += pixels[k];
|
|
305
|
+
g += pixels[k + 1];
|
|
306
|
+
b += pixels[k + 2];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const cell = [r / 9, g / 9, b / 9];
|
|
310
|
+
const l = 0.2126 * cell[0] + 0.7152 * cell[1] + 0.0722 * cell[2];
|
|
311
|
+
lo = Math.min(lo, l);
|
|
312
|
+
hi = Math.max(hi, l);
|
|
313
|
+
row.push(cell);
|
|
314
|
+
}
|
|
315
|
+
cells.push(row);
|
|
316
|
+
}
|
|
317
|
+
const span = hi - lo;
|
|
318
|
+
const level = boost && span > 8 ? Math.min(MAX_GAIN, 255 / (hi - lo * 0.5)) : 1;
|
|
319
|
+
const offset = boost && span > 8 ? lo * 0.5 : 0;
|
|
320
|
+
const saturation = boost ? 1.35 : 1;
|
|
321
|
+
const clamp = (v) => Math.round(Math.min(255, Math.max(0, v)));
|
|
322
|
+
return cells.map(
|
|
323
|
+
(row) => row.map(([r, g, b]) => {
|
|
324
|
+
let [nr, ng, nb] = [(r - offset) * level, (g - offset) * level, (b - offset) * level];
|
|
325
|
+
const l = 0.2126 * nr + 0.7152 * ng + 0.0722 * nb;
|
|
326
|
+
nr = l + (nr - l) * saturation;
|
|
327
|
+
ng = l + (ng - l) * saturation;
|
|
328
|
+
nb = l + (nb - l) * saturation;
|
|
329
|
+
return `rgb(${clamp(nr)}, ${clamp(ng)}, ${clamp(nb)})`;
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
export {
|
|
334
|
+
BLEED,
|
|
335
|
+
COL_PHASE,
|
|
336
|
+
CYCLE,
|
|
337
|
+
DATAMOSH_PALETTES,
|
|
338
|
+
DEFAULT_COLORS,
|
|
339
|
+
DEFAULT_COLUMNS,
|
|
340
|
+
DEFAULT_POWER,
|
|
341
|
+
DEFAULT_TILES,
|
|
342
|
+
FALL_STAGGER,
|
|
343
|
+
MAX_GAIN,
|
|
344
|
+
SOURCE_SPAN,
|
|
345
|
+
STRETCH,
|
|
346
|
+
STRIP_LENGTH,
|
|
347
|
+
TAIL_SLOPE,
|
|
348
|
+
buildStrip,
|
|
349
|
+
columnBand,
|
|
350
|
+
columnDelays,
|
|
351
|
+
columnEdges,
|
|
352
|
+
columnFlow,
|
|
353
|
+
columnTiles,
|
|
354
|
+
coverFit,
|
|
355
|
+
createCoverage,
|
|
356
|
+
easeEdge,
|
|
357
|
+
luminance,
|
|
358
|
+
mulberry32,
|
|
359
|
+
oddTiles,
|
|
360
|
+
paletteRoles,
|
|
361
|
+
resolvePalette,
|
|
362
|
+
sampleSource,
|
|
363
|
+
settleCoverage,
|
|
364
|
+
sourceSlot,
|
|
365
|
+
sourceSlotY,
|
|
366
|
+
springStep,
|
|
367
|
+
stepCoverage,
|
|
368
|
+
stripIndex,
|
|
369
|
+
tileEdge
|
|
370
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DatamoshTransition } from "./DatamoshTransition.js";
|
|
2
|
+
export type { DatamoshTransitionProps, DatamoshTransitionPhase, DatamoshTransitionHandle, } from "./DatamoshTransition.js";
|
|
3
|
+
export { DATAMOSH_PALETTES, type DatamoshEffect, type DatamoshSweep, type DatamoshPaletteName, } from "./datamosh-transition-core.js";
|
|
@@ -3,7 +3,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useRef, useEffect } from "react";
|
|
4
4
|
import { useLiveRef } from "../../internals/dom/use-live-ref.js";
|
|
5
5
|
import { cn } from "../../utils.js";
|
|
6
|
-
import { hexToRgb, DEV, wrap, pointerPrototype, scaleRadiusForContainer, HSVtoRGB } from "./fluid-shared.js";
|
|
6
|
+
import { hexToRgb, clickBoost, DEV, wrap, pointerPrototype, scaleRadiusForContainer, HSVtoRGB } from "./fluid-shared.js";
|
|
7
7
|
import { startWebGpuFluid } from "./webgpu-engine.js";
|
|
8
8
|
function FluidCursor({
|
|
9
9
|
simResolution = 128,
|
|
@@ -38,13 +38,19 @@ function FluidCursor({
|
|
|
38
38
|
ditherLevels = 4,
|
|
39
39
|
onReady
|
|
40
40
|
}) {
|
|
41
|
-
const [derived] = useState(() =>
|
|
42
|
-
clampedColorIntensity
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const [derived] = useState(() => {
|
|
42
|
+
const clampedColorIntensity = Math.max(0, Math.min(1, colorIntensity));
|
|
43
|
+
return {
|
|
44
|
+
clampedColorIntensity,
|
|
45
|
+
// A click's extra brightness, capped so it never compounds
|
|
46
|
+
// colorIntensity into the dither pass's clamp — see clickBoost().
|
|
47
|
+
clickColorBoost: clickBoost(clampedColorIntensity),
|
|
48
|
+
clampedHdrBoost: Math.max(1, Math.min(4, hdrBoost)),
|
|
49
|
+
clampedDitherPixelSize: Math.max(1, ditherPixelSize),
|
|
50
|
+
clampedDitherLevels: Math.max(2, Math.min(16, Math.floor(ditherLevels))),
|
|
51
|
+
resolvedBackColor: typeof backColor === "string" ? hexToRgb(backColor) : backColor
|
|
52
|
+
};
|
|
53
|
+
});
|
|
48
54
|
const canvasRef = useRef(null);
|
|
49
55
|
const activeInstanceRef = useRef(null);
|
|
50
56
|
const mountCounterRef = useRef(0);
|
|
@@ -61,11 +67,11 @@ function FluidCursor({
|
|
|
61
67
|
const onReadyRef = useLiveRef(onReady);
|
|
62
68
|
function getScaledColor(hex) {
|
|
63
69
|
const { r, g, b } = hexToRgb(hex);
|
|
64
|
-
return {
|
|
70
|
+
return Object.freeze({
|
|
65
71
|
r: r * derived.clampedColorIntensity,
|
|
66
72
|
g: g * derived.clampedColorIntensity,
|
|
67
73
|
b: b * derived.clampedColorIntensity
|
|
68
|
-
};
|
|
74
|
+
});
|
|
69
75
|
}
|
|
70
76
|
function getCachedFluidColor(hex) {
|
|
71
77
|
const cache = colorCacheRef.current;
|
|
@@ -186,7 +192,8 @@ function FluidCursor({
|
|
|
186
192
|
autoSplatInterval,
|
|
187
193
|
pauseWhenHidden,
|
|
188
194
|
splatOnMount,
|
|
189
|
-
generateColor
|
|
195
|
+
generateColor,
|
|
196
|
+
clickBoost: derived.clickColorBoost
|
|
190
197
|
}).then((handle) => {
|
|
191
198
|
if (disposed) {
|
|
192
199
|
handle?.cleanup();
|
|
@@ -581,13 +588,19 @@ function FluidCursor({
|
|
|
581
588
|
uniform vec3 color;
|
|
582
589
|
uniform vec2 point;
|
|
583
590
|
uniform float radius;
|
|
591
|
+
uniform float saturate;
|
|
584
592
|
|
|
585
593
|
void main () {
|
|
586
594
|
vec2 p = vUv - point.xy;
|
|
587
595
|
p.x *= aspectRatio;
|
|
588
596
|
vec3 splat = exp(-dot(p, p) / radius) * color;
|
|
589
597
|
vec3 base = texture2D(uTarget, vUv).xyz;
|
|
590
|
-
|
|
598
|
+
// A click splat lifts each texel to its own profile instead of adding
|
|
599
|
+
// to it: what is already there never rises past what one click would
|
|
600
|
+
// leave at that spot, so a burst of clicks paints the same disc once
|
|
601
|
+
// instead of summing towards white and widening with every click.
|
|
602
|
+
vec3 sum = saturate > 0.5 ? max(base, splat) : base + splat;
|
|
603
|
+
gl_FragColor = vec4(sum, 1.0);
|
|
591
604
|
}
|
|
592
605
|
`
|
|
593
606
|
);
|
|
@@ -1211,15 +1224,17 @@ function FluidCursor({
|
|
|
1211
1224
|
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color);
|
|
1212
1225
|
}
|
|
1213
1226
|
function clickSplat(pointer) {
|
|
1214
|
-
const
|
|
1215
|
-
color
|
|
1216
|
-
|
|
1217
|
-
|
|
1227
|
+
const base = generateColor();
|
|
1228
|
+
const color = {
|
|
1229
|
+
r: base.r * derived.clickColorBoost,
|
|
1230
|
+
g: base.g * derived.clickColorBoost,
|
|
1231
|
+
b: base.b * derived.clickColorBoost
|
|
1232
|
+
};
|
|
1218
1233
|
const dx = 10 * (Math.random() - 0.5);
|
|
1219
1234
|
const dy = 30 * (Math.random() - 0.5);
|
|
1220
|
-
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color);
|
|
1235
|
+
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, color, true);
|
|
1221
1236
|
}
|
|
1222
|
-
function splat(x, y, dx, dy, color) {
|
|
1237
|
+
function splat(x, y, dx, dy, color, saturate = false) {
|
|
1223
1238
|
splatProgram.bind();
|
|
1224
1239
|
if (splatProgram.uniforms.uTarget) {
|
|
1225
1240
|
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0));
|
|
@@ -1233,6 +1248,9 @@ function FluidCursor({
|
|
|
1233
1248
|
if (splatProgram.uniforms.color) {
|
|
1234
1249
|
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0);
|
|
1235
1250
|
}
|
|
1251
|
+
if (splatProgram.uniforms.saturate) {
|
|
1252
|
+
gl.uniform1f(splatProgram.uniforms.saturate, 0);
|
|
1253
|
+
}
|
|
1236
1254
|
if (splatProgram.uniforms.radius) {
|
|
1237
1255
|
let radius = correctRadius(config.SPLAT_RADIUS / 100);
|
|
1238
1256
|
if (containedRef.current) {
|
|
@@ -1248,6 +1266,9 @@ function FluidCursor({
|
|
|
1248
1266
|
if (splatProgram.uniforms.color) {
|
|
1249
1267
|
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b);
|
|
1250
1268
|
}
|
|
1269
|
+
if (splatProgram.uniforms.saturate) {
|
|
1270
|
+
gl.uniform1f(splatProgram.uniforms.saturate, saturate ? 1 : 0);
|
|
1271
|
+
}
|
|
1251
1272
|
blit(dye.write);
|
|
1252
1273
|
dye.swap();
|
|
1253
1274
|
}
|
|
@@ -1347,6 +1368,9 @@ function FluidCursor({
|
|
|
1347
1368
|
updatePointerDownData(pointer, -1, posX, posY);
|
|
1348
1369
|
clickSplat(pointer);
|
|
1349
1370
|
}
|
|
1371
|
+
function handlePointerUp() {
|
|
1372
|
+
pointers[0].down = false;
|
|
1373
|
+
}
|
|
1350
1374
|
function handleFirstMouseMove(e) {
|
|
1351
1375
|
const pointer = pointers[0];
|
|
1352
1376
|
const { x: posX, y: posY } = getCanvasPos(e.clientX, e.clientY);
|
|
@@ -1389,11 +1413,14 @@ function FluidCursor({
|
|
|
1389
1413
|
}
|
|
1390
1414
|
if (interactive) {
|
|
1391
1415
|
window.addEventListener("mousedown", handleMouseDown);
|
|
1416
|
+
window.addEventListener("mouseup", handlePointerUp);
|
|
1392
1417
|
document.body.addEventListener("mousemove", handleFirstMouseMove);
|
|
1393
1418
|
window.addEventListener("mousemove", handleMouseMove);
|
|
1394
1419
|
document.body.addEventListener("touchstart", handleFirstTouchStart);
|
|
1395
1420
|
window.addEventListener("touchstart", handleTouchStart, false);
|
|
1396
1421
|
window.addEventListener("touchmove", handleTouchMove, false);
|
|
1422
|
+
window.addEventListener("touchend", handlePointerUp);
|
|
1423
|
+
window.addEventListener("touchcancel", handlePointerUp);
|
|
1397
1424
|
}
|
|
1398
1425
|
let observer = null;
|
|
1399
1426
|
if (pauseWhenHidden) {
|
|
@@ -1432,11 +1459,14 @@ function FluidCursor({
|
|
|
1432
1459
|
if (autoSplatTimer) clearInterval(autoSplatTimer);
|
|
1433
1460
|
if (interactive) {
|
|
1434
1461
|
window.removeEventListener("mousedown", handleMouseDown);
|
|
1462
|
+
window.removeEventListener("mouseup", handlePointerUp);
|
|
1435
1463
|
document.body.removeEventListener("mousemove", handleFirstMouseMove);
|
|
1436
1464
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
1437
1465
|
document.body.removeEventListener("touchstart", handleFirstTouchStart);
|
|
1438
1466
|
window.removeEventListener("touchstart", handleTouchStart);
|
|
1439
1467
|
window.removeEventListener("touchmove", handleTouchMove);
|
|
1468
|
+
window.removeEventListener("touchend", handlePointerUp);
|
|
1469
|
+
window.removeEventListener("touchcancel", handlePointerUp);
|
|
1440
1470
|
}
|
|
1441
1471
|
if (contained) {
|
|
1442
1472
|
window.removeEventListener("resize", updateCanvasRectCache);
|
|
@@ -36,6 +36,24 @@ export declare function correctRadius(radius: number, width: number, height: num
|
|
|
36
36
|
* length under ~30% of the container height.
|
|
37
37
|
*/
|
|
38
38
|
export declare function scaleRadiusForContainer(radius: number, containerHeight: number, viewportHeight: number): number;
|
|
39
|
+
/**
|
|
40
|
+
* Dye a click deposits at its centre, in multiples of the hue: the engine's
|
|
41
|
+
* default `colorIntensity` (0.15) times the ×10 boost the solver has always
|
|
42
|
+
* given a click — the amount that reads as a bright pop on every renderer.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CLICK_PEAK = 1.5;
|
|
45
|
+
/**
|
|
46
|
+
* The boost a click applies to an already intensity-scaled colour, chosen so
|
|
47
|
+
* the peak lands on CLICK_PEAK whatever `colorIntensity` is.
|
|
48
|
+
*
|
|
49
|
+
* The historical fixed ×10 compounded `colorIntensity`. At the default 0.15
|
|
50
|
+
* that is a 1.5 peak; but the dither pass asks consumers to raise the
|
|
51
|
+
* intensity to ≈0.4+, and at 0.6 the same ×10 stacked 5.4 per click into an
|
|
52
|
+
* unclamped half-float dye buffer that the dither display then clamps to 1
|
|
53
|
+
* and quantises — every click grew a flat, saturated disc that never read as
|
|
54
|
+
* a splat. Capping the boost at 10 keeps intensities ≤ 0.15 byte-identical.
|
|
55
|
+
*/
|
|
56
|
+
export declare function clickBoost(colorIntensity: number): number;
|
|
39
57
|
export declare function correctDeltaX(delta: number, width: number, height: number): number;
|
|
40
58
|
export declare function correctDeltaY(delta: number, width: number, height: number): number;
|
|
41
59
|
/**
|
|
@@ -95,6 +95,11 @@ function scaleRadiusForContainer(radius, containerHeight, viewportHeight) {
|
|
|
95
95
|
if (k <= 1) return radius;
|
|
96
96
|
return Math.max(radius, Math.min(radius * k, 0.09));
|
|
97
97
|
}
|
|
98
|
+
const CLICK_PEAK = 1.5;
|
|
99
|
+
function clickBoost(colorIntensity) {
|
|
100
|
+
if (!(colorIntensity > 0)) return 0;
|
|
101
|
+
return Math.min(10, CLICK_PEAK / colorIntensity);
|
|
102
|
+
}
|
|
98
103
|
function correctDeltaX(delta, width, height) {
|
|
99
104
|
const aspectRatio = width / height;
|
|
100
105
|
if (aspectRatio < 1) return delta * aspectRatio;
|
|
@@ -107,8 +112,10 @@ function correctDeltaY(delta, width, height) {
|
|
|
107
112
|
}
|
|
108
113
|
const DEV = typeof process !== "undefined" && process.env.NODE_ENV !== "production";
|
|
109
114
|
export {
|
|
115
|
+
CLICK_PEAK,
|
|
110
116
|
DEV,
|
|
111
117
|
HSVtoRGB,
|
|
118
|
+
clickBoost,
|
|
112
119
|
correctDeltaX,
|
|
113
120
|
correctDeltaY,
|
|
114
121
|
correctRadius,
|
|
@@ -20,6 +20,12 @@ export interface WebGpuFluidOptions {
|
|
|
20
20
|
pauseWhenHidden: boolean;
|
|
21
21
|
splatOnMount: boolean;
|
|
22
22
|
generateColor: () => ColorRGB;
|
|
23
|
+
/**
|
|
24
|
+
* Multiplier a click applies to the (already intensity-scaled) colour
|
|
25
|
+
* `generateColor()` returns — `clickBoost(colorIntensity)` from
|
|
26
|
+
* fluid-shared, so the peak is the same whatever the intensity.
|
|
27
|
+
*/
|
|
28
|
+
clickBoost: number;
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Full WebGPU setup for FluidCursor: engine + pointer wiring + rAF loop +
|