Home / Blog / The ball doesn't read the docs
CricCuts Blog · Launch

The ball doesn't
read the docs.

CricCuts is out. It began as a bake-off between three AI coding agents and ended as thirteen weeks of a cricket ball patiently explaining to all of them that they were wrong. This is the launch post, the scorecard, and an honest account of what agents are extraordinary at, what they are still hopeless at, and the one thing that decided every argument: real footage.

⏱ ~18 min read 🚀 Launch 🤖 Agentic development 🏏 267 commits

There are two kinds of product you can build in this new age of AI. You can build the thing that changes the world. Or you can build the thing that buys you back the time to go and build the thing that changes the world.

This is emphatically the second kind. CricCuts watches your net session or your weekend match and hands you back the highlights — the shots worth keeping, cut, framed, and ready to send — without you scrubbing through ninety minutes of footage on a phone with a cracked screen. Everything happens on the device. Nothing is uploaded. It costs nothing.

I have been writing software for fourteen years. I expected to find this project easy and instructive. It was neither. It was hard, and it was humbling, and the humbling did not come from the AI — the AI was magnificent. It came from a leather ball travelling at seventy kilometres an hour, which turns out to have strong opinions about signal processing and absolutely no interest in what any language model believes.

🏏
The one-line thesis. Language models are astonishing at everything that has already been written down. Cricket, played badly, on a Tuesday evening, in a net with three other nets going at once, has not been written down. That gap is the whole story of this app.

The scorecard, up front

I'll put the numbers here rather than making you dig for them, because the rest of this post is an argument and you should be able to check the evidence first. Everything below is measured at commit 6b86a89, the tip of the branch on launch day.

267
commits, 28 May → 22 Aug 2026
104,342
lines of Kotlin in the app
699
unit tests
29,250
lines of engineering docs
33
visual effects, hand-drawn in code
54.3%
of commits between 10pm and 9am
26,232
agent tool calls in five weeks
71.7 MiB
shipped APK, fully offline

That 54.3% deserves a footnote, because it corrects a story I had been telling myself. Weekends account for only 23.2% of commits — below the 28.6% you'd get from a uniform week. So this was never a weekend project. I didn't build CricCuts on weekends. I built it instead of sleeping.

Part one: the bake-off nobody planned

The honest origin is that I wanted to compare agents. Google Antigravity had just landed, everyone was loud about it, and I wanted a real project — not a to-do app — to find out whether the new generation of agentic IDEs could carry something with genuine engineering in it.

I picked cricket highlights for three reasons. It is multimodal: audio and video and, later, speech. It is edge-constrained: I wanted to know what a phone can really do in 2026 with nothing but its own silicon. And it is a domain no model has lived in, which I suspected would be the most interesting part. I was right about that, though not in the way I expected.

What I did not expect was that the project would leave a forensic trail precise enough to reconstruct to the minute. It did. Here is what actually happened on 24 May 2026.

# 2026-05-24, reconstructed from filesystem timestamps and IDE config 07:13 sample footage staged "sample uncut videos/" 17:12 design spec saved 78,624 bytes — downloaded from a claude.ai chat 17:29 Antigravity opens the folder WSL: ~/shortshots, package com.cricketclip 18:14 app/ exists 18:31 build.gradle.kts → 58 Kotlin files, 3,511 lines, in about an hour 23:19 files ported to Windows — a single 2-second burst of CreationTimes 23:29 Android Studio's agent, first planning session 23:31 …Gemini switched to a paid tier. Two minutes later.

There is a detail in that timeline I only found while writing this post, and it quietly rewrites the running order. The 78KB spec at 17:12 — the document that told Antigravity what to build, seventeen minutes before it opened the folder — still carries the Windows metadata stamp saying where it was downloaded from. The referrer is a claude.ai chat. So Claude didn't arrive third at all. It wrote the brief in hour one, handed it to a Google agent to build, and then spent three weeks off the record before showing up under its own name.

Which it did on 12 June, when the first recorded Claude Code prompt on this project reads, in full: "refer the docs to understand about criccuts editor app." Followed immediately by "create a launch website for the app that can be hosted for free." The website you are reading this on is older than most of the app.

Innings one: Antigravity, and the hour that set the shape

The scaffold Antigravity produced still exists. It is sitting in a WSL folder called shortshots, under the project's original name, CricketClip. I went and measured it against today's codebase, and the result genuinely surprised me.

In roughly an hour, Antigravity wrote 58 Kotlin files and 3,511 lines: the audio DSP primitives, the Room database layer, the person-selector screen, the timeline, the exporter. Today the app is 364 Kotlin files and just under 120,000 lines. 52 of those original 58 files are still in the tree. Only six died.

And it isn't only the filenames that survived. Comparing line by line across ten load-bearing files, 64% of the scaffold's distinct lines are still there verbatim. But the distribution of that survival is the interesting part, and it is a near-perfect X-ray of what a scaffolder is and isn't good at:

Scaffold fileStill present todayWhat it does
PeakPicker.kt98%Find local maxima. Arithmetic.
ShortTimeEnergy.kt95%Sum squares over a window. Arithmetic.
Converters.kt90%Database type mapping. Mechanical.
AdaptiveThreshold.kt71%Rolling median. Mostly arithmetic.
ClipBuilder.kt50%Where a clip starts and ends. Judgement.
AudioSpikeDetector.kt44%Is that a cricket shot? Judgement.
DetectionConfig.kt25%Every tuning constant. Judgement.
ButterworthFilter.kt21%Which frequencies matter. Judgement.

Read that table twice. The maths survived almost untouched. Everything requiring a judgement about the real world was gutted and rewritten. That is not a criticism of Antigravity — it is a precise description of what a scaffolder is for, and it did that job in an hour for free.

The characteristic failure of that phase is on the same axis. Somewhere in the original detector, a spike threshold of "8 dB" was being applied as a linear factor of 8× instead of a ratio of about 2.5×. The code compiled. It ran. It produced a beautifully structured pipeline that silently rejected most real cricket shots. A scaffolder optimises for looks complete, and signal-processing units are exactly where that goes wrong.

🙏
Genuine credit to Google here, and I want to be specific about it. Antigravity has a real $0 tier that needs no subscription at all, and — unusually — its free plan lists agent access to Anthropic's Claude models and an open-weights OpenAI model alongside Google's own Gemini. Handing free users a choice of frontier models from three different labs is not the obvious commercial move, and it is why an experiment like this could start at all. One honest correction to my own assumption, though: free is not unlimited. The only things Google's pricing page calls unlimited are tab completions and command requests; agent work runs under what the same page calls "basic weekly rate limits", on every tier including the paid ones. (All plan details as of August 2026 — Google has restructured them at least five times since launch, so check antigravity.google/pricing.)

Innings two: Android Studio, and the twelve-minute handover

Nine minutes after the code landed on Windows, Android Studio's Gemini agent logged its first planning session. Two minutes after that, the IDE config records the account switching to a paid AI Pro tier. From "the free scaffolder is done" to "I am paying Google for the next phase" took twelve minutes, which tells you something about how convincing that first hour had been.

This phase ran eight agent sessions over nine days, and it did exactly the work the IDE is uniquely good at: Gradle, Compose, ExoPlayer, MediaStore, the build graph, the device. The first git commit for CricCuts lands on 28 May — in the middle of this phase, not at the start of the project. The most memorable fix of the period is an out-of-memory crash caused by five simultaneous ExoPlayer instances, which is textbook "I have a real device in my hand" debugging.

Its characteristic failure was the mirror image of Antigravity's: locally correct, globally unaware. It would fix the symptom in the screen you were looking at without asking whether the abstraction was right across the other eight files. Brilliant at why won't this compile. Weaker at should this exist.

Innings three: Claude, and why I stopped switching

I want to be careful here, because "the tool I ended up using is the best tool" is the least interesting claim anyone can make, and Gemini was genuinely impressive early. So let me name the actual reason, which is narrower and more useful than "it's smarter."

I moved to Claude because it gave me more knobs, and because it told me what it was thinking before it did the thing. As a co-programmer, I could steer it. That is the entire difference.

Concretely: I could read a plan and edit it before a single file changed. I could see the reasoning and catch a wrong premise at the premise, rather than three hundred lines later. I could choose the model per task instead of per subscription. When it was about to do something expensive or irreversible it said so. Steering a strong agent badly produces worse code than steering a weaker agent well, and transparency is what makes steering possible at all.

