I am rebuilding Annoying Dino, the Chrome extension with more installs than anything else I have shipped, from 1.1.0 to a 2.0. Before any of that happens, there is a plan document. This post is about the document.
I want to be exact about what is and is not here, because a devlog that describes unbuilt features as though they exist is just a press release with worse formatting. Nothing in this plan has been executed. No code has moved. There is no v2 build, no volcano, no meteor, no second species. What exists is a plan, and the plan is the artifact worth looking at.
The part that surprised me
I asked for a v2 with better motion, better graphics, effects, outfits, tools, revamped eggs and pterodactyl, a new volcano and an updated meteor. That is a wish list. What came back opened by reading the shipped extension first: 504 lines, one IIFE, one shadow root, one rAF loop, eight modes, five hats, eight jokes, seven synthesized sound effects.
Then it told me something I had asked for did not exist. I said updated meteor. There is no meteor in 1.1.0. The only trace of one anywhere in the build is a single speech line reading Comet? What comet?. A plan that had simply agreed with me would have produced a phase for updating a feature that has never been written.
It also did the inverse, and that mattered more. It has a section for what is already right and should not be touched: the shadow root with all: initial, the document.hidden pause, the audio suspend on tab switch, the double-injection guard, the re-entrancy flags in the popup. On a rewrite of this size, the list of things not to rewrite is the load-bearing one.
Symptom, then cause
The strongest section is a table that never once says the animation needs polish. Every row names a symptom I could describe from using the thing, and pairs it with the line of code responsible. Feet skate because the leg keyframes run on a fixed timer that has never been told how fast the dino is moving. That is a diagnosis, and it is what makes Phase 1 buildable instead of aspirational.
The plan, in full
The whole document is below. It is collapsed by default because it is long, and expanded it is the actual thing rather than my summary of it.
Plan: Annoying Dino 1.1.0 to 2.0
Why this route
Eight phases, four sub-domains, one ship tail. Not a direct route, because there are multiple branches and a manifest-permission decision to make. Categorised as extension rather than frontend, because the artifact is an MV3 content-script engine with a live store listing: the expensive failure here is a store rejection or a permission scare for sixty existing users, not a layout bug. Categorising it as an idle game was rejected, since there is no economy and no offline accrual.
Risk. The persistence decision changes manifest.json permissions, which invalidates every downstream gate verdict if it is answered late. It gets answered at plan time or Phase 6 is cut. The second hazard is that the page-interaction features are simultaneously the highest-value and the highest-breakage, mitigated only by the never-mutate-host-DOM rule, which is non-negotiable.
Exit criteria
A headless behavioural simulation over thirty simulated minutes showing no entity leak, no unbounded array and no double-hatch. A real-browser frame-time trace on a heavy page, at or under 2ms per frame of engine cost at p95. Ten of ten on the extension preflight. A loaded-unpacked smoke test on five real sites. And a DOM-mutation assertion: the host document node count and innerHTML unchanged after ten minutes of runtime.
What 1.1.0 actually is
Read, not assumed: dino.js, 504 lines, one IIFE, shadow root, one rAF loop. Eight modes. One dino silhouette, five hats, eight jokes, ten speech lines, seven synthesized sound effects. One pterodactyl fly-by. No volcano and no meteor exist in this build.
| Symptom | Cause in the code |
|---|---|
| Feet skate while walking | Leg keyframes run on a fixed .3s cycle, decoupled from S.speed. |
| Motion feels robotic | moveToward is linear. Constant speed, no acceleration, no arrival ease, and an instant scaleX flip on turn. |
| Looks flat | The body is a single path. Only legs, arm and eyes are addressable, so nothing can be posed. |
| The pterodactyl is a paper cutout | A static SVG plus one 7s linear CSS translateX. The wings never move. |
| Eggs are just a timer | A 3.2s setTimeout resolves to a hatch or a joke. No crack stages, no rarity, no interaction, and they do not survive a reload despite the popup joking that they do. |
| There is no world | Zero DOM awareness. The dino walks over a page it cannot see. This is the whole gap against Untitled Goose Game. |
| No accessibility floor | prefers-reduced-motion is unhandled. |
| Dead code | leadY is computed and never read in updateFriends. Chase offsets are inconsistent: -60 target against a +70 hit test. |
Kept, because it is already right
The shadow root with all: initial. The document.hidden pause. Audio suspend on tab switch. The double-injection guard. The popup’s busy re-entrancy flags. And one thing the comparable Annoying Goose extension does that this one should keep not doing: that extension blocks clicks on the page beneath it. This one does not. Do not copy it.
The eight phases
Foundation
Split the 504-line IIFE into modules bundled back to one classic script, because MV3 executeScript needs a classic script and 504 lines is about to become roughly 4,000. Stand up a jsdom harness with a monkeypatched performance.now and a manual rAF pump, so thirty simulated minutes runs in milliseconds. Record 1.1.0 behaviour in that harness before a single motion change lands, so that later on, it feels different can be told apart from it regressed.
Motion core
The largest perceived-quality win, and none of it is new art. A velocity model with acceleration, friction and arrival easing replaces moveToward. Leg-cycle duration is derived from actual speed each frame, so feet stop skating. Procedural squash and stretch on the body group only, so the head stays readable. Turn-around interpolates scaleX through zero over about 120ms with a lean. Head and tail follow on two critically damped springs, which is the effect that reads as expensive. Real gravity with ground contact and a landing dust puff. Pupils track the cursor. Idle micro-motion, because a perfectly still idle character reads as broken. And prefers-reduced-motion honoured, plus an explicit Calm Mode.
Art system
Re-rig the dino into layered groups with named anchors: pelvis, a two-segment spine, neck, head, shoulder-arm, hip-leg-foot, and a three-segment tail, with draw order fixed so arms sit in front of the torso. Nothing in Phase 1 works until the art can be posed. Fix a style contract once: light from upper-left, three tone levels, one hero hue per species, three line weights. Five species, each with its own silhouette and gait. Four outfit slots rather than one, each anchored to a rig node so hats bob with the head instead of floating. A real ground contact shadow that scales and fades with jump height. Particles on one pooled canvas, not DOM nodes, because the meteor and the volcano need hundreds at once.
Entities
Eggs become a three-stage visible crack with shard particles and five rarity types, draggable, clickable to speed hatching, and persisted per-origin so that the eggs remember stops being a joke. The pterodactyl gets real flapping tied to speed, a bezier swoop, a tracking ground shadow, and four behaviours instead of one fly-by, including perching on a real DOM element and stealing the dino’s hat. A volcano is new: rate-limited, never twice in a row, rumbling the shadow layer only and never the page. A meteor is new as well, with a shockwave ring, a crater decal, and a rare extinction easter egg.
Activities and tools
The layer that separates v2 from a nicer sprite: the dino stops walking over the page and starts using it. Perching on a real heading. Cloning an image and dragging the copy. Riding fast scrolls and wiping out on a hard stop. Nine held tools, each with its own animation and a reason to exist. The hard rule, restated on every step: the host DOM is never mutated. Props are found by reading getBoundingClientRect, throttled and cached and gated behind an IntersectionObserver, and every interaction is a clone or an overlay drawn inside our own shadow root.
Content and sound
Eighty-plus jokes across themed decks, served from a shuffle bag with a persisted seed so nothing repeats until the deck is exhausted. Eight jokes on a loop is the fastest way to make a novelty extension feel small. Cards get a redesign with the punchline hidden until tapped. An optional deck of real cited palaeo facts as an alternative to jokes. A layered sound kit rather than bare oscillators, with a master volume slider and ducking when the page plays its own media.
Shell
A popup redesign with a live mini-preview of your dino wearing its current outfit, a species picker, four outfit slots, a chaos slider driving event frequency, and per-site allow and deny. An options page with the full wardrobe and stats. An unlock system, because sixty users need a reason to leave it on past the first laugh. This phase is cut entirely if the second open question below is answered no.
Ship tail
Coverage check, authored tests, then a mutation-testing kill-rate campaign specifically on the egg and unlock persistence paths, because a bare green suite is not ship evidence on persisted user data. Then a naive-user walkthrough of the new popup, an adversarial design review, a ten-check extension preflight whose ninth check is privacy egress and whose tenth is a DOM snapshot diff, and finally the store assets.
Answers needed before Phase 0
1. Meteor provenance
The uploaded 1.1.0 has no meteor code at all. Is there a newer local build to work from, or is the updated meteor aspirational? Building it fresh is what the plan above assumes. If a build exists, extending it is better.
2. Cross-navigation persistence
Making the dino survive page loads requires host_permissions on all URLs, which triggers a store re-review and shows every existing user a read and change all your data on all websites warning on update. That is a real risk to a sixty-user base. Ship it, or keep the current per-page activation model?
What I think the plan gets right
Three things, and none of them are the feature list.
It records a baseline before it changes anything. Phase 0 stands up the harness and captures how 1.1.0 behaves in it before a single motion change lands. Without that, every later report is unfalsifiable: it feels different and it regressed produce identical evidence. That step buys nothing on the day it runs and is the only thing that makes Phase 1 reviewable.
It has one rule it will not trade away. Never mutate the host DOM. Everything in Phase 4 is written to route around it: props are read, not moved, and every interaction is a clone drawn inside our own shadow root. The rule is restated on every step of that phase rather than stated once at the top, which is the difference between a principle and a note.
It knows what it is protecting. The category argument is explicit: this is an extension, not a frontend project, because the expensive failure is a store rejection or a permission warning shown to sixty people who already trust the thing. Ordering falls out of that. The gates at the end are not ceremony, they are the reason the rest is allowed to be ambitious.
And what it refused to do
It ends in two unanswered questions instead of eight tidy phases. One of them, the host_permissions question, has a cost I would not have priced: making the dino survive page navigation means every one of the sixty current users sees a warning saying the extension can read and change all their data on every website, at update time, for a feature that is a nicety. The plan does not decide that for me. It marks the phase as cut if the answer is no, and moves on.
That is the part I would hold up if someone asked whether this kind of planning is worth doing. Not the eight phases. The two questions at the bottom, and the fact that the document is honest that it cannot proceed cleanly without them.
Where this goes
I answer the two questions, then Phase 0 runs and produces a baseline I can point at. Until then Annoying Dino 1.1.0 is what is on the Chrome Web Store and what this post is about is a document, not a build. The project page now carries a change history so the version story stays legible from one place, and I will write the next entry when there is a measured before-and-after to put in it.
More on how this site gets planned and shipped: the orchestrator layer and the full devlog index.
Update: both questions answered
Added after publishing. The two questions above were still open when this post went up, and the body is left as written rather than edited to look prescient.
Meteor provenance: there is no newer build. The meteor gets written from nothing, exactly as the plan assumed.
Cross-navigation persistence: no. The extension keeps its current per-page activation and does not take a permission on all URLs. That means no store re-review, and more to the point, nobody who already has this installed gets a warning saying it can read and change their data on every website they visit. The dino still dies on every page load. Phase 6 loses cross-navigation persistence and keeps the rest of the shell.
That is the trade the plan was built to surface: a nicety is not worth spending sixty people’s trust on. Phase 0 starts from here.

Leave a Reply