Skip to content
Arunish Rajput@arunishrajput

Systems & interfaces

Frame In Goa

Drop a photo, get a branded card in two seconds — the preview is provably the same pixels as the file.

Live2026Solo buildHacker House Goa 2026 — Task 1 shortlisting
Next.js 15TypeScriptTailwind v4Canvas 2DMediaPipeVercel BlobVitest

ENV VARS NEEDED

0

OUTPUT FORMATS

3



The problem

Every "add our frame to your photo" tool has the same bug and nobody fixes it: the preview is a styled <div> and the download is a canvas render. They are two different code paths, so they drift — the preview looks right, the file you get doesn't, and you only find out after posting it.

Add the real-world inputs. iPhone photos arrive as HEIC, which browsers won't decode. Faces land anywhere in the frame. And a circular profile picture has to survive X's own avatar crop, which is not the crop you designed for.


What I built

A single render(spec) Canvas 2D pipeline. Everything that ends up in a PNG is drawn by it — including the on-screen preview. There is no second path to drift away from, because there is no second path.

Three outputs: a circular PFP frame built to survive X's avatar crop, a Builder ID card, and a Crew Card that fits two to four people in one frame.


How it works

HEIC decoding (heic-to) and face-aware auto-framing (@mediapipe/tasks-vision) are both lazy-loaded and both fail silently to a sensible default — an iPhone user gets their photo, a face-detection miss gets centre-framing, and neither ever shows an error for something the person can't fix.

The share flow uploads to Vercel Blob only at the moment someone taps "Post on X", never on generation. /s/[id] then serves real per-card OG metadata, so the link preview on X shows the actual generated card rather than a static fallback.

The whole thing runs with zero environment variables. Clone it, pnpm dev, it works.


Decisions

Chose

One Canvas render path for preview and download

Over

A styled DOM preview with a separate export renderer

Because

Two renderers of the same thing will disagree, and the disagreement surfaces after someone has already posted. One path costs a little layout convenience and makes the preview a guarantee rather than an approximation.

Chose

Upload to Blob only on share intent

Over

Uploading every generated image

Because

Most people generate several and post one. Uploading everything means storing images nobody asked to publish, for a share feature that needs exactly one — a privacy cost with no product benefit.

Chose

Lazy-load MediaPipe and HEIC decoding with silent fallbacks

Over

Bundling them and requiring them to succeed

Because

Both are large and both are conditional — most photos are JPEGs with a findable face. Loading them on demand keeps the first paint fast, and failing silently means a rare edge case degrades to centre-cropping instead of throwing an error at someone who just wants a picture.