The second reason is less philosophical and I suspect more common than people admit: it worked beautifully inside VS Code. There is a real irony here worth stating, because I was building an Android app. Claude Code does run in Android Studio — Android Studio is named explicitly in Anthropic's supported JetBrains IDE list — but that integration is still marked beta and works by driving the CLI in the IDE's terminal. The VS Code extension is a native interface: inline diffs you accept or redirect, plan review before execution, several conversations in tabs. So I ended up in the slightly absurd position of keeping Android Studio open to build and run an Android app, and doing all the actual thinking in a different editor next to it. That was a real deciding factor, and it is a genuinely odd thing to have to say out loud.

Innings one · ~1 hour

Antigravity

Breadth, instantly. 58 files, 3,511 lines, the whole module skeleton. 52 of those files are still here. Its arithmetic survived at 95–98%; its judgement calls at 21–25%.

Best at: 0→1 surface area
Innings two · 8 sessions

Android Studio + Gemini

Make it real. Gradle, Compose, ExoPlayer, MediaStore, the device. Locally excellent; it fixes the screen in front of it and doesn't ask about the other eight.

Best at: the build-and-run loop
Innings three · 267 commits

Claude

The hard reasoning. Cross-file refactors, the detection maths, the long bug hunts, the measurement harnesses that killed my own favourite ideas.

Best at: holding the whole pipeline at once
Throughout

The cricket ball

Undefeated. Overruled all three, repeatedly, on points none of them could have reasoned their way to from first principles.

Best at: being right

Who actually wrote it: the commit-level answer

215 of the 267 commits carry a co-author trailer naming a specific model, so this is measurable rather than remembered. And measuring it corrected me on one point immediately.

My recollection was "Sonnet first, then Opus." That is wrong. The first Claude model to commit to this repo was Opus, on 1 July; Sonnet's first commit is 25 July, twenty-four days later. What I was actually remembering is the within-generation order — Sonnet 5 did arrive before Opus 5 — which is a real thing that happened, filed under the wrong heading in my memory. This is a small and slightly embarrassing example of the post's own thesis: I was confidently wrong about my own project, and only the log settled it.

On "the bulk of the work was Opus", the answer depends entirely on what you count — and the measures disagree with each other:

MeasureOpus familySonnet 5Verdict
Attributed commits163 · 75.8%52 · 24.2%Opus
Active days209Opus
Output tokens16.7M · 53.5%14.0M · 44.6%Opus
Assistant turns12,306 · 47.3%13,395 · 51.5%Sonnet
Kotlin lines added50,64754,430Sonnet

Three to two, and I could tidy that away, and I'm not going to. Opus made most of the commits and ran most of the days; Sonnet answered more turns and added more of the Kotlin. That is the honest answer, and the disagreement between measures is more informative than any single row.

Line counts in particular are a bad unit, because both models have one enormous commit distorting their total. Sonnet's largest is 17 August, titled "Recover pre-reset WIP from stash + reconcile Recoil Smoke FX" — an out-of-band git reset had destroyed a day of uncommitted work, the stash survived, and Sonnet reconciled every conflict and put it back. Those lines weren't authored that day, they were rescued. Opus has its own: a "project checkpoint" adding 11,358 Kotlin lines, slightly more than Sonnet's 10,869. My first instinct was to exclude Sonnet's and declare Opus the winner. Excluding both, which is the only defensible version, Sonnet still leads by 11%. Git credits whichever model happened to run the commit, not whoever wrote the code.

🪞
And now the embarrassing part, which I'm including because it is exactly what this post is about. My first pass at these numbers had Opus at 64.8% of output tokens and 56.3% of turns, and I was ready to publish that. Then a verification pass found the defect: the transcript log writes several records per message, and — fatally — the duplication rate is not constant across models. Opus 4.8 repeats 4.90× per message, Opus 5 3.48×, Sonnet 5 only 2.79×. So counting raw log lines silently inflated Opus by roughly the ratio between those numbers. Deduplicating by message id drops the output total from 137 million tokens to 31 million, and flips the "assistant turns" row from Opus to Sonnet. The measurement was fooling me in favour of the answer I already believed. That is the fourth time in this project, and the only one where I was the subject.

So the fair summary: Opus did the majority of the commits, the sessions and the reasoning — 20 active days to Sonnet's nine — Sonnet did more of the turns and the typing, and on one memorable night it saved the innings. Every side needs a nightwatchman.

The tool-call mix is its own portrait of the work: roughly 7,200 shell commands, 6,900 edits, 5,600 file reads and 2,600 searches — about one file inspection for every edit made. And barely forty web searches. In five weeks. This was almost entirely local reasoning over a codebase that only existed here.

Ten times the machine told me to go to bed

I was on the $100/month Max plan, and I hit limits. I assumed I hit them constantly. The logs disagree: across 37 active days out of a 38-day stretch, I hit a usage limit exactly ten times — eight session limits and, twice on 10 August, the weekly one. For the volume involved — 26,024 assistant messages and 31 million tokens of model output — that is a lot less friction than it felt like at the time.

The message is completely undramatic:

You've hit your session limit · resets 2:30pm (Asia/Calcutta)

What the transcripts caught is my behaviour after it, which I find funnier than the limits themselves. On 19 August the limit landed and named a reset time. My next message went out nine minutes after that reset. On 10 August the weekly limit named 6:30am; I was typing again ninety-seven seconds past it. I was sitting there waiting by the clock like a batter padded up at the non-striker's end.

Two other things the logs settle. The limit is account-level, not per-session: on 19 August I had three sessions running and two of them stopped ninety seconds apart. And dropping to a cheaper model is not a reliable escape hatch — in one session I switched Opus→Sonnet and hit the limit anyway eight minutes later. There is also a clean behavioural fingerprint at the start: my first week was Opus only, then came a night with four limit hits in fourteen hours, and the next morning I opened a fresh session on Sonnet — the first time Sonnet appears anywhere in the logs. I don't remember making that decision. The transcript made it for me.

📐
A number I'm deliberately not quoting. The input-token total for this project runs to about 9.2 billion, and it would make a wonderful headline. It would also be meaningless: essentially all of it is cache reads — the same context re-read on every turn, not 9.2 billion distinct tokens. The honest figure is 31 million tokens of output. If you see someone quoting a token count in the billions, ask them which side of the ledger it came from — and whether they deduplicated it.

Part two: everything the ball had to explain

Here is the part I actually wanted to write. Every genuinely good idea in CricCuts is a response to footage, not a clever guess. And the corrections were not small tuning nudges — several of them overturned a premise that reads as obviously true in prose and is flatly false on a cricket ground.

1. The loudest bang in a cricket net is not the bat

The entire pipeline was built on an assumption nobody thinks to question: the moment of the shot is the loudest moment. So clips were centred on the biggest audio spike. Users kept reporting the same defect in different words — "the contact is right at the start of the clip", windows that open on dead air.

When we finally measured the audio anchor against sixty-plus hand-placed contact pins across five sessions, it was systematically 600 to 1,100 milliseconds early. On a bowling-machine session, 1,574 ms early. The reason is pure ground geometry that no text corpus states plainly: filmed from the bowler's end, the bat meets the ball about twenty metres from the phone and is genuinely quiet, while the bowler's release, the run-up footfalls, the machine's launch and the ball smacking the side net are all loud and all much nearer.

The number that settled it is the one I still find startling. Of 30 contacts on a clean net session, 25 had no loud audio spike within 250 ms of where a human placed the contact. On the machine session it was 18 out of 18. There was nothing there to snap to. We had built a search that only ever walked forward to a louder peak, and the thing we were looking for was quieter than the noise floor.

The fix was to stop listening and start looking: in-box motion tracks the human pins with a slope of about 1.0 and correlation 0.93–0.99. But note the shape of this — an AI cannot get here by reasoning. It has to be told, by data, that the central assumption is inverted.

2. Voice after the shot is applause. Voice during the shot is a nuisance.

People wanted an "only the action" mode that strips the chatter, so the obvious version got built: if a clip is dominated by speech, don't auto-select it. On the benchmark it immediately deleted a loud, unambiguous, excellent shot — one that was full of talking precisely because it was good and everyone reacted to it. Meanwhile the talking false positive it was aimed at had less chatter.

The correction is one sentence of cricket-ground social knowledge: voice after the impact is a reaction and should be rewarded; voice during the impact is someone talking over the ball and should be demoted. Same acoustic material, opposite meaning, separated only by which side of the ball it lands on. Rewritten that way it kept every real shot and correctly demoted eight non-events.

