socle

favicon: the pedestal glyph, in the theme hue

commit f748a0a9425d99f8e8afd0aa827aa651249128b3 · parent 9d1cce7ca2 · browse this tree
Ximianth, 2026-10-30 14:36:27 UTC

One asset, not per-scheme: a favicon renders on tab chrome whose color socle
cannot know, so it wears the theme hue at a middle lightness that stays
visible on both light and dark tab bars, with proportionally bold strokes
(checked by eye at 16/32/64px against mock tabs in both chromes).
Known limit: Safari ignores SVG favicons and keeps 404ing on /favicon.ico.
diff --git a/chrome.go b/chrome.go
index b009eeb..0f0e5d5 100644
--- a/chrome.go
+++ b/chrome.go
@@ -75,6 +75,7 @@ var soclePageTmpl = template.Must(template.New("socle").Parse(`<!doctype html>
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>{{.Title}}</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
diff --git a/sketch.go b/sketch.go
index c4cf578..adf619e 100644
--- a/sketch.go
+++ b/sketch.go
@@ -5,6 +5,8 @@ import (
 	"hash/fnv"
 	"math"
 	"strings"
+
+	"vitrine/display"
 )
 
 // This file generates the sketch assets: small SVGs of hand-wobbled strokes, boxes,
@@ -146,6 +148,7 @@ func sketchAssets(seed string) map[string][]byte {
 			m["circle-"+v+"-"+p.scheme+".svg"] = circleSVG(seed+"/"+v, p)
 		}
 	}
+	m["favicon.svg"] = faviconSVG(seed)
 	return m
 }
 
@@ -345,6 +348,30 @@ func plinthSVG(seed string, p sketchPalette) []byte {
 	return []byte(sb.String())
 }
 
+// faviconSVG is the tab's identity mark: the pedestal glyph in the theme hue.
+// One asset, not per-scheme: a favicon renders on tab chrome whose color socle
+// cannot know, so instead of the accent's paper-tuned per-scheme lightnesses it
+// wears the same hue at a middle lightness that stays visible on both.
+// Strokes are proportionally bold: at tab size the usual hairlines would vanish.
+func faviconSVG(seed string) []byte {
+	ink := fmt.Sprintf("hsl(%d 70%% 50%%)", display.ThemeHue(seed))
+	rng := newSketchRand(seed + "/favicon")
+	var sb strings.Builder
+	svgOpen(&sb, 64, 64)
+	boxes := func(d *strings.Builder, amp, over float64) {
+		roughBox(d, rng, 12, 8, 40, 9, amp, over)
+		roughBox(d, rng, 20, 17, 24, 29, amp, over)
+		roughBox(d, rng, 6, 46, 52, 10, amp, over)
+	}
+	var d1, d2 strings.Builder
+	boxes(&d1, 1.0, 2.0)
+	boxes(&d2, 1.5, 2.5)
+	svgPath(&sb, d1.String(), ink, 5.0, 0.92)
+	svgPath(&sb, d2.String(), ink, 3.0, 0.4)
+	sb.WriteString("</svg>\n")
+	return []byte(sb.String())
+}
+
 // chipSVG is the frame's small sibling: a border-image tile for inline chips
 // (the ledger's sha buttons).  A 48x48 tile sliced at 8 with lines at inset 4,
 // so a border a hair under the slice renders the stroke near 1:1; displacement
diff --git a/testdata/pages.md b/testdata/pages.md
index cfd6cd5..f4fe89d 100644
--- a/testdata/pages.md
+++ b/testdata/pages.md
@@ -30,6 +30,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>test</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -79,6 +80,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -137,6 +139,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama @ main</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -185,6 +188,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama/docs @ main</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -239,6 +243,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama/README.md @ main</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -284,6 +289,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama/docs/with space.txt @ main</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -329,6 +335,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama/README.md @ ced91a8519</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -378,6 +385,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama log @ main</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -435,6 +443,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama commit ced91a8519</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
@@ -547,6 +556,7 @@ Content-Type: text/html; charset=utf-8
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>fixturama commit 64ac67a35d</title>
 <link rel="stylesheet" href="/-/style.css">
+<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
 </head>
 <body>
 <div class="flank left"></div><div class="flank right"></div>
diff --git a/testdata/sketch.md b/testdata/sketch.md
index 826c9b7..129b07b 100644
--- a/testdata/sketch.md
+++ b/testdata/sketch.md
@@ -227,3 +227,13 @@ Regenerate with `go test -run TestSketchSnapshots -testmark.regen` and review th
 </svg>
 ```
 
+### favicon.svg
+
+[testmark]:# (favicon.svg)
+```svg
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<path d="M10.9 7.7C24.9 8.1 38.8 8.6 52.8 8.1M52.2 6.5C51.9 10.8 51.3 15.1 51.5 19.4M52.2 17.4C38.4 18.0 24.5 16.9 10.7 16.8M12.4 16.6C11.6 13.4 12.1 10.1 11.7 6.9M20.2 16.6C28.6 17.0 37.1 16.2 45.5 16.6M44.1 16.3C43.3 26.7 44.7 37.2 43.8 47.6M44.5 46.2C35.9 45.7 27.2 46.0 18.6 46.1M19.7 47.0C19.0 37.1 20.1 27.2 19.8 17.3M5.7 46.0C23.3 46.4 40.9 45.2 58.5 45.5M58.2 46.0C58.8 49.9 58.2 53.9 58.2 57.8M59.7 56.4C41.8 55.7 23.9 56.0 6.0 55.7M6.1 56.4C6.6 52.6 5.9 48.9 6.2 45.1" fill="none" stroke="hsl(125 70% 50%)" stroke-width="5.0" stroke-opacity="0.92" stroke-linecap="round"/>
+<path d="M10.0 8.5C24.7 7.6 39.4 9.4 54.1 7.6M51.8 6.3C53.2 10.7 53.1 15.1 52.0 19.5M51.7 16.7C38.4 18.4 25.1 16.8 11.9 17.5M11.3 19.6C12.2 15.2 11.4 10.8 11.8 6.4M17.1 17.6C26.1 18.7 35.0 16.3 44.0 17.4M43.5 16.9C42.3 27.2 45.0 37.3 44.0 47.6M43.3 46.3C35.1 45.2 26.9 46.6 18.8 46.2M20.2 47.3C20.5 36.3 20.8 25.4 19.3 14.6M5.6 46.4C23.1 46.4 40.7 45.2 58.2 46.4M57.7 44.0C59.1 48.7 57.4 53.5 57.7 58.3M59.4 55.5C41.0 55.3 22.6 56.3 4.3 56.0M6.7 56.2C7.4 52.7 5.5 49.5 5.4 46.1" fill="none" stroke="hsl(125 70% 50%)" stroke-width="3.0" stroke-opacity="0.40" stroke-linecap="round"/>
+</svg>
+```
+