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

@ -52,6 +52,9 @@ export const ApiDocs: React.FC = () => (
<Row name="taper" type="Taper" def="linearTaper" desc="Travel curve: linearTaper, logTaper (frequencies), powTaper(n) (gain), or your own." />
<Row name="origin" type="number" desc="Bipolar anchor: arcs and fader fills draw from this value instead of the minimum (origin={0} for pan/gain)." />
<Row name="detents / detentSize" type="number[] / number" def="— / 0.025" desc="Magnetic snap points: drags stick to these values within the capture radius. Wheel/keyboard unaffected." />
<Row name="wrap" type="boolean" def="false" desc="Endless encoder: values roll around min↔max. Pair with angleOffset={0} angleRange={360}." />
<Row name="animateChanges" type="boolean | {duration}" def="false" desc="Tween the pointer on programmatic changes (preset loads). Gestures never animate; respects prefers-reduced-motion." />
<Row name="valueBubble / bubbleFormat" type="boolean / fn" def="false" desc="Floating readout above the control while dragging." />
<Row name="interaction" type="InteractionMode" def="'rotary'" desc="'rotary' (grab & turn), 'vertical', 'horizontal', 'both', or absolute 'track-*' modes used by faders." />
<Row name="rotaryMode" type="RotaryMode" def="'absolute'" desc="'absolute' tracks the pointer angle; 'relative' applies angle deltas (no grab jump); 'pickup' engages once the pointer sweeps past the value (MIDI pickup)." />
<Row name="dragSensitivity" type="number" def="200" desc="Pixels of relative drag for full travel (vertical/horizontal modes)." />
@ -95,6 +98,9 @@ export const ApiDocs: React.FC = () => (
<Row name="<LEDFader />" type="linear · digital" desc="Segmented LED meter-fader. orientation, length, segments, color or zones ([{upTo, color}]), glow, digits." />
<Row name="<ImageKnob />" type="sprite" desc="Film-strip knob (KnobMan-style PNG strips): src, frames, stripOrientation. Photoreal skins with zero drawing code." />
<Row name="<Meter />" type="display" desc="Read-only LED level meter: value, zones, peakHold (ms), peakColor, orientation, seven-segment readout." />
<Row name="<XYPad />" type="2D control" desc="Two-parameter pad: x/y axis configs, drag + arrow keys, grid, crosshair, Escape cancel, double-click reset." />
<Row name="<PushButton />" type="switch" desc="Panel button with LED strip: toggle or momentary, controlled/uncontrolled, aria-pressed." />
<Row name="<AlphaDisplay />" type="digital" desc="Fourteen-segment alphanumeric LED: AZ 09 and symbols, chars padding, align, glow, skew." />
<Row name="<SegmentDisplay />" type="digital" desc="Standalone seven-segment display. digits, decimals, color, glow, skew, ghostOpacity." />
</Table>
<p className="api-note">

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" />