The veto was reverted eight minutes after it shipped. I know it was eight minutes because both commits are in the log, at 16:45 and 16:53. Fastest turnaround in the project, and the only reason it was that fast is that a human was watching an actual clip.

3. Your neighbour's crack is louder than your own

Loudness is the single best shot detector this project has. On single-batter footage it scores 0.766–0.808 AUC — better than every video signal, every pose feature, every timbre feature we tried. So: louder is better struck. Obviously.

On multi-net footage it scores 0.435 and 0.331. Below chance. Inverted. In a row of practice nets the batter in the next net is closer to your phone than your own batter is, and their middled drive is nearer and harder than your edge. The false positives averaged higher loudness than the real shots did.

The same inversion turned up again on a wicketkeeper head-cam, for a completely different cricket reason: behind the stumps the loudest things are the keeper's own glove-takes and the batter tapping the crease, while a ball actually played away toward the off side is quiet at that mic.

Your best signal can invert — not degrade, invert — when the camera moves twenty metres. Nothing in a model's training data warns you about that, because nobody has ever written it down.

4. A full swing means nothing. The fullest swing of all is a miss.

This one I proposed, and the measurement destroyed it, and I've come to think it's the deepest thing the project found. The idea was cheap and appealing: pose can't tell a bat from the person holding it, so find the long thin object near the hands and use its kinematics to separate real shots from taps.

Measured, bat-region motion is identical across labels. A real loud shot, a beaten shot and a discarded false positive all produce the same reach and the same area, to within a couple of percent. No threshold separates them anywhere.

The reason is a cricket fact rather than a computer-vision fact. Bat swing measures the batter's effort, not the outcome. A batter swings fully on a defensive push, on a leave played at, on a soft mishit and on a cover drive — and a swing-and-miss has the fullest swing of all, because there's no contact to arrest it. The only thing encoding "was that well struck" is the contact itself.

There's a lovely catch-22 buried in the footage, too. On the steep near-overhead video where the false positives genuinely are bat-taps — exactly where bat motion would separate them — the bat is foreshortened along the camera axis and effectively invisible. On the side-on video where the bat is beautifully trackable, the false positives aren't taps at all; they're defends and leaves with full swings. The situation where the idea works and the situation where it can be measured never coincide.

5. The camera veto that passed nine sessions and then deleted my best shots

Somebody nudges the tripod, the whole frame moves, and the motion signals read it as a stroke. Real problem — a pan inflates the follow-through signal by 1.81×. So a veto was added: near a detected camera move, with a high motion burst, don't auto-select. Validated across nine device sessions: 37 false positives removed, zero real shots lost. That is about as clean as an offline result gets.

I ran it on a tenth session and lost four of my best shots. Not marginal ones — four at or near maximum loudness, the sort you send to people.

The veto assumed real shots sit at least nine seconds away from a camera move. True across nine sessions. False in general, for a reason anyone who has actually set up a phone at a net knows instantly: you re-aim the camera between balls, and the bowler is already walking back to his mark. Play resumes within a second or two. Near a genuine re-aim, a real shot and a pan are indistinguishable.

Reverted. And it stands as the clearest illustration I have of why "validated on nine sessions, zero regressions" is not the same as "correct."

6. A loud knock is not a ball — and the tell is who's running in

Before analysis starts, the app picks one frame to show you for "draw a box around your batter." It was landing on a warm-up knock at five seconds instead of the first real delivery at thirty-one seconds. Every audio signal agreed with the wrong answer, because acoustically a batter knocking his bat in is a bat hitting a ball. Loudness 0.89 versus 0.99. Shot-shape 0.35 versus 0.38. Indistinguishable.

The discriminator isn't acoustic at all. A real delivery has a bowler running in beforehand. A warm-up knock has nobody running in. Reusing a whole-frame motion pass that was already being computed and thrown away, the pre-impact motion scored 0.24 for the knock and 0.96 for the first real ball. A clean separation, for free, from knowing what happens in the twenty seconds before a ball is bowled.

7. The measurement that graded its own homework

My favourite failure, because it caught both of us. Pinning a clip re-centres its window so the pin sits at 35% of the width. Which means a "content-free null" — start plus 0.35 × width, reading no audio and no video whatsoever — scores zero milliseconds of error and 100% within tolerance. We measured it: 41 of 71 pinned clips have their pin at exactly 35.00% of the window, standard deviation 0.00006.

