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> = { 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> = { 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 = { '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 = ({ 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( , ); 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(); } else { const [x1, y1, x2, y2] = DIAG[seg]!; parts.push( , ); } } cells.push( {parts} , ); 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 ( {glow > 0 && ( )} {background !== 'none' && } 0 ? `url(#${filterId})` : undefined} > {skew ? {cells} : cells} ); };