@zakkster/lite-ui-fx 1.3.0 → 1.5.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 +101 -0
- package/README.md +24 -10
- package/UIFX-RECIPE-GUIDE.md +3 -2
- package/UIFXController.d.ts +41 -1
- package/UIFXController.js +211 -17
- package/UIFXRecipes.d.ts +94 -53
- package/UIFXRecipes.js +669 -282
- package/llms.txt +33 -10
- package/package.json +1 -1
package/UIFXRecipes.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zakkster/lite-ui-fx -- Recipe Collection (all
|
|
2
|
+
* @zakkster/lite-ui-fx -- Recipe Collection (all 53)
|
|
3
3
|
*
|
|
4
4
|
* The three recipe volumes consolidated into one shipped, typed, versioned
|
|
5
|
-
* module, exposed as the ./recipes subpath export
|
|
6
|
-
* decisions/0001-recipes-position.md.
|
|
5
|
+
* module, exposed as the ./recipes subpath export, plus the U4a additions for
|
|
6
|
+
* the new native element types. See decisions/0001-recipes-position.md.
|
|
7
7
|
*
|
|
8
8
|
* Vol.1 (10): SwarmToggle, LiquidToggle, NeonPulseToggle, MagneticButton,
|
|
9
9
|
* ShatterButton, ConfettiButton, GlitchButton, SparkSlider,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* HeatMap, DayNightToggle, ReactionPicker, NotificationBell,
|
|
19
19
|
* TypewriterField, SoundWaveBtn, UploadProgress, ScratchReveal,
|
|
20
20
|
* TimerCountdown, PullRefresh
|
|
21
|
+
* U4a (3): TickDraw, IndeterminateScan (CHECKBOX), LiquidFill (PROGRESS)
|
|
21
22
|
*
|
|
22
23
|
* Registry: RECIPES (null-prototype), RECIPE_META (live), RECIPE_NAMES,
|
|
23
24
|
* registerRecipe(id, factory, meta?), mountRecipe(container, id, options?).
|
|
@@ -100,6 +101,81 @@ function fr(c,w,h,r){c.strokeStyle='rgba(110,231,182,.5)';c.lineWidth=2;c.setLin
|
|
|
100
101
|
const PI2=Math.PI*2;
|
|
101
102
|
|
|
102
103
|
|
|
104
|
+
// ---------------------------------------------------------
|
|
105
|
+
// THEME RESOLUTION (U3b, decisions/0002) -- COLD, once per instance in the
|
|
106
|
+
// factory / init. The hot tick() reads only the const strings resolved here.
|
|
107
|
+
// ---------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a recipe palette. DEFAULT is a role map of the recipe's historical
|
|
111
|
+
* literals ({ accent, dim, surface, ...extras }), so a bare mount is
|
|
112
|
+
* byte-identical to 1.3.0. options overlay it: theme { light, mid, dark } maps to
|
|
113
|
+
* { accent, dim, surface }; colors[] overrides positionally over
|
|
114
|
+
* Object.keys(DEFAULT) (accent, dim, surface, extras...), applied AFTER theme so
|
|
115
|
+
* colors win. Returns a fresh per-instance object -- DEFAULT is never mutated.
|
|
116
|
+
*/
|
|
117
|
+
function resolveTheme(o, DEFAULT) {
|
|
118
|
+
const p = Object.assign({}, DEFAULT);
|
|
119
|
+
if (o) {
|
|
120
|
+
const t = o.theme;
|
|
121
|
+
if (t) {
|
|
122
|
+
// Prefix roles so a recipe with two muted variants (dim, dim2) or two
|
|
123
|
+
// accents (accent, accent2) maps them all from one theme anchor while
|
|
124
|
+
// its DEFAULT keeps the distinct literals for byte-parity.
|
|
125
|
+
for (const k in p) {
|
|
126
|
+
if (k.indexOf('accent') === 0) p[k] = t.light;
|
|
127
|
+
else if (k.indexOf('dim') === 0) p[k] = t.mid;
|
|
128
|
+
else if (k.indexOf('surface') === 0) p[k] = t.dark;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const c = o.colors;
|
|
132
|
+
if (c) {
|
|
133
|
+
const ks = Object.keys(DEFAULT);
|
|
134
|
+
for (let i = 0; i < c.length && i < ks.length; i++) {
|
|
135
|
+
if (c[i] != null) p[ks[i]] = c[i];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return p;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Hex (#rgb / #rrggbb) -> "r,g,b" for building rgba() ramps. Returns `def` for a
|
|
143
|
+
* non-hex color (e.g. 'oklch(...)'), so a caller degrades gracefully rather than
|
|
144
|
+
* emitting NaN channels. COLD. */
|
|
145
|
+
function rgbTriplet(hex, def) {
|
|
146
|
+
if (typeof hex !== 'string' || hex[0] !== '#') return def;
|
|
147
|
+
let h = hex.slice(1);
|
|
148
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
149
|
+
if (h.length !== 6) return def;
|
|
150
|
+
const n = parseInt(h, 16);
|
|
151
|
+
if (!Number.isFinite(n)) return def;
|
|
152
|
+
return ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Hex -> "rgba(r,g,b,a)". COLD and only ever called on the themed path -- a bare
|
|
156
|
+
* mount keeps its historical rgba() literal, so this string's exact formatting is
|
|
157
|
+
* never compared for byte-parity. Falls back to the input untouched for a non-hex
|
|
158
|
+
* theme color, degrading a derived tint to the solid rather than emitting NaN. */
|
|
159
|
+
function rgbaOf(hex, a) {
|
|
160
|
+
const t = rgbTriplet(hex, null);
|
|
161
|
+
return t === null ? hex : 'rgba(' + t + ',' + a + ')';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve a recipe's visible canvas label: options.text, else the accessible
|
|
165
|
+
* options.label, else the recipe default -- so a mount that sets only `label`
|
|
166
|
+
* still drives the visible text (WCAG 2.5.3 label-in-name by construction). */
|
|
167
|
+
function pickText(o, def) {
|
|
168
|
+
if (o) {
|
|
169
|
+
if (o.text != null) return o.text;
|
|
170
|
+
if (o.label != null && o.label !== '') return o.label;
|
|
171
|
+
}
|
|
172
|
+
return def;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Resolve a font string: options.font, else the recipe's historical literal. */
|
|
176
|
+
function pickFont(o, def) { return (o && o.font) || def; }
|
|
177
|
+
|
|
178
|
+
|
|
103
179
|
|
|
104
180
|
// ===========================================================
|
|
105
181
|
// TOGGLE RECIPES
|
|
@@ -110,7 +186,10 @@ const PI2=Math.PI*2;
|
|
|
110
186
|
* On toggle, they explode outward then regroup at the new position.
|
|
111
187
|
* Uses sunflower phyllotaxis for the packed formation.
|
|
112
188
|
*/
|
|
113
|
-
export function SwarmToggle(
|
|
189
|
+
export function SwarmToggle(o = {}) {
|
|
190
|
+
const { seed = 42, count = 150 } = o;
|
|
191
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', dim: '#9999b8', dim2: '#8888aa' });
|
|
192
|
+
const onTint = (o.theme || o.colors) ? rgbaOf(P.accent, .2) : 'rgba(110,231,182,.2)';
|
|
114
193
|
const rng = new Random(seed);
|
|
115
194
|
const N = count;
|
|
116
195
|
const px = new Float32Array(N), py = new Float32Array(N);
|
|
@@ -142,19 +221,19 @@ export function SwarmToggle({ seed = 42, count = 150 } = {}) {
|
|
|
142
221
|
|
|
143
222
|
tick(ctx, dt, now, st) {
|
|
144
223
|
// Track
|
|
145
|
-
ctx.fillStyle = st.toggled ?
|
|
224
|
+
ctx.fillStyle = st.toggled ? onTint : 'rgba(255,255,255,.06)';
|
|
146
225
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2);
|
|
147
226
|
ctx.fill();
|
|
148
227
|
|
|
149
228
|
// State label
|
|
150
229
|
drawStateLabel(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14,
|
|
151
|
-
st.toggled ?
|
|
230
|
+
st.toggled ? P.accent : P.dim2);
|
|
152
231
|
|
|
153
232
|
// Knob target position
|
|
154
233
|
const tx = st.toggled ? st.w - 18 : 18;
|
|
155
234
|
|
|
156
235
|
// Render particles (spring toward formation)
|
|
157
|
-
ctx.fillStyle = st.toggled ?
|
|
236
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
158
237
|
for (let i = 0; i < N; i++) {
|
|
159
238
|
vx[i] += ((tx + ox[i]) - px[i]) * 15 * dt;
|
|
160
239
|
vy[i] += ((st.h / 2 + oy[i]) - py[i]) * 15 * dt;
|
|
@@ -179,7 +258,9 @@ export function SwarmToggle({ seed = 42, count = 150 } = {}) {
|
|
|
179
258
|
* Liquid Toggle -- Metaball-style stretching knob.
|
|
180
259
|
* The knob elongates in the direction of motion, squashes perpendicular.
|
|
181
260
|
*/
|
|
182
|
-
export function LiquidToggle() {
|
|
261
|
+
export function LiquidToggle(o = {}) {
|
|
262
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', dim: '#9999b8', dim2: '#8888aa' });
|
|
263
|
+
const onTint = (o.theme || o.colors) ? rgbaOf(P.accent, .2) : 'rgba(110,231,182,.2)';
|
|
183
264
|
let knobX = 18;
|
|
184
265
|
|
|
185
266
|
return {
|
|
@@ -189,19 +270,19 @@ export function LiquidToggle() {
|
|
|
189
270
|
const stretch = Math.abs(knobX - tx) * 0.5;
|
|
190
271
|
|
|
191
272
|
// Track
|
|
192
|
-
ctx.fillStyle = st.toggled ?
|
|
273
|
+
ctx.fillStyle = st.toggled ? onTint : 'rgba(255,255,255,.06)';
|
|
193
274
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2);
|
|
194
275
|
ctx.fill();
|
|
195
276
|
|
|
196
277
|
// Knob (stretched ellipse)
|
|
197
|
-
ctx.fillStyle = st.toggled ?
|
|
278
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
198
279
|
ctx.beginPath();
|
|
199
280
|
ctx.ellipse(knobX, st.h / 2, 14 + stretch, 14 - stretch * 0.2, 0, 0, Math.PI * 2);
|
|
200
281
|
ctx.fill();
|
|
201
282
|
|
|
202
283
|
// State label
|
|
203
284
|
drawStateLabel(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14,
|
|
204
|
-
st.toggled ?
|
|
285
|
+
st.toggled ? P.accent : P.dim2);
|
|
205
286
|
|
|
206
287
|
if (st.focused) drawFocusRing(ctx, st.w, st.h, st.h / 2);
|
|
207
288
|
},
|
|
@@ -212,7 +293,9 @@ export function LiquidToggle() {
|
|
|
212
293
|
/**
|
|
213
294
|
* Neon Pulse Toggle -- Expanding shockwave rings on toggle.
|
|
214
295
|
*/
|
|
215
|
-
export function NeonPulseToggle() {
|
|
296
|
+
export function NeonPulseToggle(o = {}) {
|
|
297
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#9999b8', dim2: '#8888aa' });
|
|
298
|
+
const onTint = (o.theme || o.colors) ? rgbaOf(P.accent, .2) : 'rgba(56,189,248,.2)';
|
|
216
299
|
// Fixed ring pool (dead when life <= 0) -- no push/splice on the hot path.
|
|
217
300
|
const RINGS = 8;
|
|
218
301
|
const ringR = new Float64Array(RINGS);
|
|
@@ -238,13 +321,13 @@ export function NeonPulseToggle() {
|
|
|
238
321
|
knobX = lerp(knobX, st.toggled ? st.w - 18 : 18, dt * 15);
|
|
239
322
|
|
|
240
323
|
// Track
|
|
241
|
-
ctx.fillStyle = st.toggled ?
|
|
324
|
+
ctx.fillStyle = st.toggled ? onTint : 'rgba(255,255,255,.06)';
|
|
242
325
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2);
|
|
243
326
|
ctx.fill();
|
|
244
327
|
|
|
245
328
|
// Shockwave rings -- const stroke color, per-ring alpha via globalAlpha
|
|
246
329
|
// (was `rgba(56,189,248,${life})` built per ring per frame).
|
|
247
|
-
ctx.strokeStyle =
|
|
330
|
+
ctx.strokeStyle = P.accent;
|
|
248
331
|
ctx.lineWidth = 2;
|
|
249
332
|
for (let i = 0; i < RINGS; i++) {
|
|
250
333
|
if (ringLife[i] <= 0) continue;
|
|
@@ -259,13 +342,13 @@ export function NeonPulseToggle() {
|
|
|
259
342
|
ctx.globalAlpha = 1;
|
|
260
343
|
|
|
261
344
|
// Knob
|
|
262
|
-
ctx.fillStyle = st.toggled ?
|
|
345
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
263
346
|
ctx.beginPath();
|
|
264
347
|
ctx.arc(knobX, st.h / 2, 14, 0, Math.PI * 2);
|
|
265
348
|
ctx.fill();
|
|
266
349
|
|
|
267
350
|
drawStateLabel(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14,
|
|
268
|
-
st.toggled ?
|
|
351
|
+
st.toggled ? P.accent : P.dim2);
|
|
269
352
|
|
|
270
353
|
if (st.focused) drawFocusRing(ctx, st.w, st.h, st.h / 2);
|
|
271
354
|
},
|
|
@@ -281,7 +364,11 @@ export function NeonPulseToggle() {
|
|
|
281
364
|
* Magnetic Button -- The entire button follows the cursor with spring physics.
|
|
282
365
|
* Squashes on click, springs back.
|
|
283
366
|
*/
|
|
284
|
-
export function MagneticButton(
|
|
367
|
+
export function MagneticButton(o = {}) {
|
|
368
|
+
const { maxPull = 15 } = o;
|
|
369
|
+
const P = resolveTheme(o, { accent: '#a78bfa', surface: '#1e1e2f', dim: '#e2e2f0' });
|
|
370
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
371
|
+
const TXT = pickText(o, 'MAGNETIC');
|
|
285
372
|
let bx = 0, by = 0, textScale = 1;
|
|
286
373
|
|
|
287
374
|
return {
|
|
@@ -297,19 +384,19 @@ export function MagneticButton({ maxPull = 15 } = {}) {
|
|
|
297
384
|
ctx.translate(bx, by);
|
|
298
385
|
|
|
299
386
|
// Button body
|
|
300
|
-
ctx.fillStyle = st.hover ?
|
|
301
|
-
ctx.strokeStyle = st.hover ?
|
|
387
|
+
ctx.fillStyle = st.hover ? P.surface : 'rgba(255,255,255,.05)';
|
|
388
|
+
ctx.strokeStyle = st.hover ? P.accent : 'rgba(255,255,255,.1)';
|
|
302
389
|
ctx.lineWidth = 1;
|
|
303
390
|
roundRect(ctx, 0, 0, st.w, st.h, 10);
|
|
304
391
|
ctx.fill(); ctx.stroke();
|
|
305
392
|
|
|
306
393
|
// Label
|
|
307
|
-
ctx.fillStyle = st.hover ?
|
|
308
|
-
ctx.font =
|
|
394
|
+
ctx.fillStyle = st.hover ? P.accent : P.dim;
|
|
395
|
+
ctx.font = FONT;
|
|
309
396
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
310
397
|
ctx.translate(st.w / 2, st.h / 2);
|
|
311
398
|
ctx.scale(textScale, textScale);
|
|
312
|
-
ctx.fillText(
|
|
399
|
+
ctx.fillText(TXT, 0, 0);
|
|
313
400
|
|
|
314
401
|
if (st.focused) {
|
|
315
402
|
ctx.setTransform(st.dpr, 0, 0, st.dpr, 0, 0);
|
|
@@ -324,7 +411,12 @@ export function MagneticButton({ maxPull = 15 } = {}) {
|
|
|
324
411
|
/**
|
|
325
412
|
* Shatter Button -- Click explodes into falling shards, reforms after 1.5s.
|
|
326
413
|
*/
|
|
327
|
-
export function ShatterButton(
|
|
414
|
+
export function ShatterButton(o = {}) {
|
|
415
|
+
const { seed = 42 } = o;
|
|
416
|
+
const P = resolveTheme(o, { accent: '#ffffff', dim: '#e2e2f0' });
|
|
417
|
+
const shardColor = (o.theme || o.colors) ? rgbaOf(P.accent, .5) : 'rgba(255,255,255,.5)';
|
|
418
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
419
|
+
const TXT = pickText(o, 'SHATTER');
|
|
328
420
|
const rng = new Random(seed);
|
|
329
421
|
// Fixed shard pool (live flag) -- no push/splice on the hot path.
|
|
330
422
|
const SHARDS = 32;
|
|
@@ -360,16 +452,16 @@ export function ShatterButton({ seed = 42 } = {}) {
|
|
|
360
452
|
ctx.fillStyle = st.active ? 'rgba(255,255,255,.15)' : 'rgba(255,255,255,.08)';
|
|
361
453
|
roundRect(ctx, 0, 0, st.w, st.h, 10);
|
|
362
454
|
ctx.fill();
|
|
363
|
-
ctx.fillStyle =
|
|
364
|
-
ctx.font =
|
|
455
|
+
ctx.fillStyle = P.dim;
|
|
456
|
+
ctx.font = FONT;
|
|
365
457
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
366
|
-
ctx.fillText(
|
|
458
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
367
459
|
|
|
368
460
|
if (st.focused) drawFocusRing(ctx, st.w, st.h, 10);
|
|
369
461
|
}
|
|
370
462
|
|
|
371
463
|
// Falling shards -- fixed pool
|
|
372
|
-
ctx.fillStyle =
|
|
464
|
+
ctx.fillStyle = shardColor;
|
|
373
465
|
for (let i = 0; i < SHARDS; i++) {
|
|
374
466
|
const s = shard[i];
|
|
375
467
|
if (!s.live) continue;
|
|
@@ -390,7 +482,15 @@ export function ShatterButton({ seed = 42 } = {}) {
|
|
|
390
482
|
/**
|
|
391
483
|
* Confetti Button -- 3D tumbling confetti burst from click point.
|
|
392
484
|
*/
|
|
393
|
-
export function ConfettiButton(
|
|
485
|
+
export function ConfettiButton(o = {}) {
|
|
486
|
+
const { seed = 42 } = o;
|
|
487
|
+
// `colors` is the confetti palette (legacy option, still honored); `theme`
|
|
488
|
+
// drives the crimson body and, absent colors, seeds the palette too.
|
|
489
|
+
const colors = o.colors ? o.colors
|
|
490
|
+
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark] : ['#6ee7b6', '#38bdf8', '#a78bfa', '#fbbf24', '#f43f5e']);
|
|
491
|
+
const bodyColor = (o.theme && o.theme.light) || '#DC143C';
|
|
492
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
493
|
+
const TXT = pickText(o, 'CONFETTI');
|
|
394
494
|
const rng = new Random(seed);
|
|
395
495
|
// Fixed confetti pool (live flag) -- no push/splice on the hot path.
|
|
396
496
|
const CONF = 80;
|
|
@@ -425,13 +525,13 @@ export function ConfettiButton({ seed = 42, colors = ['#6ee7b6', '#38bdf8', '#a7
|
|
|
425
525
|
ctx.translate(-st.w / 2, -st.h / 2);
|
|
426
526
|
|
|
427
527
|
// Button body
|
|
428
|
-
ctx.fillStyle =
|
|
528
|
+
ctx.fillStyle = bodyColor;
|
|
429
529
|
roundRect(ctx, 0, 0, st.w, st.h, 10);
|
|
430
530
|
ctx.fill();
|
|
431
531
|
ctx.fillStyle = '#fff';
|
|
432
|
-
ctx.font =
|
|
532
|
+
ctx.font = FONT;
|
|
433
533
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
434
|
-
ctx.fillText(
|
|
534
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
435
535
|
|
|
436
536
|
// Tumbling confetti -- fixed pool
|
|
437
537
|
for (let i = 0; i < CONF; i++) {
|
|
@@ -462,7 +562,11 @@ export function ConfettiButton({ seed = 42, colors = ['#6ee7b6', '#38bdf8', '#a7
|
|
|
462
562
|
/**
|
|
463
563
|
* Glitch Button -- RGB channel split on hover. Random slice displacement.
|
|
464
564
|
*/
|
|
465
|
-
export function GlitchButton(
|
|
565
|
+
export function GlitchButton(o = {}) {
|
|
566
|
+
const { seed = 42 } = o;
|
|
567
|
+
const P = resolveTheme(o, { accent: '#ff0055', accent2: '#00ffcc' });
|
|
568
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
569
|
+
const TXT = pickText(o, 'GLITCH');
|
|
466
570
|
const rng = new Random(seed);
|
|
467
571
|
let glitchIntensity = 0;
|
|
468
572
|
|
|
@@ -475,9 +579,9 @@ export function GlitchButton({ seed = 42 } = {}) {
|
|
|
475
579
|
ctx.fill();
|
|
476
580
|
ctx.globalAlpha = 1;
|
|
477
581
|
ctx.fillStyle = '#fff';
|
|
478
|
-
ctx.font =
|
|
582
|
+
ctx.font = FONT;
|
|
479
583
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
480
|
-
ctx.fillText(
|
|
584
|
+
ctx.fillText(TXT, st.w / 2 + ox, st.h / 2 + oy);
|
|
481
585
|
}
|
|
482
586
|
|
|
483
587
|
return {
|
|
@@ -489,8 +593,8 @@ export function GlitchButton({ seed = 42 } = {}) {
|
|
|
489
593
|
const off = rng.range(2, 6) * glitchIntensity;
|
|
490
594
|
// Alternate between split and normal for flicker
|
|
491
595
|
if (rng.next() > 0.4) {
|
|
492
|
-
drawBase(ctx, st, -off, 0,
|
|
493
|
-
drawBase(ctx, st, off, 0,
|
|
596
|
+
drawBase(ctx, st, -off, 0, P.accent, 0.6 * glitchIntensity);
|
|
597
|
+
drawBase(ctx, st, off, 0, P.accent2, 0.6 * glitchIntensity);
|
|
494
598
|
} else {
|
|
495
599
|
drawBase(ctx, st, 0, 0, 'rgba(255,255,255,.1)', 1);
|
|
496
600
|
}
|
|
@@ -513,7 +617,10 @@ export function GlitchButton({ seed = 42 } = {}) {
|
|
|
513
617
|
* Spark Slider -- Emits directional sparks based on drag velocity.
|
|
514
618
|
* Sparks fly opposite to drag direction with motion-blur stretch.
|
|
515
619
|
*/
|
|
516
|
-
export function SparkSlider(
|
|
620
|
+
export function SparkSlider(o = {}) {
|
|
621
|
+
const { seed = 42 } = o;
|
|
622
|
+
// `color` (legacy singular) seeds the default accent; theme/colors override it.
|
|
623
|
+
const P = resolveTheme(o, { accent: o.color || '#fbbf24', dim: '#9999b8' });
|
|
517
624
|
const rng = new Random(seed);
|
|
518
625
|
// Fixed spark pool (life <= 0 == dead) -- no push/splice on the hot path.
|
|
519
626
|
const SPARKS = 64;
|
|
@@ -552,7 +659,7 @@ export function SparkSlider({ seed = 42, color = '#fbbf24' } = {}) {
|
|
|
552
659
|
|
|
553
660
|
// Filled track
|
|
554
661
|
const tx = st.val * st.w;
|
|
555
|
-
ctx.fillStyle =
|
|
662
|
+
ctx.fillStyle = P.accent;
|
|
556
663
|
roundRect(ctx, 0, 12, tx, 4, 2);
|
|
557
664
|
ctx.fill();
|
|
558
665
|
|
|
@@ -562,12 +669,12 @@ export function SparkSlider({ seed = 42, color = '#fbbf24' } = {}) {
|
|
|
562
669
|
ctx.fill();
|
|
563
670
|
|
|
564
671
|
// Value label
|
|
565
|
-
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10,
|
|
672
|
+
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10, P.dim);
|
|
566
673
|
|
|
567
674
|
// Sparks -- fixed pool, themed color, alpha via globalAlpha
|
|
568
675
|
// (was `rgba(251,191,36,${life})` built per spark per frame).
|
|
569
676
|
ctx.globalCompositeOperation = 'screen';
|
|
570
|
-
ctx.fillStyle =
|
|
677
|
+
ctx.fillStyle = P.accent;
|
|
571
678
|
for (let i = 0; i < SPARKS; i++) {
|
|
572
679
|
const s = spark[i];
|
|
573
680
|
if (s.life <= 0) continue;
|
|
@@ -596,7 +703,9 @@ export function SparkSlider({ seed = 42, color = '#fbbf24' } = {}) {
|
|
|
596
703
|
* Cosmic Slider -- Thumb is a black hole that sucks in background dust.
|
|
597
704
|
* Particles respawn when consumed.
|
|
598
705
|
*/
|
|
599
|
-
export function CosmicSlider(
|
|
706
|
+
export function CosmicSlider(o = {}) {
|
|
707
|
+
const { seed = 42, dustCount = 80 } = o;
|
|
708
|
+
const P = resolveTheme(o, { accent: '#a78bfa', surface: '#000', dim: '#9999b8' });
|
|
600
709
|
const rng = new Random(seed);
|
|
601
710
|
const dust = [];
|
|
602
711
|
|
|
@@ -637,13 +746,13 @@ export function CosmicSlider({ seed = 42, dustCount = 80 } = {}) {
|
|
|
637
746
|
}
|
|
638
747
|
|
|
639
748
|
// Black hole thumb with glow
|
|
640
|
-
ctx.shadowBlur = 10; ctx.shadowColor =
|
|
641
|
-
ctx.fillStyle =
|
|
749
|
+
ctx.shadowBlur = 10; ctx.shadowColor = P.accent;
|
|
750
|
+
ctx.fillStyle = P.surface; ctx.strokeStyle = P.accent; ctx.lineWidth = 2;
|
|
642
751
|
ctx.beginPath(); ctx.arc(tx, 14, 12, 0, Math.PI * 2);
|
|
643
752
|
ctx.fill(); ctx.stroke();
|
|
644
753
|
ctx.shadowBlur = 0;
|
|
645
754
|
|
|
646
|
-
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10,
|
|
755
|
+
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10, P.dim);
|
|
647
756
|
|
|
648
757
|
if (st.focused) drawFocusRing(ctx, st.w, st.h, 4);
|
|
649
758
|
},
|
|
@@ -654,7 +763,8 @@ export function CosmicSlider({ seed = 42, dustCount = 80 } = {}) {
|
|
|
654
763
|
/**
|
|
655
764
|
* Laser Slider -- Energy beam traces the filled track. Pulsing plasma thumb.
|
|
656
765
|
*/
|
|
657
|
-
export function LaserSlider() {
|
|
766
|
+
export function LaserSlider(o = {}) {
|
|
767
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#9999b8' });
|
|
658
768
|
const REF = 100; // gradient reference width; scaled to tx at paint
|
|
659
769
|
let pulseTime = 0, beam = null;
|
|
660
770
|
|
|
@@ -665,7 +775,7 @@ export function LaserSlider() {
|
|
|
665
775
|
// no per-frame createLinearGradient.
|
|
666
776
|
beam = ctx.createLinearGradient(0, 0, REF, 0);
|
|
667
777
|
beam.addColorStop(0, 'transparent');
|
|
668
|
-
beam.addColorStop(0.8,
|
|
778
|
+
beam.addColorStop(0.8, P.accent);
|
|
669
779
|
beam.addColorStop(1, '#fff');
|
|
670
780
|
},
|
|
671
781
|
tick(ctx, dt, now, st) {
|
|
@@ -682,22 +792,22 @@ export function LaserSlider() {
|
|
|
682
792
|
if (tx > 2) {
|
|
683
793
|
ctx.save();
|
|
684
794
|
ctx.scale(tx / REF, 1);
|
|
685
|
-
ctx.fillStyle = beam ||
|
|
686
|
-
ctx.shadowBlur = 10; ctx.shadowColor =
|
|
795
|
+
ctx.fillStyle = beam || P.accent;
|
|
796
|
+
ctx.shadowBlur = 10; ctx.shadowColor = P.accent;
|
|
687
797
|
roundRect(ctx, 0, 12, REF, 4, 2);
|
|
688
798
|
ctx.fill();
|
|
689
799
|
ctx.restore();
|
|
690
800
|
}
|
|
691
801
|
|
|
692
802
|
// Plasma thumb (pulsing) -- keep the glow the beam left on
|
|
693
|
-
ctx.shadowBlur = 10; ctx.shadowColor =
|
|
803
|
+
ctx.shadowBlur = 10; ctx.shadowColor = P.accent;
|
|
694
804
|
ctx.fillStyle = '#fff';
|
|
695
805
|
ctx.beginPath();
|
|
696
806
|
ctx.arc(tx, 14, 8 + Math.sin(pulseTime) * 2, 0, Math.PI * 2);
|
|
697
807
|
ctx.fill();
|
|
698
808
|
ctx.shadowBlur = 0;
|
|
699
809
|
|
|
700
|
-
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10,
|
|
810
|
+
drawStateLabel(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10, P.dim);
|
|
701
811
|
|
|
702
812
|
if (st.focused) drawFocusRing(ctx, st.w, st.h, 4);
|
|
703
813
|
},
|
|
@@ -710,7 +820,11 @@ export function LaserSlider() {
|
|
|
710
820
|
// ===========================================================
|
|
711
821
|
|
|
712
822
|
/** 1. Pendulum Toggle -- The knob swings like a pendulum with overshoot. */
|
|
713
|
-
export function PendulumToggle() {
|
|
823
|
+
export function PendulumToggle(o = {}) {
|
|
824
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', dim: '#9999b8', dim2: '#8888aa' });
|
|
825
|
+
const themed = !!(o.theme || o.colors);
|
|
826
|
+
const onTint = themed ? rgbaOf(P.accent, .2) : 'rgba(110,231,182,.2)';
|
|
827
|
+
const strokeOn = themed ? rgbaOf(P.accent, .3) : 'rgba(110,231,182,.3)';
|
|
714
828
|
let angle = -0.4, velocity = 0;
|
|
715
829
|
const STIFFNESS = 12, DAMPING = 3.5;
|
|
716
830
|
return {
|
|
@@ -721,24 +835,26 @@ export function PendulumToggle() {
|
|
|
721
835
|
velocity *= (1 - DAMPING * dt);
|
|
722
836
|
angle += velocity * dt;
|
|
723
837
|
|
|
724
|
-
ctx.fillStyle = st.toggled ?
|
|
838
|
+
ctx.fillStyle = st.toggled ? onTint : 'rgba(255,255,255,.06)';
|
|
725
839
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2); ctx.fill();
|
|
726
840
|
|
|
727
841
|
const cx = st.w / 2, cy = -10, len = st.h / 2 + 14;
|
|
728
842
|
const bx = cx + Math.sin(angle) * len, by = cy + Math.cos(angle) * len;
|
|
729
843
|
|
|
730
|
-
ctx.strokeStyle = st.toggled ?
|
|
844
|
+
ctx.strokeStyle = st.toggled ? strokeOn : 'rgba(255,255,255,.08)';
|
|
731
845
|
ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(bx, by); ctx.stroke();
|
|
732
|
-
ctx.fillStyle = st.toggled ?
|
|
846
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
733
847
|
ctx.beginPath(); ctx.arc(bx, by, 12, 0, Math.PI * 2); ctx.fill();
|
|
734
|
-
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ?
|
|
848
|
+
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ? P.accent : P.dim2);
|
|
735
849
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
736
850
|
},
|
|
737
851
|
};
|
|
738
852
|
}
|
|
739
853
|
|
|
740
854
|
/** 2. Circuit Toggle -- Electricity flows through a circuit path when ON. */
|
|
741
|
-
export function CircuitToggle(
|
|
855
|
+
export function CircuitToggle(o = {}) {
|
|
856
|
+
const { seed = 42 } = o;
|
|
857
|
+
const P = resolveTheme(o, { accent: '#22d3ee', dim: '#9999b8', dim2: '#8888aa' });
|
|
742
858
|
const rng = new Random(seed);
|
|
743
859
|
// Fixed spark pool (life <= 0 == dead) -- no push/splice on the hot path.
|
|
744
860
|
const SPARKS = 16;
|
|
@@ -766,14 +882,14 @@ export function CircuitToggle({ seed = 42 } = {}) {
|
|
|
766
882
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2); ctx.fill();
|
|
767
883
|
|
|
768
884
|
// Circuit path
|
|
769
|
-
ctx.strokeStyle = st.toggled ?
|
|
885
|
+
ctx.strokeStyle = st.toggled ? P.accent : 'rgba(255,255,255,.06)';
|
|
770
886
|
ctx.lineWidth = 2; ctx.setLineDash(DASH_FLOW); ctx.lineDashOffset = -flowT * 16;
|
|
771
887
|
ctx.beginPath(); ctx.moveTo(8, st.h / 2); ctx.lineTo(st.w - 8, st.h / 2); ctx.stroke();
|
|
772
888
|
ctx.setLineDash(DASH_NONE); ctx.lineDashOffset = 0;
|
|
773
889
|
|
|
774
890
|
// Flowing sparks -- fixed pool, per-spark alpha via globalAlpha
|
|
775
891
|
if (st.toggled) {
|
|
776
|
-
ctx.fillStyle =
|
|
892
|
+
ctx.fillStyle = P.accent;
|
|
777
893
|
let alive = 0;
|
|
778
894
|
for (let i = 0; i < SPARKS; i++) {
|
|
779
895
|
const s = spark[i];
|
|
@@ -790,16 +906,21 @@ export function CircuitToggle({ seed = 42 } = {}) {
|
|
|
790
906
|
if (alive < 12) spawn(0);
|
|
791
907
|
}
|
|
792
908
|
|
|
793
|
-
ctx.fillStyle = st.toggled ?
|
|
909
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
794
910
|
ctx.beginPath(); ctx.arc(knobX, st.h / 2, 12, 0, Math.PI * 2); ctx.fill();
|
|
795
|
-
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ?
|
|
911
|
+
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ? P.accent : P.dim2);
|
|
796
912
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
797
913
|
},
|
|
798
914
|
};
|
|
799
915
|
}
|
|
800
916
|
|
|
801
917
|
/** 3. Lightning Toggle -- Electric arc between endpoints. */
|
|
802
|
-
export function LightningToggle(
|
|
918
|
+
export function LightningToggle(o = {}) {
|
|
919
|
+
const { seed = 42 } = o;
|
|
920
|
+
const P = resolveTheme(o, { accent: '#fbbf24', dim: '#9999b8', dim2: '#8888aa' });
|
|
921
|
+
const themed = !!(o.theme || o.colors);
|
|
922
|
+
const onTint = themed ? rgbaOf(P.accent, .15) : 'rgba(251,191,36,.15)';
|
|
923
|
+
const arcColor = themed ? rgbaOf(P.accent, .7) : 'rgba(251,191,36,.7)';
|
|
803
924
|
const rng = new Random(seed);
|
|
804
925
|
let knobX = 18, arcTime = 0;
|
|
805
926
|
function bolt(ctx, x1, y1, x2, y2, depth) {
|
|
@@ -814,27 +935,28 @@ export function LightningToggle({ seed = 42 } = {}) {
|
|
|
814
935
|
knobX = lerp(knobX, st.toggled ? st.w - 18 : 18, dt * 14);
|
|
815
936
|
arcTime += dt;
|
|
816
937
|
|
|
817
|
-
ctx.fillStyle = st.toggled ?
|
|
938
|
+
ctx.fillStyle = st.toggled ? onTint : 'rgba(255,255,255,.04)';
|
|
818
939
|
roundRect(ctx, 0, 0, st.w, st.h, st.h / 2); ctx.fill();
|
|
819
940
|
|
|
820
941
|
if (st.toggled && ((arcTime * 8) | 0) % 3 !== 0) {
|
|
821
942
|
rng.reset(((now / 80) | 0) * 7 + 1);
|
|
822
|
-
ctx.strokeStyle =
|
|
943
|
+
ctx.strokeStyle = arcColor; ctx.lineWidth = 1.5;
|
|
823
944
|
ctx.beginPath(); ctx.moveTo(8, st.h / 2); bolt(ctx, 8, st.h / 2, st.w - 8, st.h / 2, 3); ctx.stroke();
|
|
824
945
|
ctx.strokeStyle = 'rgba(255,255,255,.3)'; ctx.lineWidth = 0.5;
|
|
825
946
|
ctx.beginPath(); ctx.moveTo(8, st.h / 2); bolt(ctx, 8, st.h / 2, st.w - 8, st.h / 2, 3); ctx.stroke();
|
|
826
947
|
}
|
|
827
948
|
|
|
828
|
-
ctx.fillStyle = st.toggled ?
|
|
949
|
+
ctx.fillStyle = st.toggled ? P.accent : P.dim;
|
|
829
950
|
ctx.beginPath(); ctx.arc(knobX, st.h / 2, 12, 0, Math.PI * 2); ctx.fill();
|
|
830
|
-
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ?
|
|
951
|
+
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ? P.accent : P.dim2);
|
|
831
952
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
832
953
|
},
|
|
833
954
|
};
|
|
834
955
|
}
|
|
835
956
|
|
|
836
957
|
/** 4. DNA Toggle -- Double helix wraps around the track. */
|
|
837
|
-
export function DNAToggle() {
|
|
958
|
+
export function DNAToggle(o = {}) {
|
|
959
|
+
const P = resolveTheme(o, { accent: '#a78bfa', accent2: '#f472b6', accent3: '#c084fc', dim: '#9999b8', dim2: '#8888aa' });
|
|
838
960
|
let knobX = 18, phase = 0;
|
|
839
961
|
return {
|
|
840
962
|
tick(ctx, dt, now, st) {
|
|
@@ -852,9 +974,9 @@ export function DNAToggle() {
|
|
|
852
974
|
// Const strand colors, per-dot alpha via globalAlpha (was two
|
|
853
975
|
// `rgba(...,${...})` templates built per dot per frame).
|
|
854
976
|
if (st.toggled) {
|
|
855
|
-
ctx.fillStyle =
|
|
977
|
+
ctx.fillStyle = P.accent; ctx.globalAlpha = 0.3 + Math.sin(phase + t * 6) * 0.2;
|
|
856
978
|
ctx.beginPath(); ctx.arc(x, y1, 2, 0, Math.PI * 2); ctx.fill();
|
|
857
|
-
ctx.fillStyle =
|
|
979
|
+
ctx.fillStyle = P.accent2; ctx.globalAlpha = 0.3 + Math.cos(phase + t * 6) * 0.2;
|
|
858
980
|
ctx.beginPath(); ctx.arc(x, y2, 2, 0, Math.PI * 2); ctx.fill();
|
|
859
981
|
ctx.globalAlpha = 1;
|
|
860
982
|
} else {
|
|
@@ -869,9 +991,9 @@ export function DNAToggle() {
|
|
|
869
991
|
}
|
|
870
992
|
}
|
|
871
993
|
|
|
872
|
-
ctx.fillStyle = st.toggled ?
|
|
994
|
+
ctx.fillStyle = st.toggled ? P.accent3 : P.dim;
|
|
873
995
|
ctx.beginPath(); ctx.arc(knobX, cy, 12, 0, Math.PI * 2); ctx.fill();
|
|
874
|
-
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ?
|
|
996
|
+
label(ctx, st.toggled ? 'ON' : 'OFF', st.w / 2, st.h + 14, st.toggled ? P.accent3 : P.dim2);
|
|
875
997
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
876
998
|
},
|
|
877
999
|
};
|
|
@@ -883,7 +1005,12 @@ export function DNAToggle() {
|
|
|
883
1005
|
// ===========================================================
|
|
884
1006
|
|
|
885
1007
|
/** 5. Heartbeat Button -- Heart icon pumps with particle burst on click. */
|
|
886
|
-
export function HeartbeatButton(
|
|
1008
|
+
export function HeartbeatButton(o = {}) {
|
|
1009
|
+
const { seed = 42 } = o;
|
|
1010
|
+
const P = resolveTheme(o, { accent: '#DC143C', accent2: '#ff3c64' });
|
|
1011
|
+
const themed = !!(o.theme || o.colors);
|
|
1012
|
+
const tintOn = themed ? rgbaOf(P.accent, .2) : 'rgba(220,20,60,.2)';
|
|
1013
|
+
const tintOff = themed ? rgbaOf(P.accent, .08) : 'rgba(220,20,60,.08)';
|
|
887
1014
|
const rng = new Random(seed);
|
|
888
1015
|
// Fixed particle pool (life <= 0 == dead) -- no push/splice on the hot path.
|
|
889
1016
|
const PARTS = 32;
|
|
@@ -910,12 +1037,12 @@ export function HeartbeatButton({ seed = 42 } = {}) {
|
|
|
910
1037
|
beatPhase += dt * 5;
|
|
911
1038
|
const pulse = 1 + Math.sin(beatPhase) * 0.03;
|
|
912
1039
|
|
|
913
|
-
ctx.fillStyle = st.active ?
|
|
1040
|
+
ctx.fillStyle = st.active ? tintOn : tintOff;
|
|
914
1041
|
roundRect(ctx, 0, 0, st.w, st.h, 10); ctx.fill();
|
|
915
1042
|
|
|
916
1043
|
// Heart shape
|
|
917
1044
|
ctx.save(); ctx.translate(st.w / 2, st.h / 2); ctx.scale(scale * pulse, scale * pulse);
|
|
918
|
-
ctx.fillStyle =
|
|
1045
|
+
ctx.fillStyle = P.accent;
|
|
919
1046
|
ctx.beginPath();
|
|
920
1047
|
ctx.moveTo(0, 4); ctx.bezierCurveTo(-10, -6, -20, -2, -20, 4);
|
|
921
1048
|
ctx.bezierCurveTo(-20, 14, 0, 20, 0, 20);
|
|
@@ -925,7 +1052,7 @@ export function HeartbeatButton({ seed = 42 } = {}) {
|
|
|
925
1052
|
|
|
926
1053
|
// Particles -- fixed pool, const color, alpha via globalAlpha
|
|
927
1054
|
ctx.globalCompositeOperation = 'screen';
|
|
928
|
-
ctx.fillStyle =
|
|
1055
|
+
ctx.fillStyle = P.accent2;
|
|
929
1056
|
for (let i = 0; i < PARTS; i++) {
|
|
930
1057
|
const p = part[i];
|
|
931
1058
|
if (p.life <= 0) continue;
|
|
@@ -942,13 +1069,17 @@ export function HeartbeatButton({ seed = 42 } = {}) {
|
|
|
942
1069
|
}
|
|
943
1070
|
|
|
944
1071
|
/** 6. Breathing Button -- Inhale/exhale pulse with particle halo. */
|
|
945
|
-
export function BreathingButton() {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1072
|
+
export function BreathingButton(o = {}) {
|
|
1073
|
+
const P = resolveTheme(o, { accent: '#6ee7b6' });
|
|
1074
|
+
const RGB = (o.theme || o.colors) ? rgbTriplet(P.accent, '110,231,182') : '110,231,182';
|
|
1075
|
+
const FONT = pickFont(o, "600 12px 'Space Grotesk',sans-serif");
|
|
1076
|
+
const TXT = pickText(o, 'BREATHE');
|
|
1077
|
+
// Alpha-graded accent LUT (quantized alpha) built once, so the pulsing glow is
|
|
1078
|
+
// a const-string lookup instead of three `rgba(...,${...})` templates per
|
|
1079
|
+
// frame. shadowColor's alpha is not globalAlpha, so a LUT (not globalAlpha) is
|
|
1080
|
+
// the zero-alloc fit here. RGB defaults to the historical green for byte-parity.
|
|
950
1081
|
const GREEN = [];
|
|
951
|
-
for (let i = 0; i <= 64; i++) GREEN[i] = 'rgba(
|
|
1082
|
+
for (let i = 0; i <= 64; i++) GREEN[i] = 'rgba(' + RGB + ',' + (i / 64).toFixed(3) + ')';
|
|
952
1083
|
function greenA(a) { const i = a <= 0 ? 0 : a >= 1 ? 64 : (a * 64) | 0; return GREEN[i]; }
|
|
953
1084
|
let phase = 0;
|
|
954
1085
|
return {
|
|
@@ -968,16 +1099,20 @@ export function BreathingButton() {
|
|
|
968
1099
|
ctx.lineWidth = 1;
|
|
969
1100
|
ctx.beginPath(); ctx.arc(st.w / 2, st.h / 2, st.w / 2 + radius, 0, Math.PI * 2); ctx.stroke();
|
|
970
1101
|
|
|
971
|
-
ctx.fillStyle =
|
|
972
|
-
ctx.font =
|
|
973
|
-
ctx.fillText(
|
|
1102
|
+
ctx.fillStyle = P.accent;
|
|
1103
|
+
ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1104
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
974
1105
|
if (st.focused) focusRing(ctx, st.w, st.h, 10);
|
|
975
1106
|
},
|
|
976
1107
|
};
|
|
977
1108
|
}
|
|
978
1109
|
|
|
979
1110
|
/** 7. Ink Splash Button -- Calligraphy ink splatter on click. */
|
|
980
|
-
export function InkSplashButton(
|
|
1111
|
+
export function InkSplashButton(o = {}) {
|
|
1112
|
+
const { seed = 42 } = o;
|
|
1113
|
+
const P = resolveTheme(o, { accent: '#1e1e32', dim: '#e2e2f0' });
|
|
1114
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
1115
|
+
const TXT = pickText(o, 'INK');
|
|
981
1116
|
const rng = new Random(seed);
|
|
982
1117
|
// Fixed splat pool (life <= 0 == dead).
|
|
983
1118
|
const SPLATS = 32;
|
|
@@ -1005,12 +1140,12 @@ export function InkSplashButton({ seed = 42 } = {}) {
|
|
|
1005
1140
|
ctx.save(); ctx.translate(st.w / 2, st.h / 2); ctx.scale(pressScale, pressScale); ctx.translate(-st.w / 2, -st.h / 2);
|
|
1006
1141
|
|
|
1007
1142
|
ctx.fillStyle = 'rgba(255,255,255,.06)'; roundRect(ctx, 0, 0, st.w, st.h, 10); ctx.fill();
|
|
1008
|
-
ctx.fillStyle =
|
|
1009
|
-
ctx.fillText(
|
|
1143
|
+
ctx.fillStyle = P.dim; ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1144
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
1010
1145
|
ctx.restore();
|
|
1011
1146
|
|
|
1012
1147
|
// Splats -- fixed pool, const color, alpha via globalAlpha
|
|
1013
|
-
ctx.fillStyle =
|
|
1148
|
+
ctx.fillStyle = P.accent;
|
|
1014
1149
|
for (let i = 0; i < SPLATS; i++) {
|
|
1015
1150
|
const s = splat[i];
|
|
1016
1151
|
if (s.life <= 0) continue;
|
|
@@ -1026,7 +1161,12 @@ export function InkSplashButton({ seed = 42 } = {}) {
|
|
|
1026
1161
|
}
|
|
1027
1162
|
|
|
1028
1163
|
/** 8. Pixel Dissolve Button -- Hover breaks into floating pixels, reforms on leave. */
|
|
1029
|
-
export function PixelDissolveButton(
|
|
1164
|
+
export function PixelDissolveButton(o = {}) {
|
|
1165
|
+
const { seed = 42, cols = 16, rows = 5 } = o;
|
|
1166
|
+
const P = resolveTheme(o, { accent: '#a78bfa', dim: '#e2e2f0' });
|
|
1167
|
+
const hoverTint = (o.theme || o.colors) ? rgbaOf(P.accent, .4) : 'rgba(167,139,250,.4)';
|
|
1168
|
+
const FONT = pickFont(o, "600 12px 'Space Grotesk',sans-serif");
|
|
1169
|
+
const TXT = pickText(o, 'DISSOLVE');
|
|
1030
1170
|
const rng = new Random(seed);
|
|
1031
1171
|
const N = cols * rows;
|
|
1032
1172
|
const ox = new Float32Array(N), oy = new Float32Array(N);
|
|
@@ -1047,7 +1187,7 @@ export function PixelDissolveButton({ seed = 42, cols = 16, rows = 5 } = {}) {
|
|
|
1047
1187
|
tick(ctx, dt, now, st) {
|
|
1048
1188
|
dissolveT = lerp(dissolveT, st.hover ? 1 : 0, dt * 6);
|
|
1049
1189
|
|
|
1050
|
-
ctx.fillStyle = st.hover ?
|
|
1190
|
+
ctx.fillStyle = st.hover ? hoverTint : 'rgba(255,255,255,.12)';
|
|
1051
1191
|
const pw = st.w / cols, ph = st.h / rows;
|
|
1052
1192
|
|
|
1053
1193
|
for (let i = 0; i < N; i++) {
|
|
@@ -1061,10 +1201,10 @@ export function PixelDissolveButton({ seed = 42, cols = 16, rows = 5 } = {}) {
|
|
|
1061
1201
|
ctx.globalAlpha = 1;
|
|
1062
1202
|
|
|
1063
1203
|
if (dissolveT < 0.3) {
|
|
1064
|
-
ctx.fillStyle =
|
|
1204
|
+
ctx.fillStyle = P.dim; ctx.font = FONT;
|
|
1065
1205
|
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1066
1206
|
ctx.globalAlpha = 1 - dissolveT / 0.3;
|
|
1067
|
-
ctx.fillText(
|
|
1207
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
1068
1208
|
ctx.globalAlpha = 1;
|
|
1069
1209
|
}
|
|
1070
1210
|
if (st.focused) focusRing(ctx, st.w, st.h, 0);
|
|
@@ -1073,9 +1213,16 @@ export function PixelDissolveButton({ seed = 42, cols = 16, rows = 5 } = {}) {
|
|
|
1073
1213
|
}
|
|
1074
1214
|
|
|
1075
1215
|
/** 9. Firework Button -- Shoots fireworks upward on click. */
|
|
1076
|
-
export function FireworkButton(
|
|
1216
|
+
export function FireworkButton(o = {}) {
|
|
1217
|
+
const { seed = 42 } = o;
|
|
1218
|
+
// `colors` is the firework palette (like ConfettiButton); `theme` seeds it and
|
|
1219
|
+
// recolors the label.
|
|
1220
|
+
const colors = o.colors ? o.colors
|
|
1221
|
+
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark] : ['#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff', '#ff6bcb']);
|
|
1222
|
+
const labelColor = (o.theme && o.theme.light) || '#fbbf24';
|
|
1223
|
+
const FONT = pickFont(o, "600 13px 'Space Grotesk',sans-serif");
|
|
1224
|
+
const TXT = pickText(o, '\u{1F386} FIRE');
|
|
1077
1225
|
const rng = new Random(seed);
|
|
1078
|
-
const colors = ['#ff6b6b', '#ffd93d', '#6bcb77', '#4d96ff', '#ff6bcb'];
|
|
1079
1226
|
// Fixed pools (life <= 0 == dead) -- no push/splice on the hot path.
|
|
1080
1227
|
const ROCKETS = 16, SPARKS = 256;
|
|
1081
1228
|
const rocket = [];
|
|
@@ -1110,8 +1257,8 @@ export function FireworkButton({ seed = 42 } = {}) {
|
|
|
1110
1257
|
pressScale = lerp(pressScale, 1, dt * 10);
|
|
1111
1258
|
ctx.save(); ctx.translate(st.w / 2, st.h / 2); ctx.scale(pressScale, pressScale); ctx.translate(-st.w / 2, -st.h / 2);
|
|
1112
1259
|
ctx.fillStyle = 'rgba(255,255,255,.06)'; roundRect(ctx, 0, 0, st.w, st.h, 10); ctx.fill();
|
|
1113
|
-
ctx.fillStyle =
|
|
1114
|
-
ctx.fillText(
|
|
1260
|
+
ctx.fillStyle = labelColor; ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1261
|
+
ctx.fillText(TXT, st.w / 2, st.h / 2);
|
|
1115
1262
|
ctx.restore();
|
|
1116
1263
|
|
|
1117
1264
|
// Rockets -- fixed pool; on death, burst into sparks
|
|
@@ -1148,9 +1295,12 @@ export function FireworkButton({ seed = 42 } = {}) {
|
|
|
1148
1295
|
// ===========================================================
|
|
1149
1296
|
|
|
1150
1297
|
/** 10. Aurora Slider -- Northern lights colors flow along the filled track. */
|
|
1151
|
-
export function AuroraSlider() {
|
|
1298
|
+
export function AuroraSlider(o = {}) {
|
|
1152
1299
|
const REF = 100, PHASES = 12;
|
|
1153
|
-
|
|
1300
|
+
// `colors` is the aurora gradient palette; `theme` seeds it.
|
|
1301
|
+
const colors = o.colors ? o.colors
|
|
1302
|
+
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark, o.theme.light] : ['#6ee7b6', '#38bdf8', '#a78bfa', '#c084fc']);
|
|
1303
|
+
const glowColor = (o.theme && o.theme.light) || (o.colors && o.colors[0]) || '#6ee7b6';
|
|
1154
1304
|
let time = 0;
|
|
1155
1305
|
// The shimmer animated the gradient stops per frame. Pre-build one gradient
|
|
1156
1306
|
// per animation phase in init (all 0..REF) and cycle them by time; scale to
|
|
@@ -1163,7 +1313,7 @@ export function AuroraSlider() {
|
|
|
1163
1313
|
const g = ctx.createLinearGradient(0, 0, REF, 0);
|
|
1164
1314
|
for (let i = 0; i < 4; i++) {
|
|
1165
1315
|
const pos = clamp(i / 3 + Math.sin(t + i) * 0.15, 0, 1);
|
|
1166
|
-
g.addColorStop(pos, colors[i]);
|
|
1316
|
+
g.addColorStop(pos, colors[i % colors.length]);
|
|
1167
1317
|
}
|
|
1168
1318
|
beams[p] = g;
|
|
1169
1319
|
}
|
|
@@ -1176,7 +1326,7 @@ export function AuroraSlider() {
|
|
|
1176
1326
|
const beam = beams[((time * 2) | 0) % PHASES] || beams[0];
|
|
1177
1327
|
ctx.save();
|
|
1178
1328
|
ctx.scale(tx / REF, 1);
|
|
1179
|
-
ctx.fillStyle = beam; ctx.shadowBlur = 8; ctx.shadowColor =
|
|
1329
|
+
ctx.fillStyle = beam; ctx.shadowBlur = 8; ctx.shadowColor = glowColor;
|
|
1180
1330
|
roundRect(ctx, 0, 12, REF, 4, 2); ctx.fill();
|
|
1181
1331
|
ctx.restore();
|
|
1182
1332
|
ctx.shadowBlur = 0;
|
|
@@ -1189,7 +1339,9 @@ export function AuroraSlider() {
|
|
|
1189
1339
|
}
|
|
1190
1340
|
|
|
1191
1341
|
/** 11. Wave Slider -- Audio waveform visualization on the filled track. */
|
|
1192
|
-
export function WaveSlider(
|
|
1342
|
+
export function WaveSlider(o = {}) {
|
|
1343
|
+
const { seed = 42 } = o;
|
|
1344
|
+
const P = resolveTheme(o, { accent: '#f472b6' });
|
|
1193
1345
|
const rng = new Random(seed);
|
|
1194
1346
|
const bars = new Float32Array(40);
|
|
1195
1347
|
return {
|
|
@@ -1200,10 +1352,10 @@ export function WaveSlider({ seed = 42 } = {}) {
|
|
|
1200
1352
|
for (let i = 0; i < 40; i++) {
|
|
1201
1353
|
const bx = i * bw, active = bx < tx;
|
|
1202
1354
|
const h = bars[i] * 16 * (0.5 + Math.sin(now / 300 + i * 0.3) * 0.3);
|
|
1203
|
-
ctx.fillStyle = active ?
|
|
1355
|
+
ctx.fillStyle = active ? P.accent : 'rgba(255,255,255,.06)';
|
|
1204
1356
|
ctx.fillRect(bx + 1, 14 - h / 2, bw - 2, h);
|
|
1205
1357
|
}
|
|
1206
|
-
ctx.fillStyle =
|
|
1358
|
+
ctx.fillStyle = P.accent; ctx.beginPath(); ctx.arc(tx, 14, 8, 0, Math.PI * 2); ctx.fill();
|
|
1207
1359
|
label(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10);
|
|
1208
1360
|
if (st.focused) focusRing(ctx, st.w, st.h, 4);
|
|
1209
1361
|
},
|
|
@@ -1211,7 +1363,8 @@ export function WaveSlider({ seed = 42 } = {}) {
|
|
|
1211
1363
|
}
|
|
1212
1364
|
|
|
1213
1365
|
/** 12. Elastic Band Slider -- Track rubber-bands ahead of thumb, snaps back. */
|
|
1214
|
-
export function ElasticBandSlider() {
|
|
1366
|
+
export function ElasticBandSlider(o = {}) {
|
|
1367
|
+
const P = resolveTheme(o, { accent: '#fb923c' });
|
|
1215
1368
|
let leadX = 0;
|
|
1216
1369
|
return {
|
|
1217
1370
|
tick(ctx, dt, now, st) {
|
|
@@ -1222,12 +1375,12 @@ export function ElasticBandSlider() {
|
|
|
1222
1375
|
ctx.fillStyle = 'rgba(255,255,255,.06)'; roundRect(ctx, 0, 12, st.w, 4, 2); ctx.fill();
|
|
1223
1376
|
|
|
1224
1377
|
// Elastic band (curves toward target)
|
|
1225
|
-
ctx.strokeStyle =
|
|
1378
|
+
ctx.strokeStyle = P.accent; ctx.lineWidth = 3;
|
|
1226
1379
|
ctx.beginPath(); ctx.moveTo(0, 14);
|
|
1227
1380
|
ctx.quadraticCurveTo(leadX + stretch, 14 + stretch * 0.2, tx, 14);
|
|
1228
1381
|
ctx.stroke();
|
|
1229
1382
|
|
|
1230
|
-
ctx.fillStyle =
|
|
1383
|
+
ctx.fillStyle = P.accent; ctx.beginPath(); ctx.arc(tx, 14, 9, 0, Math.PI * 2); ctx.fill();
|
|
1231
1384
|
label(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 10);
|
|
1232
1385
|
if (st.focused) focusRing(ctx, st.w, st.h, 4);
|
|
1233
1386
|
},
|
|
@@ -1235,7 +1388,8 @@ export function ElasticBandSlider() {
|
|
|
1235
1388
|
}
|
|
1236
1389
|
|
|
1237
1390
|
/** 13. Gravity Slider -- Track sags under the weight of the thumb. */
|
|
1238
|
-
export function GravitySlider() {
|
|
1391
|
+
export function GravitySlider(o = {}) {
|
|
1392
|
+
const P = resolveTheme(o, { accent: '#c084fc' });
|
|
1239
1393
|
return {
|
|
1240
1394
|
tick(ctx, dt, now, st) {
|
|
1241
1395
|
const tx = st.val * st.w, sag = 8;
|
|
@@ -1247,13 +1401,13 @@ export function GravitySlider() {
|
|
|
1247
1401
|
ctx.stroke();
|
|
1248
1402
|
|
|
1249
1403
|
// Filled portion follows the sag
|
|
1250
|
-
ctx.strokeStyle =
|
|
1404
|
+
ctx.strokeStyle = P.accent; ctx.lineWidth = 3;
|
|
1251
1405
|
ctx.beginPath(); ctx.moveTo(0, 14);
|
|
1252
1406
|
const midSag = sag * (tx / st.w);
|
|
1253
1407
|
ctx.quadraticCurveTo(tx / 2, 14 + midSag, tx, 14 + sag * Math.sin(Math.PI * st.val));
|
|
1254
1408
|
ctx.stroke();
|
|
1255
1409
|
|
|
1256
|
-
ctx.fillStyle =
|
|
1410
|
+
ctx.fillStyle = P.accent; ctx.beginPath();
|
|
1257
1411
|
ctx.arc(tx, 14 + sag * Math.sin(Math.PI * st.val), 10, 0, Math.PI * 2); ctx.fill();
|
|
1258
1412
|
|
|
1259
1413
|
label(ctx, PCT[Math.round(st.val * 100)], st.w / 2, st.h + 14);
|
|
@@ -1268,12 +1422,13 @@ export function GravitySlider() {
|
|
|
1268
1422
|
// ===========================================================
|
|
1269
1423
|
|
|
1270
1424
|
/** 14. Orbit Loader -- Planets orbit a sun. Toggle starts/stops. */
|
|
1271
|
-
export function OrbitLoader() {
|
|
1425
|
+
export function OrbitLoader(o = {}) {
|
|
1426
|
+
const P = resolveTheme(o, { accent: '#38bdf8', accent2: '#6ee7b6', accent3: '#fbbf24', dim2: '#8888aa' });
|
|
1272
1427
|
let phase = 0;
|
|
1273
1428
|
const planets = [
|
|
1274
|
-
{ r: 14, speed: 2.0, size: 3, color:
|
|
1275
|
-
{ r: 22, speed: 1.2, size: 2.5, color:
|
|
1276
|
-
{ r: 30, speed: 0.7, size: 2, color:
|
|
1429
|
+
{ r: 14, speed: 2.0, size: 3, color: P.accent },
|
|
1430
|
+
{ r: 22, speed: 1.2, size: 2.5, color: P.accent2 },
|
|
1431
|
+
{ r: 30, speed: 0.7, size: 2, color: P.accent3 },
|
|
1277
1432
|
];
|
|
1278
1433
|
return {
|
|
1279
1434
|
tick(ctx, dt, now, st) {
|
|
@@ -1291,15 +1446,16 @@ export function OrbitLoader() {
|
|
|
1291
1446
|
}
|
|
1292
1447
|
|
|
1293
1448
|
// Sun
|
|
1294
|
-
ctx.fillStyle =
|
|
1295
|
-
label(ctx, st.toggled ? 'LOADING...' : 'IDLE', st.w / 2, st.h + 14, st.toggled ?
|
|
1449
|
+
ctx.fillStyle = P.accent3; ctx.beginPath(); ctx.arc(cx, cy, 5, 0, Math.PI * 2); ctx.fill();
|
|
1450
|
+
label(ctx, st.toggled ? 'LOADING...' : 'IDLE', st.w / 2, st.h + 14, st.toggled ? P.accent3 : P.dim2);
|
|
1296
1451
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
1297
1452
|
},
|
|
1298
1453
|
};
|
|
1299
1454
|
}
|
|
1300
1455
|
|
|
1301
1456
|
/** 15. Helix Loader -- DNA double helix spinning. */
|
|
1302
|
-
export function HelixLoader() {
|
|
1457
|
+
export function HelixLoader(o = {}) {
|
|
1458
|
+
const P = resolveTheme(o, { accent: '#a78bfa', accent2: '#f472b6', accent3: '#c084fc', dim2: '#8888aa' });
|
|
1303
1459
|
let phase = 0;
|
|
1304
1460
|
return {
|
|
1305
1461
|
tick(ctx, dt, now, st) {
|
|
@@ -1314,16 +1470,16 @@ export function HelixLoader() {
|
|
|
1314
1470
|
|
|
1315
1471
|
// Const strand colors, per-node alpha via globalAlpha (was three
|
|
1316
1472
|
// `rgba(...,${...})` templates per node per frame).
|
|
1317
|
-
ctx.fillStyle =
|
|
1473
|
+
ctx.fillStyle = P.accent; ctx.globalAlpha = 0.2 + depth * 0.6;
|
|
1318
1474
|
ctx.beginPath(); ctx.arc(x1, y1, 2 + depth, 0, Math.PI * 2); ctx.fill();
|
|
1319
|
-
ctx.fillStyle =
|
|
1475
|
+
ctx.fillStyle = P.accent2; ctx.globalAlpha = 0.2 + (1 - depth) * 0.6;
|
|
1320
1476
|
ctx.beginPath(); ctx.arc(x2, y2, 2 + (1 - depth), 0, Math.PI * 2); ctx.fill();
|
|
1321
1477
|
|
|
1322
1478
|
ctx.strokeStyle = '#ffffff'; ctx.globalAlpha = 0.03 + depth * 0.04;
|
|
1323
1479
|
ctx.lineWidth = 0.5; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
|
|
1324
1480
|
}
|
|
1325
1481
|
ctx.globalAlpha = 1;
|
|
1326
|
-
label(ctx, st.toggled ? 'PROCESSING' : 'IDLE', st.w / 2, st.h + 14, st.toggled ?
|
|
1482
|
+
label(ctx, st.toggled ? 'PROCESSING' : 'IDLE', st.w / 2, st.h + 14, st.toggled ? P.accent3 : P.dim2);
|
|
1327
1483
|
if (st.focused) focusRing(ctx, st.w, st.h, st.h / 2);
|
|
1328
1484
|
},
|
|
1329
1485
|
};
|
|
@@ -1335,7 +1491,8 @@ export function HelixLoader() {
|
|
|
1335
1491
|
// ===========================================================
|
|
1336
1492
|
|
|
1337
1493
|
/** 16. Ripple Checkbox -- Material-style ripple ring + morphing checkmark. */
|
|
1338
|
-
export function RippleCheck() {
|
|
1494
|
+
export function RippleCheck(o = {}) {
|
|
1495
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', surface: '#000', dim2: '#8888aa' });
|
|
1339
1496
|
// Fixed ripple pool (life <= 0 == dead) -- no push/splice on the hot path.
|
|
1340
1497
|
const RIPPLES = 8;
|
|
1341
1498
|
const ripR = new Float64Array(RIPPLES);
|
|
@@ -1353,11 +1510,11 @@ export function RippleCheck() {
|
|
|
1353
1510
|
const sz = Math.min(st.w, st.h), cx = sz / 2, cy = sz / 2;
|
|
1354
1511
|
|
|
1355
1512
|
// Box
|
|
1356
|
-
ctx.fillStyle = st.toggled ?
|
|
1513
|
+
ctx.fillStyle = st.toggled ? P.accent : 'rgba(255,255,255,.06)';
|
|
1357
1514
|
roundRect(ctx, 0, 0, sz, sz, 6); ctx.fill();
|
|
1358
1515
|
|
|
1359
1516
|
// Ripples -- fixed pool, const color, alpha via globalAlpha
|
|
1360
|
-
ctx.strokeStyle =
|
|
1517
|
+
ctx.strokeStyle = P.accent; ctx.lineWidth = 2;
|
|
1361
1518
|
for (let i = 0; i < RIPPLES; i++) {
|
|
1362
1519
|
if (ripLife[i] <= 0) continue;
|
|
1363
1520
|
ripR[i] += 50 * dt; ripLife[i] -= 2 * dt;
|
|
@@ -1369,7 +1526,7 @@ export function RippleCheck() {
|
|
|
1369
1526
|
|
|
1370
1527
|
// Checkmark (animated draw)
|
|
1371
1528
|
if (checkT > 0.01) {
|
|
1372
|
-
ctx.strokeStyle =
|
|
1529
|
+
ctx.strokeStyle = P.surface; ctx.lineWidth = 3; ctx.lineCap = 'round';
|
|
1373
1530
|
ctx.beginPath();
|
|
1374
1531
|
const p1 = clamp(checkT * 2, 0, 1); // First stroke
|
|
1375
1532
|
const p2 = clamp(checkT * 2 - 1, 0, 1); // Second stroke
|
|
@@ -1378,14 +1535,15 @@ export function RippleCheck() {
|
|
|
1378
1535
|
if (p2 > 0) ctx.lineTo(cx + 14 * p2, cy + 7 - 14 * p2);
|
|
1379
1536
|
ctx.stroke(); ctx.lineCap = 'butt';
|
|
1380
1537
|
}
|
|
1381
|
-
label(ctx, st.toggled ? '\u2713' : '\u25CB', sz / 2, sz + 12, st.toggled ?
|
|
1538
|
+
label(ctx, st.toggled ? '\u2713' : '\u25CB', sz / 2, sz + 12, st.toggled ? P.accent : P.dim2);
|
|
1382
1539
|
if (st.focused) focusRing(ctx, sz, sz, 6);
|
|
1383
1540
|
},
|
|
1384
1541
|
};
|
|
1385
1542
|
}
|
|
1386
1543
|
|
|
1387
1544
|
/** 17. Morph Checkbox -- X morphs into checkmark smoothly. */
|
|
1388
|
-
export function MorphCheck() {
|
|
1545
|
+
export function MorphCheck(o = {}) {
|
|
1546
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#9999b8', dim2: '#8888aa' });
|
|
1389
1547
|
let t = 0;
|
|
1390
1548
|
return {
|
|
1391
1549
|
tick(ctx, dt, now, st) {
|
|
@@ -1397,12 +1555,12 @@ export function MorphCheck() {
|
|
|
1397
1555
|
ctx.fillStyle = 'rgba(255,255,255,.06)';
|
|
1398
1556
|
roundRect(ctx, 0, 0, sz, sz, 6); ctx.fill();
|
|
1399
1557
|
} else {
|
|
1400
|
-
ctx.fillStyle =
|
|
1558
|
+
ctx.fillStyle = P.accent; ctx.globalAlpha = lerp(0, 0.2, t);
|
|
1401
1559
|
roundRect(ctx, 0, 0, sz, sz, 6); ctx.fill();
|
|
1402
1560
|
ctx.globalAlpha = 1;
|
|
1403
1561
|
}
|
|
1404
1562
|
|
|
1405
|
-
ctx.strokeStyle = t > 0.5 ?
|
|
1563
|
+
ctx.strokeStyle = t > 0.5 ? P.accent : P.dim; ctx.lineWidth = 2.5; ctx.lineCap = 'round';
|
|
1406
1564
|
|
|
1407
1565
|
// Morph: X -> checkmark by interpolating endpoints
|
|
1408
1566
|
const x1a = cx - 8, y1a = cy - 8; // X top-left
|
|
@@ -1425,19 +1583,101 @@ export function MorphCheck() {
|
|
|
1425
1583
|
ctx.stroke();
|
|
1426
1584
|
ctx.lineCap = 'butt';
|
|
1427
1585
|
|
|
1428
|
-
label(ctx, st.toggled ? '\u2713' : '\u2715', sz / 2, sz + 12, st.toggled ?
|
|
1586
|
+
label(ctx, st.toggled ? '\u2713' : '\u2715', sz / 2, sz + 12, st.toggled ? P.accent : P.dim2);
|
|
1429
1587
|
if (st.focused) focusRing(ctx, sz, sz, 6);
|
|
1430
1588
|
},
|
|
1431
1589
|
};
|
|
1432
1590
|
}
|
|
1433
1591
|
|
|
1592
|
+
/** Tick Draw (U4a CHECKBOX) -- the checkmark strokes itself on; a centred dash
|
|
1593
|
+
* when indeterminate; empty when off. Reads st.toggled + st.indeterminate. */
|
|
1594
|
+
export function TickDraw(o = {}) {
|
|
1595
|
+
const P = resolveTheme(o, { accent: '#34d399', surface: '#0a0a12', dim2: '#8888aa' });
|
|
1596
|
+
const boxOff = 'rgba(255,255,255,.06)';
|
|
1597
|
+
const FONT = pickFont(o, "500 9px 'JetBrains Mono',monospace");
|
|
1598
|
+
let drawT = 0; // 0..1 stroke progress of the check
|
|
1599
|
+
return {
|
|
1600
|
+
tick(ctx, dt, now, st) {
|
|
1601
|
+
// Indeterminate suppresses the check; a full check draws only when on.
|
|
1602
|
+
drawT = lerp(drawT, st.indeterminate ? 0 : (st.toggled ? 1 : 0), dt * 12);
|
|
1603
|
+
const sz = Math.min(st.w, st.h), cx = sz / 2, cy = sz / 2, r = 6;
|
|
1604
|
+
|
|
1605
|
+
ctx.fillStyle = (st.toggled || st.indeterminate) ? P.accent : boxOff;
|
|
1606
|
+
roundRect(ctx, 0, 0, sz, sz, r); ctx.fill();
|
|
1607
|
+
|
|
1608
|
+
ctx.strokeStyle = P.surface; ctx.lineWidth = 3; ctx.lineCap = 'round';
|
|
1609
|
+
if (st.indeterminate) {
|
|
1610
|
+
// Standard indeterminate glyph: one centred dash.
|
|
1611
|
+
ctx.beginPath(); ctx.moveTo(sz * 0.28, cy); ctx.lineTo(sz * 0.72, cy); ctx.stroke();
|
|
1612
|
+
} else if (drawT > 0.01) {
|
|
1613
|
+
const p1 = clamp(drawT * 2, 0, 1), p2 = clamp(drawT * 2 - 1, 0, 1);
|
|
1614
|
+
ctx.beginPath();
|
|
1615
|
+
ctx.moveTo(cx - 7, cy);
|
|
1616
|
+
ctx.lineTo(cx - 7 + 7 * p1, cy + 7 * p1);
|
|
1617
|
+
if (p2 > 0) ctx.lineTo(cx + 14 * p2, cy + 7 - 14 * p2);
|
|
1618
|
+
ctx.stroke();
|
|
1619
|
+
}
|
|
1620
|
+
ctx.lineCap = 'butt';
|
|
1621
|
+
|
|
1622
|
+
label(ctx, st.indeterminate ? '\u2212' : (st.toggled ? '\u2713' : '\u25cb'),
|
|
1623
|
+
cx, sz + 12, (st.toggled || st.indeterminate) ? P.accent : P.dim2, FONT);
|
|
1624
|
+
if (st.focused) focusRing(ctx, sz, sz, r);
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** Indeterminate Scan (U4a CHECKBOX) -- sweeps a scan line while indeterminate,
|
|
1630
|
+
* settles to a stroked check when toggled, empty when off. */
|
|
1631
|
+
export function IndeterminateScan(o = {}) {
|
|
1632
|
+
const P = resolveTheme(o, { accent: '#60a5fa', surface: '#0a0a12', dim2: '#8888aa' });
|
|
1633
|
+
const FONT = pickFont(o, "500 9px 'JetBrains Mono',monospace");
|
|
1634
|
+
let scan = 0; // 0..1 sweep phase
|
|
1635
|
+
let checkT = 0; // 0..1 check reveal
|
|
1636
|
+
return {
|
|
1637
|
+
tick(ctx, dt, now, st) {
|
|
1638
|
+
checkT = lerp(checkT, (!st.indeterminate && st.toggled) ? 1 : 0, dt * 12);
|
|
1639
|
+
const sz = Math.min(st.w, st.h), cx = sz / 2, cy = sz / 2, r = 6;
|
|
1640
|
+
|
|
1641
|
+
ctx.fillStyle = (st.toggled && !st.indeterminate) ? P.accent : 'rgba(255,255,255,.06)';
|
|
1642
|
+
roundRect(ctx, 0, 0, sz, sz, r); ctx.fill();
|
|
1643
|
+
|
|
1644
|
+
if (st.indeterminate) {
|
|
1645
|
+
// Scan line sweeping down, fading at the sweep ends (globalAlpha,
|
|
1646
|
+
// not a per-frame colour string).
|
|
1647
|
+
scan += dt * 1.6; if (scan > 1) scan -= 1;
|
|
1648
|
+
const y = 4 + scan * (sz - 8);
|
|
1649
|
+
ctx.strokeStyle = P.accent; ctx.lineWidth = 2; ctx.lineCap = 'round';
|
|
1650
|
+
ctx.globalAlpha = Math.sin(scan * Math.PI);
|
|
1651
|
+
ctx.beginPath(); ctx.moveTo(4, y); ctx.lineTo(sz - 4, y); ctx.stroke();
|
|
1652
|
+
ctx.globalAlpha = 1; ctx.lineCap = 'butt';
|
|
1653
|
+
} else if (checkT > 0.01) {
|
|
1654
|
+
const p1 = clamp(checkT * 2, 0, 1), p2 = clamp(checkT * 2 - 1, 0, 1);
|
|
1655
|
+
ctx.strokeStyle = P.surface; ctx.lineWidth = 3; ctx.lineCap = 'round';
|
|
1656
|
+
ctx.beginPath();
|
|
1657
|
+
ctx.moveTo(cx - 7, cy);
|
|
1658
|
+
ctx.lineTo(cx - 7 + 7 * p1, cy + 7 * p1);
|
|
1659
|
+
if (p2 > 0) ctx.lineTo(cx + 14 * p2, cy + 7 - 14 * p2);
|
|
1660
|
+
ctx.stroke(); ctx.lineCap = 'butt';
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
label(ctx, st.indeterminate ? '\u2212' : (st.toggled ? '\u2713' : '\u25cb'),
|
|
1664
|
+
cx, sz + 12, (st.toggled || st.indeterminate) ? P.accent : P.dim2, FONT);
|
|
1665
|
+
if (st.focused) focusRing(ctx, sz, sz, r);
|
|
1666
|
+
},
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1434
1670
|
|
|
1435
1671
|
// ===========================================================
|
|
1436
1672
|
// COUNTERS (use slider val as input)
|
|
1437
1673
|
// ===========================================================
|
|
1438
1674
|
|
|
1439
1675
|
/** 18. Flame Counter -- Number with rising heat particles driven by slider value. */
|
|
1440
|
-
export function FlameCounter(
|
|
1676
|
+
export function FlameCounter(o = {}) {
|
|
1677
|
+
const { seed = 42 } = o;
|
|
1678
|
+
const P = resolveTheme(o, { accent: '#ff6b6b', accent2: '#fbbf24', dim: '#9999b8' });
|
|
1679
|
+
const FONT = pickFont(o, "700 28px 'JetBrains Mono',monospace");
|
|
1680
|
+
const TXT = pickText(o, 'HEAT');
|
|
1441
1681
|
const rng = new Random(seed);
|
|
1442
1682
|
// Fixed ember pool + heat-color LUT (rgb quantized by value), so neither the
|
|
1443
1683
|
// pool nor the per-ember color allocates. The number string is rebuilt only
|
|
@@ -1480,18 +1720,22 @@ export function FlameCounter({ seed = 42 } = {}) {
|
|
|
1480
1720
|
ctx.globalCompositeOperation = 'source-over';
|
|
1481
1721
|
|
|
1482
1722
|
// Number
|
|
1483
|
-
ctx.fillStyle = st.val > 0.7 ?
|
|
1484
|
-
ctx.font =
|
|
1723
|
+
ctx.fillStyle = st.val > 0.7 ? P.accent : st.val > 0.3 ? P.accent2 : P.dim;
|
|
1724
|
+
ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1485
1725
|
ctx.fillText(numStr, st.w / 2, st.h / 2);
|
|
1486
1726
|
|
|
1487
|
-
label(ctx,
|
|
1727
|
+
label(ctx, TXT, st.w / 2, st.h + 12);
|
|
1488
1728
|
if (st.focused) focusRing(ctx, st.w, st.h, 10);
|
|
1489
1729
|
},
|
|
1490
1730
|
};
|
|
1491
1731
|
}
|
|
1492
1732
|
|
|
1493
1733
|
/** 19. Glitch Counter -- Number glitches and jitters as slider value increases. */
|
|
1494
|
-
export function GlitchCounter(
|
|
1734
|
+
export function GlitchCounter(o = {}) {
|
|
1735
|
+
const { seed = 42 } = o;
|
|
1736
|
+
const P = resolveTheme(o, { accent: '#ff0055', accent2: '#00ffcc', dim: '#e2e2f0' });
|
|
1737
|
+
const FONT = pickFont(o, "700 28px 'JetBrains Mono',monospace");
|
|
1738
|
+
const TXT = pickText(o, 'SIGNAL');
|
|
1495
1739
|
const rng = new Random(seed);
|
|
1496
1740
|
let numStr = '0', lastNum = -1;
|
|
1497
1741
|
return {
|
|
@@ -1515,22 +1759,22 @@ export function GlitchCounter({ seed = 42 } = {}) {
|
|
|
1515
1759
|
|
|
1516
1760
|
if (intensity > 0.3 && rng.next() > 0.6) {
|
|
1517
1761
|
ctx.globalCompositeOperation = 'screen';
|
|
1518
|
-
ctx.font =
|
|
1762
|
+
ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1519
1763
|
// RGB split -- const channel colors, shared alpha via globalAlpha
|
|
1520
1764
|
ctx.globalAlpha = intensity * 0.4;
|
|
1521
|
-
ctx.fillStyle =
|
|
1765
|
+
ctx.fillStyle = P.accent;
|
|
1522
1766
|
ctx.fillText(numStr, st.w / 2 + jx - 2, st.h / 2 + jy);
|
|
1523
|
-
ctx.fillStyle =
|
|
1767
|
+
ctx.fillStyle = P.accent2;
|
|
1524
1768
|
ctx.fillText(numStr, st.w / 2 + jx + 2, st.h / 2 + jy);
|
|
1525
1769
|
ctx.globalAlpha = 1;
|
|
1526
1770
|
ctx.globalCompositeOperation = 'source-over';
|
|
1527
1771
|
}
|
|
1528
1772
|
|
|
1529
|
-
ctx.fillStyle =
|
|
1530
|
-
ctx.font =
|
|
1773
|
+
ctx.fillStyle = P.dim;
|
|
1774
|
+
ctx.font = FONT; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
|
1531
1775
|
ctx.fillText(numStr, st.w / 2 + jx, st.h / 2 + jy);
|
|
1532
1776
|
|
|
1533
|
-
label(ctx,
|
|
1777
|
+
label(ctx, TXT, st.w / 2, st.h + 12);
|
|
1534
1778
|
if (st.focused) focusRing(ctx, st.w, st.h, 10);
|
|
1535
1779
|
},
|
|
1536
1780
|
};
|
|
@@ -1542,7 +1786,9 @@ export function GlitchCounter({ seed = 42 } = {}) {
|
|
|
1542
1786
|
// ===========================================================
|
|
1543
1787
|
|
|
1544
1788
|
/** 20. Bubble Rating -- 5 bubbles inflate based on slider position. Click pops them. */
|
|
1545
|
-
export function BubbleRating(
|
|
1789
|
+
export function BubbleRating(o = {}) {
|
|
1790
|
+
const { seed = 42 } = o;
|
|
1791
|
+
const P = resolveTheme(o, { accent: '#38bdf8', dim: '#9999b8' });
|
|
1546
1792
|
const rng = new Random(seed);
|
|
1547
1793
|
const R5 = ['0 / 5', '1 / 5', '2 / 5', '3 / 5', '4 / 5', '5 / 5']; // const labels
|
|
1548
1794
|
const POPS = 32;
|
|
@@ -1575,11 +1821,11 @@ export function BubbleRating({ seed = 42 } = {}) {
|
|
|
1575
1821
|
const cx = gap * i + gap / 2, cy = st.h / 2;
|
|
1576
1822
|
|
|
1577
1823
|
// Bubble -- const color, active alpha via globalAlpha
|
|
1578
|
-
if (active) { ctx.fillStyle =
|
|
1824
|
+
if (active) { ctx.fillStyle = P.accent; ctx.globalAlpha = 0.3 + sizes[i] / 20; }
|
|
1579
1825
|
else { ctx.fillStyle = 'rgba(255,255,255,.05)'; ctx.globalAlpha = 1; }
|
|
1580
1826
|
ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0, Math.PI * 2); ctx.fill();
|
|
1581
1827
|
ctx.globalAlpha = 1;
|
|
1582
|
-
ctx.strokeStyle = active ?
|
|
1828
|
+
ctx.strokeStyle = active ? P.accent : 'rgba(255,255,255,.08)';
|
|
1583
1829
|
ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(cx, cy, sizes[i], 0, Math.PI * 2); ctx.stroke();
|
|
1584
1830
|
|
|
1585
1831
|
// Highlight
|
|
@@ -1590,7 +1836,7 @@ export function BubbleRating({ seed = 42 } = {}) {
|
|
|
1590
1836
|
}
|
|
1591
1837
|
|
|
1592
1838
|
// Pop particles -- fixed pool, const color, alpha via globalAlpha
|
|
1593
|
-
ctx.fillStyle =
|
|
1839
|
+
ctx.fillStyle = P.accent;
|
|
1594
1840
|
for (let i = 0; i < POPS; i++) {
|
|
1595
1841
|
const p = pop[i];
|
|
1596
1842
|
if (p.life <= 0) continue;
|
|
@@ -1601,7 +1847,7 @@ export function BubbleRating({ seed = 42 } = {}) {
|
|
|
1601
1847
|
}
|
|
1602
1848
|
ctx.globalAlpha = 1;
|
|
1603
1849
|
|
|
1604
|
-
label(ctx, R5[rating], st.w / 2, st.h + 12, rating >= 4 ?
|
|
1850
|
+
label(ctx, R5[rating], st.w / 2, st.h + 12, rating >= 4 ? P.accent : P.dim);
|
|
1605
1851
|
if (st.focused) focusRing(ctx, st.w, st.h, 4);
|
|
1606
1852
|
},
|
|
1607
1853
|
onDrag(val) {
|
|
@@ -1620,14 +1866,18 @@ export function BubbleRating({ seed = 42 } = {}) {
|
|
|
1620
1866
|
// ===========================================================
|
|
1621
1867
|
|
|
1622
1868
|
/** 1. Volume Knob -- Rotary dial with tick marks and arc indicator. */
|
|
1623
|
-
export function VolumeKnob() {
|
|
1869
|
+
export function VolumeKnob(o = {}) {
|
|
1870
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#38bdf8', dim: '#e2e2f0' });
|
|
1871
|
+
const tickOn = (o.theme || o.colors) ? rgbaOf(P.accent, .5) : 'rgba(110,231,182,.5)';
|
|
1872
|
+
const FONT = pickFont(o, "700 14px 'JetBrains Mono',monospace");
|
|
1873
|
+
const TXT = pickText(o, 'VOLUME');
|
|
1624
1874
|
let displayVal = 0, grad = null;
|
|
1625
1875
|
let numStr = '0', lastNum = -1;
|
|
1626
1876
|
return {
|
|
1627
1877
|
init(c, w, h) {
|
|
1628
1878
|
// Value-arc gradient depends only on w/h (fixed per mount) -> build once.
|
|
1629
1879
|
grad = c.createLinearGradient(0, h, w, 0);
|
|
1630
|
-
grad.addColorStop(0,
|
|
1880
|
+
grad.addColorStop(0, P.accent); grad.addColorStop(1, P.accent2);
|
|
1631
1881
|
},
|
|
1632
1882
|
tick(c,dt,now,st) {
|
|
1633
1883
|
displayVal = lerp(displayVal, st.val, dt * 12);
|
|
@@ -1647,7 +1897,7 @@ export function VolumeKnob() {
|
|
|
1647
1897
|
for(let i=0;i<=10;i++){
|
|
1648
1898
|
const a=startA+i/10*range;
|
|
1649
1899
|
const inner=R-6, outer=R+2;
|
|
1650
|
-
c.strokeStyle=i/10<=displayVal?
|
|
1900
|
+
c.strokeStyle=i/10<=displayVal?tickOn:'rgba(255,255,255,.08)';
|
|
1651
1901
|
c.lineWidth=1; c.beginPath();
|
|
1652
1902
|
c.moveTo(cx+Math.cos(a)*inner,cy+Math.sin(a)*inner);
|
|
1653
1903
|
c.lineTo(cx+Math.cos(a)*outer,cy+Math.sin(a)*outer);
|
|
@@ -1665,18 +1915,19 @@ export function VolumeKnob() {
|
|
|
1665
1915
|
// Value text (rebuilt only when the integer changes)
|
|
1666
1916
|
const v = Math.round(displayVal * 100);
|
|
1667
1917
|
if (v !== lastNum) { lastNum = v; numStr = String(v); }
|
|
1668
|
-
c.fillStyle=
|
|
1918
|
+
c.fillStyle=P.dim; c.font=FONT; c.textAlign='center'; c.textBaseline='middle';
|
|
1669
1919
|
c.fillText(numStr,cx,cy);
|
|
1670
|
-
lbl(c,
|
|
1920
|
+
lbl(c,TXT,cx,st.h+10);
|
|
1671
1921
|
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
1672
1922
|
},
|
|
1673
1923
|
};
|
|
1674
1924
|
}
|
|
1675
1925
|
|
|
1676
1926
|
/** 2. Compass Knob -- Needle points based on slider value (0=N, 0.5=S, 1=N). */
|
|
1677
|
-
export function CompassKnob() {
|
|
1927
|
+
export function CompassKnob(o = {}) {
|
|
1928
|
+
const P = resolveTheme(o, { accent: '#ff6b6b', dim: '#9999b8', surface: '#333' });
|
|
1678
1929
|
const dirs = ['N', 'E', 'S', 'W']; // hoisted out of tick
|
|
1679
|
-
const cols = [
|
|
1930
|
+
const cols = [P.accent, P.dim, P.dim, P.dim]; // (were per-frame arrays)
|
|
1680
1931
|
let needleA = 0;
|
|
1681
1932
|
let degStr = '0\u00B0', lastDeg = -1;
|
|
1682
1933
|
return {
|
|
@@ -1698,14 +1949,14 @@ export function CompassKnob() {
|
|
|
1698
1949
|
|
|
1699
1950
|
// Needle
|
|
1700
1951
|
c.save(); c.translate(cx,cy); c.rotate(needleA-Math.PI/2);
|
|
1701
|
-
c.fillStyle=
|
|
1952
|
+
c.fillStyle=P.accent;
|
|
1702
1953
|
c.beginPath(); c.moveTo(0,-R+18); c.lineTo(-4,4); c.lineTo(4,4); c.closePath(); c.fill();
|
|
1703
1954
|
c.fillStyle='rgba(255,255,255,.15)';
|
|
1704
1955
|
c.beginPath(); c.moveTo(0,R-18); c.lineTo(-4,-4); c.lineTo(4,-4); c.closePath(); c.fill();
|
|
1705
1956
|
c.restore();
|
|
1706
1957
|
|
|
1707
1958
|
// Center pin
|
|
1708
|
-
c.fillStyle=
|
|
1959
|
+
c.fillStyle=P.surface; c.beginPath(); c.arc(cx,cy,4,0,PI2); c.fill();
|
|
1709
1960
|
c.strokeStyle='rgba(255,255,255,.1)'; c.lineWidth=1; c.beginPath(); c.arc(cx,cy,4,0,PI2); c.stroke();
|
|
1710
1961
|
|
|
1711
1962
|
const deg = Math.round(st.val * 360);
|
|
@@ -1722,7 +1973,11 @@ export function CompassKnob() {
|
|
|
1722
1973
|
// ===========================================================
|
|
1723
1974
|
|
|
1724
1975
|
/** 3. Ring Progress -- Circular progress with animated fill and particles at the tip. */
|
|
1725
|
-
export function RingProgress(
|
|
1976
|
+
export function RingProgress(o = {}) {
|
|
1977
|
+
const {seed=42}=o;
|
|
1978
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#a78bfa', dim: '#e2e2f0' });
|
|
1979
|
+
const tail = (o.theme || o.colors) ? rgbaOf(P.accent, .2) : 'rgba(110,231,182,.2)';
|
|
1980
|
+
const FONT = pickFont(o, "700 16px 'JetBrains Mono',monospace");
|
|
1726
1981
|
const rng=new Random(seed);
|
|
1727
1982
|
// Fixed spark pool + a bounded set of conic gradients (one per progress
|
|
1728
1983
|
// level) built in init, so the green stop still tracks the arc tip with zero
|
|
@@ -1743,9 +1998,9 @@ export function RingProgress({seed=42}={}) {
|
|
|
1743
1998
|
init(c, w, h) {
|
|
1744
1999
|
for (let p = 0; p < GPHASES; p++) {
|
|
1745
2000
|
const g = c.createConicGradient(-Math.PI / 2, w / 2, h / 2);
|
|
1746
|
-
g.addColorStop(0,
|
|
1747
|
-
g.addColorStop((p / (GPHASES - 1)) * 0.95,
|
|
1748
|
-
g.addColorStop(1,
|
|
2001
|
+
g.addColorStop(0, P.accent2);
|
|
2002
|
+
g.addColorStop((p / (GPHASES - 1)) * 0.95, P.accent);
|
|
2003
|
+
g.addColorStop(1, tail);
|
|
1749
2004
|
grads[p] = g;
|
|
1750
2005
|
}
|
|
1751
2006
|
},
|
|
@@ -1767,7 +2022,7 @@ export function RingProgress({seed=42}={}) {
|
|
|
1767
2022
|
const tx=cx+Math.cos(ea)*R,ty=cy+Math.sin(ea)*R;
|
|
1768
2023
|
spawn(tx, ty);
|
|
1769
2024
|
}
|
|
1770
|
-
c.fillStyle =
|
|
2025
|
+
c.fillStyle = P.accent;
|
|
1771
2026
|
for(let i=0;i<SPARKS;i++){
|
|
1772
2027
|
const s=spark[i];
|
|
1773
2028
|
if(s.life<=0)continue;
|
|
@@ -1779,7 +2034,7 @@ export function RingProgress({seed=42}={}) {
|
|
|
1779
2034
|
c.globalAlpha=1;
|
|
1780
2035
|
|
|
1781
2036
|
// Percentage
|
|
1782
|
-
c.fillStyle=
|
|
2037
|
+
c.fillStyle=P.dim;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
1783
2038
|
c.fillText(PCT[Math.round(displayVal*100)],cx,cy);
|
|
1784
2039
|
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
1785
2040
|
},
|
|
@@ -1787,7 +2042,9 @@ export function RingProgress({seed=42}={}) {
|
|
|
1787
2042
|
}
|
|
1788
2043
|
|
|
1789
2044
|
/** 4. Battery Gauge -- Battery icon that fills and changes color. */
|
|
1790
|
-
export function BatteryGauge() {
|
|
2045
|
+
export function BatteryGauge(o = {}) {
|
|
2046
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#fbbf24', accent3: '#ff6b6b', dim: '#e2e2f0' });
|
|
2047
|
+
const FONT = pickFont(o, "600 10px 'JetBrains Mono',monospace");
|
|
1791
2048
|
let displayVal=0;
|
|
1792
2049
|
return {
|
|
1793
2050
|
tick(c,dt,now,st) {
|
|
@@ -1803,7 +2060,7 @@ export function BatteryGauge() {
|
|
|
1803
2060
|
|
|
1804
2061
|
// Fill
|
|
1805
2062
|
const fillW=Math.max(0,(bw-6)*displayVal);
|
|
1806
|
-
const col=displayVal<.2?
|
|
2063
|
+
const col=displayVal<.2?P.accent3:displayVal<.5?P.accent2:P.accent;
|
|
1807
2064
|
c.fillStyle=col;
|
|
1808
2065
|
rr(c,bx+3,by+3,fillW,bh-6,2);c.fill();
|
|
1809
2066
|
|
|
@@ -1820,7 +2077,7 @@ export function BatteryGauge() {
|
|
|
1820
2077
|
c.fillStyle='rgba(255,80,80,.15)';rr(c,bx,by,bw,bh,r);c.fill();
|
|
1821
2078
|
}
|
|
1822
2079
|
|
|
1823
|
-
c.fillStyle=
|
|
2080
|
+
c.fillStyle=P.dim;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
1824
2081
|
c.fillText(PCT[Math.round(displayVal*100)],bx+bw/2,by+bh/2);
|
|
1825
2082
|
if(st.focused)fr(c,st.w+4,st.h,r);
|
|
1826
2083
|
},
|
|
@@ -1828,7 +2085,8 @@ export function BatteryGauge() {
|
|
|
1828
2085
|
}
|
|
1829
2086
|
|
|
1830
2087
|
/** 5. Signal Meter -- WiFi-style signal bars. */
|
|
1831
|
-
export function SignalMeter() {
|
|
2088
|
+
export function SignalMeter(o = {}) {
|
|
2089
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#fbbf24', accent3: '#ff6b6b', dim: '#9999b8' });
|
|
1832
2090
|
let bars=[0,0,0,0,0];
|
|
1833
2091
|
return {
|
|
1834
2092
|
tick(c,dt,now,st) {
|
|
@@ -1844,10 +2102,10 @@ export function SignalMeter() {
|
|
|
1844
2102
|
const maxH=8+i*6, h=maxH*bars[i]+2;
|
|
1845
2103
|
const x=ox+i*(bw+gap), y=st.h-6-h;
|
|
1846
2104
|
const active=bars[i]>.5;
|
|
1847
|
-
c.fillStyle=active?(st.val>.6?
|
|
2105
|
+
c.fillStyle=active?(st.val>.6?P.accent:st.val>.3?P.accent2:P.accent3):'rgba(255,255,255,.06)';
|
|
1848
2106
|
rr(c,x,y,bw,h,2);c.fill();
|
|
1849
2107
|
}
|
|
1850
|
-
lbl(c,level+'/5',st.w/2,st.h+10,st.val>.6?
|
|
2108
|
+
lbl(c,level+'/5',st.w/2,st.h+10,st.val>.6?P.accent:P.dim);
|
|
1851
2109
|
if(st.focused)fr(c,st.w,st.h,4);
|
|
1852
2110
|
},
|
|
1853
2111
|
};
|
|
@@ -1858,8 +2116,62 @@ export function SignalMeter() {
|
|
|
1858
2116
|
// CONTROLS
|
|
1859
2117
|
// ===========================================================
|
|
1860
2118
|
|
|
2119
|
+
/** Liquid Fill (U4a PROGRESS) -- a container that fills with a waving liquid to
|
|
2120
|
+
* the progress value, with a percentage readout. Non-interactive. */
|
|
2121
|
+
export function LiquidFill(o = {}) {
|
|
2122
|
+
const P = resolveTheme(o, { accent: '#22d3ee', accent2: '#0ea5e9', dim: '#e2e2f0' });
|
|
2123
|
+
const FONT = pickFont(o, "700 14px 'JetBrains Mono',monospace");
|
|
2124
|
+
let displayVal = 0, phase = 0, liquid = null;
|
|
2125
|
+
return {
|
|
2126
|
+
init(c, w, h) {
|
|
2127
|
+
// Vertical liquid gradient, built ONCE (cold): crest -> base.
|
|
2128
|
+
const g = c.createLinearGradient(0, 0, 0, h);
|
|
2129
|
+
g.addColorStop(0, P.accent);
|
|
2130
|
+
g.addColorStop(1, P.accent2);
|
|
2131
|
+
liquid = g;
|
|
2132
|
+
},
|
|
2133
|
+
tick(c, dt, now, st) {
|
|
2134
|
+
displayVal = lerp(displayVal, st.val, dt * 5);
|
|
2135
|
+
phase += dt * 3;
|
|
2136
|
+
const w = st.w, h = st.h, r = Math.min(12, h / 2);
|
|
2137
|
+
|
|
2138
|
+
// Container outline
|
|
2139
|
+
c.strokeStyle = 'rgba(255,255,255,.14)'; c.lineWidth = 2;
|
|
2140
|
+
roundRect(c, 1, 1, w - 2, h - 2, r); c.stroke();
|
|
2141
|
+
|
|
2142
|
+
// Liquid body, clipped to the container, with a sine surface. Const
|
|
2143
|
+
// gradient fill + fixed-step sampling -> zero per-frame allocation.
|
|
2144
|
+
c.save();
|
|
2145
|
+
roundRect(c, 1, 1, w - 2, h - 2, r); c.clip();
|
|
2146
|
+
const surface = h - displayVal * h;
|
|
2147
|
+
const amp = displayVal > 0.01 && displayVal < 0.99 ? 3 : 0;
|
|
2148
|
+
c.fillStyle = liquid || P.accent;
|
|
2149
|
+
c.beginPath();
|
|
2150
|
+
c.moveTo(0, h);
|
|
2151
|
+
c.lineTo(0, surface);
|
|
2152
|
+
for (let x = 0; x <= w; x += 6) {
|
|
2153
|
+
c.lineTo(x, surface + Math.sin(x * 0.15 + phase) * amp);
|
|
2154
|
+
}
|
|
2155
|
+
c.lineTo(w, h);
|
|
2156
|
+
c.closePath();
|
|
2157
|
+
c.fill();
|
|
2158
|
+
c.restore();
|
|
2159
|
+
|
|
2160
|
+
// Percentage readout (PCT LUT -- no per-frame string build)
|
|
2161
|
+
c.fillStyle = P.dim; c.font = FONT; c.textAlign = 'center'; c.textBaseline = 'middle';
|
|
2162
|
+
c.fillText(PCT[Math.round(displayVal * 100)], w / 2, h / 2);
|
|
2163
|
+
if (st.focused) fr(c, w, h, r);
|
|
2164
|
+
},
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1861
2168
|
/** 6. Pill Tabs -- 3 segmented tabs with sliding indicator. */
|
|
1862
|
-
export function PillTabs() {
|
|
2169
|
+
export function PillTabs(o = {}) {
|
|
2170
|
+
const P = resolveTheme(o, { accent: '#c4b5fd', accent2: '#a78bfa', dim: '#9999b8' });
|
|
2171
|
+
const themed = !!(o.theme || o.colors);
|
|
2172
|
+
const indFill = themed ? rgbaOf(P.accent2, .12) : 'rgba(167,139,250,.12)';
|
|
2173
|
+
const indStroke = themed ? rgbaOf(P.accent2, .25) : 'rgba(167,139,250,.25)';
|
|
2174
|
+
const FONT = pickFont(o, "600 11px 'Space Grotesk',sans-serif");
|
|
1863
2175
|
let indicatorX=0, indicatorW=0, selected=0;
|
|
1864
2176
|
const labels=['Alpha','Beta','Gamma'];
|
|
1865
2177
|
return {
|
|
@@ -1877,13 +2189,13 @@ export function PillTabs() {
|
|
|
1877
2189
|
c.fillStyle='rgba(255,255,255,.03)';rr(c,0,0,st.w,st.h,st.h/2);c.fill();
|
|
1878
2190
|
|
|
1879
2191
|
// Indicator
|
|
1880
|
-
c.fillStyle=
|
|
2192
|
+
c.fillStyle=indFill;c.strokeStyle=indStroke;c.lineWidth=1;
|
|
1881
2193
|
rr(c,indicatorX+2,2,indicatorW-4,st.h-4,st.h/2-2);c.fill();c.stroke();
|
|
1882
2194
|
|
|
1883
2195
|
// Labels
|
|
1884
|
-
c.font=
|
|
2196
|
+
c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
1885
2197
|
for(let i=0;i<3;i++){
|
|
1886
|
-
c.fillStyle=i===selected?
|
|
2198
|
+
c.fillStyle=i===selected?P.accent:P.dim;
|
|
1887
2199
|
c.fillText(labels[i],tw*i+tw/2,st.h/2);
|
|
1888
2200
|
}
|
|
1889
2201
|
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
@@ -1892,7 +2204,12 @@ export function PillTabs() {
|
|
|
1892
2204
|
}
|
|
1893
2205
|
|
|
1894
2206
|
/** 7. Stepper -- +/- buttons with spring counter. */
|
|
1895
|
-
export function Stepper() {
|
|
2207
|
+
export function Stepper(o = {}) {
|
|
2208
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#ff6b6b', dim: '#e2e2f0' });
|
|
2209
|
+
const themed = !!(o.theme || o.colors);
|
|
2210
|
+
const minusFlash = themed ? rgbaOf(P.accent2, .15) : 'rgba(255,100,100,.15)';
|
|
2211
|
+
const plusFlash = themed ? rgbaOf(P.accent, .15) : 'rgba(110,231,182,.15)';
|
|
2212
|
+
const FONT = pickFont(o, "700 18px 'JetBrains Mono',monospace");
|
|
1896
2213
|
let count=0,displayCount=0,flashDir=0,flashTimer=0;
|
|
1897
2214
|
return {
|
|
1898
2215
|
onClick(x,y,st) {
|
|
@@ -1909,18 +2226,18 @@ export function Stepper() {
|
|
|
1909
2226
|
|
|
1910
2227
|
// Minus zone
|
|
1911
2228
|
const third=st.w/3;
|
|
1912
|
-
c.fillStyle=flashDir===-1&&flashTimer>0?
|
|
2229
|
+
c.fillStyle=flashDir===-1&&flashTimer>0?minusFlash:'rgba(255,255,255,.03)';
|
|
1913
2230
|
rr(c,2,2,third-4,st.h-4,8);c.fill();
|
|
1914
|
-
c.fillStyle=
|
|
2231
|
+
c.fillStyle=P.accent2;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
1915
2232
|
c.fillText('\u2212',third/2,st.h/2);
|
|
1916
2233
|
|
|
1917
2234
|
// Plus zone
|
|
1918
|
-
c.fillStyle=flashDir===1&&flashTimer>0?
|
|
2235
|
+
c.fillStyle=flashDir===1&&flashTimer>0?plusFlash:'rgba(255,255,255,.03)';
|
|
1919
2236
|
rr(c,third*2+2,2,third-4,st.h-4,8);c.fill();
|
|
1920
|
-
c.fillStyle=
|
|
2237
|
+
c.fillStyle=P.accent;c.fillText('+',third*2+third/2,st.h/2);
|
|
1921
2238
|
|
|
1922
2239
|
// Counter
|
|
1923
|
-
c.fillStyle=
|
|
2240
|
+
c.fillStyle=P.dim;c.font="700 20px 'JetBrains Mono',monospace";
|
|
1924
2241
|
c.fillText(Math.round(displayCount),st.w/2,st.h/2);
|
|
1925
2242
|
|
|
1926
2243
|
if(st.focused)fr(c,st.w,st.h,10);
|
|
@@ -1929,9 +2246,14 @@ export function Stepper() {
|
|
|
1929
2246
|
}
|
|
1930
2247
|
|
|
1931
2248
|
/** 8. Radio Orbit -- 4 options arranged in a circle. Slider picks one. */
|
|
1932
|
-
export function RadioOrbit() {
|
|
2249
|
+
export function RadioOrbit(o = {}) {
|
|
1933
2250
|
let selectedGlow=new Float32Array(4);
|
|
1934
|
-
const names=['A','B','C','D']
|
|
2251
|
+
const names=['A','B','C','D'];
|
|
2252
|
+
// `colors` is the 4-node palette; `theme` seeds it. Normalised to length 4 so
|
|
2253
|
+
// the body's colors[i] stays valid for any override.
|
|
2254
|
+
const base = o.colors ? o.colors
|
|
2255
|
+
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark, o.theme.light] : ['#ff6b6b','#fbbf24','#6ee7b6','#38bdf8']);
|
|
2256
|
+
const colors = base.length >= 4 ? base : [base[0], base[1 % base.length], base[2 % base.length], base[3 % base.length]];
|
|
1935
2257
|
const OPTS=['OPTION A','OPTION B','OPTION C','OPTION D']; // const (was 'OPTION '+name concat)
|
|
1936
2258
|
return {
|
|
1937
2259
|
tick(c,dt,now,st) {
|
|
@@ -1972,9 +2294,14 @@ export function RadioOrbit() {
|
|
|
1972
2294
|
// ===========================================================
|
|
1973
2295
|
|
|
1974
2296
|
/** 9. Password Strength -- Segmented bar with color progression and label. */
|
|
1975
|
-
export function PasswordStrength() {
|
|
2297
|
+
export function PasswordStrength(o = {}) {
|
|
1976
2298
|
let segs=[0,0,0,0];
|
|
1977
|
-
const labels=['WEAK','FAIR','GOOD','STRONG']
|
|
2299
|
+
const labels=['WEAK','FAIR','GOOD','STRONG'];
|
|
2300
|
+
// `colors` is the 4-step strength ramp; `theme` seeds it. Normalised to 4.
|
|
2301
|
+
const base = o.colors ? o.colors
|
|
2302
|
+
: (o.theme ? [o.theme.light, o.theme.mid, o.theme.dark, o.theme.light] : ['#ff6b6b','#fbbf24','#38bdf8','#6ee7b6']);
|
|
2303
|
+
const colors = base.length >= 4 ? base : [base[0], base[1 % base.length], base[2 % base.length], base[3 % base.length]];
|
|
2304
|
+
const noneColor = (o.theme && o.theme.mid) || '#666';
|
|
1978
2305
|
return {
|
|
1979
2306
|
tick(c,dt,now,st) {
|
|
1980
2307
|
const level=Math.ceil(st.val*4);
|
|
@@ -1993,14 +2320,23 @@ export function PasswordStrength() {
|
|
|
1993
2320
|
}
|
|
1994
2321
|
|
|
1995
2322
|
const idx=clamp(level-1,0,3);
|
|
1996
|
-
lbl(c,level>0?labels[idx]:'NONE',st.w/2,st.h/2+16,level>0?colors[idx]:
|
|
2323
|
+
lbl(c,level>0?labels[idx]:'NONE',st.w/2,st.h/2+16,level>0?colors[idx]:noneColor);
|
|
1997
2324
|
if(st.focused)fr(c,st.w,st.h,4);
|
|
1998
2325
|
},
|
|
1999
2326
|
};
|
|
2000
2327
|
}
|
|
2001
2328
|
|
|
2002
2329
|
/** 10. Water Level -- Animated wave surface inside a container. */
|
|
2003
|
-
export function WaterLevel() {
|
|
2330
|
+
export function WaterLevel(o = {}) {
|
|
2331
|
+
const P = resolveTheme(o, { accent: '#38bdf8', accent2: '#6ee7b6', accent3: '#fbbf24', dim: '#e2e2f0' });
|
|
2332
|
+
const themed = !!(o.theme || o.colors);
|
|
2333
|
+
const surfHi = themed ? rgbaOf(P.accent, .3) : 'rgba(56,189,248,.3)';
|
|
2334
|
+
const surfMid = themed ? rgbaOf(P.accent2, .25) : 'rgba(110,231,182,.25)';
|
|
2335
|
+
const surfLo = themed ? rgbaOf(P.accent3, .2) : 'rgba(251,191,36,.2)';
|
|
2336
|
+
const deepHi = themed ? rgbaOf(P.accent, .15) : 'rgba(56,189,248,.15)';
|
|
2337
|
+
const deepMid = themed ? rgbaOf(P.accent2, .1) : 'rgba(110,231,182,.1)';
|
|
2338
|
+
const deepLo = themed ? rgbaOf(P.accent3, .08) : 'rgba(251,191,36,.08)';
|
|
2339
|
+
const FONT = pickFont(o, "700 12px 'JetBrains Mono',monospace");
|
|
2004
2340
|
let displayVal=0, wavePhase=0;
|
|
2005
2341
|
return {
|
|
2006
2342
|
tick(c,dt,now,st) {
|
|
@@ -2016,7 +2352,7 @@ export function WaterLevel() {
|
|
|
2016
2352
|
c.save();c.beginPath();rr(c,bx+1,by+1,bw-2,bh-2,5);c.clip();
|
|
2017
2353
|
|
|
2018
2354
|
// Wave surface
|
|
2019
|
-
c.fillStyle=displayVal>.7?
|
|
2355
|
+
c.fillStyle=displayVal>.7?surfHi:displayVal>.3?surfMid:surfLo;
|
|
2020
2356
|
c.beginPath();c.moveTo(bx,by+bh);
|
|
2021
2357
|
for(let x=0;x<=bw;x++){
|
|
2022
2358
|
const wave=Math.sin(wavePhase+x*0.08)*3+Math.sin(wavePhase*1.5+x*0.12)*2;
|
|
@@ -2025,12 +2361,12 @@ export function WaterLevel() {
|
|
|
2025
2361
|
c.lineTo(bx+bw,by+bh);c.closePath();c.fill();
|
|
2026
2362
|
|
|
2027
2363
|
// Deeper water
|
|
2028
|
-
c.fillStyle=displayVal>.7?
|
|
2364
|
+
c.fillStyle=displayVal>.7?deepHi:displayVal>.3?deepMid:deepLo;
|
|
2029
2365
|
c.fillRect(bx,waterY+5,bw,bh);
|
|
2030
2366
|
|
|
2031
2367
|
c.restore();
|
|
2032
2368
|
|
|
2033
|
-
c.fillStyle=
|
|
2369
|
+
c.fillStyle=P.dim;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2034
2370
|
c.fillText(PCT[Math.round(displayVal*100)],st.w/2,st.h/2);
|
|
2035
2371
|
if(st.focused)fr(c,st.w,st.h,6);
|
|
2036
2372
|
},
|
|
@@ -2038,14 +2374,21 @@ export function WaterLevel() {
|
|
|
2038
2374
|
}
|
|
2039
2375
|
|
|
2040
2376
|
/** 11. Heat Map -- 5×3 grid of cells that heat up based on slider. */
|
|
2041
|
-
export function HeatMap(
|
|
2377
|
+
export function HeatMap(o = {}) {
|
|
2378
|
+
const {seed=42}=o;
|
|
2379
|
+
const P = resolveTheme(o, { accent: '#ff5014', dim: '#9999b8' });
|
|
2042
2380
|
const rng=new Random(seed);
|
|
2043
2381
|
const N=15,thresholds=new Float32Array(N);
|
|
2044
2382
|
let vals=new Float32Array(N);
|
|
2045
|
-
// Heat-color LUT (rgb quantized by heat): a const lookup + globalAlpha
|
|
2046
|
-
//
|
|
2383
|
+
// Heat-color LUT (rgb quantized by heat): a const lookup + globalAlpha instead
|
|
2384
|
+
// of a per-cell rgb() template every frame. Default = the historical fire ramp
|
|
2385
|
+
// (byte-identical); a theme/colors override interpolates cool -> accent.
|
|
2386
|
+
const hotP = (o.theme || o.colors) ? rgbTriplet(P.accent, '255,80,20').split(',').map(Number) : null;
|
|
2047
2387
|
const HEATC=[];
|
|
2048
|
-
for(let i=0;i<=32;i++)
|
|
2388
|
+
for(let i=0;i<=32;i++){
|
|
2389
|
+
if(hotP){const p=i/32;HEATC[i]='rgb('+Math.round(100+(hotP[0]-100)*p)+','+Math.round(60+(hotP[1]-60)*p)+','+Math.round(60+(hotP[2]-60)*p)+')';}
|
|
2390
|
+
else HEATC[i]='rgb('+Math.round(100+i/32*155)+','+Math.round(60+i/32*20)+','+Math.round(60-i/32*40)+')';
|
|
2391
|
+
}
|
|
2049
2392
|
return {
|
|
2050
2393
|
init(){for(let i=0;i<N;i++)thresholds[i]=rng.range(0,1)},
|
|
2051
2394
|
tick(c,dt,now,st) {
|
|
@@ -2063,7 +2406,7 @@ export function HeatMap({seed=42}={}) {
|
|
|
2063
2406
|
rr(c,x,y,cw,ch,3);c.fill();
|
|
2064
2407
|
c.globalAlpha=1;
|
|
2065
2408
|
}
|
|
2066
|
-
lbl(c,PCT[Math.round(st.val*100)],st.w/2,st.h+10);
|
|
2409
|
+
lbl(c,PCT[Math.round(st.val*100)],st.w/2,st.h+10,P.dim);
|
|
2067
2410
|
if(st.focused)fr(c,st.w,st.h,3);
|
|
2068
2411
|
},
|
|
2069
2412
|
};
|
|
@@ -2075,7 +2418,9 @@ export function HeatMap({seed=42}={}) {
|
|
|
2075
2418
|
// ===========================================================
|
|
2076
2419
|
|
|
2077
2420
|
/** 12. Day/Night Toggle -- Sun/moon transition with star particles. */
|
|
2078
|
-
export function DayNightToggle(
|
|
2421
|
+
export function DayNightToggle(o = {}) {
|
|
2422
|
+
const {seed=42}=o;
|
|
2423
|
+
const P = resolveTheme(o, { accent: '#fbbf24', dim: '#9999b8', surface: '#d4d4e8' });
|
|
2079
2424
|
const rng=new Random(seed);
|
|
2080
2425
|
// Sky-color LUT (day->night rgb quantized by t): a const lookup, not an
|
|
2081
2426
|
// rgb() template per frame. Stars are a fixed pool filled in init.
|
|
@@ -2108,14 +2453,14 @@ export function DayNightToggle({seed=42}={}) {
|
|
|
2108
2453
|
const orbX=lerp(18,st.w-18,t);
|
|
2109
2454
|
if(t<.5){
|
|
2110
2455
|
// Sun
|
|
2111
|
-
c.fillStyle=
|
|
2456
|
+
c.fillStyle=P.accent;c.beginPath();c.arc(orbX,st.h/2,10,0,PI2);c.fill();
|
|
2112
2457
|
// Rays -- const color, alpha via globalAlpha
|
|
2113
|
-
c.strokeStyle=
|
|
2458
|
+
c.strokeStyle=P.accent;c.globalAlpha=.3*(1-t*2);c.lineWidth=1;
|
|
2114
2459
|
for(let i=0;i<8;i++){const a=now/800+i*Math.PI/4;c.beginPath();c.moveTo(orbX+Math.cos(a)*12,st.h/2+Math.sin(a)*12);c.lineTo(orbX+Math.cos(a)*16,st.h/2+Math.sin(a)*16);c.stroke()}
|
|
2115
2460
|
c.globalAlpha=1;
|
|
2116
2461
|
} else {
|
|
2117
2462
|
// Moon
|
|
2118
|
-
c.fillStyle=
|
|
2463
|
+
c.fillStyle=P.surface;c.beginPath();c.arc(orbX,st.h/2,10,0,PI2);c.fill();
|
|
2119
2464
|
// Crater shadows -- const black, alpha via globalAlpha
|
|
2120
2465
|
c.fillStyle='#000000';c.globalAlpha=(t-.5)*2*.15;
|
|
2121
2466
|
c.beginPath();c.arc(orbX-3,st.h/2-2,3,0,PI2);c.fill();
|
|
@@ -2123,16 +2468,20 @@ export function DayNightToggle({seed=42}={}) {
|
|
|
2123
2468
|
c.globalAlpha=1;
|
|
2124
2469
|
}
|
|
2125
2470
|
|
|
2126
|
-
lbl(c,st.toggled?'NIGHT':'DAY',st.w/2,st.h+14,st.toggled?
|
|
2471
|
+
lbl(c,st.toggled?'NIGHT':'DAY',st.w/2,st.h+14,st.toggled?P.dim:P.accent);
|
|
2127
2472
|
if(st.focused)fr(c,st.w,st.h,st.h/2);
|
|
2128
2473
|
},
|
|
2129
2474
|
};
|
|
2130
2475
|
}
|
|
2131
2476
|
|
|
2132
2477
|
/** 13. Reaction Picker -- 5 emoji-style circles that inflate on hover region. */
|
|
2133
|
-
export function ReactionPicker() {
|
|
2478
|
+
export function ReactionPicker(o = {}) {
|
|
2134
2479
|
let sizes=new Float32Array(5), selected=-1;
|
|
2135
|
-
const emojis=['\u{1F610}','\u{1F642}','\u{1F60A}','\u{1F604}','\u{1F929}']
|
|
2480
|
+
const emojis=['\u{1F610}','\u{1F642}','\u{1F60A}','\u{1F604}','\u{1F929}'];
|
|
2481
|
+
// `colors` is the 5-reaction palette; `theme` seeds it. Normalised to 5.
|
|
2482
|
+
const base = o.colors ? o.colors
|
|
2483
|
+
: (o.theme ? [o.theme.light,o.theme.mid,o.theme.dark,o.theme.light,o.theme.mid] : ['#9999b8','#fbbf24','#fb923c','#f472b6','#ff6b6b']);
|
|
2484
|
+
const colors = base.length>=5 ? base : Array.from({length:5},(_,i)=>base[i%base.length]);
|
|
2136
2485
|
const colors30=colors.map((col)=>col+'30'); // active fill (was `${color}30`)
|
|
2137
2486
|
const REACT_LABELS=['MEH','OK','NICE','GREAT','LOVE']; // was a per-frame array literal
|
|
2138
2487
|
const FONTS=[]; for(let i=0;i<=48;i++)FONTS[i]=i+'px sans-serif'; // emoji font by rounded size
|
|
@@ -2169,7 +2518,9 @@ export function ReactionPicker() {
|
|
|
2169
2518
|
}
|
|
2170
2519
|
|
|
2171
2520
|
/** 14. Notification Bell -- Bell icon with bounce and count badge. */
|
|
2172
|
-
export function NotificationBell() {
|
|
2521
|
+
export function NotificationBell(o = {}) {
|
|
2522
|
+
const P = resolveTheme(o, { accent: '#fbbf24', accent2: '#ff6b6b' });
|
|
2523
|
+
const TXT = pickText(o, 'NOTIFY');
|
|
2173
2524
|
let count=0, bellAngle=0, bellVel=0, badgeScale=0;
|
|
2174
2525
|
let badgeStr='0'; // rebuilt only on click (count change), not per frame
|
|
2175
2526
|
return {
|
|
@@ -2183,23 +2534,23 @@ export function NotificationBell() {
|
|
|
2183
2534
|
|
|
2184
2535
|
// Bell body
|
|
2185
2536
|
c.save();c.translate(cx,cy-4);c.rotate(bellAngle*0.3);
|
|
2186
|
-
c.fillStyle=
|
|
2537
|
+
c.fillStyle=P.accent;
|
|
2187
2538
|
c.beginPath();c.moveTo(-10,0);c.quadraticCurveTo(-12,-14,0,-18);c.quadraticCurveTo(12,-14,10,0);c.lineTo(-10,0);c.fill();
|
|
2188
2539
|
// Clapper
|
|
2189
|
-
c.fillStyle=
|
|
2540
|
+
c.fillStyle=P.accent;c.beginPath();c.arc(0,3,3,0,PI2);c.fill();
|
|
2190
2541
|
c.restore();
|
|
2191
2542
|
|
|
2192
2543
|
// Badge
|
|
2193
2544
|
if(count>0){
|
|
2194
2545
|
const bx=cx+10,by=cy-16;
|
|
2195
2546
|
c.save();c.translate(bx,by);c.scale(badgeScale,badgeScale);
|
|
2196
|
-
c.fillStyle=
|
|
2547
|
+
c.fillStyle=P.accent2;c.beginPath();c.arc(0,0,8,0,PI2);c.fill();
|
|
2197
2548
|
c.fillStyle='#fff';c.font="700 8px 'JetBrains Mono',monospace";c.textAlign='center';c.textBaseline='middle';
|
|
2198
2549
|
c.fillText(badgeStr,0,0);
|
|
2199
2550
|
c.restore();
|
|
2200
2551
|
}
|
|
2201
2552
|
|
|
2202
|
-
lbl(c,
|
|
2553
|
+
lbl(c,TXT,cx,st.h+10);
|
|
2203
2554
|
if(st.focused)fr(c,st.w,st.h,4);
|
|
2204
2555
|
},
|
|
2205
2556
|
};
|
|
@@ -2211,8 +2562,10 @@ export function NotificationBell() {
|
|
|
2211
2562
|
// ===========================================================
|
|
2212
2563
|
|
|
2213
2564
|
/** 15. Typewriter Field -- Characters appear one by one with cursor blink. */
|
|
2214
|
-
export function TypewriterField() {
|
|
2215
|
-
const
|
|
2565
|
+
export function TypewriterField(o = {}) {
|
|
2566
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', dim2: '#8888aa' });
|
|
2567
|
+
const FONT = pickFont(o, "500 13px 'JetBrains Mono',monospace");
|
|
2568
|
+
const text=pickText(o, 'Hello World');
|
|
2216
2569
|
let charIdx=0, timer=0, cursorBlink=0, typing=false;
|
|
2217
2570
|
let display='', dispW=0, lastIdx=-1; // substring rebuilt only when a char lands
|
|
2218
2571
|
return {
|
|
@@ -2224,24 +2577,26 @@ export function TypewriterField() {
|
|
|
2224
2577
|
c.fillStyle='rgba(255,255,255,.04)';rr(c,0,0,st.w,st.h,6);c.fill();
|
|
2225
2578
|
c.strokeStyle='rgba(255,255,255,.06)';c.lineWidth=1;rr(c,0,0,st.w,st.h,6);c.stroke();
|
|
2226
2579
|
|
|
2227
|
-
c.fillStyle=
|
|
2580
|
+
c.fillStyle=P.accent;c.font=FONT;c.textAlign='left';c.textBaseline='middle';
|
|
2228
2581
|
// Rebuild the visible substring + its width only when a char is added.
|
|
2229
2582
|
if(charIdx!==lastIdx){lastIdx=charIdx;display=text.substring(0,charIdx);dispW=c.measureText(display).width;}
|
|
2230
2583
|
c.fillText(display,8,st.h/2);
|
|
2231
2584
|
|
|
2232
2585
|
// Cursor
|
|
2233
2586
|
if(cursorBlink<1){
|
|
2234
|
-
c.fillStyle=
|
|
2587
|
+
c.fillStyle=P.accent;c.fillRect(9+dispW,st.h/2-8,1.5,16);
|
|
2235
2588
|
}
|
|
2236
2589
|
|
|
2237
|
-
lbl(c,typing?'TYPING...':'TOGGLE TO TYPE',st.w/2,st.h+10,typing?
|
|
2590
|
+
lbl(c,typing?'TYPING...':'TOGGLE TO TYPE',st.w/2,st.h+10,typing?P.accent:P.dim2);
|
|
2238
2591
|
if(st.focused)fr(c,st.w,st.h,6);
|
|
2239
2592
|
},
|
|
2240
2593
|
};
|
|
2241
2594
|
}
|
|
2242
2595
|
|
|
2243
2596
|
/** 16. Sound Wave Button -- Oscillating waveform on press, static on idle. */
|
|
2244
|
-
export function SoundWaveBtn() {
|
|
2597
|
+
export function SoundWaveBtn(o = {}) {
|
|
2598
|
+
const P = resolveTheme(o, { accent: '#f472b6', dim: '#9999b8' });
|
|
2599
|
+
const FONT = pickFont(o, "600 11px 'Space Grotesk',sans-serif");
|
|
2245
2600
|
let intensity=0, phase=0;
|
|
2246
2601
|
return {
|
|
2247
2602
|
onClick(){intensity=1},
|
|
@@ -2253,7 +2608,7 @@ export function SoundWaveBtn() {
|
|
|
2253
2608
|
|
|
2254
2609
|
// Waveform
|
|
2255
2610
|
const cy=st.h/2, amp=10*intensity;
|
|
2256
|
-
c.strokeStyle=intensity>.1?
|
|
2611
|
+
c.strokeStyle=intensity>.1?P.accent:'rgba(255,255,255,.08)';c.lineWidth=2;
|
|
2257
2612
|
c.beginPath();
|
|
2258
2613
|
for(let x=0;x<=st.w;x++){
|
|
2259
2614
|
const freq=x*0.1;
|
|
@@ -2268,7 +2623,7 @@ export function SoundWaveBtn() {
|
|
|
2268
2623
|
c.beginPath();c.moveTo(10,cy);c.lineTo(st.w-10,cy);c.stroke();
|
|
2269
2624
|
}
|
|
2270
2625
|
|
|
2271
|
-
c.fillStyle=intensity>.1?
|
|
2626
|
+
c.fillStyle=intensity>.1?P.accent:P.dim;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2272
2627
|
c.fillText(intensity>.1?'\u25CF REC':'RECORD',st.w/2,st.h/2);
|
|
2273
2628
|
if(st.focused)fr(c,st.w,st.h,10);
|
|
2274
2629
|
},
|
|
@@ -2276,7 +2631,8 @@ export function SoundWaveBtn() {
|
|
|
2276
2631
|
}
|
|
2277
2632
|
|
|
2278
2633
|
/** 17. Upload Progress -- File icon fills from bottom as slider increases. */
|
|
2279
|
-
export function UploadProgress() {
|
|
2634
|
+
export function UploadProgress(o = {}) {
|
|
2635
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#38bdf8', dim: '#9999b8' });
|
|
2280
2636
|
let displayVal=0;
|
|
2281
2637
|
return {
|
|
2282
2638
|
tick(c,dt,now,st) {
|
|
@@ -2292,8 +2648,8 @@ export function UploadProgress() {
|
|
|
2292
2648
|
|
|
2293
2649
|
// Fill
|
|
2294
2650
|
const fillH=fh*displayVal;
|
|
2295
|
-
const col=displayVal>=1?
|
|
2296
|
-
c.fillStyle=
|
|
2651
|
+
const col=displayVal>=1?P.accent:P.accent2;
|
|
2652
|
+
c.fillStyle=col+'30'; // was `${col}30`
|
|
2297
2653
|
c.save();c.beginPath();c.rect(fx+1,fy+fh-fillH,fw-1,fillH);c.clip();
|
|
2298
2654
|
c.beginPath();c.moveTo(fx,fy);c.lineTo(fx+fw-fold,fy);c.lineTo(fx+fw,fy+fold);c.lineTo(fx+fw,fy+fh);c.lineTo(fx,fy+fh);c.closePath();c.fill();
|
|
2299
2655
|
c.restore();
|
|
@@ -2308,7 +2664,7 @@ export function UploadProgress() {
|
|
|
2308
2664
|
}
|
|
2309
2665
|
c.lineCap='butt';
|
|
2310
2666
|
|
|
2311
|
-
lbl(c,displayVal>=.99?'DONE':PCT[Math.round(displayVal*100)],cx,st.h+10,displayVal>=.99?
|
|
2667
|
+
lbl(c,displayVal>=.99?'DONE':PCT[Math.round(displayVal*100)],cx,st.h+10,displayVal>=.99?P.accent:P.dim);
|
|
2312
2668
|
if(st.focused)fr(c,st.w,st.h,4);
|
|
2313
2669
|
},
|
|
2314
2670
|
};
|
|
@@ -2320,7 +2676,11 @@ export function UploadProgress() {
|
|
|
2320
2676
|
// ===========================================================
|
|
2321
2677
|
|
|
2322
2678
|
/** 18. Scratch Reveal -- Drag to erase a mask and reveal the prize beneath. */
|
|
2323
|
-
export function ScratchReveal(
|
|
2679
|
+
export function ScratchReveal(o = {}) {
|
|
2680
|
+
const {seed=42}=o;
|
|
2681
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', dim: '#9999b8' });
|
|
2682
|
+
const prizeBg = (o.theme || o.colors) ? rgbaOf(P.accent, .08) : 'rgba(110,231,182,.08)';
|
|
2683
|
+
const FONT = pickFont(o, "700 14px 'JetBrains Mono',monospace");
|
|
2324
2684
|
const rng=new Random(seed);
|
|
2325
2685
|
// Fixed scratch pool (holes persist). Once all SCR slots are used the scratch
|
|
2326
2686
|
// stops adding holes -- existing marks are never overwritten, so earlier
|
|
@@ -2345,10 +2705,10 @@ export function ScratchReveal({seed=42}={}) {
|
|
|
2345
2705
|
tick(c,dt,now,st) {
|
|
2346
2706
|
lastW=st.w;
|
|
2347
2707
|
// Prize background
|
|
2348
|
-
c.fillStyle=
|
|
2708
|
+
c.fillStyle=prizeBg;rr(c,0,0,st.w,st.h,8);c.fill();
|
|
2349
2709
|
|
|
2350
2710
|
// Prize text -- const color, alpha via globalAlpha
|
|
2351
|
-
c.fillStyle=
|
|
2711
|
+
c.fillStyle=P.accent;c.globalAlpha=.1+revealed*.6;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2352
2712
|
c.fillText(revealed>.6?'\u{1F389} WINNER!':'? ? ?',st.w/2,st.h/2);
|
|
2353
2713
|
c.globalAlpha=1;
|
|
2354
2714
|
|
|
@@ -2371,14 +2731,16 @@ export function ScratchReveal({seed=42}={}) {
|
|
|
2371
2731
|
c.fillRect(shimX-2,0,4,st.h);
|
|
2372
2732
|
}
|
|
2373
2733
|
|
|
2374
|
-
lbl(c,revealed>.6?'REVEALED!':'DRAG TO SCRATCH',st.w/2,st.h+10,revealed>.6?
|
|
2734
|
+
lbl(c,revealed>.6?'REVEALED!':'DRAG TO SCRATCH',st.w/2,st.h+10,revealed>.6?P.accent:P.dim);
|
|
2375
2735
|
if(st.focused)fr(c,st.w,st.h,8);
|
|
2376
2736
|
},
|
|
2377
2737
|
};
|
|
2378
2738
|
}
|
|
2379
2739
|
|
|
2380
2740
|
/** 19. Timer Countdown -- Circular countdown timer. Toggle starts/stops. */
|
|
2381
|
-
export function TimerCountdown() {
|
|
2741
|
+
export function TimerCountdown(o = {}) {
|
|
2742
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#fbbf24', accent3: '#ff6b6b', dim: '#e2e2f0', dim2: '#8888aa' });
|
|
2743
|
+
const FONT = pickFont(o, "700 16px 'JetBrains Mono',monospace");
|
|
2382
2744
|
let timeLeft=10, running=false, flashAlpha=0;
|
|
2383
2745
|
let timeStr='10.0s', lastTenths=-1; // rebuilt at ~10 Hz, not per frame
|
|
2384
2746
|
return {
|
|
@@ -2398,24 +2760,25 @@ export function TimerCountdown() {
|
|
|
2398
2760
|
|
|
2399
2761
|
// Progress arc
|
|
2400
2762
|
const progress=timeLeft/10;
|
|
2401
|
-
const col=timeLeft>3?
|
|
2763
|
+
const col=timeLeft>3?P.accent:timeLeft>1?P.accent2:P.accent3;
|
|
2402
2764
|
c.strokeStyle=col;c.lineWidth=4;
|
|
2403
2765
|
c.beginPath();c.arc(cx,cy,R,-Math.PI/2,-Math.PI/2+progress*PI2,false);c.stroke();
|
|
2404
2766
|
|
|
2405
2767
|
// Time text
|
|
2406
2768
|
const tenths=Math.round(timeLeft*10);
|
|
2407
2769
|
if(tenths!==lastTenths){lastTenths=tenths;timeStr=(tenths/10).toFixed(1)+'s';}
|
|
2408
|
-
c.fillStyle=
|
|
2770
|
+
c.fillStyle=P.dim;c.font=FONT;c.textAlign='center';c.textBaseline='middle';
|
|
2409
2771
|
c.fillText(timeStr,cx,cy);
|
|
2410
2772
|
|
|
2411
|
-
lbl(c,running?(timeLeft>0?'RUNNING':'TIME UP!'):'TOGGLE TO START',cx,st.h+10,running?col:
|
|
2773
|
+
lbl(c,running?(timeLeft>0?'RUNNING':'TIME UP!'):'TOGGLE TO START',cx,st.h+10,running?col:P.dim2);
|
|
2412
2774
|
if(st.focused)fr(c,st.w,st.h,R);
|
|
2413
2775
|
},
|
|
2414
2776
|
};
|
|
2415
2777
|
}
|
|
2416
2778
|
|
|
2417
2779
|
/** 20. Pull Refresh -- Drag down to charge, release to spin. */
|
|
2418
|
-
export function PullRefresh() {
|
|
2780
|
+
export function PullRefresh(o = {}) {
|
|
2781
|
+
const P = resolveTheme(o, { accent: '#6ee7b6', accent2: '#38bdf8', dim: '#9999b8' });
|
|
2419
2782
|
let pullAmt=0, spinning=false, spinAngle=0, spinTimer=0;
|
|
2420
2783
|
return {
|
|
2421
2784
|
onDrag(val) { if(!spinning) pullAmt=val; },
|
|
@@ -2435,25 +2798,25 @@ export function PullRefresh() {
|
|
|
2435
2798
|
// Spinner arcs
|
|
2436
2799
|
for(let i=0;i<3;i++){
|
|
2437
2800
|
const a=spinAngle+i*PI2/3;
|
|
2438
|
-
c.strokeStyle=
|
|
2801
|
+
c.strokeStyle=P.accent2;c.globalAlpha=.3+i*.2;c.lineWidth=3;
|
|
2439
2802
|
c.beginPath();c.arc(cx,cy,16,a,a+.8);c.stroke();
|
|
2440
2803
|
}
|
|
2441
2804
|
c.globalAlpha=1;
|
|
2442
|
-
lbl(c,'LOADING...',cx,st.h+10,
|
|
2805
|
+
lbl(c,'LOADING...',cx,st.h+10,P.accent2);
|
|
2443
2806
|
} else {
|
|
2444
2807
|
// Arrow that stretches with pull
|
|
2445
2808
|
const stretch=pullAmt*14;
|
|
2446
|
-
c.strokeStyle=pullAmt>.6?
|
|
2809
|
+
c.strokeStyle=pullAmt>.6?P.accent:'rgba(255,255,255,.15)';c.lineWidth=2;c.lineCap='round';
|
|
2447
2810
|
c.beginPath();c.moveTo(cx,cy-8-stretch);c.lineTo(cx,cy+4);c.stroke();
|
|
2448
2811
|
c.beginPath();c.moveTo(cx-5,cy+0);c.lineTo(cx,cy+4);c.lineTo(cx+5,cy+0);c.stroke();
|
|
2449
2812
|
c.lineCap='butt';
|
|
2450
2813
|
|
|
2451
2814
|
// Fill ring
|
|
2452
2815
|
if(pullAmt>0){
|
|
2453
|
-
c.strokeStyle=pullAmt>.6?
|
|
2816
|
+
c.strokeStyle=pullAmt>.6?P.accent:'rgba(255,255,255,.1)';c.lineWidth=2;
|
|
2454
2817
|
c.beginPath();c.arc(cx,cy,20,-Math.PI/2,-Math.PI/2+pullAmt*PI2);c.stroke();
|
|
2455
2818
|
}
|
|
2456
|
-
lbl(c,pullAmt>.6?'RELEASE!':'DRAG SLIDER',cx,st.h+10,pullAmt>.6?
|
|
2819
|
+
lbl(c,pullAmt>.6?'RELEASE!':'DRAG SLIDER',cx,st.h+10,pullAmt>.6?P.accent:P.dim);
|
|
2457
2820
|
}
|
|
2458
2821
|
if(st.focused)fr(c,st.w,st.h,24);
|
|
2459
2822
|
},
|
|
@@ -2491,9 +2854,17 @@ export const UIFXRecipes3 = {
|
|
|
2491
2854
|
ScratchReveal, TimerCountdown, PullRefresh,
|
|
2492
2855
|
};
|
|
2493
2856
|
|
|
2857
|
+
// U4a additions -- new native element types (CHECKBOX, PROGRESS). Kept out of the
|
|
2858
|
+
// Vol.1-3 historical snapshots above so those stay accurate; all recipes remain
|
|
2859
|
+
// reachable via RECIPES / RECIPE_META and their named exports regardless.
|
|
2860
|
+
export const UIFXRecipes4 = {
|
|
2861
|
+
TickDraw, IndeterminateScan,
|
|
2862
|
+
LiquidFill,
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2494
2865
|
|
|
2495
2866
|
// ===========================================================
|
|
2496
|
-
// DEFAULT EXPORT -- combined all-
|
|
2867
|
+
// DEFAULT EXPORT -- combined all-53 namespace
|
|
2497
2868
|
// ===========================================================
|
|
2498
2869
|
|
|
2499
2870
|
export default {
|
|
@@ -2524,6 +2895,8 @@ export default {
|
|
|
2524
2895
|
HelixLoader,
|
|
2525
2896
|
RippleCheck,
|
|
2526
2897
|
MorphCheck,
|
|
2898
|
+
TickDraw,
|
|
2899
|
+
IndeterminateScan,
|
|
2527
2900
|
FlameCounter,
|
|
2528
2901
|
GlitchCounter,
|
|
2529
2902
|
BubbleRating,
|
|
@@ -2532,6 +2905,7 @@ export default {
|
|
|
2532
2905
|
RingProgress,
|
|
2533
2906
|
BatteryGauge,
|
|
2534
2907
|
SignalMeter,
|
|
2908
|
+
LiquidFill,
|
|
2535
2909
|
PillTabs,
|
|
2536
2910
|
Stepper,
|
|
2537
2911
|
RadioOrbit,
|
|
@@ -2587,6 +2961,8 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
2587
2961
|
helixLoader: HelixLoader,
|
|
2588
2962
|
rippleCheck: RippleCheck,
|
|
2589
2963
|
morphCheck: MorphCheck,
|
|
2964
|
+
tickDraw: TickDraw,
|
|
2965
|
+
indeterminateScan: IndeterminateScan,
|
|
2590
2966
|
flameCounter: FlameCounter,
|
|
2591
2967
|
glitchCounter: GlitchCounter,
|
|
2592
2968
|
bubbleRating: BubbleRating,
|
|
@@ -2595,6 +2971,7 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
2595
2971
|
ringProgress: RingProgress,
|
|
2596
2972
|
batteryGauge: BatteryGauge,
|
|
2597
2973
|
signalMeter: SignalMeter,
|
|
2974
|
+
liquidFill: LiquidFill,
|
|
2598
2975
|
pillTabs: PillTabs,
|
|
2599
2976
|
stepper: Stepper,
|
|
2600
2977
|
radioOrbit: RadioOrbit,
|
|
@@ -2619,65 +2996,73 @@ export const RECIPES = Object.assign(Object.create(null), {
|
|
|
2619
2996
|
*
|
|
2620
2997
|
* type 'toggle' | 'button' | 'slider' -- the native element it mounts on
|
|
2621
2998
|
* family display grouping (Toggles, Buttons, Sliders, Knobs, ...)
|
|
2622
|
-
* themeable accepts { colors, theme } (
|
|
2623
|
-
* motionSafe inherently-calm under prefers-reduced-motion (false for all)
|
|
2999
|
+
* themeable accepts { colors, theme } (true for all as of U3b/1.4.0)
|
|
3000
|
+
* motionSafe inherently-calm under prefers-reduced-motion (false for all -- U5)
|
|
2624
3001
|
*/
|
|
2625
3002
|
export const RECIPE_META = [
|
|
2626
|
-
{ id: 'swarmToggle', name: 'Swarm Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2627
|
-
{ id: 'liquidToggle', name: 'Liquid Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2628
|
-
{ id: 'neonPulseToggle', name: 'Neon Pulse Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2629
|
-
{ id: 'magneticButton', name: 'Magnetic Button', type: 'button', family: 'Buttons', themeable:
|
|
2630
|
-
{ id: 'shatterButton', name: 'Shatter Button', type: 'button', family: 'Buttons', themeable:
|
|
2631
|
-
{ id: 'confettiButton', name: 'Confetti Button', type: 'button', family: 'Buttons', themeable:
|
|
2632
|
-
{ id: 'glitchButton', name: 'Glitch Button', type: 'button', family: 'Buttons', themeable:
|
|
2633
|
-
{ id: 'sparkSlider', name: 'Spark Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2634
|
-
{ id: 'cosmicSlider', name: 'Cosmic Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2635
|
-
{ id: 'laserSlider', name: 'Laser Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2636
|
-
{ id: 'pendulumToggle', name: 'Pendulum Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2637
|
-
{ id: 'circuitToggle', name: 'Circuit Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2638
|
-
{ id: 'lightningToggle', name: 'Lightning Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2639
|
-
{ id: 'dnaToggle', name: 'Dna Toggle', type: 'toggle', family: 'Toggles', themeable:
|
|
2640
|
-
{ id: 'heartbeatButton', name: 'Heartbeat Button', type: 'button', family: 'Buttons', themeable:
|
|
2641
|
-
{ id: 'breathingButton', name: 'Breathing Button', type: 'button', family: 'Buttons', themeable:
|
|
2642
|
-
{ id: 'inkSplashButton', name: 'Ink Splash Button', type: 'button', family: 'Buttons', themeable:
|
|
2643
|
-
{ id: 'pixelDissolveButton', name: 'Pixel Dissolve Button', type: 'button', family: 'Buttons', themeable:
|
|
2644
|
-
{ id: 'fireworkButton', name: 'Firework Button', type: 'button', family: 'Buttons', themeable:
|
|
2645
|
-
{ id: 'auroraSlider', name: 'Aurora Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2646
|
-
{ id: 'waveSlider', name: 'Wave Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2647
|
-
{ id: 'elasticBandSlider', name: 'Elastic Band Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2648
|
-
{ id: 'gravitySlider', name: 'Gravity Slider', type: 'slider', family: 'Sliders', themeable:
|
|
2649
|
-
{ id: 'orbitLoader', name: 'Orbit Loader', type: 'toggle', family: 'Loaders', themeable:
|
|
2650
|
-
{ id: 'helixLoader', name: 'Helix Loader', type: 'toggle', family: 'Loaders', themeable:
|
|
2651
|
-
{ id: 'rippleCheck', name: 'Ripple Check', type: '
|
|
2652
|
-
{ id: 'morphCheck', name: 'Morph Check', type: '
|
|
2653
|
-
{ id: '
|
|
2654
|
-
{ id: '
|
|
2655
|
-
{ id: '
|
|
2656
|
-
{ id: '
|
|
2657
|
-
{ id: '
|
|
2658
|
-
{ id: '
|
|
2659
|
-
{ id: '
|
|
2660
|
-
{ id: '
|
|
2661
|
-
{ id: '
|
|
2662
|
-
{ id: '
|
|
2663
|
-
{ id: '
|
|
2664
|
-
{ id: '
|
|
2665
|
-
{ id: '
|
|
2666
|
-
{ id: '
|
|
2667
|
-
{ id: '
|
|
2668
|
-
{ id: '
|
|
2669
|
-
{ id: '
|
|
2670
|
-
{ id: '
|
|
2671
|
-
{ id: '
|
|
2672
|
-
{ id: '
|
|
2673
|
-
{ id: '
|
|
2674
|
-
{ id: '
|
|
2675
|
-
{ id: '
|
|
3003
|
+
{ id: 'swarmToggle', name: 'Swarm Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3004
|
+
{ id: 'liquidToggle', name: 'Liquid Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3005
|
+
{ id: 'neonPulseToggle', name: 'Neon Pulse Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3006
|
+
{ id: 'magneticButton', name: 'Magnetic Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3007
|
+
{ id: 'shatterButton', name: 'Shatter Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3008
|
+
{ id: 'confettiButton', name: 'Confetti Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3009
|
+
{ id: 'glitchButton', name: 'Glitch Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3010
|
+
{ id: 'sparkSlider', name: 'Spark Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3011
|
+
{ id: 'cosmicSlider', name: 'Cosmic Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3012
|
+
{ id: 'laserSlider', name: 'Laser Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3013
|
+
{ id: 'pendulumToggle', name: 'Pendulum Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3014
|
+
{ id: 'circuitToggle', name: 'Circuit Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3015
|
+
{ id: 'lightningToggle', name: 'Lightning Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3016
|
+
{ id: 'dnaToggle', name: 'Dna Toggle', type: 'toggle', family: 'Toggles', themeable: true, motionSafe: false },
|
|
3017
|
+
{ id: 'heartbeatButton', name: 'Heartbeat Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3018
|
+
{ id: 'breathingButton', name: 'Breathing Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3019
|
+
{ id: 'inkSplashButton', name: 'Ink Splash Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3020
|
+
{ id: 'pixelDissolveButton', name: 'Pixel Dissolve Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3021
|
+
{ id: 'fireworkButton', name: 'Firework Button', type: 'button', family: 'Buttons', themeable: true, motionSafe: false },
|
|
3022
|
+
{ id: 'auroraSlider', name: 'Aurora Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3023
|
+
{ id: 'waveSlider', name: 'Wave Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3024
|
+
{ id: 'elasticBandSlider', name: 'Elastic Band Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3025
|
+
{ id: 'gravitySlider', name: 'Gravity Slider', type: 'slider', family: 'Sliders', themeable: true, motionSafe: false },
|
|
3026
|
+
{ id: 'orbitLoader', name: 'Orbit Loader', type: 'toggle', family: 'Loaders', themeable: true, motionSafe: false },
|
|
3027
|
+
{ id: 'helixLoader', name: 'Helix Loader', type: 'toggle', family: 'Loaders', themeable: true, motionSafe: false },
|
|
3028
|
+
{ id: 'rippleCheck', name: 'Ripple Check', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3029
|
+
{ id: 'morphCheck', name: 'Morph Check', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3030
|
+
{ id: 'tickDraw', name: 'Tick Draw', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3031
|
+
{ id: 'indeterminateScan', name: 'Indeterminate Scan', type: 'checkbox', family: 'Checkboxes', themeable: true, motionSafe: false },
|
|
3032
|
+
{ id: 'flameCounter', name: 'Flame Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
3033
|
+
{ id: 'glitchCounter', name: 'Glitch Counter', type: 'slider', family: 'Counters', themeable: true, motionSafe: false },
|
|
3034
|
+
{ id: 'bubbleRating', name: 'Bubble Rating', type: 'slider', family: 'Rating', themeable: true, motionSafe: false },
|
|
3035
|
+
{ id: 'volumeKnob', name: 'Volume Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3036
|
+
{ id: 'compassKnob', name: 'Compass Knob', type: 'knob', family: 'Knobs', themeable: true, motionSafe: false },
|
|
3037
|
+
{ id: 'ringProgress', name: 'Ring Progress', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3038
|
+
{ id: 'batteryGauge', name: 'Battery Gauge', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3039
|
+
{ id: 'signalMeter', name: 'Signal Meter', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3040
|
+
{ id: 'liquidFill', name: 'Liquid Fill', type: 'progress', family: 'Progress', themeable: true, motionSafe: false },
|
|
3041
|
+
{ id: 'pillTabs', name: 'Pill Tabs', type: 'button', family: 'Controls', themeable: true, motionSafe: false },
|
|
3042
|
+
{ id: 'stepper', name: 'Stepper', type: 'button', family: 'Controls', themeable: true, motionSafe: false },
|
|
3043
|
+
{ id: 'radioOrbit', name: 'Radio Orbit', type: 'slider', family: 'Controls', themeable: true, motionSafe: false },
|
|
3044
|
+
{ id: 'passwordStrength', name: 'Password Strength', type: 'slider', family: 'Indicators', themeable: true, motionSafe: false },
|
|
3045
|
+
{ id: 'waterLevel', name: 'Water Level', type: 'slider', family: 'Indicators', themeable: true, motionSafe: false },
|
|
3046
|
+
{ id: 'heatMap', name: 'Heat Map', type: 'slider', family: 'Indicators', themeable: true, motionSafe: false },
|
|
3047
|
+
{ id: 'dayNightToggle', name: 'Day Night Toggle', type: 'toggle', family: 'Mood', themeable: true, motionSafe: false },
|
|
3048
|
+
{ id: 'reactionPicker', name: 'Reaction Picker', type: 'button', family: 'Mood', themeable: true, motionSafe: false },
|
|
3049
|
+
{ id: 'notificationBell', name: 'Notification Bell', type: 'button', family: 'Mood', themeable: true, motionSafe: false },
|
|
3050
|
+
{ id: 'typewriterField', name: 'Typewriter Field', type: 'toggle', family: 'Feedback', themeable: true, motionSafe: false },
|
|
3051
|
+
{ id: 'soundWaveBtn', name: 'Sound Wave Btn', type: 'button', family: 'Feedback', themeable: true, motionSafe: false },
|
|
3052
|
+
{ id: 'uploadProgress', name: 'Upload Progress', type: 'progress', family: 'Feedback', themeable: true, motionSafe: false },
|
|
3053
|
+
{ id: 'scratchReveal', name: 'Scratch Reveal', type: 'slider', family: 'Fun', themeable: true, motionSafe: false },
|
|
3054
|
+
{ id: 'timerCountdown', name: 'Timer Countdown', type: 'toggle', family: 'Fun', themeable: true, motionSafe: false },
|
|
3055
|
+
{ id: 'pullRefresh', name: 'Pull Refresh', type: 'slider', family: 'Fun', themeable: true, motionSafe: false },
|
|
2676
3056
|
];
|
|
2677
3057
|
|
|
2678
3058
|
/** Names of every built-in recipe (the keys of RECIPES at load time). */
|
|
2679
3059
|
export const RECIPE_NAMES = Object.freeze(Object.keys(RECIPES));
|
|
2680
3060
|
|
|
3061
|
+
// The valid recipe/mount types, taken from the controller's UIType so the
|
|
3062
|
+
// registry's fail-closed check and the controller's mount guard are one source
|
|
3063
|
+
// of truth (they cannot drift as U4 adds types). Built once at load (cold).
|
|
3064
|
+
const VALID_META_TYPES = new Set(Object.values(UIType));
|
|
3065
|
+
|
|
2681
3066
|
/**
|
|
2682
3067
|
* Register a custom recipe, or override a built-in. Instantly usable via
|
|
2683
3068
|
* RECIPES[id] and reflected in RECIPE_META so existing pickers keep working.
|
|
@@ -2702,9 +3087,11 @@ export function registerRecipe(id, factory, meta) {
|
|
|
2702
3087
|
const type = (meta && meta.type) || (prev && prev.type) || undefined;
|
|
2703
3088
|
// A recipe's type selects its native element; a typeless recipe cannot be
|
|
2704
3089
|
// mounted. Reject it at registration (fail closed) -- checked BEFORE any
|
|
2705
|
-
// mutation, so a rejected call leaves RECIPES/RECIPE_META untouched.
|
|
2706
|
-
|
|
2707
|
-
|
|
3090
|
+
// mutation, so a rejected call leaves RECIPES/RECIPE_META untouched. The
|
|
3091
|
+
// valid set is UIType (VALID_META_TYPES), so registry + controller never
|
|
3092
|
+
// disagree about what a type is.
|
|
3093
|
+
if (!VALID_META_TYPES.has(type)) {
|
|
3094
|
+
throw new TypeError('registerRecipe: type must be one of "button", "toggle", "slider", "checkbox", "progress", "knob"');
|
|
2708
3095
|
}
|
|
2709
3096
|
|
|
2710
3097
|
RECIPES[id] = factory;
|