/**
* Generates every brand asset in `public/` from one source of truth.
*
* bun run brand
*
* The favicons follow the construction Cursor uses for its own: a squircle
* filled with the theme background, an inset hairline at 20% of the foreground,
* and the mark centred at roughly 70% of the canvas height. The light file
* carries the light background and the dark file the dark one, and the document
* picks between them with `prefers-color-scheme`.
*
* The open-graph card follows their brand banner: the mark and the wordmark on
* the dark ground, centred, and nothing else. It is rendered with Takumi, the
* same renderer the per-page cards go through, so every card on the site is set
* in the same typeface.
*/
import { tmpdir } from "node:os";
import { render } from "takumi-js";
import { MARK_VIEW_BOX, markPath } from "../src/lib/mark";
const PUBLIC = new URL("../public/", import.meta.url).pathname;
/** Intermediate rasters for the ICO, which are not themselves shipped. */
const TMP = `${tmpdir()}/cursor-action-brand-`;
/** The same values `src/app.css` resolves its OKLCH tokens to. */
const LIGHT = { background: "#f7f7f4", foreground: "#26251e" };
const DARK = { background: "#14120b", foreground: "#edecec" };
// ---------------------------------------------------------------------------
// Favicons
// ---------------------------------------------------------------------------
const CANVAS = 512;
/** Two decimals is the finest a 512-unit canvas can resolve. */
const round = (x: number) => Number(x.toFixed(2));
/**
* A superellipse, |x/r|^n + |y/r|^n = 1, sampled and fitted with cubics rather
* than approximated by arcs. At n = 4.2 the corner turns as continuously as the
* rounded-rectangle shape platforms use for app icons, without needing a
* per-corner smoothing parameter.
*/
const squircle = (size: number, inset: number, exponent = 5): string => {
const r = size / 2 - inset;
const c = size / 2;
const SEGMENTS = 64;
/** Sample i, wrapping, so the curve closes without a special case. */
const at = (i: number): [number, number] => {
const t =
((((i % SEGMENTS) + SEGMENTS) % SEGMENTS) / SEGMENTS) * 2 * Math.PI;
const cos = Math.cos(t);
const sin = Math.sin(t);
const p = 2 / exponent;
return [
c + Math.sign(cos) * r * Math.abs(cos) ** p,
c + Math.sign(sin) * r * Math.abs(sin) ** p,
];
};
// Catmull-Rom through the samples, converted to Bezier control points, so the
// curve passes through every sample and stays smooth across the joins.
const first = at(0);
let d = `M${round(first[0])} ${round(first[1])}`;
for (let i = 0; i < SEGMENTS; i += 1) {
const [p0, p1, p2, p3] = [at(i - 1), at(i), at(i + 1), at(i + 2)];
const c1: [number, number] = [
p1[0] + (p2[0] - p0[0]) / 6,
p1[1] + (p2[1] - p0[1]) / 6,
];
const c2: [number, number] = [
p2[0] - (p3[0] - p1[0]) / 6,
p2[1] - (p3[1] - p1[1]) / 6,
];
d += `C${round(c1[0])} ${round(c1[1])} ${round(c2[0])} ${round(c2[1])} ${round(p2[0])} ${round(p2[1])}`;
}
return `${d}Z`;
};
/**
* The mark at the small-size counter, scaled to 71% of the canvas height and
* centred — the proportion Cursor gives its own mark inside the same square.
*/
const markGroup = (fill: string): string => {
const height = CANVAS * 0.71;
const scale = height / 24;
const offset = (CANVAS - height) / 2;
return (
`