From 4b293de88791a8a863db3e1605c67bb04eceb86a Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 7 Aug 2026 17:23:01 -0400 Subject: [PATCH] feat(web): give each host its own colours, and roll a half faster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every name is set in the colour its site is mostly made of, over a halo feathered in a second colour from the same site — Sprk's green against their pink, Blacksky's lime against their periwinkle, Tangled's green against their slate. Bluesky, mu.social and Northsky have no second colour of their own, so they take a pale tint of the one they have and are lit from behind by it at full strength. The halo is four shadows at widening radii rather than one. A single wide shadow loses its chroma before its brightness and comes out white whatever colour it started as; the tight ones stack up almost solid against the letters, which is the only place the hue survives, and the wide one carries the light off them. Holds go 2.6s to 1.7s and 3.4s to 2.25s, and the roll 0.74s to 0.5s. --- web/scripts/host-cycle.test.mjs | 26 ++++++++- web/src/host-cycle.ts | 99 +++++++++++++++++++++++++++------ web/src/styles.css | 33 ++++++++--- 3 files changed, 134 insertions(+), 24 deletions(-) diff --git a/web/scripts/host-cycle.test.mjs b/web/scripts/host-cycle.test.mjs index a51e699..781aa6e 100644 --- a/web/scripts/host-cycle.test.mjs +++ b/web/scripts/host-cycle.test.mjs @@ -24,7 +24,7 @@ const cycle = await read("src/host-cycle.ts"); const home = await read("src/screens/home.ts"); const css = await read("src/styles.css"); -const hosts = [...cycle.matchAll(/^ {2}"([^"]+)",$/gm)].map((m) => m[1]); +const hosts = [...cycle.matchAll(/name: "([^"]+)"/g)].map((m) => m[1]); assert.ok( hosts.length > 1, "host-cycle.ts no longer declares HOSTS as literals", @@ -41,6 +41,30 @@ test("every host name carries its own full stop", () => { } }); +test("every host brings both of its colours, and both follow the theme", () => { + const entries = [ + ...cycle.matchAll(/name: "([^"]+)",\s*ink: (.+),\s*glow: (.+),/g), + ]; + assert.equal( + entries.length, + hosts.length, + "a host is missing an ink or a glow, and would fall back to plain text", + ); + for (const [, name, ink, glow] of entries) { + for (const [what, value] of [ + ["ink", ink], + ["glow", glow], + ]) { + assert.match( + value, + /light-dark\(|var\(--/, + `${name} picks one ${what} for both themes; the toggle cannot change it ` + + "and one of the two pages gets a colour that does not read", + ); + } + } +}); + test("the sentence does not end in a full stop of its own", () => { const at = home.indexOf("hero-lead"); const lead = home.slice(at, home.indexOf("form,", at)); diff --git a/web/src/host-cycle.ts b/web/src/host-cycle.ts index f54f1e1..ec8b6f2 100644 --- a/web/src/host-cycle.ts +++ b/web/src/host-cycle.ts @@ -27,28 +27,87 @@ import { el } from "./dom"; * discourse.atmosphere.community and the hosts named on atmosphereaccount.com, * which is where the sign-in form's heading already sends people. * - * Bluesky leads because it is the one a stranger recognises, and because it is - * what the sentence said before it started moving. "your PDS" is last because - * it is the point: this is not a list of the places we allow, it is a list that - * ends in the player's own machine. + * The first three are what the order is about: Bluesky, because it is the one + * a stranger recognises and it is what the sentence said before it started + * moving; then mu.social; then the player's own machine, which is the point + * being made. The rest follow, arranged only so that no two of them look alike + * in a row — Blacksky and Northsky are both purple, Sprk and mu.social are both + * pink. * * The full stop belongs to each name rather than to the sentence. The names are * stacked in one box, so anything after them would have to sit at the far edge * of the widest one, stranded behind a gap after every short name. + * + * Each name wears its host's own colours, sampled off their sites on + * 2026-08-07. `ink` is the type and `glow` is the halo feathered behind it. + * + * Sample the whole page, not the part above the fold. Two mistakes here were + * that. Blacksky came out lime, which is only their bracketed links over a page + * that is black and periwinkle four to one; Tangled came out green, which is + * one button on a page that is slate and white by two orders of magnitude. + * + * Both colours have to come off the site. Where a host has two of their own + * that is the pair: Sprk's green against their pink, Blacksky's lime against + * their periwinkle, Tangled's green against their slate. Bluesky, mu.social and + * Northsky are each one colour and nothing else, and an earlier set gave them a + * complement off the wheel instead — amber, cyan, gold. It read as invented, + * because it was; nothing on bsky.app is amber. So those three take a pale tint + * of their own colour as the ink and that colour at full strength as the halo, + * which reads as the brand lighting the word from behind rather than as a + * second brand nobody has. + * + * "your PDS" is the one entry that is nobody else's, so it gets ours: the + * masthead's teal lit by the accent blue. + * + * Both are light-dark() pairs. The theme control has to be able to change them, + * and light needs the opposite of what dark needs: on a dark page a halo + * carries by being lighter than the ground, on a light one by being darker. + * + * Every glow is saturated and none is a pastel. A pale colour blurred over a + * dark background has almost no chroma left to show and comes out white, which + * is what an earlier set of these all looked like. */ const HOSTS = [ - "Bluesky.", - "Blacksky.", - "Northsky.", - "mu.social.", - "Sprk.", - "Tangled.", - "your PDS.", + { + name: "Bluesky.", + ink: "light-dark(#0a5fd0, #93c8ff)", + glow: "light-dark(#0069cc, #0085ff)", + }, + { + name: "mu.social.", + ink: "light-dark(#a52d7c, #f9b3e2)", + glow: "light-dark(#db4aa6, #e0369f)", + }, + { + name: "your PDS.", + ink: "var(--brand)", + glow: "var(--accent)", + }, + { + name: "Sprk.", + ink: "light-dark(#c4006e, #ff3da6)", + glow: "light-dark(#0b6b32, #00c853)", + }, + { + name: "Blacksky.", + ink: "light-dark(#4a4ad4, #8b8bff)", + glow: "light-dark(#5f7a00, #b8e000)", + }, + { + name: "Tangled.", + ink: "light-dark(#3b4d63, #b9cce6)", + glow: "light-dark(#0f6b32, #16b356)", + }, + { + name: "Northsky.", + ink: "light-dark(#6d28d9, #cdb8ff)", + glow: "light-dark(#7325d6, #8338ec)", + }, ]; /** Long enough that the sentence gets read as written before anything moves. */ -const FIRST_HOLD_MS = 2600; -const HOLD_MS = 3400; +const FIRST_HOLD_MS = 1700; +const HOLD_MS = 2250; export function hostCycle(): HTMLElement { // Every name, all in the one grid cell, all hidden but for the one being @@ -57,9 +116,17 @@ export function hostCycle(): HTMLElement { // paragraph around it wraps the same way whichever name is up. Measuring // instead would mean measuring again after the webfont lands, and being // wrong until it did. - const words = HOSTS.map((host) => - el("span", { className: "cycle-word", textContent: host }), - ); + const words = HOSTS.map((host) => { + const word = el("span", { + className: "cycle-word", + textContent: host.name, + }); + // Set here rather than in styles.css: this is the host's colour, which is + // data about the host, not a decision about how the page looks. + word.style.setProperty("--host-ink", host.ink); + word.style.setProperty("--host-glow", host.glow); + return word; + }); const cycle = el("span", { className: "hero-cycle" }, words); let showing = words[0]!; diff --git a/web/src/styles.css b/web/src/styles.css index c5e34d4..48afff0 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -745,14 +745,14 @@ body:has(.hero) { display, because display: none would stop the unshown names sizing the box and the width would be back to changing. - Brighter than the line around it and a shade heavier, so the eye has a reason - to be there when it moves. Not --brand: the h1 above it spends that, and two - accents in one panel is one too many. */ + A shade heavier than the line around it, and each name in its host's own + colours over a halo of their second one — host-cycle.ts sets the two, because + which colour belongs to Blacksky is a fact about Blacksky. The fallbacks are + what the sentence would look like with the colours taken away. */ .hero-cycle { display: inline-grid; vertical-align: top; perspective: 340px; - color: var(--ink); font-weight: 500; } @@ -761,6 +761,25 @@ body:has(.hero) { justify-self: start; white-space: nowrap; visibility: hidden; + color: var(--host-ink, var(--ink)); + /* The second colour is an outline that fades rather than stops. Four shadows + of the one colour at widening radii: the tight ones stack up almost solid + against the letters, which is the only place a blurred colour still has the + chroma to read as itself, and each one further out is weaker, so the edge + feathers instead of ending. A single wide shadow washes out to white; a + hard -webkit-text-stroke holds the hue but draws a line around the word. + + host-cycle.ts supplies the colour flat, with no alpha of its own, so how + far it carries is decided once here rather than seven times there. */ + text-shadow: + 0 0 0.035em + color-mix(in srgb, var(--host-glow, transparent) 90%, transparent), + 0 0 0.08em + color-mix(in srgb, var(--host-glow, transparent) 75%, transparent), + 0 0 0.17em + color-mix(in srgb, var(--host-glow, transparent) 50%, transparent), + 0 0 0.65em + color-mix(in srgb, var(--host-glow, transparent) 32%, transparent); } /* The one being shown, and — for the length of a swap — the one being replaced. @@ -2326,7 +2345,7 @@ footer .debug { The delay on the arriving name is the whole of it. Run together, both names sit at half opacity in the same place at the same moment and the reader gets one word printed over another, which is what this looked like first - time round. Leaving is 0.34s, arriving starts at 0.24s, so the old name is + time round. Leaving is 0.23s, arriving starts at 0.16s, so the old name is all but gone before there is anything to read behind it. The rotation is what makes that a roll rather than two fades. @@ -2359,12 +2378,12 @@ footer .debug { .hero-cycle .cycle-in { z-index: 1; - animation: cycle-roll-in 0.5s cubic-bezier(0.16, 0.84, 0.3, 1) 0.24s both; + animation: cycle-roll-in 0.34s cubic-bezier(0.16, 0.84, 0.3, 1) 0.16s both; } .hero-cycle .cycle-out { z-index: 0; - animation: cycle-roll-out 0.34s cubic-bezier(0.45, 0, 0.9, 0.45) both; + animation: cycle-roll-out 0.23s cubic-bezier(0.45, 0, 0.9, 0.45) both; } .hero-mark::before { -- 2.51.2