Rosse is a galaxy-drawing engine. Every mark it puts on the page comes from a library of real hand drawings: dots, strokes, knots, stars and whole galaxies, drawn with a fineliner, scanned and cut out. The engine builds each galaxy in three dimensions, then places, transforms and inks those drawings onto Paper or Chalkboard.
This repository is the rebuild of Rosse v21 (a single HTML page, kept in assets/reference/ as the oracle) on WebGPU and WGSL, with a matching CPU engine for browsers that have no WebGPU. Every picture on this page, the banner's galaxies included, was drawn by this engine, not by v21.
| principle | in practice |
|---|---|
| Hand-drawn, never procedural | There is no generated mark. Dots, knots, strokes, cores, stars and 156 whole galaxies are scanned pen drawings; the engine only chooses where they go, how they are turned and how heavy the pen is. |
| On the GPU | Sampling, projection, culling, stroke expansion and merger integration run in WGSL compute passes. The CPU builds a small scene description and nothing is read back on the frame path. |
| A CPU twin for every kernel | Without WebGPU, TypeScript twins of the kernels run with the same random numbers, f32 arithmetic and a software rasteriser, and agree with the GPU within tolerance (npm run test:gpu). |
| Deterministic | A counter-based RNG keyed by seed, stream and index. The same seed gives the same galaxy, and orbiting the camera never re-rolls the marks. |
| Measured against v21 | Parity is statistical (same structure, ink, pen weight and mark counts, not the same dots) and is tested against 582 captures of v21 with a calibrated metric. |
v21 stays the oracle: the engine's core draws what v21 draws, and the goldens hold it to that. On top of that core, the page switches on a set of natural features, each an optional flag (off in the core, on in the page, listed per ADR in docs/architecture.md), so a galaxy reads as a body in space and not a flat print. The pictures below are the page's.
| the page draws | what changes | ADR |
|---|---|---|
| Depth and perspective | marks shrink with distance and as the view zooms out; the disc dims a star behind it; the near and far sides of a galaxy and its debris differ | 0084, 0086, 0088 |
| Natural dust and bulges | a dust layer in the disc, deprojected Sersic bulges, a boxy-peanut bulge for barred galaxies on request, dust as brush strokes at their own heights | 0075, 0076, 0087 |
| A thin disc and 3D line-work | a dense sharp plane with a few wanderers; arms, dust lanes, jets, streams and tails as 3D stroke bundles that stay put when you orbit | 0081, 0083, 0089 |
| Stellar populations and colour | old stars drawn fine and young ones large, clumps, halo clusters and streams of stars; a fifteen-ink colour plate whose colour follows temperature, radius, arms and dust, in pastel chalks on the Chalkboard and pencils on Paper | 0090, 0091 |
| The drawn core behind the disc | the central bitmap is drawn first, under the disc's stars, dust and strokes | 0092 |
| Mergers that keep their galaxies' character | a spiral holds its inner disc and crisper arms through the first pass, an elliptical is a soft glow, dust lanes and a bridge thin the stars and travel into the tails, tails carry tidal dwarfs, strokes taper at the tear, discs are thicker | 0093 |
Open ?dustAuto=0&bulgeAuto=0&starsAuto=0&cosmicAuto=0&strokesAuto=0&lineWorld=0&thinAuto=0&popAuto=0&depthAuto=0&occlAuto=0&coreAuto=0&mergeAuto=0 to see the core's drawing alone.
Every picture below was drawn by this engine, on SwiftShader (software WebGPU), at a fixed seed. The pictures are plates in the sense of an atlas: numbered, captioned, laid on a grid. Every galaxy, seed, parameter and camera path is in tools/readme-assets/plan.mjs, and one command re-draws all of them (see Regenerating). The colour you see is the engine's own (the colour plates); nothing is added.
Galaxy Zoo 2 asked volunteers a tree of questions about 239,695 SDSS galaxies. For each kind of galaxy in the catalogue's own list (smooth, spiral, barred, edge-on, merger, ring, irregular, many arms, tight arms, loose arms), these are the galaxies with the highest vote fraction for the question that defines it. Rosse draws each one from its votes alone (fromVotes), at the seed its object id gives, with the bars showing the votes it was given.
Ties are common (a fraction of 1.00 is easy to reach), so the choice is: the highest fraction, then the most volunteers, then the lowest object id. The rule is written out in GZ2_RULES in the plan, and the choices (with their votes and parameters) are recorded in gz2-picks.json.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
The catalogue has votes but no pictures. The asset pack does have the SDSS photographs of 42 real galaxies, so here they are side by side with Rosse's drawing of each, drawn from the same votes. (Photograph: 160 px, enlarged.) Where a cutout of a catalogue galaxy is in docs/img/gz2/cutouts/, the plates above show it too; see Regenerating.
![]() |
![]() |
![]() |
![]() |
All 45 presets of src/core/presets.ts, as the page draws them at seed 7. The sheet is made from the preset list itself, so a new preset appears in it the next time the pictures are regenerated.
Each film is a camera path through the engine's own camera (azimuth, inclination, roll, zoom), eased, timed, and looped where it can be. The marks are fixed in three dimensions, so nothing is re-rolled as the camera moves.
The pen keeps its weight at any zoom, so a mark is always a mark. At the deepest zoom the nucleus resolves into the scanned core drawings it is made of, and a star into its drawn heart.
![]() 11 PUSHING IN x1.5 to x9 on the nucleus and back |
![]() 12 PUSHING IN ON A STAR spikes, rings, then the drawn heart |
A galaxy is a pile of kinds of mark, laid in a fixed order. Here is one galaxy, with each layer added in turn: dots, knots, strokes, drawings (pen lines and whole drawings), drawn stars and cores, and the sky.
![]() 14 DRAWN IN each layer spreads from the centre; the camera drifts |
A lens is a galaxy (or a cluster) in front of a source: the source's drawing is bent into rings, arcs and images. The source can be a galaxy, a quasar or one of the owner's hand drawings (Fig. 36.6).
![]() 17 EINSTEIN RING the source drifts: ring, arc, two images, ring |
![]() 18 EINSTEIN CROSS a quasar: four images follow the source round |
![]() 19 DOUBLE EINSTEIN RING two sources behind one lens |
|
Mergers are simulated: a core track integrated on the CPU and test stars moved by the GPU, with the tides stretching the discs into tails. The merger's own timeline (mTime) runs from the approach to the settled remnant; each film below moves the camera with it. On the page each galaxy keeps its character through the first pass: a spiral its arms and dust lanes, an elliptical its smooth glow, with dust lanes and tidal dwarfs carried into the tails (ADR 0093).
Shells are what a minor merger leaves behind: faint arcs round a smooth galaxy, simulated and then drawn.
![]() 27 SHELLS FORMING shellTime 12 to 90, with a slow turn |
Behind and in front of every galaxy there is sky: a deep field of small drawn galaxies, foreground stars with their spikes, rings and glare, and the faults of a real exposure (a satellite trail, a ghost reflection, cosmic rays). All of it is drawn from the same library of pen marks.
![]() 29 THE DEEP FIELD a long glide; the sky turns with the camera |
nice -n 15 npm run readme:assets # everything: banner, gallery, anatomy, gz2, gifs
nice -n 15 npm run readme:assets -- gz2 # one stage (banner, renders, gallery, anatomy, gz2, gifs)
nice -n 15 npm run readme:assets -- gifs --only=merger-mice
npm run readme:assets -- fetch-gz2 # the SDSS cutouts of the Galaxy Zoo 2 picks (needs the network)
npm run readme:checkEach stage re-draws only its own outputs, deterministically; the plan, the camera paths and the catalogue rule are in tools/readme-assets/ and docs/readme-refresh.md. fetch-gz2 downloads the SDSS cutout of each Galaxy Zoo 2 galaxy above once, into the repository; after that the plates show photograph and drawing together and regenerating needs no network.
You need Node 22 or later and a browser with WebGPU for the GPU engine (any other browser draws on the CPU engine, and says so under the plate). To validate shaders you also need naga (cargo install naga-cli).
npm ci
npm run dev # Vite dev server: the page (plate, controls, presets, timeline, exports)The page has the plate on Paper or the Chalkboard, v21's controls as a recipe of cards, preset cards drawn by this engine, the merger's timeline, the 42 real galaxies with their photographs, the Galaxy Zoo 2 catalogue, and PNG, SVG (for pen plotters, one layer per pen) and GIF (the merger's timeline, a quasar's flare) export. Drag to orbit and tilt, shift-drag or Q/E to roll, wheel to zoom. A link holds the drawing: ?preset= (or ?from=real:<n>, ?from=gz2:<DR7 object id>), ?seed=, ?az=, ?incl=, ?pa=, ?zoom=, ?surface=paper|chalk, any parameter by its name, and ?backend=cpu|webgpu; for example /?preset=Barred%20spiral&seed=11&backend=cpu.
| command | what it does |
|---|---|
npm test |
unit tests (vitest) |
npm run test:gpu |
browser tests on WebGPU (SwiftShader): RNG vectors, CPU = GPU kernels and raster, the page's UI and extras smoke tests |
npm run golden |
the golden check against v21 (see below; slow) |
npm run lint, typecheck, validate:wgsl, build |
the other CI checks |
npm run readme:assets |
re-draws every picture on this page, by stage (tools/readme-assets; needs ffmpeg and ImageMagick) |
npm run readme:check |
fails if a README image is missing or a picture is over its size budget |
npm run screenshots |
screenshots of the plate on both surfaces and both engines |
The tools that drive a browser use Playwright's Chromium (npx playwright install chromium) with software rendering, so no GPU is needed.
parameters ──► schema: which tier is dirty?
│
model dirty ───┼─► CPU scene description (a few kB): variation, curves,
│ parts, merger core track
│ GPU stipple samples · merger test stars · shells
view dirty ────┼─► GPU project + cull → scan → instances
│ curves → ribbons · vectors → capsules
present ───────┴─► GPU ink layers in v21's order → ink target
→ composite: Paper | Chalkboard + plates
| tier | re-runs when | cost of an orbit frame |
|---|---|---|
| model | a parameter that changes the galaxy | none |
| view | the camera or zoom moves | the view passes only |
| present | the surface or plates change | one composite pass |
- Kernels with CPU twins. Each compute kernel in
src/shaders/compute/(stipple,scan,project,ribbons,vector-expand,merger,merger-sprites,tide,shells, and more) has a TypeScript twin insrc/fallback/kernels/and a parity test. - Two ink pipelines. Instanced quads for bitmap drawings, and analytic capsule ribbons for pen lines, into an
rgba16floatink target; the composite blends it onto Paper or Chalkboard. - Determinism. A counter-based RNG (
pcg4d), so each sample owns its randomness and no result depends on thread scheduling. - Deliberate differences from v21: orbiting keeps the marks; integer-lattice noise replaces v21's
sin-hash noise; drawings have separate mip chains. Listed in docs/architecture.md.
Each milestone is one reviewable pull request. The engine draws single galaxies, mergers, stars, artefacts and the sky, and lensing; the page and the extras are built on it. M0 to M12 are all built; M7, M9 and M12 are in the integration pull request (#15) and reach main with it.
| # | milestone | state |
|---|---|---|
| M0 | Plan: reference notes, architecture, ADRs, v21 captures | merged |
| M1 | Paper and one bitmap mark; RNG, atlases, ink and composite | merged |
| M2 | Stipple disc from the model; the golden comparison harness | merged |
| M3 | Camera, orbit and zoom; the cache tiers | merged |
| M4 | Stroke ribbons and arms; dust lanes | merged |
| M5 | Vector marks: bars, rings, whole drawings, jets, streams | merged |
| M6 | The single-galaxy preset set; plates; Chalkboard | merged |
| M7 | Stars and artefacts; the sky | built (pull request #15) |
| M8 | Mergers and shell galaxies | merged |
| M9 | Lensing | built (pull request #15) |
| M10 | Performance pass: pooling, kept batches, the CPU engine in a worker, the profiling and L1 harnesses | merged; real-hardware numbers open |
| M11 | The page and its UI | merged |
| M12 | The extras: Galaxy Zoo 2 catalogue, real galaxies, SVG and GIF export | built (pull request #15), wired into the page |
Beyond the milestones, the pull request that follows them (#17) adds the natural features above, each behind a flag with its own ADR, and tests them on both engines.
Still open: everything that needs a real GPU (the budget table of docs/architecture.md, the L1 report on a second adapter, and recalibrating the golden thresholds, Q14); measurements on a phone; the licence (Q1) and the SDSS acknowledgement wording the page must show; and the owner's sign-off of the proposed ADRs (0035, 0036, 0061 among them).
Not yet proven: all parity numbers are measured on SwiftShader, a software adapter. Details per milestone are in docs/roadmap.md and docs/milestones/.
- Unit tests (vitest) cover the RNG vectors, schema and tiers, the scene description, camera maths, the WGSL resolver, struct layouts and the CPU kernels.
- GPU tests (
npm run test:gpu) run the WGSL kernels and the CPU twins on the same input and compare them. - Golden images.
tests/golden/reference/holds 582 captures of v21 (ink, plate and parameters). Because v21 places dots from a sequential random stream and this engine from a parallel one, the dots land elsewhere by design, so plain pixel comparison would fail. The metric compares what must match: total ink, structure (SSIM of blurred density maps), moments and extent, pen weight (stroke widths from a distance transform) and mark counts, with thresholds calibrated from "same galaxy, other dots" re-draws and negative controls that must fail. The engine is also bit-exact against its own goldens on one adapter. See tests/golden/README.md and ADR 0013 and 0015.
| read | for |
|---|---|
| docs/architecture.md | the WebGPU design, tiers, the hard parts and the ADR index |
| docs/roadmap.md | milestones M0 to M12 |
| docs/reference-notes.md | how v21 works, stage by stage |
| docs/open-questions.md | decisions waiting for the owner |
| assets/README.md | the asset pack: drawings, objects, fonts, data, and the reference page and source |
| assets/reference/design-language/ | the design language this page borrows (Swiss grid, hairlines, cream paper) |
| CONTRIBUTING.md, CHANGELOG.md | how to contribute; what changed |
- The drawings are the owner's own pen drawings (dots, strokes, knots, stars, whole galaxies), scanned and cut out.
- Galaxy Zoo 2. The classifications behind the "Galaxies from the Zoo" plates and the real-galaxy pairs are from Galaxy Zoo 2 (Willett et al. 2013, MNRAS 435, 2835; Hart et al. 2016, MNRAS 461, 3663), the work of the volunteers of galaxyzoo.org, licensed CC BY 4.0. Changes: each vote fraction is quantised to one byte and the catalogue is re-packed; the drawings are Rosse's, not Galaxy Zoo's or SDSS's.
- SDSS. The photographs of the 42 real galaxies (and any cutout fetched for the Galaxy Zoo 2 plates) are from the Sloan Digital Sky Survey. Funding for SDSS and its acknowledgement, as the survey publishes it: sdss.org/collaboration/citing-sdss. The exact wording is the owner's to confirm and paste here (docs/readme-refresh.md).
- Fonts. The banner is set in Heros (the files are TeX Gyre Heros, a Helvetica-like face) and IBM Plex Mono (SIL OFL 1.1, text in
assets/fonts/ibm-plex-mono/). The handwriting fonts (Threshold Grain, Mark, Patina, Signs) are made from the owner's handwriting. Each keeps its own terms; they are listed inassets/fonts/fonts.json. - The project's licence is undecided. Until the owner chooses one (docs/open-questions.md, Q1), all rights are reserved; see
LICENSE. Third-party material inassets/remains under its own terms.


























































