Tier 1: origin anchor, detents, theming, type-in editing, ImageKnob
- origin: bipolar anchor value — Arc from=center, Fader fill and LEDFader segments draw from origin instead of min/mid-travel - detents + detentSize: magnetic snap while dragging (normalized space, wheel/keyboard unaffected); applyDetents exported - DreamknobProvider + KnobTheme: accent/track/face/text/label/ticks/ focusRing/fonts tokens with dark and light bases; every skin resolves defaults from the theme, instance props win - editable + parseValue: double-click knobs (or click fader readouts) to type exact values; Enter/blur commits, Esc cancels, 1.2k parses; ValueInput + defaultParseValue exported; setValue added to context - ImageKnob: film-strip sprite knob (KnobMan-style strips), vertical or horizontal, with focus ring, editing, value/label - Docs: LED gallery cards for film-strip/bipolar/editing/theming, playground editable toggle, theming API section, README updates
This commit is contained in:
parent
d8992408fa
commit
804a85cca6
25 changed files with 999 additions and 162 deletions
|
|
@ -50,6 +50,8 @@ export const ApiDocs: React.FC = () => (
|
|||
<Row name="values" type="number[]" desc="Restrict to a discrete list, e.g. [0.25, 0.5, 1, 2, 4]. Overrides step." />
|
||||
<Row name="steps" type="number" desc="Number of evenly spaced detents across the travel (selector knobs)." />
|
||||
<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="interaction" type="InteractionMode" def="'rotary'" desc="'rotary' (grab & turn), 'vertical', 'horizontal', 'both', or absolute 'track-*' modes used by faders." />
|
||||
<Row name="dragSensitivity" type="number" def="200" desc="Pixels of relative drag for full travel (vertical/horizontal modes)." />
|
||||
<Row name="fineMultiplier" type="number" def="0.1" desc="Speed multiplier while Shift is held during drags and wheel." />
|
||||
|
|
@ -59,6 +61,8 @@ export const ApiDocs: React.FC = () => (
|
|||
<Row name="onChange" type="(v: number) => void" desc="Fires for every distinct snapped value." />
|
||||
<Row name="onChangeStart / onChangeEnd" type="(v: number) => void" desc="Gesture boundaries — ideal for undo history or parameter automation." />
|
||||
<Row name="disabled / readOnly" type="boolean" def="false" desc="Disable interaction (readOnly stays focusable)." />
|
||||
<Row name="editable / parseValue" type="boolean / fn" def="false" desc="Type-in editing: double-click a knob (or click a fader readout), type a value, Enter commits, Esc cancels. '1.2k' → 1200." />
|
||||
<Row name="focusRing" type="string | false" def="theme" desc="Keyboard focus ring color. Pointer grabs never show it." />
|
||||
<Row name="aria-label / getAriaValueText" type="string / fn" desc="Accessibility. Every knob is a proper role='slider' with full keyboard support." />
|
||||
</Table>
|
||||
|
||||
|
|
@ -66,8 +70,10 @@ export const ApiDocs: React.FC = () => (
|
|||
<p className="api-note">
|
||||
<code>↑/→</code> and <code>↓/←</code> step · <code>Shift</code>+arrows jump 10×
|
||||
· <code>PageUp/PageDown</code> move 10% · <code>Home/End</code> jump to min/max ·
|
||||
scroll wheel nudges (Shift = fine) · double-click resets · touch works via pointer
|
||||
capture.
|
||||
<code>Esc</code> cancels a drag and restores the start value · scroll wheel nudges
|
||||
(Shift = fine) · double-click resets (or opens the editor when{' '}
|
||||
<code>editable</code>) · touch works via pointer capture · every control shows a
|
||||
keyboard-only focus ring.
|
||||
</p>
|
||||
|
||||
<h3 className="api-h">Prebuilt skins</h3>
|
||||
|
|
@ -81,6 +87,7 @@ export const ApiDocs: React.FC = () => (
|
|||
<Row name="<SteppedKnob />" type="selector" desc="Detented switch. positions (named) or steps (numeric), color, faceColor." />
|
||||
<Row name="<Fader />" type="linear" desc="Channel fader. orientation, length, breadth, color, tickCount, showFill, unit, format." />
|
||||
<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="<SegmentDisplay />" type="digital" desc="Standalone seven-segment display. digits, decimals, color, glow, skew, ghostOpacity." />
|
||||
</Table>
|
||||
<p className="api-note">
|
||||
|
|
@ -89,6 +96,28 @@ export const ApiDocs: React.FC = () => (
|
|||
above.
|
||||
</p>
|
||||
|
||||
<h3 className="api-h">Theming</h3>
|
||||
<p className="api-note">
|
||||
Every skin resolves its color and font defaults from the active theme; per-instance
|
||||
props always win. Use <code>base="light"</code> for light backgrounds.
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={`import { DreamknobProvider } from 'dreamknob'
|
||||
|
||||
<DreamknobProvider
|
||||
base="dark" // or "light"
|
||||
theme={{
|
||||
accent: '#ff4d6b', // one accent for the whole console
|
||||
track: 'rgba(255,255,255,0.1)',
|
||||
text: '#fff',
|
||||
label: 'rgba(255,255,255,0.5)',
|
||||
fontMono: '"IBM Plex Mono", monospace',
|
||||
}}
|
||||
>
|
||||
<FlatKnob ... /> <Fader ... /> <LEDKnob ... />
|
||||
</DreamknobProvider>`}
|
||||
/>
|
||||
|
||||
<h3 className="api-h">Compose your own knob</h3>
|
||||
<p className="api-note">
|
||||
<code><Knob></code> wires up the interaction and provides a render context;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Arc,
|
||||
DreamknobProvider,
|
||||
Fader,
|
||||
FlatKnob,
|
||||
ImageKnob,
|
||||
Knob,
|
||||
KnobValue,
|
||||
LEDFader,
|
||||
|
|
@ -18,6 +20,60 @@ import {
|
|||
logTaper,
|
||||
} from 'dreamknob';
|
||||
|
||||
/** 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);
|
||||
React.useEffect(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = fs;
|
||||
canvas.height = fs * frames;
|
||||
const g = canvas.getContext('2d');
|
||||
if (!g) return;
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const cx = fs / 2;
|
||||
const cy = i * fs + fs / 2;
|
||||
const angle = ((225 + (i / (frames - 1)) * 270) * Math.PI) / 180;
|
||||
const grad = g.createRadialGradient(cx - fs * 0.15, cy - fs * 0.18, fs * 0.05, cx, cy, fs * 0.45);
|
||||
grad.addColorStop(0, '#5c5f69');
|
||||
grad.addColorStop(0.65, '#26282e');
|
||||
grad.addColorStop(1, '#0e0f12');
|
||||
g.fillStyle = grad;
|
||||
g.beginPath();
|
||||
g.arc(cx, cy, fs * 0.4, 0, Math.PI * 2);
|
||||
g.fill();
|
||||
g.strokeStyle = 'rgba(0,0,0,0.65)';
|
||||
g.lineWidth = fs * 0.025;
|
||||
g.stroke();
|
||||
g.strokeStyle = '#ffd23e';
|
||||
g.lineWidth = fs * 0.05;
|
||||
g.lineCap = 'round';
|
||||
g.beginPath();
|
||||
g.moveTo(cx + Math.sin(angle) * fs * 0.16, cy - Math.cos(angle) * fs * 0.16);
|
||||
g.lineTo(cx + Math.sin(angle) * fs * 0.33, cy - Math.cos(angle) * fs * 0.33);
|
||||
g.stroke();
|
||||
}
|
||||
setSrc(canvas.toDataURL('image/png'));
|
||||
}, [frames, fs]);
|
||||
return src;
|
||||
};
|
||||
|
||||
const ImageKnobDemo: React.FC = () => {
|
||||
const src = useGeneratedStrip();
|
||||
if (!src) return null;
|
||||
return (
|
||||
<ImageKnob
|
||||
src={src}
|
||||
frames={31}
|
||||
size={92}
|
||||
defaultValue={40}
|
||||
step={1}
|
||||
showValue
|
||||
label="Sprite"
|
||||
aria-label="Film-strip demo"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Card: React.FC<{
|
||||
title: string;
|
||||
desc: string;
|
||||
|
|
@ -131,6 +187,88 @@ export const Gallery: React.FC = () => (
|
|||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="Film-strip" desc="Photoreal sprite knobs — any KnobMan-style PNG strip works. This one is drawn at runtime." tag="<ImageKnob />">
|
||||
<ImageKnobDemo />
|
||||
</Card>
|
||||
|
||||
<Card title="Bipolar origin + detent" desc="Fill anchors at value 0 (not mid-travel) and the drag snaps magnetically at 0." tag="origin={0} detents={[0]}">
|
||||
<FlatKnob
|
||||
size={92}
|
||||
min={-60}
|
||||
max={12}
|
||||
step={0.5}
|
||||
defaultValue={0}
|
||||
origin={0}
|
||||
detents={[0]}
|
||||
arcFrom="center"
|
||||
color="#3df2ad"
|
||||
format={v => `${v > 0 ? '+' : ''}${v.toFixed(1)}`}
|
||||
label="Gain"
|
||||
aria-label="Bipolar gain demo"
|
||||
/>
|
||||
<Fader
|
||||
length={130}
|
||||
min={-60}
|
||||
max={12}
|
||||
step={0.5}
|
||||
defaultValue={0}
|
||||
origin={0}
|
||||
detents={[0]}
|
||||
color="#3df2ad"
|
||||
format={v => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
||||
label="Trim"
|
||||
aria-label="Bipolar fader demo"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="Type-in editing" desc="Double-click the knob (or click a fader readout) and type the exact value — '1.2k' works too." tag="editable">
|
||||
<FlatKnob
|
||||
size={92}
|
||||
min={20}
|
||||
max={20000}
|
||||
taper={logTaper}
|
||||
decimals={0}
|
||||
defaultValue={440}
|
||||
editable
|
||||
color="#4cc2ff"
|
||||
format={v => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${Math.round(v)}`)}
|
||||
label="Freq"
|
||||
aria-label="Editable demo"
|
||||
/>
|
||||
<Fader
|
||||
length={130}
|
||||
min={-60}
|
||||
max={12}
|
||||
step={0.5}
|
||||
defaultValue={-6}
|
||||
editable
|
||||
unit=" dB"
|
||||
label="Out"
|
||||
aria-label="Editable fader demo"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="Theming" desc="One provider restyles every control — accent, tracks, text, fonts. Light backgrounds too." tag="<DreamknobProvider />">
|
||||
<DreamknobProvider theme={{ accent: '#ff4d6b' }}>
|
||||
<FlatKnob size={70} defaultValue={62} label="A" aria-label="Themed A" />
|
||||
<RubberKnob size={70} defaultValue={35} label="B" aria-label="Themed B" />
|
||||
</DreamknobProvider>
|
||||
<div
|
||||
style={{
|
||||
background: '#eceef2',
|
||||
borderRadius: 10,
|
||||
padding: '12px 16px',
|
||||
display: 'flex',
|
||||
gap: 14,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<DreamknobProvider base="light" theme={{ accent: '#0868c8' }}>
|
||||
<FlatKnob size={70} defaultValue={62} label="Light" aria-label="Light themed" />
|
||||
</DreamknobProvider>
|
||||
</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" />
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ interface Config {
|
|||
label: string;
|
||||
unit: string;
|
||||
showValue: boolean;
|
||||
editable: boolean;
|
||||
arcFrom: 'min' | 'center';
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +68,7 @@ const DEFAULTS: Config = {
|
|||
label: 'Level',
|
||||
unit: '',
|
||||
showValue: true,
|
||||
editable: false,
|
||||
arcFrom: 'min',
|
||||
};
|
||||
|
||||
|
|
@ -86,6 +88,7 @@ const buildCode = (c: Config, controlled: boolean): string => {
|
|||
props.push(`color="${c.color}"`);
|
||||
if (c.label) props.push(`label="${c.label}"`);
|
||||
if (c.unit) props.push(`unit="${c.unit}"`);
|
||||
if (c.editable) props.push('editable');
|
||||
if (c.skin === 'flat' && c.arcFrom !== 'min') props.push(`arcFrom="center"`);
|
||||
if (c.skin === 'stepped') props.push(`steps={5}`);
|
||||
const name = SKINS[c.skin];
|
||||
|
|
@ -124,6 +127,7 @@ export const Playground: React.FC = () => {
|
|||
label: cfg.label || undefined,
|
||||
unit: cfg.unit || undefined,
|
||||
showValue: cfg.showValue,
|
||||
editable: cfg.editable,
|
||||
'aria-label': cfg.label || 'playground knob',
|
||||
};
|
||||
|
||||
|
|
@ -260,6 +264,16 @@ export const Playground: React.FC = () => {
|
|||
show value readout
|
||||
</label>
|
||||
</div>
|
||||
<div className="control">
|
||||
<label className="check">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cfg.editable}
|
||||
onChange={e => set('editable', e.target.checked)}
|
||||
/>
|
||||
type-in editing (dbl-click / click readout)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stage">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue