diff options
| author | user <user@clank> | 2026-08-30 20:24:53 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-30 20:24:53 +0200 |
| commit | 65567cfbbc301e047cf1f84fdad305e28f199a4f (patch) | |
| tree | 3bda3bab1e5992cf6e977fc712d4791cdfb1536c | |
| parent | web: the magnet scroll goes (diff) | |
| download | slosh-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-x | web/gen-logo-mask | 73 | ||||
| -rw-r--r-- | web/index.html | 23 | ||||
| -rw-r--r-- | web/site.css | 45 |
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; } |
