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:
parent
0aee48b749
commit
e9e52e43ed
12 changed files with 936 additions and 8 deletions
240
packages/dreamknob/src/digital/AlphaDisplay.tsx
Normal file
240
packages/dreamknob/src/digital/AlphaDisplay.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import * as React from 'react';
|
||||
|
||||
// Fourteen-segment geometry in a local 10 x 18 digit cell.
|
||||
// Outer ring: a top, b top-right, c bottom-right, d bottom, e bottom-left,
|
||||
// f top-left. Middle bar: g1 left, g2 right. Center verticals: i top, l
|
||||
// bottom. Diagonals: h TL, j TR, k BL, m BR.
|
||||
|
||||
const W = 10;
|
||||
const H = 18;
|
||||
const GAP = 3.4;
|
||||
const HALF = 1.05;
|
||||
const INSET = 0.45;
|
||||
|
||||
const hSeg = (y: number, x1: number, x2: number): string => {
|
||||
const a = x1 + INSET;
|
||||
const b = x2 - INSET;
|
||||
return `${a},${y} ${a + HALF},${y - HALF} ${b - HALF},${y - HALF} ${b},${y} ${b - HALF},${y + HALF} ${a + HALF},${y + HALF}`;
|
||||
};
|
||||
|
||||
const vSeg = (x: number, y1: number, y2: number): string => {
|
||||
const a = y1 + INSET;
|
||||
const b = y2 - INSET;
|
||||
return `${x},${a} ${x + HALF},${a + HALF} ${x + HALF},${b - HALF} ${x},${b} ${x - HALF},${b - HALF} ${x - HALF},${a + HALF}`;
|
||||
};
|
||||
|
||||
type Seg14 =
|
||||
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
|
||||
| 'g1' | 'g2' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm';
|
||||
|
||||
const POLY: Partial<Record<Seg14, string>> = {
|
||||
a: hSeg(1, 1, 9),
|
||||
d: hSeg(17, 1, 9),
|
||||
g1: hSeg(9, 1, 5),
|
||||
g2: hSeg(9, 5, 9),
|
||||
f: vSeg(1, 1, 9),
|
||||
b: vSeg(9, 1, 9),
|
||||
e: vSeg(1, 9, 17),
|
||||
c: vSeg(9, 9, 17),
|
||||
i: vSeg(5, 1, 9),
|
||||
l: vSeg(5, 9, 17),
|
||||
};
|
||||
|
||||
/** Diagonals render as thick lines: [x1, y1, x2, y2]. */
|
||||
const DIAG: Partial<Record<Seg14, [number, number, number, number]>> = {
|
||||
h: [2.2, 2.7, 4.1, 7.2],
|
||||
j: [7.8, 2.7, 5.9, 7.2],
|
||||
k: [4.1, 10.8, 2.2, 15.3],
|
||||
m: [5.9, 10.8, 7.8, 15.3],
|
||||
};
|
||||
|
||||
const CHARS: Record<string, Seg14[]> = {
|
||||
'0': ['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
'1': ['b', 'c'],
|
||||
'2': ['a', 'b', 'g1', 'g2', 'e', 'd'],
|
||||
'3': ['a', 'b', 'c', 'd', 'g1', 'g2'],
|
||||
'4': ['f', 'g1', 'g2', 'b', 'c'],
|
||||
'5': ['a', 'f', 'g1', 'g2', 'c', 'd'],
|
||||
'6': ['a', 'f', 'g1', 'g2', 'e', 'c', 'd'],
|
||||
'7': ['a', 'b', 'c'],
|
||||
'8': ['a', 'b', 'c', 'd', 'e', 'f', 'g1', 'g2'],
|
||||
'9': ['a', 'b', 'c', 'd', 'f', 'g1', 'g2'],
|
||||
A: ['a', 'b', 'c', 'e', 'f', 'g1', 'g2'],
|
||||
B: ['a', 'b', 'c', 'd', 'i', 'l', 'g2'],
|
||||
C: ['a', 'd', 'e', 'f'],
|
||||
D: ['a', 'b', 'c', 'd', 'i', 'l'],
|
||||
E: ['a', 'd', 'e', 'f', 'g1', 'g2'],
|
||||
F: ['a', 'e', 'f', 'g1', 'g2'],
|
||||
G: ['a', 'c', 'd', 'e', 'f', 'g2'],
|
||||
H: ['b', 'c', 'e', 'f', 'g1', 'g2'],
|
||||
I: ['a', 'd', 'i', 'l'],
|
||||
J: ['b', 'c', 'd', 'e'],
|
||||
K: ['e', 'f', 'g1', 'j', 'm'],
|
||||
L: ['d', 'e', 'f'],
|
||||
M: ['b', 'c', 'e', 'f', 'h', 'j'],
|
||||
N: ['b', 'c', 'e', 'f', 'h', 'm'],
|
||||
O: ['a', 'b', 'c', 'd', 'e', 'f'],
|
||||
P: ['a', 'b', 'e', 'f', 'g1', 'g2'],
|
||||
Q: ['a', 'b', 'c', 'd', 'e', 'f', 'm'],
|
||||
R: ['a', 'b', 'e', 'f', 'g1', 'g2', 'm'],
|
||||
S: ['a', 'c', 'd', 'f', 'g1', 'g2'],
|
||||
T: ['a', 'i', 'l'],
|
||||
U: ['b', 'c', 'd', 'e', 'f'],
|
||||
V: ['e', 'f', 'j', 'k'],
|
||||
W: ['b', 'c', 'e', 'f', 'k', 'm'],
|
||||
X: ['h', 'j', 'k', 'm'],
|
||||
Y: ['h', 'j', 'l'],
|
||||
Z: ['a', 'd', 'j', 'k'],
|
||||
'-': ['g1', 'g2'],
|
||||
'_': ['d'],
|
||||
'=': ['g1', 'g2', 'd'],
|
||||
'+': ['g1', 'g2', 'i', 'l'],
|
||||
'*': ['g1', 'g2', 'h', 'i', 'j', 'k', 'l', 'm'],
|
||||
'/': ['j', 'k'],
|
||||
'\\': ['h', 'm'],
|
||||
'?': ['a', 'b', 'g2', 'l'],
|
||||
' ': [],
|
||||
};
|
||||
|
||||
const ALL_SEGMENTS = Object.keys({ ...POLY, ...DIAG }) as Seg14[];
|
||||
|
||||
export interface AlphaDisplayProps {
|
||||
/** Text: A–Z, 0–9, space, - _ = + * / \ ? and '.' (attaches to the previous cell). */
|
||||
value: string;
|
||||
/** Fixed number of character cells; the text is padded/cropped to fit. */
|
||||
chars?: number;
|
||||
align?: 'left' | 'right';
|
||||
/** Character height in px. Default: 22. */
|
||||
height?: number;
|
||||
color?: string;
|
||||
/** Panel background. Set to 'none' to disable. */
|
||||
background?: string;
|
||||
/** Opacity of unlit segments. Default: 0.08. */
|
||||
ghostOpacity?: number;
|
||||
/** Italic skew in degrees. Default: 6. */
|
||||
skew?: number;
|
||||
/** LED glow strength (0 disables). Default: 1.4. */
|
||||
glow?: number;
|
||||
padding?: number;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fourteen-segment alphanumeric LED display — preset names, modes, messages.
|
||||
* The alphanumeric sibling of `SegmentDisplay`.
|
||||
*/
|
||||
export const AlphaDisplay: React.FC<AlphaDisplayProps> = ({
|
||||
value,
|
||||
chars,
|
||||
align = 'left',
|
||||
height = 22,
|
||||
color = '#ffb84d',
|
||||
background = '#0a0d0c',
|
||||
ghostOpacity = 0.08,
|
||||
skew = 6,
|
||||
glow = 1.4,
|
||||
padding = 6,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
const filterId = React.useId();
|
||||
|
||||
let text = value.toUpperCase();
|
||||
if (chars !== undefined) {
|
||||
const cellCount = text.replace(/\./g, '').length;
|
||||
if (cellCount < chars) {
|
||||
const pad = ' '.repeat(chars - cellCount);
|
||||
text = align === 'right' ? pad + text : text + pad;
|
||||
} else if (cellCount > chars) {
|
||||
let kept = 0;
|
||||
let out = '';
|
||||
for (const ch of text) {
|
||||
if (ch !== '.' && kept === chars) break;
|
||||
if (ch !== '.') kept++;
|
||||
out += ch;
|
||||
}
|
||||
text = out;
|
||||
}
|
||||
}
|
||||
|
||||
const cells: React.ReactNode[] = [];
|
||||
let x = 0;
|
||||
let key = 0;
|
||||
for (const ch of text) {
|
||||
if (ch === '.') {
|
||||
cells.push(
|
||||
<circle key={key++} cx={x - GAP / 2 + 0.2} cy={H - 1.2} r={1.15} fill={color} />,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const lit = new Set(CHARS[ch] ?? []);
|
||||
const parts: React.ReactNode[] = [];
|
||||
for (const seg of ALL_SEGMENTS) {
|
||||
const on = lit.has(seg);
|
||||
if (!on && ghostOpacity <= 0) continue;
|
||||
const opacity = on ? 1 : ghostOpacity;
|
||||
const poly = POLY[seg];
|
||||
if (poly) {
|
||||
parts.push(<polygon key={seg} points={poly} fill={color} opacity={opacity} />);
|
||||
} else {
|
||||
const [x1, y1, x2, y2] = DIAG[seg]!;
|
||||
parts.push(
|
||||
<line
|
||||
key={seg}
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="butt"
|
||||
opacity={opacity}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
cells.push(
|
||||
<g key={key++} transform={`translate(${x} 0)`}>
|
||||
{parts}
|
||||
</g>,
|
||||
);
|
||||
x += W + GAP;
|
||||
}
|
||||
|
||||
const contentW = Math.max(x - GAP, 0);
|
||||
const scale = height / H;
|
||||
const w = contentW * scale + padding * 2 + height * 0.15;
|
||||
const h = height + padding * 2;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={w}
|
||||
height={h}
|
||||
viewBox={`0 0 ${w} ${h}`}
|
||||
className={className}
|
||||
style={style}
|
||||
role="img"
|
||||
aria-label={value}
|
||||
>
|
||||
{glow > 0 && (
|
||||
<defs>
|
||||
<filter id={filterId} x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feGaussianBlur stdDeviation={glow} result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
)}
|
||||
{background !== 'none' && <rect x={0} y={0} width={w} height={h} rx={4} fill={background} />}
|
||||
<g
|
||||
transform={`translate(${padding + height * 0.12} ${padding}) scale(${scale})`}
|
||||
filter={glow > 0 ? `url(#${filterId})` : undefined}
|
||||
>
|
||||
{skew ? <g transform={`skewX(${-Math.abs(skew)})`}>{cells}</g> : cells}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue