Add light mode that follows the device setting

Dark stays the default. A device set to light gets the picks from the
light motif board: the Dawn chart sky (dawn wash over faint star chart
lines, atlas marks in the hero), deep gold line seals, Midnight, violet and
gold dust, the deep gold to teal filament and the morning sky placeholder.
Large standalone seals (404, Inquiries, Inquiry sent, the blog's lead) are
gold foil in both modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-18 18:54:56 +00:00
parent 8e5e5cbef8
commit 7aa2910b12
101 changed files with 541 additions and 161 deletions
+65
View File
@@ -0,0 +1,65 @@
/**
* 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). Run after a motif 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`);