aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-30 20:24:53 +0200
committeruser <user@clank>2026-08-30 20:24:53 +0200
commit65567cfbbc301e047cf1f84fdad305e28f199a4f (patch)
tree3bda3bab1e5992cf6e977fc712d4791cdfb1536c
parentweb: the magnet scroll goes (diff)
downloadslosh-65567cfbbc301e047cf1f84fdad305e28f199a4f.tar.gz
slosh-65567cfbbc301e047cf1f84fdad305e28f199a4f.tar.xz
slosh-65567cfbbc301e047cf1f84fdad305e28f199a4f.zip
web: the logo becomes geometry -- fonts cannot break rects
Android reports arrived of the logo rendering shattered, and the cause is the ascii itself: the default monospace there has no block glyphs, so every ▀▄█ falls back to whatever font volunteers, each with its own metrics, and the grid comes apart. No font stack fixes a missing glyph, and shipping a font is against the house rules. So the logo stops being text: web/gen-logo-mask turns logo.txt into an SVG of merged rects, inlined into site.css as a data URI and used as a CSS mask on an empty element. The animated background layers show through the mask exactly as they showed through background-clip: text, so every effect survives -- gradient slide, shine, hue, glow -- and two old hacks retire with the fonts: the letter-spacing pinstripe fix, and (after one round of seams at fractional mask scales) a vertical overlap baked into the rects. Same 318x84 box, same placement math, and a hero that reads as role=img with a label instead of seven rows of block glyphs.
-rwxr-xr-xweb/gen-logo-mask73
-rw-r--r--web/index.html23
-rw-r--r--web/site.css45
3 files changed, 105 insertions, 36 deletions
diff --git a/web/gen-logo-mask b/web/gen-logo-mask
new file mode 100755
index 0000000..8997a5c
--- /dev/null
+++ b/web/gen-logo-mask
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""Regenerate the logo mask in site.css from logo.txt.
+
+The site draws the logo as geometry -- an SVG of rects used as a CSS mask --
+because the half-block glyphs the ascii original is typed in do not survive
+every platform's font fallback (Android's default monospace has none of
+them). This script turns logo.txt into that SVG and splices it into
+site.css's --logo-svg custom property in place.
+
+Each character cell is 6x10 units; ▀ fills the top half, ▄ the bottom, █
+both. Horizontal runs merge into single rects, and every rect is drawn with
+a little vertical overlap (EPS): stacked halves land on fractional pixels
+when the mask scales, and the seams read as pinstripes without it.
+"""
+
+import os
+import re
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+LOGO = os.path.join(os.path.dirname(HERE), "logo.txt")
+CSS = os.path.join(HERE, "site.css")
+
+CW, CH, EPS = 6, 10, 0.4
+
+
+def runs(row, want):
+ out, start = [], None
+ for i, ch in enumerate(row + " "):
+ hit = ch in want
+ if hit and start is None:
+ start = i
+ if not hit and start is not None:
+ out.append((start, i))
+ start = None
+ return out
+
+
+def num(v):
+ return f"{v:.1f}".rstrip("0").rstrip(".")
+
+
+lines = open(LOGO).read().rstrip("\n").splitlines()
+rects = []
+for j, row in enumerate(lines):
+ for a, b in runs(row, "\u2580\u2588"):
+ rects.append((a * CW, j * CH - EPS, (b - a) * CW, CH / 2 + 2 * EPS))
+ for a, b in runs(row, "\u2584\u2588"):
+ rects.append((a * CW, j * CH + CH / 2 - EPS, (b - a) * CW, CH / 2 + 2 * EPS))
+
+w = max(len(l) for l in lines) * CW
+h = len(lines) * CH
+body = "".join(
+ f"<rect x='{num(x)}' y='{num(y)}' width='{num(rw)}' height='{num(rh)}'/>"
+ for x, y, rw, rh in rects
+)
+svg = f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {w} {h}'>{body}</svg>"
+uri = svg.replace("<", "%3C").replace(">", "%3E").replace("#", "%23")
+
+css = open(CSS).read()
+out = re.sub(
+ r'--logo-svg: url\("data:image/svg\+xml,[^"]*"\);',
+ f'--logo-svg: url("data:image/svg+xml,{uri}");',
+ css,
+ count=1,
+)
+if out == css and "--logo-svg" not in css:
+ sys.exit("gen-logo-mask: no --logo-svg property in site.css to replace")
+open(CSS, "w").write(out)
+print(
+ f"gen-logo-mask: {len(rects)} rects, viewBox 0 0 {w} {h}, "
+ f"{len(uri)} URI bytes -> site.css"
+)
diff --git a/web/index.html b/web/index.html
index 9c3adf4..fc50242 100644
--- a/web/index.html
+++ b/web/index.html
@@ -14,29 +14,16 @@
v-centering leaves empty. It fades in when the hero scrolls away
(the observer at the bottom of the page) and is a road back up. -->
<a class="logo-float" href="#" aria-label="slosh — back to top">
-<pre aria-hidden="true">
-▄██████▄ ███ ▄██████▄ ▄██████▄ ▄██ ▄██
-███ ▀▀ ███ ███ ███ ███ ▀▀ ███ ███
-▀██████▄ ███ ▄▄▄ ███ ███ ▀██████▄ ████████
-▄▄ ███ ███ ███ ███ ███ ▄▄ ███ ███ ███
-▀██████▀ ███████▀ ▀██████▀ ███████▀ ███ ███
-▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
-▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
-</pre>
+<span class="logo-art"></span>
</a>
<main>
<header class="hero">
-<pre class="logo">
-▄██████▄ ███ ▄██████▄ ▄██████▄ ▄██ ▄██
-███ ▀▀ ███ ███ ███ ███ ▀▀ ███ ███
-▀██████▄ ███ ▄▄▄ ███ ███ ▀██████▄ ████████
-▄▄ ███ ███ ███ ███ ███ ▄▄ ███ ███ ███
-▀██████▀ ███████▀ ▀██████▀ ███████▀ ███ ███
-▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
-▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
-</pre>
+<!-- The logo as geometry (a masked div; see .logo-art in site.css): the
+ ascii original lives in logo.txt, but block glyphs do not survive every
+ platform's font fallback, and rects do. -->
+<div class="logo logo-art" role="img" aria-label="slosh"></div>
<p class="tagline">a modern terminal multiplexer written in C on top of
<a href="https://github.com/ghostty-org/ghostty">libghostty-vt</a>.</p>
diff --git a/web/site.css b/web/site.css
index 0525c77..9977b92 100644
--- a/web/site.css
+++ b/web/site.css
@@ -65,6 +65,27 @@ kbd {
font-size: 0.85em;
}
+/* ---- the logo, as geometry ------------------------------------------- */
+/* The ascii logo drawn as rects and used as a mask, because the block
+ * glyphs it was typed in do not survive every platform: Android's default
+ * monospace has none of them, so each one falls back to whatever font
+ * volunteers, each with its own metrics, and the grid shatters. Geometry
+ * has no fallback to shatter through. Generated from logo.txt by
+ * web/gen-logo-mask (44x7 cells of half-blocks) -- and the animated background
+ * layers show through the mask exactly as they showed through
+ * background-clip: text. Rects that touch also join exactly, which
+ * retires the letter-spacing hack that closed the fonts' pinstripes. */
+:root {
+ --logo-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 264 70'%3E%3Crect x='6' y='-0.4' width='36' height='5.8'/%3E%3Crect x='54' y='-0.4' width='18' height='5.8'/%3E%3Crect x='114' y='-0.4' width='36' height='5.8'/%3E%3Crect x='168' y='-0.4' width='36' height='5.8'/%3E%3Crect x='222' y='-0.4' width='12' height='5.8'/%3E%3Crect x='252' y='-0.4' width='12' height='5.8'/%3E%3Crect x='0' y='4.6' width='48' height='5.8'/%3E%3Crect x='54' y='4.6' width='18' height='5.8'/%3E%3Crect x='108' y='4.6' width='48' height='5.8'/%3E%3Crect x='162' y='4.6' width='48' height='5.8'/%3E%3Crect x='216' y='4.6' width='18' height='5.8'/%3E%3Crect x='246' y='4.6' width='18' height='5.8'/%3E%3Crect x='0' y='9.6' width='18' height='5.8'/%3E%3Crect x='36' y='9.6' width='12' height='5.8'/%3E%3Crect x='54' y='9.6' width='18' height='5.8'/%3E%3Crect x='108' y='9.6' width='18' height='5.8'/%3E%3Crect x='138' y='9.6' width='18' height='5.8'/%3E%3Crect x='162' y='9.6' width='18' height='5.8'/%3E%3Crect x='198' y='9.6' width='12' height='5.8'/%3E%3Crect x='216' y='9.6' width='18' height='5.8'/%3E%3Crect x='246' y='9.6' width='18' height='5.8'/%3E%3Crect x='0' y='14.6' width='18' height='5.8'/%3E%3Crect x='54' y='14.6' width='18' height='5.8'/%3E%3Crect x='108' y='14.6' width='18' height='5.8'/%3E%3Crect x='138' y='14.6' width='18' height='5.8'/%3E%3Crect x='162' y='14.6' width='18' height='5.8'/%3E%3Crect x='216' y='14.6' width='18' height='5.8'/%3E%3Crect x='246' y='14.6' width='18' height='5.8'/%3E%3Crect x='0' y='19.6' width='42' height='5.8'/%3E%3Crect x='54' y='19.6' width='18' height='5.8'/%3E%3Crect x='108' y='19.6' width='18' height='5.8'/%3E%3Crect x='138' y='19.6' width='18' height='5.8'/%3E%3Crect x='162' y='19.6' width='42' height='5.8'/%3E%3Crect x='216' y='19.6' width='48' height='5.8'/%3E%3Crect x='6' y='24.6' width='42' height='5.8'/%3E%3Crect x='54' y='24.6' width='18' height='5.8'/%3E%3Crect x='84' y='24.6' width='18' height='5.8'/%3E%3Crect x='108' y='24.6' width='18' height='5.8'/%3E%3Crect x='138' y='24.6' width='18' height='5.8'/%3E%3Crect x='168' y='24.6' width='42' height='5.8'/%3E%3Crect x='216' y='24.6' width='48' height='5.8'/%3E%3Crect x='30' y='29.6' width='18' height='5.8'/%3E%3Crect x='54' y='29.6' width='18' height='5.8'/%3E%3Crect x='84' y='29.6' width='18' height='5.8'/%3E%3Crect x='108' y='29.6' width='18' height='5.8'/%3E%3Crect x='138' y='29.6' width='18' height='5.8'/%3E%3Crect x='192' y='29.6' width='18' height='5.8'/%3E%3Crect x='216' y='29.6' width='18' height='5.8'/%3E%3Crect x='246' y='29.6' width='18' height='5.8'/%3E%3Crect x='0' y='34.6' width='12' height='5.8'/%3E%3Crect x='30' y='34.6' width='18' height='5.8'/%3E%3Crect x='54' y='34.6' width='18' height='5.8'/%3E%3Crect x='84' y='34.6' width='18' height='5.8'/%3E%3Crect x='108' y='34.6' width='18' height='5.8'/%3E%3Crect x='138' y='34.6' width='18' height='5.8'/%3E%3Crect x='162' y='34.6' width='12' height='5.8'/%3E%3Crect x='192' y='34.6' width='18' height='5.8'/%3E%3Crect x='216' y='34.6' width='18' height='5.8'/%3E%3Crect x='246' y='34.6' width='18' height='5.8'/%3E%3Crect x='0' y='39.6' width='48' height='5.8'/%3E%3Crect x='54' y='39.6' width='48' height='5.8'/%3E%3Crect x='108' y='39.6' width='48' height='5.8'/%3E%3Crect x='162' y='39.6' width='48' height='5.8'/%3E%3Crect x='216' y='39.6' width='18' height='5.8'/%3E%3Crect x='246' y='39.6' width='18' height='5.8'/%3E%3Crect x='6' y='44.6' width='36' height='5.8'/%3E%3Crect x='54' y='44.6' width='42' height='5.8'/%3E%3Crect x='114' y='44.6' width='36' height='5.8'/%3E%3Crect x='162' y='44.6' width='42' height='5.8'/%3E%3Crect x='216' y='44.6' width='18' height='5.8'/%3E%3Crect x='246' y='44.6' width='18' height='5.8'/%3E%3Crect x='0' y='54.6' width='264' height='5.8'/%3E%3Crect x='0' y='64.6' width='264' height='5.8'/%3E%3C/svg%3E");
+}
+
+.logo-art {
+ display: block;
+ aspect-ratio: 264 / 70;
+ -webkit-mask: var(--logo-svg) center / 100% 100% no-repeat;
+ mask: var(--logo-svg) center / 100% 100% no-repeat;
+}
+
/* ---- the floating logo ---------------------------------------------- */
/* The hero logo again, small, in the corner the stage's v-centering leaves
* empty on desktop. Faded in by the observer in index.html once the big one
@@ -96,14 +117,8 @@ kbd {
* (the global a:hover) is a mess, and a colour flash is not much better.
* The pointer cursor alone says clickable. */
.logo-float:hover { text-decoration: none; }
-.logo-float pre {
- margin: 0;
- font-family: var(--mono);
- font-size: 12px;
- line-height: 1em;
- /* Block glyphs' ink can round a hair narrower than the advance width,
- * leaving pinstripe gaps between ██; tuck each glyph into the last. */
- letter-spacing: -0.05em;
+.logo-float .logo-art {
+ width: 318px; /* what 44 columns of 12px mono came to */
/* The colour cycles, slowly: a gradient of the pink's neighbours clipped
* to the ink and slid along it -- the logo's own little colour pass, in
* the spirit of the shaders it advertises. Horizontal on purpose: the
@@ -123,9 +138,6 @@ kbd {
background-size: 60% 100%, 200% 100%;
background-repeat: no-repeat, repeat;
background-position: -150% 0, 0% 0;
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
}
/* Two motions. The position slide scrolls the gradient through the ink --
* except in WebKit, which caches a text-clipped paint and never repaints it
@@ -133,7 +145,7 @@ kbd {
* rotation is the motion Safari does honour: filters run post-paint on the
* compositor, so every pixel cycles the wheel even over the cached clip.
* Chrome and Firefox get both, WebKit gets the rotation alone. */
-.logo-float.show pre {
+.logo-float.show .logo-art {
animation: logo-cycle 5s linear infinite, logo-hue 4s linear infinite;
}
/* One set of keyframes drives both layers: the colour slide moves linearly
@@ -157,7 +169,7 @@ kbd {
/* The preference is about motion, not colour: the slide and the shine --
* patterns that travel -- go; the hue rotation, colour changing in
* place, stays. Nothing moves, everything still breathes. */
- .logo-float.show pre { animation: logo-hue 5s linear infinite; }
+ .logo-float.show .logo-art { animation: logo-hue 5s linear infinite; }
}
@media (max-width: 54rem) {
@@ -178,8 +190,8 @@ main {
}
.hero .logo {
- line-height: 1em;
- letter-spacing: -0.05em; /* see .logo-float pre */
+ width: min(88vw, 352px); /* what 44 columns of the default mono came to */
+ margin: 0 auto;
/* The same dress as the floating logo, in truer colours: no hue
* rotation (that is what walks the little one off into blues), just a
* slide through the palette's own pinks -- and --dead's sunset orange,
@@ -194,9 +206,6 @@ main {
background-size: 60% 100%, 200% 100%;
background-repeat: no-repeat, repeat;
background-position: -150% 0, 0% 0;
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
filter: drop-shadow(0 6px 26px rgba(255, 95, 215, 0.25));
animation: hero-cycle 4s linear infinite;
}