Skip to content
File

Blob: worker/src/client/exhibit/board/board-illustration.tsx

typescript160 lines
1import { useId } from 'react';
2import type { CSSProperties } from 'react';
3import type { RGB } from '@/shared/contracts/robot.ts';
4import { anchors } from '@/client/exhibit/diagram/diagram-model.ts';
5 
6export function ledColor(rgb?: RGB): string {
7 return rgb ? `rgb(${rgb.map((value) => Math.min(255, value * 8)).join(' ')})` : '#697366';
8}
9 
10/** Board artwork and logical stream anchors; the LED follows reported hardware state. */
11export function BoardIllustration({ led }: { led?: RGB }) {
12 const patternId = useId();
13 const color = ledColor(led),
14 known = !!led,
15 ledOn = !!led?.some((value) => value > 0);
16 return (
17 <svg
18 className="pcb h-auto w-full drop-shadow-md"
19 viewBox="0 0 230 365"
20 role="img"
21 aria-label={`ESP32-S3 board. UART connector on the left; USB connector on the right. Onboard LED ${known ? led!.join(', ') : 'state unknown'}.`}
22 >
23 <defs>
24 <pattern id={patternId} width="14" height="14" patternUnits="userSpaceOnUse">
25 <circle cx="7" cy="7" r=".65" fill="#608579" opacity=".35" />
26 </pattern>
27 </defs>
28 <rect
29 x="27"
30 y="9"
31 width="176"
32 height="345"
33 rx="11"
34 fill="#284f43"
35 stroke="#183d32"
36 strokeWidth="2"
37 />
38 <rect x="30" y="12" width="170" height="339" rx="8" fill={`url(#${patternId})`} />
39 {[0, 1].map((side) => (
40 <g key={side}>
41 {Array.from({ length: 16 }, (_, i) => (
42 <g key={i}>
43 <rect
44 x={side ? 198 : 19}
45 y={54 + i * 17}
46 width="13"
47 height="9"
48 rx="1"
49 fill="#b7a76f"
50 />
51 <circle cx={side ? 194 : 36} cy={58 + i * 17} r="3" fill="#d6c893" />
52 <circle cx={side ? 194 : 36} cy={58 + i * 17} r="1.4" fill="#2a342a" />
53 </g>
54 ))}
55 </g>
56 ))}
57 <path
58 d="M72 50V27h18v25h18V27h18v25h18V27h17v32"
59 stroke="#bab97f"
60 strokeWidth="3"
61 fill="none"
62 />
63 <path
64 d="M54 115h-8v56h17m112-57h10v106h-15M80 228v19H62v41m97-49v30h16v40M98 255v42h17v26"
65 stroke="#739383"
66 fill="none"
67 />
68 <rect x="60" y="72" width="111" height="148" rx="5" fill="#c7cdc0" stroke="#8f9b90" />
69 <path d="M64 77h103M64 215h103" stroke="#e8ebe2" />
70 <text x="115" y="106" textAnchor="middle" className="pcb-brand">
71 ESPRESSIF
72 </text>
73 <text x="115" y="136" textAnchor="middle" className="pcb-chip">
74 ESP32-S3
75 </text>
76 <text x="115" y="156" textAnchor="middle" className="pcb-model">
77 DUAL CORE · 240 MHz
78 </text>
79 <path d="M78 169h74" stroke="#a5afa4" />
80 <text x="115" y="188" textAnchor="middle" className="pcb-model">
81 32 MiB FLASH
82 </text>
83 <text x="115" y="203" textAnchor="middle" className="pcb-model">
84 16 MiB PSRAM
85 </text>
86 <rect x="139" y="237" width="24" height="14" fill="#23382d" stroke="#769481" />
87 <rect x="105" y="237" width="24" height="14" fill="#23382d" stroke="#769481" />
88 <rect x="71" y="234" width="17" height="19" rx="2" fill="#e4e5cf" />
89 <circle
90 cx="79.5"
91 cy="243.5"
92 r="5.5"
93 fill={color}
94 className={ledOn ? 'led-lit' : ''}
95 style={{ '--led-color': color } as CSSProperties}
96 data-testid="board-led"
97 data-rgb={led?.join(',') ?? 'unknown'}
98 />
99 <text x="80" y="272" textAnchor="middle" className="pcb-label">
100 RGB / 38
101 </text>
102 <rect x="55" y="291" width="28" height="19" rx="3" fill="#192f25" stroke="#6e8c7c" />
103 <circle cx="69" cy="300" r="6" fill="#899384" />
104 <rect x="145" y="291" width="28" height="19" rx="3" fill="#192f25" stroke="#6e8c7c" />
105 <circle cx="159" cy="300" r="6" fill="#899384" />
106 <text x="69" y="323" textAnchor="middle" className="pcb-label">
107 BOOT
108 </text>
109 <text x="159" y="323" textAnchor="middle" className="pcb-label">
110 RESET
111 </text>
112 <g data-connector="uart">
113 <rect x="60" y="328" width="35" height="32" rx="4" fill="#bdc4b8" stroke="#829488" />
114 <rect x="65" y="343" width="25" height="12" rx="5" fill="#27382c" />
115 </g>
116 <g data-connector="usb">
117 <rect x="138" y="328" width="35" height="32" rx="4" fill="#bdc4b8" stroke="#829488" />
118 <rect x="143" y="343" width="25" height="12" rx="5" fill="#27382c" />
119 </g>
120 {/* Logical Wi-Fi streams meet the edge of the illustration. These
121 markers do not represent USB connections or GPIO assignments. */}
122 <g aria-hidden="true" className="hidden lg:block">
123 <circle
124 data-anchor={anchors.sources.audio}
125 cx="213"
126 cy="140"
127 r="3.5"
128 className="fill-surface stroke-audio"
129 strokeWidth="1.5"
130 />
131 <circle
132 data-anchor={anchors.sources.spectrum}
133 cx="213"
134 cy="192"
135 r="3.5"
136 className="fill-surface stroke-spectrum"
137 strokeWidth="1.5"
138 />
139 <circle
140 data-anchor={anchors.sources.robot}
141 cx="213"
142 cy="244"
143 r="3.5"
144 className="fill-surface stroke-robot"
145 strokeWidth="1.5"
146 />
147 </g>
148 <circle
149 data-anchor={anchors.wifi}
150 cx="213"
151 cy="64"
152 r="3.5"
153 aria-hidden="true"
154 className="fill-surface stroke-muted lg:hidden"
155 strokeWidth="1.5"
156 />
157 </svg>
158 );
159}