Any estimator whose search range derived from the window inherited that leak. As the internal note puts it: "it reproduced to the millisecond three times and was still meaningless, because everyone re-derived it over the same leaking interval."

The same disease, same week, in the other direction: "pose beats audio on 20 of 21 loud pins" was measured, believed, shipped — and reverted the same day, because a pinned clip re-probes anchored on the pin, so the pose answer equalled the pin by construction. The ground truth that settled it wasn't a statistic. It was that after shipping the change, I had more clips to hand-correct: 34 instead of 20.

🧪
This is why I now believe testing is the newest form of software development. Not unit tests — those pin behaviour you already understand. I mean measurement harnesses over real captured data, built specifically to try to kill the idea you just had. An agent will write you a plausible algorithm in ninety seconds. The bottleneck moved: it is no longer can you build it, it is can you prove it isn't fooling you. Most of my thirteen weeks went there.

Part three: the things I had to learn

I came into this as a backend and platform engineer. I left it knowing a genuinely new field, and this was the most enjoyable part of the whole exercise.

And the discovery I did not anticipate: you can use an agent to tune a cricket model that is written in Kotlin. Not train — tune. Run the harness, read the numbers, form a hypothesis, change a constant, re-run, discard it, try again. Dozens of times, overnight, with the reasoning written down. That loop is the actual unlock of this generation of tools, and it looks nothing like what "AI writes code" usually means.

Part four: where every agent is still weakest

Design. Not visual polish — empathy. Every agent I used, including the one that wrote most of this app, is measurably poor at imagining a person holding a phone. Some receipts:

🤖 What the agent did

  • Put white text on the app's teal accent — a 2.17:1 contrast ratio against a 4.5:1 standard. At seven separate places. One file did it correctly 360 lines below the place it did it wrong, with a comment claiming the wrong one was chosen for contrast.
  • Moved a slow calculation out of the loading screen to make loading faster.
  • Built frame-accurate pinning with a ±1-frame nudge button.
  • Answered "the gust animation fires too early" with a smaller number. Four separate times: 450 ms → 350 → 260 → 100.
  • Reported that a change had removed 193 clips.

🧍 What I said

  • You cannot read that. Over real footage it's worse. A code comment claiming good contrast is not evidence — measure it.
  • Now 39 clips paint gold and repaint one by one over the next minute. That doesn't look slow, it looks wrong. Better to wait and show accurate results than fast wrong ones.
  • That expects too much from the user and clutters the UI. (Measured afterwards: two pins of the same ball land 43 ms apart, against an anchor error of 249–550 ms. The nudge was polishing a term six to thirteen times smaller than the dominant error.)
  • It's still early. (The fifth round finally changed the premise instead of the constant — the "lead into the downswing" idea the number came from was worth less than it looked.)
  • I removed about 90, not 193. (One delivery fires several audio spikes. The app had three different units for "a thing" and was reporting the wrong one to me.)

The pattern in the right-hand column is that none of those are aesthetic preferences. They are all "you have not imagined the person" — the person squinting at a phone in sunlight, the person who reads a colour change as a data error, the person who does not want a ±1-frame button, the person who counts shots and not clips.

The gust one is the most instructive, and the most human-shaped failure of the lot. Four rounds of the same complaint, four smaller numbers. The agent treated "too early" as a magnitude problem when it was a premise problem. Humans do this too, of course. But a human who watches the clip five times usually stops trusting the model in their head. That's the move that's missing.

And in the interest of not grading my own homework: the agent ran a twelve-dimension pre-launch audit on this codebase, raised 209 findings, and concluded — about itself — "most icon controls are unlabelled, the app is effectively unusable with TalkBack, and no dimension looked at accessibility." 62% of the app's icons still have no label. That's a real gap, it's mine to fix, and it's next.

Part five: grass, a temple, and Dragon Ball Z

The other reason I built this was creative freedom. I got to design a whole product's look with nobody to ask, and I aimed it at something specific: the elements of nature that make cricket possible. Turf, light, weather, dust. Cricket is the only major sport whose rules are openly at the mercy of grass and cloud, and I wanted the app to feel like it knew that.

