@pepperui/illustrations
v0.10.0
Published
The Mobupps design system's illustration library (DL-26): isometric scenes built from one library of objects, each placed on a page by name with <pepper-illustration scene="…">. A small element paints the scene's still (drawn at build time in every palett
Maintainers
Readme
@pepperui/illustrations
The illustration library (DL-26, milestone M3.7): isometric 3D objects built once from shared building blocks and composed into scenes. Every scene is one definition drawn two ways:
- live: three.js, for the web. A slow idle drift, the pointer turns it, and a signal runs along its arrows;
- still: an SVG for pages, slides and PDFs, with text kept as text and Poppins inside the file, plus a PNG at 2×.
A page places a scene by its name, the way it places an icon: one element, one attribute. The library is a growing
bank — a new scene is its description dropped into scenes/, built, and released as a minor.
Install
npm install @pepperui/illustrationsServe the package's dist/ folder as it is — the element finds its engine and its stills beside its own script — and
write the scene where it goes:
<pepper-illustration scene="cross-screen" palette="red" label="One user across three screens"></pepper-illustration>
<script src="node_modules/@pepperui/illustrations/dist/pepper-illustration.js"></script>A page that serves the files from elsewhere says where on the script tag, data-base="/assets/illustrations/".
dist/catalogue.json lists every scene and object, each one's own palette and direction, the files, and the
attributes below.
The element
| Attribute | What it takes |
|---|---|
| scene | a library scene, by its name — cross-screen, cross-screen-close (the MAFO page's), intelligence, flow, infra, release, screens, and the Travel page's eight: demand-board, one-event, city-map, booking-window, deep-link, creative-rotation, source-pricing, geo-holdout; and the solution pages' twelve (3.7.30): igaming-1-deposit-stream, igaming-2-holdout-gap, igaming-3-deposit-cost, gaming-5-organic-baseline, gaming-7-reddit-partner, ecommerce-1-purchase-ledger, ecommerce-5-supply-sources, ecommerce-6-holdout-lift, finance-1-value-event-ledger, finance-5-overlap-suppressed, finance-7-channels-one-price, finance-alt-5-incrementality-read |
| object | one library object alone, by its name, in place of a scene — it takes palette and direction as a scene does |
| palette | red · yellow · blue · three — one system colour or the three; the scene's own without it, the three for an object alone |
| direction | left · right — which way the objects face: right when the picture sits to the left of its words. The camera stands at the other corner; nothing is mirrored, so the words still read. The scene's own without it, left for an object alone |
| still | the still, drawn at build time in this palette and direction — no engine, no motion |
| idle | on · off — the slow drift on its own; on without it |
| interactive | on · off — the pointer turns the picture; on without it |
| label | what the picture shows, for a screen reader — the element is an image |
| loading | lazy · eager — lazy without it: the engine starts when the picture comes into view |
| src | the address of a description (JSON), for a scene that is not in the library |
Width is the element's, and its height follows the picture's proportions. Reduced motion gives the still.
data-state says where the element is: poster, live, paused or still.
What a page pays
A live picture is a WebGL render loop, and a browser allows about sixteen WebGL contexts before it drops one. So:
- The still is painted first. Every scene is drawn at build time in every palette and facing both ways
(
dist/stills/<scene>--<palette>--<direction>.svg), and so is every object alone (dist/objects/<object>--<palette>--<direction>.svg); the element shows that file at once — the page's first paint never waits for the engine. A live picture takes over from it without a jump: the still is scaled to the live picture's first frame. A still's outline never draws thinner than one device pixel, as the live picture's never does: each file draws a one-pixel floor, and on a denser screen the element asks for the file's#dpr2(or#dpr3), where the floor is one device pixel. A page placing a still itself on a Retina screen adds#dpr2to the address the same way. - The engine loads only when a picture has to move.
pepper-illustration.jsis the element alone; the engine — three.js, the library and every scene,pepper-illustration-engine.js— is fetched after the page has loaded, the first time a live picture comes into view. A page of stills downloads no engine at all. - Lazy, and paused. A live picture starts when it comes near the viewport and pauses when it leaves; with no live picture in view, no frame is drawn.
- One renderer. Every live picture on a page shares the engine's one WebGL renderer, so a page opens one context however many pictures it holds.
The page pattern: one live scene per page, the rest stills — the hero moves, the others are the same scenes with
still. At most one live scene in view at a time. The stills are renders of the same descriptions, so a mixed page
matches exactly, in the same palette and direction.
The files
| In dist/ | What it is |
|---|---|
| pepper-illustration.js | the element — a small classic script; it defines <pepper-illustration> and paints stills |
| pepper-illustration-engine.js | the engine: three.js 0.184.0, the library, every scene and its motion, the design system's Poppins |
| stills/<scene>--<palette>--<direction>.svg | every scene in every palette, facing both ways, Poppins inside, its outline never under a device pixel (#dpr2 / #dpr3 on a dense screen) |
| scenes/<scene>.svg · <scene>@2x.png | each scene in its own palette, as an SVG and a PNG at 2× — for slides and PDFs |
| objects/<object>.svg · <object>@2x.png | each object alone, in the three, facing left |
| objects/<object>--<palette>--<direction>.svg | each object alone in every palette, facing both ways — what the element paints for object |
| descriptions/<scene>.json | each scene's description — a scene is data |
| catalogue.json | what the package holds: the scenes, the objects, the element's attributes |
| manifest.json | every file's sha1, and what it was made from |
Adding a scene
A new scene is a description — plain JSON on the library's objects (below, A scene is data); it draws nothing.
- Author it: in Claude Design with
Pepper.Scene(the system's README there carries the format), or by hand. An object the library lacks is added to the library first, never drawn on a page. - Try it:
python3 scripts/illustrations.py --scene FILEdraws it live and still and runs its checks. - Drop it into
scenes/asscenes/<name>.json, once it is approved. The file's name is the scene's name. - Build:
npm run illustrations. The run draws it in every palette and direction, runs every check, adds its still to the baseline (a new picture moves no approved one), and the element anddist/catalogue.jsonlist it. - Release a minor:
python3 scripts/release.py(a scene added is a name added — RELEASING.md §2). - On the page: install the new version and write
<pepper-illustration scene="<name>">.
Versions
The package follows RELEASING.md like the others. Its public API is dist/catalogue.json: the scene and object names
and the element's attributes. A scene added is a minor; a scene redrawn is a minor listed under Changed; a
name removed is a major. It stays under 1.0 while the catalogue grows — the first releases are 0.x.
For the library itself
What follows is how the library is built and checked, in this repository. The rules an illustration follows are
decisions D116–D123 in docs/03_DECISIONS.md: the style, one colour or the three, line widths as tokens, the screen
colour, rounded ends, arrows, and camera and motion; how the package loads on a page is D164. The whole brief, with
the loop that makes a new illustration, is Design_system/illustration-test/CHAPTER.md.
What is here
| Path | What it is |
|---|---|
| library/tokens.js | GENERATED by npm run build from tokens/*.json: the illustration/… tokens (D124) and the primitives beside them — edit tokens/, never this file |
| library/style.js | names those token values for the objects: colours, outline, connector, arrow, badge, the screen fills; plus the lights and the one camera direction, which are rendering constants |
| library/content.js | what is drawn on surfaces (screens, labels, badges, icon tiles, cards), as drawing steps replayed to a canvas (live) or written as SVG (still) |
| library/batch2.js | batch 2's eight build functions, word for word as Claude Design wrote them on its canvas — a module, not a string run through new Function, so the library also runs where a content security policy forbids eval (3.7.9). The kit hands them the starter's names as one scope, helpers(colour) |
| library/objects.js | the objects, the shared stage and camera, the outlines (thick lines, sized before every render — below), and the helpers that keep arrows clear of their targets and outlines on round shapes |
| library/scene.js · library/catalogue.json | a scene is data (3.7.12, D134 — below): compose(description) builds any description on the library's stage; problems(description) says what is wrong with one in plain words; the catalogue is what a description may say about each object |
| library/checks.js | the two arrow measures the runner takes — crossings and the tightest bend — read from a still in the browser, so whoever composes a scene is told at once |
| library/icons.js | GENERATED by node build/illustration-icons.js (inside npm run build) from lucide-react, the design system's icon set: 106 icons, each as one path, beside the fourteen glyphs content.js writes out, which the approved pictures carry |
| scenes/*.json | the scenes, each a description, named by its file — cross-screen, intelligence (the slide), flow, infra, release, screens, cross-screen-close, the Travel page's eight (D170) and the solution pages' twelve (D175). A new scene is one more file here |
| scenes/index.js · scenes/meta.js | GENERATED by the runner from scenes/*.json and the baseline: every description by its name (the engine's), and each scene's size, palette and direction (the element's, before any engine) |
| scenes/<name>.js · scenes/sheet.js | the first six scenes' thin modules, which hand their JSON on to the animated pages; sheet.js (one object alone) is still code |
| component.js · engine.js · engine-global.js · library/motion.js | the element, the engine it loads, the engine's classic-script entry, and how a live picture moves (read by both) |
| svg.js | the still: surfaces back to front under the same lighting, outlines split where hidden, connectors, flat arrows, icons as text-keeping decals |
| render3d.html · export.html · view-svg.html | load a scene (?scene=, or any description by its address, ?src=) live, export it to SVG, or re-render a stored SVG |
| *-live.js · *-live.html | the first six scenes' animated pages; the runner bundles each into out/*.bundle.js so a page opens straight from disk. scene-live.html?scene=<name> is any scene's, through dist/ |
| vendor/ | three.js 0.184.0 and its line add-ons, vendored (imports point at this copy); the runner fails on any other revision |
| baseline/ | the committed record: every scene's and object's SVG, and how far each still differs from its live version on each platform (parity/darwin.json; linux's is recorded from CI) |
Running it
npm run illustrations # = python3 scripts/illustrations.py: renders everything into out/, runs the checks, writes catalogue.html
python3 scripts/illustrations.py --bless # after a change that is MEANT to move a picture: the run becomes the baseline
python3 scripts/illustrations.py --static # no browser: the committed record whole and its arrows sound — npm test runs this
python3 scripts/illustrations.py --quick # a library change NOT meant to move a picture: every scene still against the baseline, byte for byte, in seconds (D135)
python3 scripts/illustrations.py --stills # …every scene and object in every palette and direction drawn again into dist/ (two minutes)
python3 scripts/illustrations.py --component # …then bundle the element and its engine into dist/ again, without the full run
python3 scripts/illustrations.py --scene FILE # any scene description: drawn live and still, its checks run, its files in out/composed/<name>/Where the checks run (3.7.6). npm test runs the static half: every still the runner draws has its committed SVG
and its recorded figure, nothing else is in baseline/, and the committed scenes' arrows cross no outline and bend no
tighter than 16 px. So a picture with a broken arrow cannot be blessed and committed while npm test passes. The
render half runs in the pipeline's Illustrations step, and locally with npm run illustrations.
Per platform. The SVGs are held byte for byte everywhere. How far a still differs from its live version is
anti-aliasing, which depends on the platform, so each platform has its own figures in baseline/parity/. linux's
are blessed from a CI run's out/parity-linux.json, after a look. Until then the step runs with --missing=warn,
which reports that one check instead of failing it. A --bless on linux writes linux's figures only; the SVGs are
blessed on the platform where the pictures are approved.
It needs Python Playwright with Chromium, fontTools and Pillow; esbuild comes from the repository's node_modules.
out/ and catalogue.html are rebuilt on every run and are not committed. Open catalogue.html to see every scene and
object, still first, with each scene's animated version one click away.
The checks, and exit 1 if one fails:
- three: the vendored three.js is revision 184.
- widths: the outline's width is the token's in both versions. One object is drawn again at twice the width
(
?outline=2on the runner's two pages) and its outline, measured across an edge, must come out about twice as wide live and still. At today's width a live outline is one device pixel, which is also what WebGL's own lines were, so no other check can tell whether the live version follows the token. - baseline: every SVG is the committed one, byte for byte, or the same pieces in another order with an identical render. Hairlines vary slightly between renders, so the proof is the file, not a screenshot.
- still: each still differs from its live version no more than the baseline recorded, plus 0.05 points. The difference is anti-aliasing on lines and text: 0.44–0.69 % of pixels for a scene, up to 1.52 % for an object. A missing or shifted piece makes it grow.
- palette: every scene, and each scene drawn in another palette (scene 1 in red; the slide in yellow and in blue), shows exactly the colours it was built in. The check reads the still: every coloured value in it, however it is shaded, is sorted to the nearest of red, yellow and blue, and neutrals are left out — a value nearest blue needs a little more colour, because the ground light gives a neutral face a faint blue cast. Every object is also drawn in blue alone, and must show blue and nothing else.
- arrows: no arrow crosses another object's outline, and the tightest bend is at least 16 px (D122).
- files: every still's file, as
dist/carries it (Poppins inside), renders placed as an image as it does inline. Both renders are softened by 1 px first: the 0.5 px hairlines anti-alias a little differently each time, which counted 621 px on a scene, and the blur makes that 0. A word in another face still differs by 30 403 px, and at another weight by 17 099. The fonts inside each file are read to check they cover every character the file uses. This Mac has Poppins installed, so a missing font would fall back to it without a trace. The slide's file is byte for byte the approved slide file (pinned by its sha1:three-layers-of-intelligence-v4, the slide with the library's one signal and round corners however near it is held, approved 2026-09-23 — D156; v3 was approved 2026-09-21 — D136; v2 is the file frozen in the illustration test'sreport/). - stills: every scene in every palette and facing both ways shows only its palette's colours, and its arrows cross nothing and bend no tighter than 16 px. In the three, a scene with fewer coloured objects shows fewer colours, and the check says which.
- objects: every object alone in every palette and facing both ways shows only its palette's colours (none, if it carries none), and every object with a front turns it: its right-facing still is not its left. Only the arc, a bare line, draws the same picture both ways.
- component:
<pepper-illustration>on a bare page, with no Poppins and no other script. A still is its file, drawn at build time — the same picture as the file placed as an image — and no engine loads. What has no file (a description by its address) the engine draws as the live render does, with its own three Poppins faces. Live, it moves. An object alone told to face right is its right-facing file as a still, and live the engine draws it facing right — nearer its right still than its left (five objects with an unmistakable front: card, shield, phone, TV, user). - page: what a page pays, on a page shaped like the site's — one live scene, six stills, two live scenes far below. A page of stills downloads no engine and opens no WebGL context; the live scene's still is painted before the engine arrives; a live scene below the fold is not built until it is scrolled to; the three share one WebGL context; out of view a picture pauses, and with none in view no frame is asked for.
- bundles and browser: the animated pages bundle, and no page logs an error.
npm test holds the tokens: every value in tokens.js equals its token in tokens.css, and no colour is typed in the
drawing code (the library, the scenes, the exporter, the animated pages).
A scene is data (3.7.12, D134)
A scene is a description: plain JSON that says which library objects stand where, what they carry and which arrows run
between them. library/scene.js composes it on the library's own stage — the one camera, the lights, the outlines, the
palette, the arrow rules — so whoever writes one composes a picture and draws nothing. The format is documented at the
top of that file; this is the shape:
{ "format": 1, "title": "…", "size": [1000, 470], "palette": "three",
"objects": [
{ "id": "platform", "object": "disc", "at": [0, 0, 0], "radius": 1.05, "t": 0.24, "colour": 1 },
{ "id": "engine", "object": "block", "on": "platform", "icon": "cpu", "colour": 1 },
{ "id": "results", "object": "bars", "at": [5, 0, 0], "tinted": true, "colour": 2 } ],
"arrows": [ { "id": "out", "from": "engine", "to": "results", "signal": true } ],
"motion": { "signal": ["out"] } }- Places are along the screen's own axes: x to the right, z toward the viewer (down the screen, at four tenths of
x's scale), y up. An object always stands square to the world. The library's first two scenes keep the world axes
they were laid out in (
"axes": "world").onstands an object on another;colouris which of the palette's colours it takes."mirror": trueis for a cluster placed by hand round one object, where the side a part sticks out of is the picture: facing right, what standsonanother object stands on its other side, so the picture is the mirror image of its left-facing self (3.7.28, D172). Without it, nothing is mirrored. - What an object takes is
library/catalogue.json: its options, each with its type. A description that names an object, an option or an icon the library does not have is refused, with a sentence that says what it may say instead. - Arrows name the two objects they join. The library picks the face an arrow leaves through — of the two sides the
camera sees, the one turned toward the target, at the point nearest it; the top when the target stands behind — aims
it at the target's middle and ends it where that line meets the target, then stops it 16 px clear of every outline. Its start is walked forward the same way (D176, 3.7.31), so an arrow stands in the middle of the space between the two objects it joins; the face route of two devices standing close keeps its start on the source's face.
Several arrows into one object arrive side by side. Between two thin objects that face the viewer standing close side
by side — two phones — an arrow through the gap would be a stub, so it leaves the source's face low, bows out toward
the viewer and comes to the target from in front of its near edge (3.7.17, D157 c). An end may also be
{ "of": id, "at": [x, y, z], "viewer": r }— a point measured from an object along its own axes — which is how the approved scenes keep their arrows exactly. - The shape comes first, and the objects lead (D155, D157, D158).
sizeis"square","portrait"or"landscape"(1000 × 1000, 1000 × 1250, 1000 × 560) or[w, h]. A description that gives no object anatis laid out by the library (D158), in the order its objects are listed: a row across a landscape (or a picture with no size), a zig-zag down a portrait or a square, neighbours half their mean size apart on screen (LAYOUTinstyle.js); an object withongoes with what it stands on, and an arrow that runs down the picture leaves its source at the foot. Place objects by hand only for a picture that is not one chain. The result'swarningssay when what the picture holds fills under six tenths of its frame one way — a composition made for another shape — and when the space an arrow crosses between the two masses it joins is more thanARROW.lead(0.85) times the larger of them: the arrow leads. - The frame fits what is drawn, and a landscape lies on one line (D177, 3.7.31). The frame is fitted on every vertex of every part, through the camera — never a part's box — so the margins are even by themselves. In a landscape (at least 1.4 times as wide as tall), what stands one above the other is a column, and every column is centred on the picture's middle line: a single object on it, a branch split evenly above and below it. A row the library lays out lifts each mass to the middle of the tallest; a picture placed by hand gets a warning naming the column, how far off the line it stands, and how far to move it in
y. A stack whose front card carries the words is ascreenCardwithsheets(nought to three plain cards behind, the code stack's staircase); ashieldcarries aniconon its inner shield. When the still's draw order has no answer, what is left is drawn far to near, never in the order the scene lists it. - Four rules of composition (D135, D139). Data on an arrow is the signal:
"signal": truerests the three-sphere trail on the arrow, sized and spaced by what shows on screen; an arrow carries no packets andpacketsis refused. A signal is the colour of its source (D139): it takes the colour of the object the arrow comes from — a blue source sends a blue signal — without anything being said, and an arrow'scolouris only for saying something its source cannot; an arrow that starts at a bare point takes the palette's first colour. What stands on a platform stands in its centre:oncentres it, and nothing is given beside it. A name stands upright: a plate that is only a name — a source, a channel, an item of a list — takes"upright": trueand stands, its label and badge on its face (plateUpright()inobjects.js; the approvedplate()is untouched); a plate lies flat only when it is a platform. Standing plates in a column are 3.4 apart. - The signal is one component (D136).
K.signal()builds it,SIGNALinstyle.jsis its standard — three spheres in the colour's 300 / 200 / 200 steps; a lead of 8 px in a 1000-px picture, never under 2.2 half-widths of the line it rides; 2.4 lead radii apart along what shows; resting 0.6 of the way in a still; live, 140 px a second on every arrow, 0.4 s between arrows, 1.5 s before the round starts again — andshownPath()/placeSignal()put it on a path. A description says where a signal runs, never its look or its pace, and which colour only to override the source's (D139). No scene draws its own signal: the slide's rod carries the component too (D136 e, approved 2026-09-21). - A ring carries the signal too. A ring named in
motionis one leg per hop around what it joins — it leaves each object for the next and comes back to the first — so the signal takes each source's own colour in turn (D139) and each target lights up as it arrives, as along an arrow. The still is untouched: a ring rests no signal on itself. - What composed but deserves a look is the result's
warnings: two objects in one place, an arrow with no room.library/checks.jsadds what the still says about its arrows: a crossing, a bend under 16 px. - The six library scenes are descriptions, and every still is byte for byte the committed one.
--staticfails a scene module that builds its scene itself. - A description from anywhere — a Claude Design canvas, a page, a hand — is drawn and checked by
python3 scripts/illustrations.py --scene FILE: the still with Poppins inside, the PNG at 2×, the live render, the palette it shows, its arrows. It joins the library once approved: its JSON inscenes/, its thin module, a line in the runner'sSCENESand incomponent.js, its baseline.
A device's screen, a close-up, and which way a picture faces (3.7.16, D154)
- A screen shows the system's page. A
phone,laptoportvtakes"screen"in place oflabelandicon:{ "ui": [ … ], "dark": true }— each item ONE ofeyebrow,heading,body,button,icon,lines(a count of grey lines that stand for copy, D170), or acardholding those — laid out top to bottom byuiSteps()incontent.js, in the device's canvas (a phone web-mobile, a laptop or a TV web-desktop) from the tokensUIinstyle.jsnames: the eyebrow islabel/medium, a heading the page'sheadline/mediumand a card'stitle/medium, copybody/medium, the button the large one (size/control/large,radius/8). Light, it is a page; dark, the footer's ground and inks. Every word is<text>in the still, wrapped to the screen; what runs past the foot is cut and said. The face sizes are the phone's 340 × 720, the laptop's 768 × 489, the TV's 768 × 431. - A picture is the exception:
{ "image": an address }, drawn cover-and-top on the screen and carried in the still as the file itself (data-raster="picture").loadImages(description)loads it before a build — the element, the runner's pages andPepper.Scenedo — and a scene whose picture has not loaded draws the plain screen and says so. - A close-up needs no word.
lineWeight()instyle.jsreads how near the picture is held (px to a scene unit in a 1000-px picture: the first five scenes are 101–152, two phones filling the frame 469) and from 160 thickens the outline, the arrows and the signal — its size, its spacing and its speed — until from 240 they are as thick against the objects as the cross-screen scene's (121.43). The result'sweightis the factor; the still's outline and the live one both read it through the camera (cam.userData.weight). The arrow's 16 px clearance is a distance on screen and does not scale. No library scene is near enough to move. direction:leftorright. Right stands the camera at the other front corner, mirrored across x, with the lights (cameraAt(),lights()inobjects.js); every object turns its front the other way and nothing is mirrored.layoutFor()keeps a world-axes scene where it stood on screen; a screen-axes scene keeps it by its own axes. Aturnturns the other way too, and an arrow leaves through the two sides this camera sees. The element andPepper.Scenetake it as an attribute, over the description's own.- The still draws what is shown:
exportSVG()walks visible objects only (a signal ball with no room on a short arrow was drawn at the scene's origin before).
Line widths (D119, D126)
Three tokens set every line: illustration/outline/width, illustration/arrow/width and
illustration/connector/radius. Change one, npm run build, and the live version and the still both follow.
- Outlines are three.js's thick lines (
LineSegments2), the type the arrows use. The token is the width in px of a still at its own size. A still exported at another size scales it with the picture; the live version draws the same line at the size it is shown (stage()sizes the outlines before every render), and never under one device pixel, below which WebGL breaks a line into dots. - A renderer needs
stencil: true. The outline's ink is not opaque, and a thick line's segments overlap at every joint; the stencil lets the ink lie once on a pixel, as the still's single outline path does. Without it the picture still draws, with darker joints on curves. - The faces step back (a slope-scaled polygon offset) by the outline's half-width, so the half of a line that lies over its own face is not cut. It grows with the width and is 1 at today's.
- Connectors stay tubes: a tube's radius is the token's, in scene units, in both versions already.
One colour or three (D118, D127)
A picture is one of the system's colours or the three. A scene takes palette: 'red', 'yellow', 'blue', or
'three' (red, yellow and blue). Anything else stops the build.
render3d.html?scene=intelligence&palette=blue export.html?scene=cross-screen&palette=red slide-live.html?palette=yellow- Defaults. Scene 1 is in the three: the TV is red, the phone yellow and the laptop blue, as in the first example. Every other scene is in red. The object sheet is in the three, so the catalogue keeps its look.
- What follows the palette is every icon tile, tint, highlight, rim, light and signal. The kit is made in a palette
(
kit({ palette })), andK.colour(i)is the steps (100–400) the i-th coloured object takes: always the one colour, or red, yellow and blue in turn. An object takes its colour from itscolouroption, or from the palette's first colour when it has none. - In the three: each scene gives its coloured objects the three in reading order. A target lights up in its own colour's 100 tint, and a signal takes the colour of the object it comes from (D139) — the palette's first colour only when it comes from no object.
- The Claude Design source (
library/batch2.js) names its one colourRED. The name stays word for word, and the kit binds it to the object's colour.
The outputs (3.7.7; D164)
dist/pepper-illustration.js the element, a small classic script
dist/pepper-illustration-engine.js the engine it loads: three.js, the library, every scene
dist/stills/<scene>--<palette>--<direction>.svg every scene in every palette and direction
dist/scenes/<scene>.svg · <scene>@2x.png the scenes in their own palette, and the three held variants (<scene>--<palette>)
dist/objects/<object>.svg · <object>@2x.png the objects
dist/descriptions/<scene>.json the scenes' descriptions
dist/catalogue.json the package's public API
dist/manifest.json every file's sha1, and what each was made from- A still's file is its SVG with the design system's own Poppins put inside, cut to the characters it uses (a
still without words carries none). It renders correctly placed as an image, in a page, a slide or a PDF. The PNG is
that image render at twice the size, transparent. A scene's own palette and direction, and each held variant, is
the committed baseline SVG; the rest of
stills/are drawn by the same exporter from the same description. - The element is the page's half:
component.js, bundled alone, knows each scene's size and its own palette and direction (scenes/meta.js) and nothing of three.js. It paints the still, and the first time a picture has to move it addspepper-illustration-engine.jsbeside itself — after the page's load event, a classic script, so it also works from disk. The engine carries Poppins 400, 500 and 600 cut to Latin (library/poppins.js, generated), so its words are Poppins on any page. It holds ONE WebGL renderer, off the page, and each live picture is a 2D canvas it copies a frame into, drawn in a corner of the renderer's canvas the picture's size — so any number of live pictures cost one context. One loop serves them all, and a picture takes part only while its element is in view. - When
dist/changes. It is written bynpm run illustrationson the platform where the pictures are approved, once every check has passed, and only where a source changed;--blessrewrites it all.npm testholds it: every file is the one the manifest pins and was made from the baseline and the library as they are now (the stills in every palette carry a fingerprint of everything they are drawn from, so a token change leaves them stale until the next build draws them); each SVG inscenes/andobjects/is its baseline with Poppins put in and nothing else changed; each PNG is twice the still's size; the catalogue, the descriptions and the two generated modules are what the sources write; the element's attributes are the catalogue's; and the element and the engine are what their sources bundle to.
In Claude Design (3.7.9, D132)
Pepper UI is a design system in Claude Design, and this library is its bundle. design-system.js is the bundle's entry:
scripts/claude_design.py bundles it into one classic script that assigns window.Pepper, with no eval, no import and
no network, which a design system's preview frame forbids.
Pepper.Illustration— a library scene or one library object:sceneorobject,palette,label, andstill. Live, it is<pepper-illustration>. Withstillit is the still itself: the exporter's SVG drawn in the page, in a shadow root so two stills' ids never meet. A still holds no WebGL context, so a canvas can carry any number of them.Pepper.Scene(3.7.12, D134) — a NEW scene, composed on the canvas:sceneis a description,palette,labelandstillas above. What the description has wrong with it, and what composed but deserves a look — the scene's warnings and whatlibrary/checks.jsreads from its still — is written under the picture in plain words. The system's README in Claude Design carries the method, the format, every object with its size and options (fromlibrary/catalogue.json) and the icons;index.d.tstypes the description option by option.Pepper.Draft— an object that is not in the library yet, built on the canvas:build(h, options)returns aTHREE.Groupmade with the helpers the kit hands batch 2 (helpers(colour):THREE,MAT,C,RED,slab,roundedShape,outlined,round,tinted,dot,iconSlot,TINT), under the library's stage, lights, camera and outlines. The element draws it through itssourceproperty.stillshows what the exporter makes of it, where pieces that overlap show before the import and not after. The object insidebuildstays one plain named function, and comes into the library word for word, as batch 2 did.- The starter canvas is
claude-design/starter/— Pepper illustration starter in Claude Design: Pepper installed, one artboard composing a new scene from a description, one placing a library scene, one drafting an object live beside its still. The next batch starts from a copy of it, not from an attachment pack. - Republishing is
npm run claude-design(build), then the publish in a Claude Code session;npm run claude-design:checksays whether what is published is what the repository builds.
Known: the working pages here (render3d.html, export.html, the animated pages) and the baseline SVGs load
Poppins from Google Fonts, as they did when the scenes were approved. Only the outputs carry the repository's own
files, because moving the baseline would change every SVG.
