// haiku-ish vector icons: a thick dark outline, light from the top left, one accent each.
// they're drawn once into an inline sprite per page (see Sprite.astro) and used by id, so
// the gradients live in a rendered svg. a gradient inside a display:none svg doesn't paint.
type Draw = (g: Grad, ...args: string[]) => string;
class Grad {
private n = 0;
defs = "";
constructor(private prefix: string) {}
private id() {
return `${this.prefix}-${++this.n}`;
}
lin(c1: string, c2: string, dir: "v" | "h" | "d" = "v") {
const id = this.id();
const [x2, y2] = dir === "d" ? [1, 1] : dir === "h" ? [1, 0] : [0, 1];
this.defs += `<linearGradient id="${id}" x1="0" y1="0" x2="${x2}" y2="${y2}"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></linearGradient>`;
return `url(#${id})`;
}
rad(c1: string, c2: string) {
const id = this.id();
this.defs += `<radialGradient id="${id}" cx=".35" cy=".3" r=".8"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></radialGradient>`;
return `url(#${id})`;
}
}
const O = 'stroke="#262626" stroke-width="1.6" stroke-linejoin="round"';
const ICONS: Record<string, Draw> = {
hydrant(g) {
const a = g.lin("#ff8a72", "#a8200f", "d"), b = g.lin("#ffd86a", "#c88a00");
return `<path d="M16 14a8 8 0 0 1 16 0z" fill="${a}" ${O}/><rect x="21.5" y="6" width="5" height="3.5" rx="1" fill="${a}" ${O}/>
<rect x="13" y="14" width="22" height="4" rx="1" fill="${b}" ${O}/><rect x="15" y="18" width="18" height="21" fill="${a}" ${O}/>
<rect x="8" y="22" width="7" height="7" rx="1.5" fill="${a}" ${O}/><rect x="33" y="22" width="7" height="7" rx="1.5" fill="${a}" ${O}/>
<circle cx="24" cy="27" r="4" fill="${b}" ${O}/><rect x="11" y="39" width="26" height="4" rx="1" fill="${b}" ${O}/>
<path d="M18 20v16" stroke="#fff" stroke-opacity=".45" stroke-width="2"/>`;
},
doc(g, c = "#4a82d4") {
const a = g.lin("#ffffff", "#d8d8d8", "d"), b = g.lin(c, "#1b1b1b");
return `<path d="M11 5h19l8 8v30H11z" fill="${a}" ${O}/><path d="M30 5v8h8" fill="#bcbcbc" ${O}/>
<rect x="11" y="5" width="5" height="38" fill="${b}" opacity=".85"/><path d="M11 5h19l8 8v30H11z" fill="none" ${O}/>
<path d="M20 19h13M20 24h13M20 29h13M20 34h9" stroke="#8a8a8a" stroke-width="1.6"/>`;
},
flag(g) {
const a = g.lin("#9fe08f", "#2f7a22", "d"), b = g.lin("#e8e8e8", "#8a8a8a", "h");
return `<rect x="9" y="5" width="4" height="39" rx="1" fill="${b}" ${O}/>
<path d="M13 8c8-4 12 4 26 0v18c-14 4-18-4-26 0z" fill="${a}" ${O}/><path d="M16 12c6-2 10 3 18 1" stroke="#fff" stroke-opacity=".5" stroke-width="1.6" fill="none"/>`;
},
sliders(g) {
const a = g.lin("#f4f4f4", "#bdbdbd"), b = g.lin("#8ab4ee", "#1f4f9c", "d"), c = g.lin("#ffe46e", "#d09200", "d");
return `<rect x="6" y="8" width="36" height="32" rx="2" fill="${a}" ${O}/><path d="M15 13v22M24 13v22M33 13v22" stroke="#444" stroke-width="2.4"/>
<rect x="11" y="25" width="8" height="5" rx="1" fill="${b}" ${O}/><rect x="20" y="15" width="8" height="5" rx="1" fill="${c}" ${O}/><rect x="29" y="22" width="8" height="5" rx="1" fill="${b}" ${O}/>`;
},
chip(g) {
const a = g.lin("#5a5a5a", "#1b1b1b", "d");
const pins = [14, 20, 26, 32].map((y) => `<rect x="4" y="${y}" width="6" height="3" fill="#d8d8d8" ${O}/><rect x="38" y="${y}" width="6" height="3" fill="#d8d8d8" ${O}/>`).join("");
return `${pins}<rect x="10" y="9" width="28" height="30" rx="2" fill="${a}" ${O}/><circle cx="15" cy="14" r="2" fill="#8a8a8a"/>
<path d="M14 33h20" stroke="#ffcb00" stroke-width="2"/><path d="M12 11h22" stroke="#fff" stroke-opacity=".3" stroke-width="1.6"/>`;
},
stream(g) {
// water running past: two wavy bands
const a = g.lin("#bfe4ff", "#2a6ac0"), b = g.lin("#8ac4ff", "#1f4f9c");
const band = (y: number, fill: string) =>
`<path d="M4 ${y}c6-5 10-5 14 0s8 5 13 0 9-5 13 0v10c-4-5-8-5-13 0s-9 5-13 0-8-5-14 0z" fill="${fill}" ${O}/>`;
return `${band(9, a)}${band(25, b)}
<path d="M7 12.5c4-3 7-3 9.5-.5M7 28.5c4-3 7-3 9.5-.5" stroke="#fff" stroke-opacity=".75" stroke-width="1.8" stroke-linecap="round" fill="none"/>`;
},
jetstream() {
// wind: two gusts that curl at the end. a flat stroke has no height for a gradient to
// span, so the air is one colour
const gust = (d: string) =>
`<path d="${d}" stroke="#262626" stroke-width="7.4" stroke-linecap="round" fill="none"/><path d="${d}" stroke="#8fd8f0" stroke-width="4.2" stroke-linecap="round" fill="none"/>`;
return gust("M5 19h24a6 6 0 1 0-5.44-8.54") + gust("M9 29h28a6 6 0 1 1-5.44 8.54") +
`<path d="M7 19h20M11 29h24" stroke="#fff" stroke-opacity=".85" stroke-width="1.4" stroke-linecap="round" transform="translate(0 -1)"/>`;
},
nozzle(g) {
// a fire hose: a canvas hose with a loop in it, into a brass nozzle, spraying
const brass = g.lin("#ffe9a0", "#b07a10"), dark = g.lin("#e0b040", "#7a5208"), hose = g.lin("#f2ecdc", "#a89a7a");
const run = (d: string) => `<path d="${d}" stroke="#262626" stroke-width="7.4" stroke-linecap="round" fill="none"/><path d="${d}" stroke="${hose}" stroke-width="4.2" stroke-linecap="round" fill="none"/>`;
return `${run("M3 43.5c8 0 17-2 18-11s-7-12-12-9-5 12 1 14")}${run("M10 37.5c6 1.5 9-7 14-12.5")}
<g transform="translate(24 25) rotate(-40) scale(.78) translate(-15 -28)">
<rect x="15" y="22" width="7" height="12" rx="1.5" fill="${dark}" ${O}/>
<path d="M22 23.5l13 2.5v4l-13 2.5z" fill="${brass}" ${O}/>
<rect x="35" y="25" width="3" height="6" rx="1" fill="${dark}" ${O}/>
<path d="M23 25.5l11 2" stroke="#fff" stroke-opacity=".7" stroke-width="1.4"/>
<path d="M40 25.5l5.5-4M40.5 28h7M40 30.5l5.5 4" stroke="#1f5a9c" stroke-width="3.6" stroke-linecap="round"/>
<path d="M40 25.5l5.5-4M40.5 28h7M40 30.5l5.5 4" stroke="#8ac4ff" stroke-width="1.6" stroke-linecap="round"/>
</g>`;
},
info(g) {
// drawn for 16px: a fat glyph with no outline of its own, so it doesn't turn to mud
const a = g.rad("#a8d0ff", "#1f4f9c");
return `<circle cx="24" cy="24" r="20" fill="${a}" ${O}/><circle cx="24" cy="13.5" r="4.5" fill="#fff"/>
<rect x="19.5" y="20.5" width="9" height="18" rx="2.5" fill="#fff"/>`;
},
sun(g) {
const a = g.rad("#fff6b0", "#f0a000");
const rays = [0, 45, 90, 135, 180, 225, 270, 315].map((r) => `<path d="M24 3v7" transform="rotate(${r} 24 24)" stroke="#262626" stroke-width="6" stroke-linecap="round"/><path d="M24 3v7" transform="rotate(${r} 24 24)" stroke="#ffcb00" stroke-width="2.6" stroke-linecap="round"/>`).join("");
return `${rays}<circle cx="24" cy="24" r="10" fill="${a}" ${O}/>`;
},
moon(g) {
const a = g.rad("#fffbe0", "#d0b860");
return `<path d="M30 5a19 19 0 1 0 13 26A15 15 0 0 1 30 5z" fill="${a}" ${O}/><circle cx="18" cy="30" r="2.5" fill="#000" opacity=".12"/><circle cx="24" cy="38" r="1.8" fill="#000" opacity=".12"/>`;
},
// follow the system: half day, half night
dusk(g) {
const a = g.lin("#fffbe0", "#f0c040", "d"), b = g.lin("#5a7ab0", "#1f3a6a", "d");
return `<path d="M24 5a19 19 0 0 0 0 38z" fill="${a}"/><path d="M24 5a19 19 0 0 1 0 38z" fill="${b}"/><circle cx="24" cy="24" r="19" fill="none" ${O}/><path d="M24 5v38" ${O}/>`;
},
cyl(g, c1 = "#c8d8f0", c2 = "#4a6a9a") {
const a = g.lin(c1, c2, "h");
return `<path d="M8 12v24c0 4 7 7 16 7s16-3 16-7V12" fill="${a}" ${O}/><ellipse cx="24" cy="12" rx="16" ry="6" fill="#e8eef8" ${O}/>
<path d="M8 20c0 4 7 7 16 7s16-3 16-7M8 28c0 4 7 7 16 7s16-3 16-7" fill="none" ${O} stroke-opacity=".55"/><circle cx="34" cy="33" r="1.6" fill="#7fe07a" stroke="#1b4a1a"/>`;
},
funnel(g) {
const a = g.lin("#ffb08a", "#c8341f", "d");
return `<path d="M5 8h38L28 26v14l-8 4V26z" fill="${a}" ${O}/><ellipse cx="24" cy="8" rx="19" ry="3" fill="#ffe0d0" ${O}/><path d="M12 13l10 11" stroke="#fff" stroke-opacity=".5" stroke-width="2"/>`;
},
globe(g, c1 = "#b8e0ff", c2 = "#1f5a9c") {
const a = g.rad(c1, c2);
return `<circle cx="24" cy="24" r="18" fill="${a}" ${O}/><ellipse cx="24" cy="24" rx="8" ry="18" fill="none" ${O} stroke-opacity=".6"/>
<path d="M6 24h36M9 15h30M9 33h30" fill="none" ${O} stroke-opacity=".6"/>`;
},
sheets(g) {
return `<g transform="translate(6 -3)" opacity=".9">${ICONS.doc(g, "#8a8a8a")}</g><g transform="translate(-4 3)">${ICONS.doc(g, "#c8341f")}</g>`;
},
repo(g) {
// a folder of the account's records, as in "a social filesystem"
const front = g.lin("#ffe8a0", "#d0a040"), back = g.lin("#f0d080", "#a87a20"), who = g.rad("#d8e8ff", "#4a82d4");
const cards = [["#8ab4ee", 9, -8], ["#9fe08f", 17, 0], ["#ffe46e", 25, 8]]
.map(([c, x, r]) => `<rect x="${x}" y="9" width="13" height="18" rx="1.5" fill="${c}" ${O} transform="rotate(${r} ${+x + 6} 20)"/>`).join("");
return `<path d="M6 9h12l3 4h21v29H6z" fill="${back}" ${O}/>${cards}<path d="M3 20h42l-3 22H6z" fill="${front}" ${O}/>
<circle cx="33" cy="33" r="8" fill="${who}" ${O}/><circle cx="33" cy="30.4" r="2.8" fill="#fff"/><path d="M28.3 38.3a5.3 5.3 0 0 1 9.4 0" fill="#fff"/>`;
},
pieces(g) {
const a = g.lin("#8ab4ee", "#1f4f9c", "d"), b = g.lin("#ffe46e", "#d09200", "d");
return `<path d="M4 13h20v4a4.5 4.5 0 1 1 0 9v10H4z" fill="${a}" ${O}/><path d="M24 10h20v23H24v-7a4.5 4.5 0 1 0 0-9z" fill="${b}" ${O}/>
<path d="M7 16h13M27 13h13" stroke="#fff" stroke-opacity=".55" stroke-width="1.6" stroke-linecap="round"/>`;
},
switch(g) {
const a = g.lin("#5aa84a", "#1f5a1a"), b = g.lin("#ffffff", "#bdbdbd");
return `<rect x="4" y="14" width="40" height="20" rx="10" fill="${a}" ${O}/><circle cx="34" cy="24" r="8" fill="${b}" ${O}/><path d="M12 20v8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/>`;
},
glass(g) {
const a = g.rad("#e8f6ff", "#7ab0d8"), b = g.lin("#a07040", "#4a2a10", "d");
return `<path d="M29 29l11 11" stroke="#262626" stroke-width="8" stroke-linecap="round"/><path d="M29 29l11 11" stroke="${b}" stroke-width="5" stroke-linecap="round"/>
<circle cx="20" cy="20" r="13" fill="${a}" stroke="#262626" stroke-width="4"/><circle cx="20" cy="20" r="13" fill="none" stroke="#c8c8c8" stroke-width="1.6"/>
<path d="M13 17a8 8 0 0 1 6-6" stroke="#fff" stroke-width="2.4" stroke-linecap="round" fill="none"/>`;
},
waves(g) {
const a = g.lin("#e8e8e8", "#8a8a8a", "h");
return `<path d="M22 26l-8 18h20l-8-18" fill="${a}" ${O}/><circle cx="24" cy="22" r="4.5" fill="#ff6a4a" ${O}/>
<path d="M15 13a13 13 0 0 0 0 18M33 13a13 13 0 0 1 0 18M9 7a21 21 0 0 0 0 30M39 7a21 21 0 0 1 0 30" stroke="#1f4f9c" stroke-width="2.6" fill="none" stroke-linecap="round"/>`;
},
server(g) {
const a = g.lin("#e8e8e8", "#9a9a9a", "h");
const slots = [10, 18, 26].map((y) => `<rect x="16" y="${y}" width="16" height="5" fill="#3a3a3a" ${O}/>`).join("");
return `<rect x="12" y="4" width="24" height="40" rx="2" fill="${a}" ${O}/>${slots}
<circle cx="18" cy="37" r="2" fill="#7fe07a" stroke="#1b4a1a"/><circle cx="24" cy="37" r="2" fill="#ffd21a" stroke="#6a4a00"/>`;
},
card(g) {
const a = g.lin("#fff6d0", "#e0c060", "d");
return `<rect x="4" y="10" width="40" height="28" rx="3" fill="${a}" ${O}/><circle cx="15" cy="22" r="5" fill="#4a82d4" ${O}/><path d="M8 34c1-5 13-5 14 0" fill="#4a82d4" ${O}/>
<path d="M26 18h13M26 24h13M26 30h9" stroke="#8a7a40" stroke-width="2"/>`;
},
link() {
return `<rect x="4" y="17" width="22" height="14" rx="7" fill="none" stroke="#262626" stroke-width="6"/><rect x="4" y="17" width="22" height="14" rx="7" fill="none" stroke="#c8c8c8" stroke-width="3"/>
<rect x="22" y="17" width="22" height="14" rx="7" fill="none" stroke="#262626" stroke-width="6"/><rect x="22" y="17" width="22" height="14" rx="7" fill="none" stroke="#8ab4ee" stroke-width="3"/>`;
},
hourglass(g) {
const a = g.lin("#a07040", "#4a2a10"), b = g.lin("#ffe8a8", "#d0a040");
return `<rect x="9" y="4" width="30" height="5" rx="1" fill="${a}" ${O}/><rect x="9" y="39" width="30" height="5" rx="1" fill="${a}" ${O}/>
<path d="M13 9h22c0 8-8 11-8 15s8 7 8 15H13c0-8 8-11 8-15s-8-7-8-15z" fill="#e8f4ff" fill-opacity=".8" ${O}/>
<path d="M17 37c2-5 12-5 14 0z" fill="${b}"/><path d="M19 15h10l-5 6z" fill="${b}"/>`;
},
trash(g) {
const a = g.lin("#e8e8e8", "#7a7a7a", "h");
return `<path d="M10 14h28l-3 29H13z" fill="${a}" ${O}/><rect x="7" y="9" width="34" height="5" rx="1" fill="${a}" ${O}/><path d="M20 6h8v3h-8z" fill="#bdbdbd" ${O}/>
<path d="M18 19l1 19M24 19v19M30 19l-1 19" stroke="#5a5a5a" stroke-width="1.8"/>`;
},
scale(g) {
const a = g.lin("#ffe46e", "#c88a00", "d");
return `<path d="M24 8v32M14 42h20M8 12h32" stroke="#262626" stroke-width="3" stroke-linecap="round"/>
<path d="M8 12l-5 12h10zM40 12l-5 12h10z" fill="none" stroke="#262626" stroke-width="1.6"/>
<path d="M2 24a6 4 0 0 0 12 0zM34 24a6 4 0 0 0 12 0z" fill="${a}" ${O}/><circle cx="24" cy="8" r="3" fill="${a}" ${O}/>`;
},
};
/** "globe:#ffd0c0:#a8200f" -> "i-globe-ffd0c0-a8200f" */
export function iconId(spec: string): string {
return "i-" + spec.replace(/[^a-z0-9]+/gi, "-").replace(/-+$/, "").toLowerCase();
}
/** one <symbol> per spec, gradients inside it */
export function symbol(spec: string): string {
const [kind, ...args] = spec.split(":");
const draw = ICONS[kind];
if (!draw) throw new Error(`no icon called ${kind}`);
const id = iconId(spec);
const g = new Grad(id);
const body = draw(g, ...args);
return `<symbol id="${id}" viewBox="0 0 48 48">${g.defs}${body}</symbol>`;
}
/** one icon as a file of its own, for the favicon */
export function standalone(spec: string): string {
const [kind, ...args] = spec.split(":");
const g = new Grad(iconId(spec));
const body = ICONS[kind](g, ...args);
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">${g.defs}${body}</svg>`;
}
export function icon(spec: string, size = 48, cls = "icon"): string {
return `<svg class="${cls}" width="${size}" height="${size}" aria-hidden="true"><use href="#${iconId(spec)}"/></svg>`;
}