So there are 407 lines of code in this app whose entire job is to grow grass on a button. Every primary action button in CricCuts has curved blades of turf growing inward from all four edges, thicker in the corners, over a radial pitch gradient, thinning as they approach the label so the text stays readable. Same seed every time, so the same button always grows the same grass. It appears at 54 places in the app. It is utterly unnecessary. I would not remove it.

Then the effects, which grew — as these things do — into 33 of them, 29,021 lines of hand-written drawing code across 89 files. That is 27.8% of this entire application. More than a quarter of CricCuts, by volume, is drawings.

The temple

The one I'm proudest of is a ruined jungle temple gateway you can put behind the batter — Harappan and South-Indian gopuram shapes, weathered stone, roots pulling the masonry apart. It's the largest single art file in the app at 1,907 lines, and it imports exactly one thing from Android's graphics library: Bitmap. No canvas, no paths, no paint. It is a raw array of pixels, written one scanline at a time, because that's the only way to unit-test the geometry on a laptop.

Inside it: 420 individually placed leaf ellipses, 18 roots, 25 hanging vines, 7 orchids, 14 carved relief figures, 41 named colours. Of its eighteen layers, exactly one moves — twelve fireflies, of which four are alive at any moment, drifting on a four-and-a-half second cycle from one shared glow sprite. Everything else is baked static, because a phone re-rendering a jungle every frame is a phone that gets hot and dies.

And it has my favourite constraint in the codebase. The temple's arch is corbelled — it steps inward in seven discrete stone courses rather than curving smoothly. The obvious way to build the steps is to sample the underlying curve at each band's midpoint. Do that and the step is up to 26% narrower than the curve it came from, which on roughly half of real framings puts about sixty pixels of solid stone over the batter's helmet. The fix is to sample each band at its widest point, which makes "the stone never covers anyone the smooth arch wouldn't" a structural guarantee rather than a hope. There is a test that sweeps two hundred poses to keep it that way.

A decorative arch has a load-bearing safety property, and that property is "don't cover the batter's head with a rock." I did not have that on my 2026 bingo card.

The anime

The rest is a direct raid on childhood. There is a Dragon Ball Z aura — an egg-shaped shell of fire around the whole batter that powers up twice, dim navy in the stance, brightening at the trigger movement, snapping to its brightest blue at contact. It's driven purely by a clock relative to the moment of impact; not one frame of it is measured, and it doesn't need to be.

There are manga speed lines, and getting those right was its own small lesson. The first version drew ten lines evenly around a full circle, which meant drawing straight across the batter's face. Now they cover a 240° arc with a 120° wedge deliberately left empty at the bottom, so the halo rings the head without crossing the body. They anchor on the head, after two rejected alternatives, and the reason is pleasingly cricket-shaped: keep your head still is a real batting fundamental, so the head is the one point that stays where you put it through a stroke.

The fractals

Two effects are fractal, and they're the ones that behave most like real physics.

The lightning arc is a fractal-displacement channel: five levels of subdivision, each displacing the midpoint by 60% of the level above. The trick that makes it look like lightning rather than noise is that most vertices are left collinear — only about a third of them at the deeper levels are allowed to snap — so you get a few decisive 40–60° kinks instead of a fuzzy wobble. Real lightning is mostly straight with sudden commitments. The volcano scene reuses the same generator, reoriented and recoloured cool white into violet, and reads as a completely different phenomenon.

The glass crack is the biggest single effect in the app — 2,139 lines — and simulates a laminated pane in front of the camera taking the ball 900 milliseconds after contact. The fracture propagates for 220 ms and then freezes. Its one non-negotiable rule is T-junctions only: every ring crack must terminate on a radial crack at roughly ninety degrees, because that's what real laminated glass does and your eye knows it instantly even if you've never thought about why.

My favourite implementation detail in the whole project lives here. A real crack does two opposite things at once: it scatters transmitted light, making it darker, and it specularly reflects ambient light off the fracture faces, making it brighter. Which one you see depends entirely on what's behind it. So the effect draws both, at once, and lets the video compositor's own alpha blend pick per pixel. Over a white sightscreen the dark gasket wins; over dark trees the bright reflection wins. No branch, no brightness detection, no code. The maths that was already there does the deciding.

Part six: opinions versus knobs — the actual hard problem

Here is the design question I spent the most time on, and the one I think generalises furthest beyond cricket.

