| 1 | /** |
| 2 | * Single source of truth for Alchemy's brand mark — a Sri-Yantra-style |
| 3 | * downward water triangle inscribed in a circle, with the centroid dot. |
| 4 | * |
| 5 | * Both the runtime Astro component and the build-time asset generators |
| 6 | * (favicon, OG images) consume this module so the geometry is defined once. |
| 7 | */ |
| 8 | |
| 9 | const YANTRA = { |
| 10 | viewBox: [0, 0, 24, 24], |
| 11 | center: 12, |
| 12 | circleRadius: 9.5, |
| 13 | binduRadius: 1.1, |
| 14 | strokeWidth: 1.1, |
| 15 | } as const; |
| 16 | |
| 17 | /** |
| 18 | * Path for the equilateral triangle (apex down), centered on the circle: |
| 19 | * |
| 20 | * apex : (cx, cy + r) |
| 21 | * top-left : (cx - r·cos30°, cy - r·sin30°) |
| 22 | * top-right: (cx + r·cos30°, cy - r·sin30°) |
| 23 | * |
| 24 | * `r` is the circle's radius pulled in by a quarter stroke width. A round join |
| 25 | * extends half a stroke width past the vertex, so this lands each tip's outer |
| 26 | * edge midway between the ring's centerline and its outer edge: far enough in |
| 27 | * that the tips don't bulge the ring's silhouette, but still deep enough into |
| 28 | * the band that the corners read as merged rather than butted up against it. |
| 29 | * (A half-width inset stops the tips at the centerline, which opens a visible |
| 30 | * notch at each corner.) The triangle's centroid stays at the circle center |
| 31 | * regardless, so the bindu is shared. |
| 32 | * |
| 33 | * Derived rather than transcribed, so the coordinates cannot drift from the |
| 34 | * formulas above. Rounded to 4 decimals — well under a pixel at any size we |
| 35 | * rasterize. |
| 36 | */ |
| 37 | function yantraTrianglePath(): string { |
| 38 | const r = YANTRA.circleRadius - YANTRA.strokeWidth / 4; |
| 39 | const round = (n: number) => Number(n.toFixed(4)); |
| 40 | const dx = round(r * Math.cos(Math.PI / 6)); |
| 41 | const topY = round(YANTRA.center - r * Math.sin(Math.PI / 6)); |
| 42 | const apexY = round(YANTRA.center + r); |
| 43 | return `M${YANTRA.center} ${apexY} L${YANTRA.center - dx} ${topY} L${YANTRA.center + dx} ${topY} Z`; |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * Brand palette, one entry per theme — mirrors tokens.css. Every rendering of |
| 48 | * the mark is painted from one of these two entries, so the favicons, app |
| 49 | * icons, OG cards and README hero can't drift apart. |
| 50 | */ |
| 51 | export const YANTRA_THEMES = { |
| 52 | light: { |
| 53 | /** `--alc-accent-deep` */ |
| 54 | stroke: "#3f5a2a", |
| 55 | /** `--alc-terracotta-deep`, mirrored by `--alc-yantra-dot` */ |
| 56 | dot: "#9a4f27", |
| 57 | /** `--alc-bg`. Only used where an asset must be opaque. */ |
| 58 | bg: "#f5efe3", |
| 59 | }, |
| 60 | dark: { |
| 61 | /** `--alc-accent-deep` (dark block) */ |
| 62 | stroke: "#a3c473", |
| 63 | /** `--alc-terracotta` (dark block), mirrored by `--alc-yantra-dot` */ |
| 64 | dot: "#d8835a", |
| 65 | /** `--alc-bg` (dark block) */ |
| 66 | bg: "#14110d", |
| 67 | }, |
| 68 | } as const; |
| 69 | |
| 70 | export type YantraTheme = keyof typeof YANTRA_THEMES; |
| 71 | |
| 72 | export interface YantraOptions { |
| 73 | /** Pixel size of the rendered SVG (square). Default 24. */ |
| 74 | size?: number; |
| 75 | /** Brand palette. Default light; `auto` follows theme tokens in inline SVG. */ |
| 76 | theme?: YantraTheme | "auto"; |
| 77 | /** Output format. `svg` for inline SVG, `url` for data URL. Default `svg`. */ |
| 78 | output?: "svg" | "url"; |
| 79 | } |
| 80 | |
| 81 | export function yantraSvg({ |
| 82 | size = 24, |
| 83 | theme = "light", |
| 84 | output = "svg", |
| 85 | }: YantraOptions = {}): string { |
| 86 | const colors = YANTRA_THEMES[theme === "auto" ? "light" : theme]; |
| 87 | |
| 88 | const stroke = |
| 89 | theme === "auto" |
| 90 | ? `var(--alc-accent-deep, ${colors.stroke})` |
| 91 | : colors.stroke; |
| 92 | |
| 93 | const dot = |
| 94 | theme === "auto" ? `var(--alc-yantra-dot, ${colors.dot})` : colors.dot; |
| 95 | |
| 96 | const svg = [ |
| 97 | `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${YANTRA.viewBox.join(" ")}" fill="none" stroke="${stroke}" stroke-width="${YANTRA.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">`, |
| 98 | `<circle cx="${YANTRA.center}" cy="${YANTRA.center}" r="${YANTRA.circleRadius}"/>`, |
| 99 | `<path d="${yantraTrianglePath()}"/>`, |
| 100 | `<circle cx="${YANTRA.center}" cy="${YANTRA.center}" r="${YANTRA.binduRadius}" fill="${dot}" stroke="none"/>`, |
| 101 | `</svg>`, |
| 102 | ].join(""); |
| 103 | |
| 104 | return output === "url" |
| 105 | ? `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}` |
| 106 | : svg; |
| 107 | } |