A config editor that runs the real engine - #6
Merged
Merged
Conversation
Writing awan.json blind is the friction that's left. The Action already renders and commits the GIF; what nobody can do is picture the reel before they have one, so scene order and wording get chosen by guessing and then never revisited. This is a static page — arrange the beats, watch it play, take three files. It has to stay static. The Action's whole promise is that a banner keeps working whether or not we do: the config lives in the reader's repo and the workflow runs on their runner. A page with a server behind it is a page that can go down and take READMEs with it. The preview is not a mock-up. web/crate is a wasm-bindgen bridge over awan-core, which survives the trip because the engine is a pure function of (tick, character) — no clock, no RNG, no I/O. The canvas draws the frames CI draws. overlays.ts does for the canvas what profile/src does for the GIF, since the engine publishes a scene's shapes and leaves its words to whoever is rendering; without that, the two headline acts play to an empty stage. The length meter exists because a reel gets too long one innocent beat at a time, and the sample had reached 96 seconds without anyone deciding to. Every act carries its cost on the label now, and the total says out loud when nobody will watch it. The calendar behind the year wall is the one thing the page can't know: it lives in GraphQL, GraphQL wants a token, and no cartoon is worth pasting a token into a web page for. It shows a seeded year and says so.
The last build looked close enough to ship and wrong enough to notice, which is the worst thing a preview can be: someone builds a config against it and CI hands them something else. It drew the caption in HTML underneath the canvas, in Courier. CI draws it inside the image, in font8x8, at 1056x416. So the canvas is that size now and the text comes from the engine's own glyphs, shared rather than reimplemented — icons.rs moves into awan-core, because there are two renderers now and a preview that draws its own idea of an icon is a preview that lies. Two bugs surfaced from checking it against a real render rather than trusting it. Rust computes 303 / 2 = 151 and JavaScript gives 151.5, so every glyph landed on a half-pixel and the canvas antialiased its edges — a third of the ink stopped matching. And [150, 150, 160] is #9696a0, not #96969f: I wrote the hex by eye. The caption strip is now identical to the GIF's, 4833 ink pixels and the same bounding box. The rest is what the shape should have been. Everything drawing lives in its own file, split by what it does: lib for data, ui for primitives, stage for the reel, story for the running order, steps for the wizard. Cards don't nest — a card inside a card is two borders saying the same thing. Three steps rather than one page, because one page of everything buried the preview under a form. Neobrutalism because it suits him: he's flat blocks of colour with hard edges, and a soft interface around him reads as an apology for how he's drawn. The emoji are gone — they arrived in whatever style the reader's OS ships, anti-aliased, beside a character made of 33px squares. These are 8x8 pictures written as pictures, in his own palette.
cargo fmt, missed after moving icons.rs into the engine.
Ten things, most of them small, one of them not. The character picker is the one that needed the engine to move: spec::load read a file, and a browser has the text but no filesystem. It's spec::parse now, with load as that plus a read. The cast comes from the repo's own TOML at build time, so the preview restyles from the same specs the CLI does and adding a character stays a TOML-only job. A spec that doesn't parse falls back rather than failing — the editor should keep drawing while someone is halfway through breaking their own file. The draft survives a refresh. Rewriting seven captions because you reached for reload out of habit is a page you don't come back to. It's localStorage: no account to make, nothing sent anywhere, and clearing your history clears it, which is what clearing your history is supposed to mean. Solo plays one beat alone, because deleting the rest to look at a scene means rebuilding the story afterwards. Every act now wears its own colour on the timeline, and the blocks are buttons, so the bar reads as the story and doubles as a way into it. Light mode repaints the furniture and never the stage: the canvas keeps #0d1117 because that is the colour the GIF has, and a white stage would be showing someone a banner they aren't going to get. Plus: an example filler that uses the project's own handle rather than a stranger's details, a GitHub mark drawn as a path instead of fetched, and just enough highlighting to stop the config being a wall of grey — a tokeniser, not a syntax library, to colour twenty lines.
Both crossed 200 lines while I was adding to them, and I committed anyway, which is how a line cap quietly stops meaning anything. spec.rs was doing two jobs: serde says whether the TOML is a document, validate says whether the document is a character. The second is where all the friendly errors live — someone writing pixel art in a text file should get the line they got wrong — so it earns its own file rather than being the tail of another one. The wasm bridge grew the same way. layout.rs is what a renderer needs to draw a scene's overlays: the panel, the wall, the font, the icons. The reel isn't involved, and neither should the file about the reel be.
Two parity bugs, both spotted by looking rather than by any test I'd written. The clouds drifted straight through the gaps between days. The engine stopped clearing that patch of sky when the wall started fading in — the fade is the clearing now — and wall.rs veils the band before drawing a square. overlays.ts never ported that, so the preview had weather behind its calendar. It reads as broken rather than atmospheric, and the spotlit month is exactly where you'd notice. And the caption ignored everything you typed. fill() only ever read the sample numbers, so a page whose whole job is showing you *your* banner was quietly showing you ours. It takes your fields first now and falls back to the samples only where you've left a blank — the numbers stay ours regardless, because those are CI's to fetch. The row's play button goes. The timeline already had blocks that knew which beat they were; two ways to do one thing, and the bar is the better of them because it shows you where the beat sits in the loop while you play it.
Three copy buttons made you the build system. You had to know that awan.yml
belongs in .github/workflows/ and not beside it, which is exactly the step
someone gets wrong at midnight and then can't debug, because nothing errors —
the workflow simply never runs. A zip carries the paths, so unzipping is the
setup. The files are still there to read, folded away, because the button was
the whole answer for most people.
handle becomes username. It's what GitHub calls it, it's what names the repo
this goes in, and 'handle' was jargon we picked. The old key still parses:
every field is serde(default), so a plain rename wouldn't have errored on the
configs already out there — it would have quietly blanked them, which is not a
rename, it's a trap. {handle} still fills too. Checked both spellings render.
And Back on the first step is gone. A button whose only job is to be greyed out
is furniture, and this page had enough of that already.
Unzipped the real download and rendered from it: paths land right, YAML and
JSON parse, 723 frames out the other side.
Nobody asked for the copy and save buttons to go. The ask was to add a zip, and I removed two things while adding one — the zip is the fast path, not the only one, and a file you can copy is a file you can paste into an editor you already have open. Every file has both again, and folds open if you'd rather read it first. Next on the last step was the same flaw I'd just fixed at the other end, and I only fixed one end. Worse than furniture: greyed out but present, it implies a fourth step that doesn't exist. Neither arrow appears where it has nowhere to go now — the comment above it already said why.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Writing
awan.jsonblind is the friction that's left. The Action alreadyrenders and commits the GIF — what nobody can do is picture the reel before
they have one, so scene order and wording get chosen by guessing and then never
revisited.
A static page: arrange the beats, watch it play, take three files.
The preview is the engine, not a mock-up
web/crateis awasm-bindgenbridge overawan-core, which survives the tripbecause the engine is a pure function of
(tick, character)— no clock, no RNG,no I/O. The canvas draws the frames CI draws, at the same 33×30 cells.
overlays.tsdoes for the canvas whatprofile/srcdoes for the GIF: theengine publishes a scene's shapes and leaves its words and numbers to
whoever is rendering. Without that the two headline acts play to an empty
stage — which is exactly what the first build did, and what caught it.
It has to stay static
The Action's whole promise is that a banner keeps working whether or not we do:
the config lives in the reader's repo and the workflow runs on their runner. A
page with a server behind it is a page that can go down and take READMEs with
it. So: no API routes, no database, no accounts, no stored links. The build
publishes to Pages and that's the end of it.
The length meter
A reel gets too long one innocent-looking beat at a time — the sample had
reached 96 seconds without anyone deciding to. Every act now carries its cost
on the label, and the total says out loud when nobody will watch it. Dropping
one beat in the editor takes it 62s → 49s and the band changes colour; the
problem I'd flagged three times solves itself the moment you can feel it.
What the page can't know
The calendar behind the year wall lives only in GraphQL, and GraphQL wants a
token. Nobody should paste a token into a web page to look at a cartoon, so the
preview shows a year from a fixed seed and says so. CI has a token of its own.
Vite + React + TS + Tailwind, deployed to Pages. React over Svelte purely for
the contributor pool. Not Next: it brings a server, and a server is the one
thing this must not have.
web/crateis its own cargo workspace, so wasm-bindgen's dependency tree neverdrags the engine's 1.85 MSRV up. The engine is untouched: 47 tests, clippy
clean, every source file still under 200 lines.