How opinionated should the machine be about your highlights? Every opinion it holds saves you effort and risks being wrong about your session. Every knob it gives you is control you didn't ask for and a decision you now have to make.

The position CricCuts lands on is explicit: we do not claim to be 100% accurate, and we don't design as if we were. Instead, the pipeline is deliberately lossless — no stage between detection and the screen ever throws an event away — and precision is the interface's job, not the algorithm's. Your picks are sticky and survive every recalculation. Nothing you chose is ever quietly un-chosen.

Practically that means: five analysis settings on import; clip length as a range from half a second to twelve; a target reel length that greedily fills itself with the best shots and leaves the rest visible but unticked; a choice between tight action-only cuts and cuts with room for the shout. Then a curation screen that works in both directions — "find more", which ranks everything it didn't pick with the best first, and "filter current", which ranks what it did pick with the weakest first. Two lists, opposite sorts, because adding a missed shot and removing a bad one are different jobs.

And the emblem of the whole philosophy is shot direction. The comic gust sweeps the way you hit the ball, so it needs to know off side from leg. We tried four independent ways to measure it — optical flow twice, body motion from pose, and head motion — and all four failed, one of them refuted at the source. It turns out a right-hander's body drifts leg-ward in every phase of both shot types, because for a leg-side shot that's the shot and for an off-side shot that's the counterbalance. The body never points at an off-side stroke. And from the bowler's end a straight drive travels toward the camera, so it has no horizontal component to measure at all.

So we stopped measuring and added a three-way control: Auto, Left, Right. It takes one tap and it is always right, because you were there.

That is not a defeat. It is the correct answer to "the machine cannot know this", and I'd rather ship an honest knob than a confident guess. The standing rule in the codebase now reads: prefer direction-free effects, since those cannot be wrong.

The animation system is where this philosophy costs the most and pays the most. Doing this kind of work in After Effects means keyframing every impact by hand — finding the frame, tracking the body, placing the burst, timing the sound. On a phone, CricCuts decides where (from the batter's head, chest and ground contact, resolved through eight separate spatial anchors) and when (from the contact moment, which is itself a two-legged decision — on a loud shot the crack really is the contact, but on a quiet edge or a play-and-miss it falls back to the motion centroid, because the crack isn't there). You choose the effect. It handles the placement. Twenty one-tap templates exist for people who don't want to choose at all.

Where I've clearly overshot is discoverability. The per-clip effects sheet is six to seven full screens of scrolling. An internal review of my own app put it perfectly: "it is not discoverable; it is huntable." Fixing that is the next real piece of design work, and it is a much harder problem than adding the thirty-fourth effect would be.

What this taught me about the next decade

The thing I keep coming back to isn't about cricket or even about code. It's that I spent thirteen weeks doing nothing but figuring out how a human and a machine should divide a hard problem, and the answer looked the same at both scales.

I chose Claude over a genuinely good alternative because it gave me knobs and told me what it was thinking. Then I spent three months designing an app whose entire philosophy is: don't pretend to be certain, show your work, and give the human the controls. I made the same argument as a user that I was making as a designer, and it took me until writing this post to notice.

That is the shape of the next decade, I think. Not "will the AI be good enough" — it is already extraordinary, and it wrote most of this application. The question is whether we build systems that let a human steer them, or systems that are confidently opaque. Because the failure mode isn't the model being wrong. Every interesting system is wrong sometimes. The failure mode is the model being wrong in a way you cannot see, cannot question, and cannot override.

Twenty-five of thirty contacts had no sound. My best signal inverted when the camera moved twenty metres. A full swing means nothing. The most confident measurement in the project was grading its own homework. Not one of those was discoverable by reasoning, however good the reasoning.

They were discoverable by going to a net, filming, watching, and being told by a cricket ball that you had it backwards.

The ball doesn't read the docs. That's the whole job.

Point it at your session

Free, fully on-device, no upload, no account required. Bring the worst footage you have — a phone propped on a kit bag, nobody behind the camera, three nets going at once. That's exactly what it was built on.

Get CricCuts → How it hears a shot

Related reading: the gust that fixed the engine — how a cartoon whoosh exposed the timing bug above; the idea the AI missed; build with a genius, ship a machine; and timbre, onset & luma. More on the CricCuts blog.

Comments

Thoughts, questions, corrections — all welcome. No account needed.