diff --git a/specs/STATUS.md b/specs/STATUS.md index 78428eb..d549705 100644 --- a/specs/STATUS.md +++ b/specs/STATUS.md @@ -7,10 +7,10 @@ _This file is a **card, not a ledger**: an entry that is done and no longer guides the work gets **deleted**, not archived. History lives in git and PR bodies; measured facts live in the spec they verify._ -_Last updated: 2026-08-07 (spec 10 §6.1 art direction landed: the -quiet-constellation skin — night-ground palette, wide-terminal sky panel with -braille mist + floating pet, narrow fallback unchanged; by-eye tuning stays -issue #79)_ +_Last updated: 2026-08-07 (spec 10 §6.1 refined per first by-eye feedback: +the pet is now the logo whisper-figure, the mist floor is an arc with the +figure in its hollow, and the palette is sampled from the concept mockup; +remaining tuning stays issue #79)_ ## Where we are diff --git a/specs/spec10/10-tui.md b/specs/spec10/10-tui.md index e377551..324018d 100644 --- a/specs/spec10/10-tui.md +++ b/specs/spec10/10-tui.md @@ -418,8 +418,10 @@ only the client — one shutdown path. call; the feed contract is the same.) - **As built (2026-08-07, §6.1)**: the bar strip is the narrow composition; wide terminals re-render the SAME feed as the constellation mist — - braille-cell particles climbing from a floating baseline, density following - the bin level, a sparse fallout drifting below it. The sky paints on the + braille-cell particles climbing from an **arced floor** (high at the panel's + edges, dipping at the center, the figure floating in the hollow), density + following the bin level, jittered so cells stay fine-grained, fading out at + the panel's side walls, a sparse fallout drifting below. The sky paints on the client's own 12fps clock and viz frames only feed the smoother, so stars twinkle and the pet animates even when the engine is silent. All of it is client-side arithmetic (`tui/src/constellation.ts`); the feed contract is @@ -615,7 +617,12 @@ the skin is `04-quiet-constellation`.** Its terms: (02 — per-scene hand art, cost without a daily payoff), the zine poster (03 — display type has no CJK form and no small-terminal degradation). -Still open, still this session's property: the pet's **identity** (the -whisper-figure logo motif and the stitched texture of concept 03 are the -reference material; today's creature holds the slot), and the by-eye tuning -of all of the above in a real terminal (issue #79). +**The pet's identity is the murmur logo (decided 2026-08-07):** the +whisper-figure in profile, rendered in the solid-fill cross-stitch reading — +hair as the dim accent mass, skin as the bright mass, dark seams carrying +the closed eye, mouth, and finger gaps. The committed sprites are generated +over a downscale of `assets/murmur-logo.svg`; whisper sparkles and drifting +notes are the pose overlays. Still open: the by-eye tuning of all of the +above in a real terminal (issue #79) — mist arc/density/fade, star density, +twinkle, figure size and placement, per-scene accents (sampled from the +concept mockup; values in `tui/src/palette.ts`). diff --git a/test/tui-constellation.test.ts b/test/tui-constellation.test.ts index d5889fb..3b3e6ea 100644 --- a/test/tui-constellation.test.ts +++ b/test/tui-constellation.test.ts @@ -9,7 +9,7 @@ import { describe, expect, it } from 'vitest' -import { Constellation, panelWidth, WIDE_MIN } from '../tui/src/constellation.ts' +import { Constellation, hash01, panelWidth, WIDE_MIN } from '../tui/src/constellation.ts' import { accentFor } from '../tui/src/palette.ts' const ACCENT = accentFor('late-night') @@ -101,6 +101,23 @@ describe('Constellation (§6.1: starfield + particle mist)', () => { expect(withPet.length).toBeGreaterThan(0) }) + it('bows the mist floor into an arc — edge columns bottom out higher than center ones', () => { + const rows = new Constellation(48, 20, 7).frame(Array.from({ length: 24 }, () => 0.6), ACCENT, null) + const deepestBraille = (fromCol: number, toCol: number): number => { + let deepest = -1 + rows.forEach((row, y) => { + const chars = [...rowText(row)] + for (let x = fromCol; x < toCol; x++) { + if (isBraille(chars[x] ?? ' ')) deepest = Math.max(deepest, y) + } + }) + return deepest + } + const edge = Math.max(deepestBraille(0, 6), deepestBraille(42, 48)) + const center = deepestBraille(21, 27) + expect(center).toBeGreaterThan(edge) + }) + it('survives hostile bins and degenerate panels', () => { const sky = new Constellation(2, 2, 7) for (const frame of [[], [Number.NaN], [2, -1], [0.5]]) { @@ -111,6 +128,18 @@ describe('Constellation (§6.1: starfield + particle mist)', () => { }) }) +describe('hash01 (the survival dice behind every density knob)', () => { + it('stays inside 0..1 — a signed hash passes every threshold and defeats the knobs', () => { + for (let x = 0; x < 200; x++) { + for (const [y, tick] of [[0, 0], [7, 3], [-5, 11], [1000, 999]]) { + const roll = hash01(x, y!, tick!) + expect(roll).toBeGreaterThanOrEqual(0) + expect(roll).toBeLessThan(1) + } + } + }) +}) + describe('panelWidth (the one §6.1 breakpoint)', () => { it('declines a panel below the wide minimum', () => { expect(panelWidth(WIDE_MIN - 1)).toBeNull() diff --git a/tui/assets/pet/doze.pix b/tui/assets/pet/doze.pix index f352f88..7986501 100644 --- a/tui/assets/pet/doze.pix +++ b/tui/assets/pet/doze.pix @@ -1,17 +1,20 @@ -............ -............ -..oooooooo.. -..obbbbbbo.. -..obobbobo.. -..obbbbbbo.. -...obbbbo... -....oooo.... - -............ -............ -............ -..oooooooo.. -..obobbobo.. -..obbbbbbo.. -...obbbbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +..meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. diff --git a/tui/assets/pet/idle.pix b/tui/assets/pet/idle.pix index 8ccb23f..b6c5844 100644 --- a/tui/assets/pet/idle.pix +++ b/tui/assets/pet/idle.pix @@ -1,17 +1,41 @@ -...o....o... -..obo..obo.. -..oobbbboo.. -..obbbbbbo.. -..obebbebo.. -..obbbbbbo.. -...obmmbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +..meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. -............ -..ooo..ooo.. -..oobbbboo.. -..obbbbbbo.. -..obobbobo.. -..obbbbbbo.. -...obmmbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +m.meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. diff --git a/tui/assets/pet/music.pix b/tui/assets/pet/music.pix index de7046d..5245a1c 100644 --- a/tui/assets/pet/music.pix +++ b/tui/assets/pet/music.pix @@ -1,17 +1,41 @@ -..o....o.... -.obo..obo... -.oobbbboo... -.obbbbbbo... -.obebbebo... -.obbbbbbo... -..obmmbo.... -...oooo..... +.................. +.......bbbbbb..... +......bbbbbbbb.... +..b..bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +.m.bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +..meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. -....o....o.. -...obo..obo. -...oobbbboo. -...obbbbbbo. -...obebbebo. -...obbbbbbo. -....obmmbo.. -.....oooo... +.................. +.......bbbbbb..... +...m..bbbbbbbb.... +.....bbbbbbbbbb... +.b..bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +..meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. diff --git a/tui/assets/pet/talk.pix b/tui/assets/pet/talk.pix index 1af0f1a..01ae431 100644 --- a/tui/assets/pet/talk.pix +++ b/tui/assets/pet/talk.pix @@ -1,17 +1,62 @@ -...o....o... -..obo..obo.. -..oobbbboo.. -..obbbbbbo.. -..obebbebo.. -..obbbbbbo.. -...obmmbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +m.meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +..memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. -...o....o... -..obo..obo.. -..oobbbboo.. -..obbbbbbo.. -..obebbebo.. -..obbbbbbo.. -...obbbbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +...mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +m.meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +m.memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +..mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. + +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +.b.mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +m.meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +m.memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +m.mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. diff --git a/tui/assets/pet/wake.pix b/tui/assets/pet/wake.pix index 04a6147..f24daec 100644 --- a/tui/assets/pet/wake.pix +++ b/tui/assets/pet/wake.pix @@ -1,8 +1,20 @@ -..o......o.. -..obo..obo.. -..oobbbboo.. -..obbbbbbo.. -..oeebbeeo.. -..obbbbbbo.. -...obmmbo... -....oooo.... +.................. +.......bbbbbb..... +......bbbbbbbb.... +.....bbbbbbbbbb... +....bbbbbbbbbbb... +....bbbbbbbbbbbb.. +...bmmmbbbbbbbbb.. +.b.mmmmbbbbbbbbb.. +..mmmmmbbbbbbbbb.. +m.meemmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +.mmmmmmmbbbbbbbb.. +m.memmmmbbbbbbbb.. +..mmemmemmbbbbbb.. +m.mmemmemmbbbbb... +..mmmmmmmmbbbb.... +..mmmmmmmmm....... +..mmmmmmmm........ +...mmmmmm......... +.................. diff --git a/tui/src/constellation.ts b/tui/src/constellation.ts index fd23ecb..37136a7 100644 --- a/tui/src/constellation.ts +++ b/tui/src/constellation.ts @@ -34,21 +34,25 @@ const DOT_BITS = [ const BRAILLE_BASE = 0x28_00 -// The mist floats: its baseline hangs at this fraction of the panel's height, -// dots climb from there and a thin fallout drifts below — glued to the panel -// floor it reads as a bar chart, not a sky. -const BASELINE = 0.78 +// The mist floor is an arc, not a flat line (§6.1): high at the panel's edges, +// dipping at the center, the pet floating in the hollow. Fractions of the +// panel's sub-pixel height, measured from the top. +const ARC_EDGE = 0.5 +const ARC_DIP = 0.86 // How much of the panel the mist may climb at full level, in sub-pixel rows. -const MIST_CEILING = 0.55 +const MIST_CEILING = 0.45 // Peak dot density — the mist is scattered squares with dark between them, -// never a filled bar; density thins toward a column's top edge. -const MIST_DENSITY = 0.32 +// never a filled bar; density thins toward a column's top edge, and every dot +// is jittered a couple of sub-rows so cells rarely clump into heavy braille +// blocks — isolated dots are what read as fine grain. +const MIST_DENSITY = 0.62 +const JITTER_SUBROWS = 3 -// The sparse trail under the baseline. -const FALLOUT_ROWS = 5 -const FALLOUT_DENSITY = 0.12 +// The sparse trail under the floor. +const FALLOUT_ROWS = 6 +const FALLOUT_DENSITY = 0.18 // Stars per cell, roughly — sparse enough to stay a sky, not a texture. const STAR_DENSITY = 0.03 @@ -77,10 +81,12 @@ function prng(seed: number): () => number { } // Deterministic per-dot jitter: same dot, same tick bucket, same verdict. -function hash01(x: number, y: number, tick: number): number { +// Exported for its regression test: the final XOR must be forced back to +// unsigned, or half of all hashes come out negative and pass any threshold. +export function hash01(x: number, y: number, tick: number): number { let mixed = (Math.imul(x, 374_761_393) + Math.imul(y, 668_265_263) + Math.imul(tick, 2_246_822_519)) >>> 0 mixed = Math.imul(mixed ^ (mixed >>> 13), 1_274_126_177) >>> 0 - return (mixed ^ (mixed >>> 16)) / 4_294_967_296 + return ((mixed ^ (mixed >>> 16)) >>> 0) / 4_294_967_296 } export class Constellation { @@ -117,9 +123,8 @@ export class Constellation { const subRows = this.height * 4 const bits = Array.from({ length: this.height }, () => Array.from({ length: this.width }, () => 0)) - // The mist: scattered surviving dots per sub-pixel column, climbing from a - // floating baseline, with a thin fallout drifting below it. - const baseline = Math.floor(subRows * BASELINE) + // The mist: scattered surviving dots per sub-pixel column, climbing from + // the arced floor, with a thin fallout drifting below it. const bucket = Math.floor(tick / SHIMMER_DIVISOR) const dot = (sc: number, fromTop: number): void => { if (fromTop < 0 || fromTop >= subRows) return @@ -129,13 +134,20 @@ export class Constellation { const level = levels.length === 0 ? 0 : clamp01(levels[Math.floor((sc / subCols) * levels.length)]!) if (level === 0) continue + // -1..1 across the panel; the floor bows from ARC_EDGE down to ARC_DIP, + // and the mist thins out before it can hit the panel's side walls. + const across = (2 * sc) / subCols - 1 + const fade = Math.min(1, (1 - Math.abs(across)) / 0.25) + const floor = Math.floor(subRows * (ARC_DIP - (ARC_DIP - ARC_EDGE) * across * across)) const target = level * subRows * MIST_CEILING for (let up = 0; up < target; up++) { - const density = MIST_DENSITY * (1 - (0.85 * up) / target) - if (hash01(sc, up, bucket) < density) dot(sc, baseline - up) + const density = fade * MIST_DENSITY * (1 - (0.85 * up) / target) + if (hash01(sc, up, bucket) >= density) continue + const jitter = Math.floor((hash01(sc, up + 1000, bucket) - 0.5) * JITTER_SUBROWS) + dot(sc, floor - up + jitter) } for (let down = 1; down <= FALLOUT_ROWS; down++) { - if (hash01(sc, -down, bucket) < FALLOUT_DENSITY * level) dot(sc, baseline + down) + if (hash01(sc, -down, bucket) < fade * FALLOUT_DENSITY * level) dot(sc, floor + down) } } @@ -144,10 +156,9 @@ export class Constellation { const petRows = pet?.length ?? 0 const petCols = petRows > 0 ? pet![0]!.length : 0 const fits = petRows > 0 && petCols <= this.width && petRows <= this.height - // Centred in the cloud's own region, not the panel's: the creature drifts - // IN the mist, a little above the baseline. + // Centred in the arc's hollow: the figure floats where the floor dips. const petX = fits ? Math.floor((this.width - petCols) / 2) : 0 - const petY = fits ? Math.max(Math.floor(this.height * 0.52) - Math.floor(petRows / 2), 0) : 0 + const petY = fits ? Math.max(Math.floor(this.height * 0.4) - Math.floor(petRows / 2), 0) : 0 const starAt = new Map() for (const star of this.stars) starAt.set(star.y * this.width + star.x, star) diff --git a/tui/src/palette.ts b/tui/src/palette.ts index 30e2f40..9e86e0d 100644 --- a/tui/src/palette.ts +++ b/tui/src/palette.ts @@ -17,14 +17,14 @@ export type Accent = { // The quiet-constellation ground (§6.1 art direction): a deep blue-charcoal // night sky, constant across scenes, with warm paper ink floating on it. export const INK = { - bg: '#0d0f16', - dim: '#565c6b', - text: '#d9d2c4', + bg: '#0a0d15', + dim: '#4e5464', + text: '#d4cabb', user: '#c9a878', - notice: '#6e7280', + notice: '#697080', } -const DEFAULT_ACCENT: Accent = { dim: '#68625a', bright: '#d9c9a8' } +const DEFAULT_ACCENT: Accent = { dim: '#6e665a', bright: '#c9bda8' } // Scene keys are spec 04's (`morning` | `afternoon` | `evening` | `late-night`), // but this is the far side of a wire: an unknown scene from a newer engine gets @@ -33,10 +33,10 @@ const DEFAULT_ACCENT: Accent = { dim: '#68625a', bright: '#d9c9a8' } // The §6.1 discipline: each scene is ONE near-monochrome family on the shared // night ground — the hour changes the warmth of the light, never the room. const ACCENTS: Record = { - morning: { dim: '#6e6656', bright: '#e8d8b0' }, - afternoon: { dim: '#67705f', bright: '#cfd9b8' }, - evening: { dim: '#7c5f4e', bright: '#e5b98c' }, - 'late-night': { dim: '#49527a', bright: '#a9b9e8' }, + morning: { dim: '#73695a', bright: '#dcceb2' }, + afternoon: { dim: '#6e6a5e', bright: '#d5ccb8' }, + evening: { dim: '#7c624e', bright: '#e0b088' }, + 'late-night': { dim: '#4e5878', bright: '#a8b6dd' }, } export function accentFor(scene: string | undefined): Accent { diff --git a/tui/src/pet.ts b/tui/src/pet.ts index e940fef..af379c9 100644 --- a/tui/src/pet.ts +++ b/tui/src/pet.ts @@ -68,16 +68,20 @@ export function loadPoses(dir = ASSET_DIR): Record { return poses } -// The palette the .pix keys resolve through. The body borrows the hour's accent -// (§3.7.2), so the pet warms and cools with the program; `fade` toward the room -// is how a dozing pet dims without a second set of assets. +// The palette the .pix keys resolve through. The figure borrows the hour's +// accent (§3.7.2), so it warms and cools with the program; `fade` toward the +// room is how a dozing figure dims without a second set of assets. +// +// The whisper figure (§6.1) reads by solid fill, cross-stitch style: hair as +// the dim mass, skin as the bright mass, and dark seams carrying the detail +// (the closed eye, the mouth, the gaps between fingers). export function petPalette(accent: Accent, fade: number): Record { const dim = (color: string): string => (fade === 0 ? color : mix(color, INK.bg, fade)) return { - o: dim('#2a221c'), // outline - b: dim(accent.dim), // body - e: dim(INK.text), // eye - m: dim(accent.bright), // muzzle / belly + o: dim('#2a2620'), // outline + b: dim(accent.dim), // hair + e: dim(mix(INK.bg, accent.dim, 0.35)), // seams: eye, mouth, finger gaps + m: dim(accent.bright), // skin } }