← Projects

penna

Draw a signature, get back an SVG that writes itself — 1 KB, no scripts. The mark in this site's header came out of it.

New message
To
hiring@example.com
Subject
Re: the frontend role

Thanks for the notes — revised deck attached.

Best,

Haris Vreto · Senior Full-stack Engineer
harisvreto.dev

The same 1.3 KB file, dropped into an email signature. No script tag, no player, nothing to load — the browser draws it.

Penna is a single page. You draw your signature on a pad, it replays as a self-writing animation, and you take it away as an SVG that animates on its own — in any browser, any img tag, any CSS background. No JavaScript ships with the file.

The signature in this site's header is a Penna export. That was the test: if I wouldn't put it on my own front page, it wasn't finished.

What it makes

Every stroke becomes one path. Each path gets a stroke-dasharray equal to its own arc length and a generated @keyframes rule that walks stroke-dashoffset to zero during that stroke's slice of the timeline. All the paths share one duration, so a single inline style block staggers the whole signature. There is no script tag in the output and nothing to fetch.

The numbers, measured on the same three strokes:

  • Animated SVG — 1.3 KB. Transparent, vector, animates anywhere an SVG renders.
  • APNG — 14.8 KB. Transparent with real 8-bit alpha. For GitHub READMEs, which strip the style block out of proxied SVGs and show them frozen.
  • GIF — 228 KB. No usable transparency, 175× the SVG. It exists for Slack and email clients that understand nothing else.

GIF's alpha is one bit — a pixel is either fully opaque or fully gone. A signature is antialiased round-cap strokes, which are almost entirely partial alpha along every edge, so a transparent GIF gives you a jagged halo instead of a cutout. That is why the GIF export mattes onto a solid colour and why APNG exists at all.

Timing is geometry

The thing that makes a replay read as handwriting rather than playback is that stroke duration is derived, not fixed. A stroke takes length / (620 × speed) seconds, with a 0.14s pause between strokes. Long flourishes take longer than short ticks, because in life they do.

The same path data feeds both the on-screen canvas and the exported file, so the preview and the export cannot drift apart.

Stack

Vite, React, TypeScript. The engine — geometry, timeline, SVG builder — is a dependency-free module with no React import and no DOM access beyond a 2D canvas context, so lifting it into a package is a move rather than a rewrite. Encoders are code-split and only load when you click their button. 40 tests, Lighthouse 100 across all four categories on mobile and desktop.

I wrote up how the animation actually works in Signatures that sign themselves.

Questions about this one? The terminal twin has opinions — ⌘K