Files
com.gregkilmer.www/packages/ui/scripts/light-motifs.mjs
Claude 08df07f221 Give the GK monogram a light mode version
In light mode the header, footer and post placeholders show the monogram
on a pale disc with deep gold rings and ink letters, with a soft edge so
it sits apart from the Starlight page. The light file is written by the
light motifs script.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-18 20:30:32 +00:00

79 lines
3.5 KiB
JavaScript

/**
* Writes the light and foil versions of the motif SVGs from the dark originals
* in src/assets/motifs, using the inks Greg picked on the light motif board
* (2026-10-18), and the light GK monogram in src/assets/brand/light. Run after
* a motif or the monogram changes: node packages/ui/scripts/light-motifs.mjs
*
* light/ deep gold line: the dark inks swapped for Starlight-page inks, glow removed
* foil/ the dark seal with its gold as foil, glow kept
* foil-light/ the light seal with its gold as foil
*/
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const dir = join(import.meta.dirname, "../src/assets/motifs");
// Dark colors as they appear in the motif files, and their light inks.
const LIGHT = {
"#C9A24A": "#8A6516",
"#14A39A": "#0F8C84",
"#0E1522": "#F4F2ED",
"#101A2A": "#ECE8DF",
"#F4F2ED": "#131C2B",
"#F2D58A": "#A57C1F",
"#F6E7B8": "#8A6516",
"#2BC2B7": "#0F8C84",
"#9A70F5": "#6A2FE0",
"#FFF": "#F4F2ED",
};
const GOLD = ["#C9A24A", "#F2D58A"];
const stops = (s) => s.map(([o, c]) => `<stop offset="${o}" stop-color="${c}"/>`).join("");
const DARK_FOIL = stops([[0, "#8A6516"], [0.3, "#C9A24A"], [0.5, "#F6E7B8"], [0.68, "#C9A24A"], [1, "#8A6516"]]);
const LIGHT_FOIL = stops([[0, "#6F5112"], [0.35, "#A57C1F"], [0.5, "#C9A24A"], [0.65, "#8A6516"], [1, "#6F5112"]]);
const hex = (c) => new RegExp(c.replace("#", "#") + "(?![0-9A-Fa-f])", "gi");
function recolor(svg, map) {
// One pass, so a swapped color is never swapped again.
const keys = Object.keys(map).sort((a, b) => b.length - a.length);
const any = new RegExp(`(${keys.join("|")})(?![0-9A-Fa-f])`, "gi");
return svg.replace(any, (m) => map[keys.find((k) => k.toLowerCase() === m.toLowerCase())]);
}
function foil(svg, gradStops, golds) {
const [, , , w, h] = svg.match(/viewBox="([-\d.]+) ([-\d.]+) ([\d.]+) ([\d.]+)"/) ?? [0, 0, 0, 400, 400];
const grad = `<linearGradient id="gkfoil" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="${w}" y2="${h}">${gradStops}</linearGradient>`;
for (const g of golds) svg = svg.replace(hex(g), "url(#gkfoil)");
return svg.includes("<defs>") ? svg.replace("<defs>", `<defs>${grad}`) : svg.replace(/(<svg[^>]*>)/, `$1<defs>${grad}</defs>`);
}
// Glow filters are for the night sky only.
const unglow = (svg) => svg.replace(/\sfilter="url\(#[^)]*\)"/g, "");
for (const sub of ["light", "foil", "foil-light"]) mkdirSync(join(dir, sub), { recursive: true });
const files = readdirSync(dir).filter((f) => f.endsWith(".svg"));
for (const f of files) {
const dark = readFileSync(join(dir, f), "utf8");
const light = recolor(unglow(dark), LIGHT);
writeFileSync(join(dir, "light", f), light);
// Only the seals stand alone large enough for foil.
if (/seal|astrolabe|transmutation/.test(f) && !f.startsWith("mini-") && !f.startsWith("filament-banner")) {
writeFileSync(join(dir, "foil", f), foil(dark, DARK_FOIL, ["#C9A24A"]));
writeFileSync(join(dir, "foil-light", f), foil(light, LIGHT_FOIL, ["#8A6516", "#A57C1F"]));
}
}
console.log(`${files.length} motifs written`);
// The GK monogram on Starlight: a pale disc, deep gold rings and ink letters.
const MONOGRAM = {
...LIGHT,
"#131C2B": "#FFFFFF",
"#1F3B49": "#ECE6D8",
"#15232F": "#FBF9F4",
};
const brand = join(dir, "../brand");
mkdirSync(join(brand, "light"), { recursive: true });
writeFileSync(join(brand, "light", "gk-monogram.svg"), recolor(readFileSync(join(brand, "gk-monogram.svg"), "utf8"), MONOGRAM));
console.log("monogram written");