1/** @jsxImportSource @alchemy.run/sigil */
2/**
3 * The yantra logo, rasterized at runtime. Geometry mirrors
4 * website/src/brand/yantra.ts (the brand's source of truth): a downward
5 * equilateral triangle inscribed in a circle, bindu at the center. Rendered
6 * as braille dots (2x4 subpixels per cell) via signed distance functions, so
7 * it scales to any column width.
8 */
9import { Box, Text } from "../ui/index.ts";
10import { useMemo } from "@alchemy.run/sigil/react";
11import type { JSX } from "react";
12import { theme } from "../../CliKit/index.ts";
13
14const CENTER = 12;
15const CIRCLE_R = 9.5;
16const BINDU_R = 1.1;
17const DOT_THRESHOLD = 0.35;
18// stroke thickness in braille subpixels, independent of logo size — keeps the
19// outline delicate instead of thickening into solid slabs as the logo grows
20const STROKE_SUBPIXELS = 2.5;
21
22const trianglePoints = (
23 strokeW: number,
24): ReadonlyArray<readonly [number, number]> => {
25 const triR = CIRCLE_R - strokeW / 4;
26 const triDx = triR * Math.cos(Math.PI / 6);
27 const triTopY = CENTER - triR * Math.sin(Math.PI / 6);
28 const triApexY = CENTER + triR;
29 return [
30 [CENTER, triApexY],
31 [CENTER - triDx, triTopY],
32 [CENTER + triDx, triTopY],
33 ];
34};
35
36const sdCircleRing = (x: number, y: number, strokeW: number) =>
37 Math.abs(Math.hypot(x - CENTER, y - CENTER) - CIRCLE_R) - strokeW / 2;
38
39const sdSegment = (
40 px: number,
41 py: number,
42 ax: number,
43 ay: number,
44 bx: number,
45 by: number,
46) => {
47 const abx = bx - ax;
48 const aby = by - ay;
49 const apx = px - ax;
50 const apy = py - ay;
51 const t = Math.max(
52 0,
53 Math.min(1, (apx * abx + apy * aby) / (abx * abx + aby * aby)),
54 );
55 return Math.hypot(apx - t * abx, apy - t * aby);
56};
57
58const sdTriangle = (
59 x: number,
60 y: number,
61 pts: ReadonlyArray<readonly [number, number]>,
62 strokeW: number,
63) => {
64 let d = Infinity;
65 for (let i = 0; i < 3; i++) {
66 const [ax, ay] = pts[i];
67 const [bx, by] = pts[(i + 1) % 3];
68 d = Math.min(d, sdSegment(x, y, ax, ay, bx, by));
69 }
70 return d - strokeW / 2;
71};
72
73const sdBindu = (x: number, y: number) =>
74 Math.hypot(x - CENTER, y - CENTER) - BINDU_R;
75
76// braille dot bit per (row 0-3, column 0-1) subpixel
77const BRAILLE_BITS = [
78 [0x01, 0x08],
79 [0x02, 0x10],
80 [0x04, 0x20],
81 [0x40, 0x80],
82] as const;
83
84interface LogoRun {
85 text: string;
86 color?: string;
87}
88
89const rasterizeLogo = (cols: number): LogoRun[][] => {
90 const width = cols * 2; // subpixel grid, square (2 wide x 4 tall per cell)
91 const rows = Math.ceil(width / 4);
92 const scale = 24 / width;
93 const strokeW = STROKE_SUBPIXELS * scale;
94 const triPts = trianglePoints(strokeW);
95 const lines: LogoRun[][] = [];
96 for (let r = 0; r < rows; r++) {
97 const runs: LogoRun[] = [];
98 const push = (text: string, color?: string) => {
99 const last = runs[runs.length - 1];
100 if (last && last.color === color) last.text += text;
101 else runs.push({ text, color });
102 };
103 for (let c = 0; c < cols; c++) {
104 let bits = 0;
105 let strokeSum = 0;
106 let binduSum = 0;
107 for (let sy = 0; sy < 4; sy++) {
108 for (let sx = 0; sx < 2; sx++) {
109 const x = (c * 2 + sx + 0.5) * scale;
110 const y = (r * 4 + sy + 0.5) * scale;
111 const dStroke = Math.min(
112 sdCircleRing(x, y, strokeW),
113 sdTriangle(x, y, triPts, strokeW),
114 );
115 const strokeCov = Math.min(1, Math.max(0, 0.5 - dStroke / scale));
116 const binduCov = Math.min(
117 1,
118 Math.max(0, 0.5 - sdBindu(x, y) / scale),
119 );
120 if (Math.max(strokeCov, binduCov) > DOT_THRESHOLD) {
121 bits |= BRAILLE_BITS[sy][sx];
122 strokeSum += strokeCov;
123 binduSum += binduCov;
124 }
125 }
126 }
127 if (bits === 0) push(" ");
128 else {
129 push(
130 String.fromCharCode(0x2800 + bits),
131 binduSum > strokeSum ? theme.color.brand : theme.color.accent,
132 );
133 }
134 }
135 // drop trailing whitespace runs
136 while (runs.length > 0) {
137 const last = runs[runs.length - 1];
138 if (last.color !== undefined || last.text.trim() !== "") break;
139 runs.pop();
140 }
141 lines.push(runs);
142 }
143 while (lines.length > 0 && lines[0].length === 0) lines.shift();
144 while (lines.length > 0 && lines[lines.length - 1].length === 0) lines.pop();
145 return lines;
146};
147
148// Braille rasterization is mojibake on non-Unicode terminals — the help
149// formatter skips mounting the logo entirely when Unicode is unavailable.
150type LogoProps = { cols: number };
151
152export function Logo({ cols }: LogoProps): JSX.Element | null {
153 const lines = useMemo(() => rasterizeLogo(cols), [cols]);
154 return (
155 <Box flexDirection="column" flexShrink={0}>
156 {lines.map((runs, i) => (
157 <Text key={i}>
158 {runs.length === 0
159 ? " "
160 : runs.map((run, j) => (
161 <Text key={j} color={run.color}>
162 {run.text}
163 </Text>
164 ))}
165 </Text>
166 ))}
167 </Box>
168 );
169}