
SurfZar Deep-Dive: Writing a Zero-Dependency Galois Field GF(256) Reed-Solomon QR Encoder & Compact URL Serializer
Table of Contents (5 sections)▸
One of the core design principles of SurfZar is that it operates with zero backend database. When a professor, researcher, or digital artist crafts an algebraic surface with custom parameters , dual-sided PBR textures, custom camera angles, cross-section clipping planes, and a 10-second multi-track keyframe animation, they should be able to share that exact studio state via a single self-contained URL or QR code that will still work 20 years from now.
Achieving that required solving two engineering problems in src/lib/UrlState.ts and src/lib/QrCodeGenerator.ts:
- Compact Delta-Encoded State Serialization (
UrlState.ts): Compressing 40+ studio state fields and keyframe arrays so the URL fits comfortably inside QR Code Version 1–10 byte limits. - A Zero-Dependency ISO/IEC 18004 QR Code Generator (
QrCodeGenerator.ts): Implementing Galois Field polynomial arithmetic, Reed-Solomon Error Correction, and ISO Mask Penalty evaluation from scratch in ~350 lines of pure TypeScript.
1. Compact Key-Mapped & Delta-Pruned URL Serialization (UrlState.ts)#
If you naively JSON.stringify() a Zustand store containing camera vectors, material presets, and animation tracks and Base64-encode it, even a simple sphere produces an 800+ character URL—exceeding the capacity of compact QR codes and getting truncated by messaging apps.
In src/lib/UrlState.ts, serializeStateToUrl() applies a three-stage compression pipeline:
- Default-Delta Pruning: Any setting that matches the studio's default state (e.g.,
roughnessA === 0.2,boundingShape === 'sphere',shellThickness === 0) is omitted completely from the payload. - Float Quantization & Short-Key Dictionary (
CompactSharePayload): Floating-point camera coordinates[x, y, z]and keyframe timestamps are rounded to 3 decimal places (Math.round(v * 1000) / 1000), and verbose property names are mapped to 1-to-3 character keys (eq,a,b,c,d,m,cA,cB,bs,cam,kfs). - UTF-8 Safe URL-Base64 Encoding (
toUrlSafeBase64): The resulting compact JSON string is UTF-8 byte-encoded and mapped to RFC 4648 URL-safe Base64 (+-,/_, stripping=padding), producing clean#share=...hashes under 180 characters.
2. Writing an ISO/IEC 18004 QR Code Generator from First Principles (QrCodeGenerator.ts)#
Ironically, many mathematical apps that render 3D surfaces on the GPU pull in 50 KB+ of legacy CommonJS <canvas> dependencies just to draw a 2D QR code modal.
Because QR codes are themselves a beautiful application of abstract algebra over finite fields, I wrote src/lib/QrCodeGenerator.ts from scratch in pure TypeScript—producing crisp, resolution-independent <svg> markup with zero external packages.
Step 1: Galois Field Log/Antilog Lookup Tables#
Reed-Solomon error correction operates over the finite field (also written ), where each byte represents a polynomial of degree with binary coefficients in modulo the QR specification's irreducible primitive polynomial:
In :
- Addition and subtraction are both bitwise XOR (
^), because . - Multiplication (
gfMul) modulo is accelerated to by precomputing the powers of the generator element intoGF_EXP(antilog) andGF_LOGtables at module initialization:
typescript// Galois Field GF(256) with primitive polynomial 0x11d (x^8 + x^4 + x^3 + x^2 + 1) const GF_EXP = new Uint8Array(512); const GF_LOG = new Uint8Array(256); (function initGF() { let x = 1; for (let i = 0; i < 255; i++) { GF_EXP[i] = x; GF_LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; // Reduce modulo p(x) } for (let i = 255; i < 512; i++) { GF_EXP[i] = GF_EXP[i - 255]; } })(); function gfMul(a: number, b: number): number { if (a === 0 || b === 0) return 0; return GF_EXP[GF_LOG[a] + GF_LOG[b]]; }
Step 2: Reed-Solomon Generator Polynomial & Synthetic Division#
To generate error-correction codewords (eccLen), we construct the Reed-Solomon generator polynomial whose roots are :
and compute the remainder of the message polynomial divided by using polynomial synthetic division over :
typescriptfunction getGeneratorPoly(degree: number): Uint8Array { let g: Uint8Array = new Uint8Array([1]); for (let i = 0; i < degree; i++) { g = polyMul(g, new Uint8Array([1, GF_EXP[i]])); } return g; } function calculateEcc(data: Uint8Array, eccLen: number): Uint8Array { const gen = getGeneratorPoly(eccLen); const msg = new Uint8Array(data.length + eccLen); msg.set(data); for (let i = 0; i < data.length; i++) { const coef = msg[i]; if (coef !== 0) { for (let j = 0; j < gen.length; j++) { msg[i + j] ^= gfMul(gen[j], coef); } } } return msg.slice(data.length); }
Step 3: Multi-Block Interleaving, Zig-Zag Placement & 8-Mask Penalty Scoring#
For larger URLs (QR Versions 5–10), the payload is split across multiple Reed-Solomon blocks whose data and parity bytes are interleaved so a localized smudge on the QR code distributes bit errors evenly across blocks.
Finally, QrCodeGenerator.ts:
- Places the three Finder Patterns, Timing Strips, Dark Module, and version-specific Alignment Patterns on the matrix while marking them in a
reservedmask. - Snakes the interleaved bitstream upward and downward in 2-column Zig-Zag strips from the bottom-right corner (skipping vertical timing column ).
- Evaluates all 8 ISO data masks (
mask 0throughmask 7) against the four ISO/IEC 18004 penalty rules (runs of identical bits, solid blocks, false finder-like1:1:3:1:1patterns, and dark/light balance ratio), selects the mask with the lowest penalty score, writes the BCH-encoded 15-bit format string, and emits a single compact<svg><path d="..."/></svg>element!