Skip to content

Commit 0826e52

Browse files
docs: banner kit — visual identity ("Diagrammatic Hush") (#140)
## Summary Adds a 1920×480 dark theorem-proof banner at the top of both READMEs. The procession of 4-node constellations diminishes from left to right as edges drop into fibers below the base axis — a direct visual of *"loss that is not total erasure"*: identity (nodes) preserved, residues (amber marks) accumulate. The design philosophy ("Diagrammatic Hush" — Bourbaki / TikZ-cd / Felice Frankel lineage, not stock vector illustration) is documented alongside the asset. ## What's added - `docs/assets/banner.svg` + `docs/assets/banner.png` — the banner asset (1920×480). - `docs/assets/banner-philosophy.md` — design rationale. - `tools/banner/build-banner.mjs` — regenerator (zero deps; `node tools/banner/build-banner.mjs` emits `docs/assets/banner.svg`; PNG must be re-rasterised separately). - `README.md` + `readme.adoc` — banner reference inserted at top, before H1. ## Verification - `node tools/banner/build-banner.mjs` regenerates byte-identical `banner.svg`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cbab984 commit 0826e52

6 files changed

Lines changed: 362 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<!-- SPDX-License-Identifier: MPL-2.0 -->
22
<!-- SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
33

4+
<p align="center">
5+
<img src="docs/assets/banner.png" alt="Echo Types — Proof-Relevant Information Residues" width="100%">
6+
</p>
7+
48
# echo-types
59

610
[![OpenSSF Best Practices](https://img.shields.io/badge/OpenSSF-Best_Practices-green?logo=opensourcesecurity)](https://www.bestpractices.dev/en/projects/new?repo_url=https://github.com/hyperpolymath/echo-types)

docs/assets/banner-philosophy.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Diagrammatic Hush
2+
3+
A visual philosophy of restrained scientific inscription. Every mark is deliberate;
4+
nothing is decorative. The composition reads as if extracted from a private notebook
5+
of a working mathematician — annotated, weighed, refined over many sittings.
6+
7+
## Form and Space
8+
9+
Horizontal procession dominates: a sequence of small geometric structures advancing
10+
left-to-right along a single quiet axis, each one a near-replica of its predecessor,
11+
each one diminished by exactly one element. The page breathes around them. Negative
12+
space is not absence; it is the field on which the structures register. Width is
13+
treated as time; vertical depth as proof content.
14+
15+
## Colour and Material
16+
17+
The palette is the palette of dark paper under low light: a deep ink ground, warm
18+
bone-coloured marks, a single warm amber reserved exclusively for the residues —
19+
the things that remain after transformation. This amber is never used for structure,
20+
only for what has fallen away and become inscribed. The discipline of the palette
21+
is the discipline of the work: no element earns colour without earning meaning.
22+
23+
## Rhythm and Hierarchy
24+
25+
The procession is steady — even spacings, even cadence, even small rotational drift
26+
— so that the eye perceives one long held note rather than eight short ones. The
27+
title sits at the centre as a brief silence in the procession, framed by space the
28+
way a theorem statement is framed by margin. The subtitle hovers above as a small
29+
field-label: tracked-out, almost a watermark, suggesting that the piece is a
30+
specimen from some larger taxonomic project.
31+
32+
## Craft and Refinement
33+
34+
This must look like the hundredth iteration, not the first. The line weights are
35+
calibrated so that the diagonals read as secondary structure beneath the primary
36+
square. The fiber marks are placed at exact, recurring slots — so that across the
37+
width they form a discrete grid the eye can read but never quite name. Every
38+
coordinate is chosen, every opacity tuned. The work should reward the reader who
39+
zooms in as much as the reader who only glances. Nothing is generic; nothing is
40+
ornamental; nothing has been left to chance.
41+
42+
## What is Avoided
43+
44+
Brackets, terminals, gears, circuits, neural meshes, gradient swirls, glowing
45+
particles, perspective grids, faux-3D. Anything that announces itself as
46+
"technology" rather than mathematics. Anything that suggests style over substance.
47+
The work belongs in the tradition of Bourbaki, of TikZ-cd, of Felice Frankel's
48+
scientific photography — not in the tradition of stock vector illustration.

docs/assets/banner.png

29.6 KB
Loading

docs/assets/banner.svg

Lines changed: 145 additions & 0 deletions
Loading

readme.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
= echo-types
22

3+
image::docs/assets/banner.png[Echo Types — Proof-Relevant Information Residues,align="center"]
4+
35
Constructive Agda development for *echo types* as a first-class notion of:
46

57
*loss that is not total erasure*.

tools/banner/build-banner.mjs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env node
2+
// SPDX-License-Identifier: MPL-2.0
3+
// SPDX-FileCopyrightText: 2025-2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
//
5+
// build-banner.mjs — Echo Types repository banner
6+
// 1920×480, dark theorem-proof aesthetic, no dependencies.
7+
//
8+
// Run: node tools/banner/build-banner.mjs
9+
// Writes: docs/assets/banner.svg (PNG must be re-rasterised separately).
10+
11+
import { writeFileSync, mkdirSync } from 'node:fs';
12+
import { fileURLToPath } from 'node:url';
13+
import { dirname, join } from 'node:path';
14+
15+
const __dirname = dirname(fileURLToPath(import.meta.url));
16+
const repoRoot = join(__dirname, '..', '..');
17+
const outDir = join(repoRoot, 'docs', 'assets');
18+
const outPath = join(outDir, 'banner.svg');
19+
20+
const W = 1920, H = 480;
21+
22+
const C = {
23+
bg: '#0d1620',
24+
ink: '#ede4d0',
25+
inkFaint: '#9aa39c',
26+
base: '#3a4555',
27+
baseFaint: '#26303a',
28+
amber: '#c9a96e',
29+
title: '#f0e8d4',
30+
subtitle: '#7d8590',
31+
};
32+
33+
const baseY = 275; // the horizontal "base space" axis
34+
const constY = 232; // vertical centre of each constellation
35+
36+
// ---- Constellation: 4-node square graph with two diagonals ------------------
37+
const nodes = [
38+
{ id: 'NW', x: -22, y: -16 },
39+
{ id: 'NE', x: 22, y: -16 },
40+
{ id: 'SW', x: -22, y: 16 },
41+
{ id: 'SE', x: 22, y: 16 },
42+
];
43+
const edges = {
44+
N: ['NW', 'NE'],
45+
E: ['NE', 'SE'],
46+
S: ['SW', 'SE'],
47+
W: ['NW', 'SW'],
48+
D1: ['NW', 'SE'],
49+
D2: ['NE', 'SW'],
50+
};
51+
const all = ['N', 'E', 'S', 'W', 'D1', 'D2'];
52+
// Order of edge loss across the procession — diagonals first, then square
53+
const removalOrder = ['D2', 'D1', 'S', 'E', 'N', 'W'];
54+
55+
const nodeOf = id => nodes.find(n => n.id === id);
56+
57+
function edgesAt(stage) {
58+
const removed = new Set(removalOrder.slice(0, stage));
59+
return all.filter(e => !removed.has(e));
60+
}
61+
62+
function constellation(cx, cy, stage, rot) {
63+
// Identity is preserved: nodes always at full ink. Only edges change.
64+
let g = `\n <g transform="translate(${cx} ${cy}) rotate(${rot})">`;
65+
for (const e of edgesAt(stage)) {
66+
const [a, b] = edges[e];
67+
const A = nodeOf(a), B = nodeOf(b);
68+
const isDiag = e === 'D1' || e === 'D2';
69+
const sw = isDiag ? 0.75 : 1.05;
70+
const opc = isDiag ? 0.78 : 0.96;
71+
g += `\n <line x1="${A.x}" y1="${A.y}" x2="${B.x}" y2="${B.y}" stroke="${C.ink}" stroke-width="${sw}" stroke-linecap="round" opacity="${opc}"/>`;
72+
}
73+
for (const n of nodes) {
74+
g += `\n <circle cx="${n.x}" cy="${n.y}" r="2.85" fill="${C.ink}"/>`;
75+
}
76+
g += `\n </g>`;
77+
return g;
78+
}
79+
80+
// ---- Fiber: vertical line descending below the axis, with residue marks -----
81+
function fiber(cx, stage) {
82+
const top = baseY + 8;
83+
const bot = 412;
84+
const slots = 7;
85+
const ys = Array.from({ length: slots },
86+
(_, i) => top + (i + 0.5) * (bot - top) / slots);
87+
88+
let g = `\n <g>`;
89+
g += `\n <line x1="${cx}" y1="${top}" x2="${cx}" y2="${bot}" stroke="${C.base}" stroke-width="0.5"/>`;
90+
const marks = Math.min(stage, slots);
91+
for (let i = 0; i < marks; i++) {
92+
const y = ys[i];
93+
// newest mark (topmost) slightly larger to read as "most recent loss"
94+
const r = i === marks - 1 ? 2.0 : 1.55;
95+
g += `\n <circle cx="${cx}" cy="${y}" r="${r}" fill="${C.amber}"/>`;
96+
}
97+
g += `\n </g>`;
98+
return g;
99+
}
100+
101+
// ---- Stage positions: 4 left of the title block, 4 right --------------------
102+
const stages = [
103+
{ stage: 0, x: 160 },
104+
{ stage: 1, x: 330 },
105+
{ stage: 2, x: 500 },
106+
{ stage: 3, x: 670 },
107+
// title gap roughly 720..1200
108+
{ stage: 4, x: 1250 },
109+
{ stage: 5, x: 1420 },
110+
{ stage: 6, x: 1590 },
111+
{ stage: 7, x: 1760 },
112+
];
113+
114+
// ---- Compose ----------------------------------------------------------------
115+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">`;
116+
117+
// Ground
118+
svg += `\n <rect width="${W}" height="${H}" fill="${C.bg}"/>`;
119+
120+
// Corner registration marks — printer's-plate / specimen-sheet aesthetic
121+
{
122+
const inset = 36, arm = 14, sw = 0.7;
123+
const corners = [
124+
{ x: inset, y: inset, dx: 1, dy: 1 },
125+
{ x: W - inset, y: inset, dx: -1, dy: 1 },
126+
{ x: inset, y: H - inset, dx: 1, dy: -1 },
127+
{ x: W - inset, y: H - inset, dx: -1, dy: -1 },
128+
];
129+
svg += `\n <g stroke="${C.base}" stroke-width="${sw}" fill="none">`;
130+
for (const c of corners) {
131+
svg += `\n <line x1="${c.x}" y1="${c.y}" x2="${c.x + arm * c.dx}" y2="${c.y}"/>`;
132+
svg += `\n <line x1="${c.x}" y1="${c.y}" x2="${c.x}" y2="${c.y + arm * c.dy}"/>`;
133+
}
134+
svg += `\n </g>`;
135+
}
136+
137+
// Base axis (dashed, very quiet)
138+
svg += `\n <line x1="80" y1="${baseY}" x2="${W - 80}" y2="${baseY}" stroke="${C.base}" stroke-width="0.6" stroke-dasharray="1 7"/>`;
139+
140+
// Tick marks at each stage position (project the fibers onto the axis)
141+
for (const s of stages) {
142+
svg += `\n <line x1="${s.x}" y1="${baseY - 3}" x2="${s.x}" y2="${baseY + 3}" stroke="${C.base}" stroke-width="0.6"/>`;
143+
}
144+
145+
// Procession
146+
for (const s of stages) {
147+
const rot = s.stage * 1.5; // gentle accumulating rotation
148+
svg += fiber(s.x, s.stage);
149+
svg += constellation(s.x, constY + s.stage * 0.5, s.stage, rot);
150+
}
151+
152+
// ---- Title block (centre of the page) ---------------------------------------
153+
// Small tracked-out italic field-label hovering above the title
154+
svg += `\n <text x="${W/2}" y="198" font-family="IBM Plex Serif" font-style="italic" font-size="14.5" fill="${C.subtitle}" text-anchor="middle" letter-spacing="7.5">Proof-Relevant Information Residues</text>`;
155+
156+
// Main title — IBM Plex Serif, regular, restrained letter-spacing
157+
svg += `\n <text x="${W/2}" y="265" font-family="IBM Plex Serif" font-size="72" font-weight="400" fill="${C.title}" text-anchor="middle" letter-spacing="2">Echo Types</text>`;
158+
159+
svg += `\n</svg>\n`;
160+
161+
mkdirSync(outDir, { recursive: true });
162+
writeFileSync(outPath, svg);
163+
console.log(`Wrote ${outPath} (${svg.length} bytes)`);

0 commit comments

Comments
 (0)