Artifact pictures
The standard for the dithered photographs: the rules, the Blue Marble tone grid and screen, the cutter, adding a picture and its lint.
An artifact picture is a photograph or scan of an object, artwork or place, screened through the house dither. In this repository they appear in three places:
| Surface | Pictures |
|---|---|
The plate port's field (src/components/plate) | earth, rosetta, calligraphy, tablet, gloss |
The brand deck's mood slides (deck/) | earth, rosetta, tablet, calligraphy, lighthouse, devanagari, gloss, cable, wave, compass |
The transition demo on /docs/handbook/libraries (src/app/craft) | earth and rosetta, the deck's grids |
On the plate port, the onboarding steps show earth, rosetta, calligraphy and
tablet, the OAuth consent states show rosetta, the device flow shows
calligraphy, and the CLI wizard shows gloss (gallery/devStates.ts), as on
the dashboard.
Procedural fields (the sign-in globe, the ramp), marks, glyph fields, the deck's shader openers and blog covers are not artifact pictures, and these rules do not apply to them.
The same standard covers gt-cloud's dashboard sign-in and onboarding field
(.agents/skills/artifact-pictures there). standard.json and
mood_tone.py are the same files in both repositories.
Files
| File | Role |
|---|---|
scripts/media/mood-tone/standard.json | The standard: screen, tone, file sizes, windows, retired names |
scripts/media/mood-tone/mood_tone.py | The cutter (Python 3 with Pillow) |
scripts/media/mood-tone/mood-tone.mjs | The recipes for the deck and plate sets, and the wrapper |
deck/shots/tone/mood-{name}.jpg | The deck's tone grids |
deck/shots/tone/manifest.json | One entry per deck grid, written by the wrapper |
public/brand/mood/mood-{name}.jpg | The plate's tone grids, the same bytes as gt-cloud's |
public/brand/mood/manifest.json | One entry per plate grid, written by the wrapper |
public/craft/mood-{earth,rosetta}.jpg | Byte copies of the deck's earth and rosetta grids for the transition demo on /docs/handbook/libraries |
src/app/craft/CraftArticle.tsx | The text around the transition demo, with the demo's credit line |
src/components/plate/brand/moodPictures.ts | The plate registry: src, placement and caption per picture |
deck/slides/NN-mood-{name}.html | The deck's mood slides, one canvas per grid, with the credit |
deck/parts/tail.html | The deck engine that screens the grids |
scripts/lint/pictures.mjs | The lint |
The rules
1. No plain English prose
A picture may not be a page of readable English: dictionary entries, book pages, documents, signs or captions. That text reads as copy and distracts from the form beside it.
Writing is welcome when it is the artistic or design subject of the object: carved inscriptions (the Rosetta Stone), calligraphy, manuscripts in other scripts or old hands (Devanagari, the gloss), signs pressed in clay (the tablet), and the lettering engraved on a historical chart or instrument (the cable chart, the compass rose).
Each recipe declares writing: 'none' | 'artistic'. The cutter's text-line
detector counts rows of six or more letter-sized marks and records the count
as textLines. A picture with any text lines must declare 'artistic'. The
detector cannot read meaning, so a reviewer checks every 'artistic'
declaration against this rule. Plain English prose never qualifies.
2. Sources
- Cut from the original scan or photograph. Never cut from a screened, two-tone or resized copy.
- The license is public domain or Creative Commons, taken in this order: public domain and CC0 first (Wikimedia Commons public domain files and the open-access collections of the Met, the Smithsonian, the Cleveland Museum of Art, the Rijksmuseum and the Library of Congress), then CC BY, then CC BY-SA only when nothing else shows the object. A CC BY-SA print is a derived work under the same license, and its credit says so. Non-commercial (NC) and no-derivatives (ND) licenses are refused, so the British Museum's images, which carry a non-commercial license, are refused.
- The subject is the original object. A cast, a replica or a modern
reprint is refused when the original can be shown. A picture shows no
flag, emblem or coat of arms, no map with borders, no caption that
states sovereignty, and no private person. The license order and these
subject rules come from the artifact pictures of the language map
(gt-cloud branch
k/language-map, 2026-10-03). - The credit is shown with the picture: on the deck slide's plate, in the
picture's caption in
moodPictures.ts, which the plate port's field shows, and in the credit line under the transition demo on /docs/handbook/libraries (CraftArticle.tsx). The demo's line carries the deck slide's credit for each picture the demo shows, without the label ("Image:", "Photograph:"). The lint checks the deck slides and the demo's line. - Sources are never committed. Each person keeps a sources directory and
passes it to the wrapper.
scripts/media/mood-tone/README.mdlists each file's name, origin, pixel size and sha256, and the wrapper refuses a source whose sha256 differs fromSOURCESinmood-tone.mjs.
3. The tone grid is the Blue Marble's
The Blue Marble on the onboarding field is the reference. Its numbers are in
standard.json under tone.
Fixed for every picture:
- no blur;
- gamma 1.2;
- autocontrast that clips 0.5 percent at each tail, over the frame, or over the subject mask for a masked object;
- an 8-bit gray JPEG at quality 82, or 75 when 82 is over the cap.
Chosen per picture, in the recipe:
- the crop;
- the channel:
red(the earth's) orgray; - the polarity:
invertso the subject is lit on a black ground; - the kind:
sceneormarks.
Solved per picture by the cutter, never set by hand: the black and white points.
- A scene lands on the Blue Marble's mean tone 0.361 and standard deviation 0.342, over the region of the grid that is shown.
- Marks (writing, engraving, lines on a plain ground) put the ground at black with an Otsu threshold and the 90th percentile of the marks at white.
The solver returns levels 80 and 230 for the plate's earth, which is the earth's own recipe.
Sizes: a cover is 1600 by 900, one grid cell per CSS px at a 1440 by 900 view. A disc is 2400 by 1350. The cap is 560 KB (573440 bytes).
4. The screen is the Blue Marble's
standard.json under screen:
- an ordered 8x8 Bayer matrix with threshold
(m + 0.5) / 64; - cells of 1 CSS px, screened live at display size. Never ship a pre-screened bitmap that is then scaled;
- tone floor 10: bytes at or under 10 read as 0;
- the loop runs at gamma 1 and bias 0, because the whole curve is in the grid;
- dark theme: white cells at opacity 0.62 over the #070707 ground. Light theme: #070707 cells at opacity 0.7 over white.
Where each surface states these:
| Surface | Cell | Tone floor | Bayer and threshold | Ink and opacity |
|---|---|---|---|---|
| Plate port | PICTURE_SCALE in FieldStack.tsx | TONE_FLOOR in lib/picture-field.ts | BAYER_8 in lib/dither.ts | --tc-picture-ink, --field-picture-opacity in plate.css |
| Deck | MOOD_CELL_PX in deck/parts/tail.html | MOOD_TONE_FLOOR in tail.html | bayer8 in tail.html | --mood-ink, --mood-opacity in deck/parts/head.html |
| /docs/handbook/libraries demo | CELL in TransitionDemo.tsx | TONE_FLOOR in TransitionDemo.tsx | BAYER_8 in src/lib/dither.ts | --ptc-picture-ink, --ptc-picture-opacity in craft.css, the dark values, because the plate is dark in both themes |
The deck engine sizes each mood canvas's backing store to its box on screen
(getBoundingClientRect, because the stage is CSS-scaled), so a cell is 1
CSS px on the sheet, in a thumbnail, in the book view and on the full-stage
backdrop. It reads the grid at cover fit, centred, and draws again on slide
activation, resize and a theme flip.
5. The windows
The cutter measures each grid over the region shown (the deck's full 1600 by
900 sheet; on the plate, the part right of the ramp at 1440 by 900; only the
disc or the masked subject where there is one) and records the stats in the
manifest. The lint holds them to standard.json under windows:
| Kind | Mean | Std | At white (250 and up) | At the floor (10 and under) |
|---|---|---|---|---|
| scene | 0.30 to 0.42 | 0.28 to 0.44 | at least 2% | at least 5% |
| marks | at most 0.35 | any | at least 0.5% | at least 50% (the ground) |
The cutter
From the repository root:
pnpm mood-tone <sources dir> [--set deck|plate] [--preview <dir>] [--check]The wrapper sends the recipes and the standard to mood_tone.py, which
writes mood-{name}.jpg for each recipe (the deck set to deck/shots/tone,
the plate set to public/brand/mood) and prints one manifest entry per
picture. The wrapper writes those entries to manifest.json and prints each
picture's levels and stats. With --preview, the cutter also saves the 1 px
screen of each grid at the view size as preview-{name}.png.
The cutter is deterministic: a run on the same sources writes the same
bytes. A grid's sha256 ties the file to its manifest entry, and the lint
checks that. It does not show that the entry came from the recipe: an entry
edited by hand next to an untouched grid passes the lint. Only --check,
run with the sources, proves that. It cuts into a temporary folder and fails
unless every grid and the manifest equal the committed files byte for byte.
The lint cannot run it, because the sources are not committed. Never edit a
grid or a manifest by hand.
A recipe holds only: name, source, the size and cap from standard.json,
crop, channel, invert, kind, writing and placement. It holds no
tone numbers. The crop kinds are documented above DECK in mood-tone.mjs.
Adding a picture
- Choose the object against rule 1. Find the original scan or photograph and confirm its license (rule 2).
- Put the source file in your sources directory, add its origin and sha256
to
SOURCESinmood-tone.mjs, and add its row to the source table inscripts/media/mood-tone/README.md. - Add a recipe to
DECKorPLATEinmood-tone.mjs. SpreadCOVERfor a cover. Setkind: 'marks'for writing or engraving on a plain ground andkind: 'scene'for anything else. Declarewriting. A deck recipe's placement is a cover at focus 0.5, 0.5. - Run the wrapper with
--previewand look atpreview-{name}.png. - If the subject is not where it should be, or the stats miss the window, change the crop, the channel, the polarity, the kind or the placement and run it again. If no crop puts a scene in its window, the picture does not meet the standard. Do not add tone settings to make it pass.
- Show it.
- On the deck: add
deck/slides/NN-mood-{name}.htmlfrom an existing mood slide, with<canvas class="mood-img" data-tone="shots/tone/mood-{name}.jpg" role="img" aria-label="...">, the plate's title, sentence and credit, then updateSLIDE_COUNTinscripts/build/deck.mjs,SECTIONSindeck/parts/tail.html,DECK_SLIDESinsrc/lib/search-index.ts, and the entry indeck/shots/OPENERS.md. Runpnpm build:deck. - On the plate port: add the name to
PictureNameand an entry toMOOD_PICTURESinmoodPictures.ts:srcis/brand/mood/mood-{name}.jpg,placementequals the recipe's (a disc also carries the manifest'sdiscvaluescx,cyandr), and the caption has a title, a note of at most 90 characters that says what the object is, and the credit. Make the same change in gt-cloud.
- On the deck: add
- Run
node scripts/lint/pictures.mjs(it also runs beforenext buildand inpnpm lint:all). - Commit the grid,
manifest.json, the recipe, the README row and the slide or registry entry. If the manifest showstextLinesabove 0, say so in the commit so the reviewer checks rule 1.
To remove a picture, delete its recipe, its grid and its slide or registry entry, run the wrapper so the manifest drops it, and move its pages to another picture.
The lint
scripts/lint/pictures.mjs runs before next build (pnpm build), in
pnpm lint:all, and on its own with pnpm lint:pictures. Its tests are in
scripts/lint/pictures.test.mjs. pnpm lint:all runs them after the lint,
and pnpm test:pictures runs them alone. It fails when:
- a grid file has no manifest entry, or an entry has no grid file;
- a grid's sha256 or byte count differs from its entry;
- a grid is not an 8-bit gray JPEG of its entry's size;
- the plate registry's names,
srcorplacementdiffer from its manifest; - a deck mood slide does not name a manifest picture on a canvas, a manifest
picture has no slide, a slide shows a bitmap, a pre-screened
mood-*file sits indeck/shots, orpublic/brand-deck.htmlnames grid files underpublic/deck-assetsother than the manifest's (runpnpm build:deck); - a deck mood slide's plate has no
<div class="credit">, or the text around the transition demo on /docs/handbook/libraries does not carry the deck credit of each picture the demo shows; - the transition demo's grids are not byte copies of the deck's, or its disc differs from the deck earth's fitted disc;
- an entry's blur, gamma, autocontrast cutoff or quality is off the standard;
- a grid's size is off its placement's size, or its bytes are over the cap;
- an entry's region stats are outside its kind's window;
writingis not'none'or'artistic';textLinesis above 0 andwritingis not'artistic';- a retired name appears in a manifest, a grid file name, the registry, a
mood-{name}token underdeck/,src/or the built deck, or a string literal in the picture code; - the screen constants differ from the standard on any of the three surfaces, or in the built deck: the 1 CSS px cell, the loop at gamma 1 and bias 0, tone floor 10, the Bayer matrix and threshold, and the picture ink and opacity per theme.
The lint checks the screen constants by declaration. It strips comments
first (Python # comments and docstrings, HTML <!-- -->, block comments
and // comments), so a value quoted in a comment does not count. Each
constant must be declared once and never assigned again. Each ink and
opacity token may be set only in its own rules, and every declaration there
must carry the standard's value: .brand-field-stack (light) and
:root[data-theme='dark'] .brand-field-stack (dark) in plate.css, :root
(light) and :root[data-theme="dark"] (dark) in the deck's sources and the
built deck, and .ptc-plate.is-transition (dark) in craft.css. The lint does
not follow how the code uses a constant: a draw call that divides by 2, or a
loop that skips the floor, passes. The backstop is to look. Run the wrapper
with --preview and compare the page with preview-{name}.png, the 1 px
screen of the grid.
Changing the standard
standard.json is the same file in gt-cloud and Prototemplate. A change to
it is made in both repositories in the same round, and every grid in both is
cut again with the new standard.
Retired pictures
These names never return as picture names, files or registry keys. They are
listed in standard.json under writing.retired, and the lint rejects them.
| Name | What it was | Why it was retired |
|---|---|---|
dictionary | A page of the Oxford English Dictionary | Plain English prose distracts as copy |
johnson | A page of Johnson's Dictionary | Plain English prose distracts as copy |
oed-volumes | The Oxford English Dictionary volumes | The spines carry plain English titles |
oxford | Any page or volume of the Oxford English Dictionary | Its pages are plain English prose |
oed | The Oxford English Dictionary by its initials | Its pages are plain English prose |
Retiring the first two deleted the deck's Design system and Documentation mood slides on 2026-10-05; the deck went from 95 to 93 slides.
Graphics pipeline
How the blog illustrations are made with the toolchain in graphics/: captures, labelled crops, the sizing rules, clips, export and hand-off.
Architecture
The code map: the direction registry, the toolchain as the single source and fork rescoping, the componentized instruments, the mirror.