Skip to content
AFM Studio
  • Pages

    AFM Studio
  • Work
  • Lab
  • About
  • CV
  • Contact
  • Projects

    AksaraA searchable library for Otorita IKN's knowledge, where the server checks every reader's access across three levels
  • Daily TaqwaTamper-resistant prayer attendance system for ~2,000 OIKN government employees at Masjid Negara IKN
  • Lantara v2A permit engine for Otorita IKN where administrators add new permit types from a screen, without a developer
  • eKiosk IKNTwo control rooms for Nusantara: kiosk content management and live bus tracking with arrival times that grade themselves
  • E-Monev IKNA monitoring platform for Nusantara's 23-year build, tracking every unit's quarterly progress against 24 national KPIs
  • JDIH OIKNOIKN's public legal document portal, with a controlled publishing workflow and AI summaries that never leave the server
  • Jejak PenanamanA check-in app for IKN tree-planting events that proves each participant was on site with a geofenced selfie
  • Dashboard SAKTIA nightly pipeline and dashboard that replaces OIKN's manual budget downloads for all six of its budget units
  • DoseRxA bedside dose calculator for 127 drugs that rounds each dose to tablets and syrups Indonesian pharmacies sell
  • CubiqA speedcubing trainer with competition-grade timing and a solver I wrote for each of the 8 major puzzles
  • ScimotionAn interactive science library of 86 articles, each explained next to animations the reader can control
  • WA Daily Scrum BotA WhatsApp bot that chases missing standup reports for a 14-person team and flags tasks stuck for days

↑ ↓ to move · Enter to open · Esc to close

Anatomi QRIS

An explainer that builds QR codes step by step and lights up the squares behind each QRIS field

Screenshots
stages on one draggable slider
9
network requests while you use it
0
QR versions matched against a reference library
1–15
to rebuild the largest code per keystroke
< 8 ms
On this page

The problem

People scan QR codes every day and have never seen one built. Online explainers stop at "here is a bitstream, here is a matrix". Error correction and masking stay black boxes, even in tutorials written for developers.

In Indonesia there is a second gap. QRIS, the national QR payment standard, is on the counter of almost every warung. The code behind the sticker is a plain, readable string that names the merchant, the payment provider and a checksum. Existing QRIS parsers show that string as a table and stop there.

I wanted to join the two. Select "merchant name" in the payment data, and the exact black squares carrying those letters light up in the code. The app is for two readers at once: a curious Indonesian with no technical background, and a developer who wants to check every number by hand.

What I built

The encoder remembers where every bit went

I wrote the QR encoder myself so it could keep a record as it builds. For every square it records why that square is dark or light: which byte and which bit it carries, or which fixed pattern it belongs to, and whether masking flipped it. Every feature reads from that one record. Field highlighting, damage tracking, the animation order and the mask comparison all come from it. I built and tested it before any screen existed, so no view could work out the layout on its own and drift from the others.

The screens show the real calculation

When the encoder adds error correction, it keeps every step of the long division. The division view draws those steps, so a reader can check one line with a pencil. The encoder also scores all eight mask patterns, even though only one wins. The mask gallery shows all eight with their penalty scores, so it always agrees with the winner.

An independent library checks every code

A test compares my output square by square with an independent QR library. It covers versions 1 to 15, all four error-correction levels, all three text modes and all eight masks, and a failure blocks the deploy. The two disagree in one place. For one penalty rule, the library rounds up where the ISO specification rounds down, which can change which mask wins. I follow the specification, and a test documents the difference so it can't go stale.

Pasted payment codes stay on your device

People paste real payment codes into this app. It makes no network requests while you use it: no analytics, no error reporting, and fonts ship with the page. A check at build time fails if anything in the output could call the network. Typed text goes into the URL so you can share a link. A pasted QRIS code never does, and the copy-link button disappears while one is loaded.

Result

Anatomi QRIS is live and public, in Indonesian and English. It walks through nine stages, from the typed text to a damaged code, on a slider you can drag. The code never moves or resizes, so halfway between two stages you see both half done. In the damage lab you scratch out squares and watch error correction repair them live. Push past its limit and it fails in front of you, with the reason. I built it in about 28 hours over two days in September 2026.

Under the hoodTechnical detail for engineers
  • The encoder returns a provenance map: for each module, codeword index and bit, or function pattern, plus a mask-flip flag. It also carries inverse maps (codeword to module coordinates, payload byte to codewords). A test decodes only the highlighted codewords to prove the mapping.
  • Reed-Solomon encoding emits a divisionTrace (lead term, generator multiple, remainder before and after), which the Division view renders directly.
  • src/engine/ is pure: no React, no DOM, no randomness, no clock. It runs unmodified under node --test.
  • encode() for a version-15 symbol stays under 8 ms, so typing re-encodes on every keystroke with no debounce and no worker. This uses precomputed GF(256) log/antilog tables, Uint8Array on the hot path and a flat row-major module array.
  • The largest matrix is 77×77, 5,929 SVG modules. One pointer listener on the matrix group hit-tests by dividing coordinates by module size. React mounts the rects once; a painter mutates fills and caches each module's last fill.
  • The oracle test demands byte-identical matrices against qrcode across versions 1–15, EC levels L/M/Q/H, numeric, alphanumeric and byte modes, both sides of every version boundary, and all eight masks. It runs in CI and blocks deploy.
  • The rule-4 disagreement: qrcode uses ceil(|pct − 50| / 5), while ISO/IEC 18004 Table 11 (and ZXing) use the floor. At 51.7% dark the spec charges 0 and qrcode charges 10. The oracle forces each mask and demands agreement on all eight, and checks rules 1–3 against qrcode's own penalty functions.
  • The QRIS lens activates when the input parses as a valid EMVCo Merchant-Presented Mode payload: it opens with 000201 and ends in a validating CRC16 under tag 63. The TLV parser, CRC16 and tag dictionary are hand-written, with an 85-entry merchant category lookup.
  • qrcode and jsqr are devDependencies only, and a test fails if either is imported from src/. Runtime dependencies are React, React DOM and three @fontsource packages.
  • scripts/check-bundle.mjs scans the build for network APIs and unexpected URLs. The favicon is inlined as a data URI after a browser test caught a 404 on /favicon.ico.
  • Seven views (Matrix, Bitstream, Division, Interleave, MaskGallery, PayloadTree, DamageLab), seven bundled samples, 86 CSS design tokens, and two flat copy files for Indonesian and English. Bundle is about 83 KB gzipped against a 250 KB budget.
  • Repair in the damage lab snapshots the damage standing when pressed and keeps what the decoder couldn't fix. Earlier, noise damage was derived from the slider on every read, so the repair button looked dead on the main path.
  • Scope left out on purpose: Kanji mode, versions 16–40, camera scanning, styled QR codes, and static-to-dynamic QRIS conversion.
  • 84 tests in 13 files, about 8,600 lines of source, 44 commits between 2 and 3 September 2026.

Screenshots

Masukan
Encode
Codeword
Koreksi Galat
Interleave
Penempatan

Hiring, or building something similar?

I'm open to full-time roles and select freelance work.