Tier 3: bubbles, animation, wrap mode, XYPad, AlphaDisplay, PushButton

- valueBubble/bubbleFormat: floating readout above knobs while dragging
  (Knob-based skins + ImageKnob)
- animateChanges: rAF ease-out tween of the pointer on programmatic
  value changes; user gestures snap; prefers-reduced-motion disables
- wrap: endless encoder mode — values and relative drags roll around
  min↔max; pairs with angleOffset={0} angleRange={360}
- XYPad: two-parameter pad with absolute drag, arrow keys (Shift
  coarse), Escape cancel, double-click reset, grid/crosshair, focus
  ring, hidden form inputs, change meta
- AlphaDisplay: fourteen-segment alphanumeric LED (A-Z, 0-9, symbols,
  decimal points, chars padding/align, glow/skew)
- PushButton: studio panel button with LED strip — toggle or momentary,
  controlled/uncontrolled, aria-pressed, hidden form input
- Docs: five new gallery cards incl. animated preset demo, API rows,
  README
This commit is contained in:
Dreamodus 2026-07-12 17:47:20 -07:00
parent 0aee48b749
commit e9e52e43ed
12 changed files with 936 additions and 8 deletions

View file

@ -1,5 +1,6 @@
import React from 'react';
import {
AlphaDisplay,
Arc,
DreamknobProvider,
Fader,
@ -13,14 +14,57 @@ import {
Meter,
NeonKnob,
Pointer,
PushButton,
RubberKnob,
SegmentDisplay,
SteppedKnob,
Ticks,
VintageKnob,
XYPad,
logTaper,
} from 'dreamknob';
const PRESETS = [
{ name: 'INIT PATCH', cutoff: 50 },
{ name: 'WARM PAD', cutoff: 22 },
{ name: 'ACID 303', cutoff: 84 },
];
const PresetDemo: React.FC = () => {
const [preset, setPreset] = React.useState(0);
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14 }}>
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<FlatKnob
size={84}
value={PRESETS[preset].cutoff}
onChange={() => undefined}
readOnly
animateChanges={{ duration: 350 }}
color="#3df2ad"
label="Cutoff"
aria-label="Preset cutoff"
/>
<AlphaDisplay value={PRESETS[preset].name} chars={10} height={16} color="#3df2ad" />
</div>
<div style={{ display: 'flex', gap: 8 }}>
{PRESETS.map((p, i) => (
<PushButton
key={p.name}
pressed={preset === i}
onChange={on => on && setPreset(i)}
color="#3df2ad"
size={34}
aria-label={`Preset ${p.name}`}
>
{String.fromCharCode(65 + i)}
</PushButton>
))}
</div>
</div>
);
};
/** Draw a film-strip of a simple hardware knob at runtime (stand-in for a KnobMan PNG). */
const useGeneratedStrip = (frames = 31, fs = 120): string | null => {
const [src, setSrc] = React.useState<string | null>(null);
@ -340,6 +384,59 @@ export const Gallery: React.FC = () => (
/>
</Card>
<Card title="Animated presets + buttons" desc="animateChanges tweens programmatic changes; PushButton latches or is momentary." tag="animateChanges · <PushButton />">
<PresetDemo />
</Card>
<Card title="XY pad" desc="Two parameters at once — cutoff/resonance, vector mixing, FX morphing." tag="<XYPad />">
<XYPad
width={190}
height={140}
x={{ min: 20, max: 20000, step: 1, defaultValue: 800 }}
y={{ min: 0, max: 100, step: 0.5, defaultValue: 30 }}
formatX={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
color="#e44cff"
label="Filter"
aria-label="Filter XY pad"
/>
</Card>
<Card title="Endless encoder" desc="wrap makes the value roll around min↔max — full-turn phase and offset dials." tag="wrap · angleRange={360}">
<NeonKnob
size={92}
min={0}
max={360}
step={1}
defaultValue={90}
wrap
angleOffset={0}
angleRange={360}
unit="°"
color="#4cc2ff"
label="Phase"
aria-label="Phase encoder"
/>
</Card>
<Card title="Drag bubble" desc="A floating readout follows the gesture — keep the knob face clean." tag="valueBubble">
<MetalKnob
size={92}
defaultValue={64}
step={1}
valueBubble
bubbleFormat={v => `${v} %`}
label="Blend"
aria-label="Bubble demo"
/>
</Card>
<Card title="Alpha display" desc="Fourteen-segment alphanumeric LED — preset names, modes, messages." tag="<AlphaDisplay />">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'center' }}>
<AlphaDisplay value="DREAMKNOB" height={20} />
<AlphaDisplay value="LP-24 SAT+2" height={16} color="#4cc2ff" />
</div>
</Card>
<Card title="Compose your own" desc="Primitives + headless core = any design you want." tag="<Knob> + primitives">
<Knob size={96} defaultValue={42} aria-label="Composed demo">
<Ticks count={28} radius={47} length={5} width={1.5} color="rgba(255,255,255,0.12)" activeColor="#ffd23e" />