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