The toy is the portfolio piece
Most design portfolios lead with a grid of work. This one leads with a thing you can play with, and puts the work underneath as a short list of names.
The reasoning is that a portfolio has about two seconds to prove that the person who made it can build something, and a generative visual engine running at full bleed proves it faster than a screenshot of a poster does. The work is one scroll away and clearly labelled; the front door is the argument for opening it.
The page is two screens. The first is the synthesizer and the wordmark. The second is a warm paper panel holding the project links, an about page and the contact route.
Sixteen modes on one canvas
An original EYESY-style engine drives a single <canvas> behind the wordmark. Each mode is a draw function operating on the same five-parameter state, so switching modes is instant and the parameters carry across — a shape you have dialled in stays dialled in when you move from Lava Lamp to Chomp.
What the canvas is doing
- Frames are throttled to roughly 30fps — the draw loop returns early if less than 32ms has passed. The modes are dense enough that the extra frames cost battery without reading as smoother.
- Device pixel ratio is capped at 1.25 on viewports wider than 720px and 2 below it. A phone gets the sharpness; a 5K display does not get asked to fill 15 million pixels thirty times a second.
- Trails are a mode of the clear step rather than a post effect: switching persistence off repaints the ground each frame, switching it on lets the previous frame bleed through.
A sheet on a phone, a panel on a desktop
The controls live in one panel with four tabs. On mobile it behaves as a draggable bottom sheet with a peek detent; on desktop it floats. It stays out of the way until asked for — the page is fully playable without ever opening it.
| Tab | Controls | Notes |
|---|---|---|
| Mode | Previous / next, plus a full select | Tapping the artwork itself also jumps to a random mode |
| Shape | Five drag knobs, labelled ♥ ✿ ★ ☯ ☻ | The same five parameters mean something different in every mode, which is the point — you dial rather than configure |
| Color | Foreground and background steppers with live swatches | Two palettes: a free hue walk, or a constrained brand set |
| Effect | Flash, Shuffle, Trails, Palette, Music | Flash fires a one-shot trigger the modes respond to |
Keyboard
The panel is operable without a pointer: ← → step the mode, space flashes, P toggles trails, R shuffles, Esc closes. The knobs are real role="slider" elements with arrow-key support, not divs with drag handlers bolted on.
Music
The Music button asks for system or tab audio via getDisplayMedia and falls back to the microphone. The stream is routed through an AudioContext analyser at fftSize 256, and the level it reads is fed into the same trigger the Flash button fires — so every mode is already audio-reactive without a single mode-specific line of audio code.
Teaching it
A help card explains the four gestures in the page's own voice: swipe the background sideways for a new pattern, tap it for a surprise, click a letter in the name for a new face, open the cloud for the full set of controls. It is shown once and remembered.
Seven letters, eight faces, one control surface
The word tristan sitting over the canvas is not an image. Every letter is its own element, and clicking one advances it to the next typeface in a cycle of eight.
| # | Face | # | Face |
|---|---|---|---|
| 0 | Shrikhand | 4 | Killuminati |
| 1 | WooowLight | 5 | Okiedoko |
| 2 | Bigsock | 6 | Boardslide |
| 3 | Mornes | 7 | Kaushan Script |
Because seven letters each hold one of eight faces independently, the wordmark has 8⁷ — just over two million — possible states. Every one of them is a legible rendering of the same word.
The details that make it feel like an object
- Each press fires a jolt: a small randomised translate, rotate and scale on that letter alone, on a stepped easing curve so it snaps rather than glides.
- A burst of particles comes off the letter, and each letter throws its own glyph —
tthrows lightning,rthrows hearts,nthrows moons. - Glyph bounds are measured with SVG
getBBoxrather than canvasmeasureText. These display faces have wildly different ascents, and canvas measurement silently falls back to a substitute font and under-reports them, which made the layout jump. Reading the real loaded outline fixed it.
A choice made on the front door follows you
This is the part that ties the site together. The faces you pick on the landing page are not decoration on that page — they are site-wide state.
Why it matters. It is a two-million-state personalisation with no account, no cookie banner and no server — about forty lines of code total. A visitor who plays with the name on the front door finds their own version of it waiting on every page after, and most people notice it exactly once, which is the right number of times.
The same letters run the panel below
On the lower panel, the same seven numbers are put to a second use. Each project link takes its underline treatment from the matching letter — link one from letter one, and so on — and the last letter sets the style of every glyph in the list.
The last three are repeating inline SVG rather than border styles, because CSS has no scalloped or wavy border and a background image tiled along the bottom edge costs nothing.
Eight glyph treatments
The seventh letter switches the whole list's marks between fill and stroke weights, dashes, and rotations:
Nothing announces any of this. The panel simply looks slightly different depending on what you did upstairs.
What is remembered, and where
Everything persists in localStorage. There is no server, no session and no analytics attached to any of it.
| Key | Holds |
|---|---|
| tristan-letters | The seven face indices for the wordmark. The one key read by every other page. |
| tristan-eyesy-v6 | Synth state — mode, the five shape parameters, foreground and background positions, trails, palette. |
| tristan-help-seen-v1 | Whether the how-to-play card has been shown. |
| tristan-touched-v4 | Whether the visitor has interacted with the canvas, used to stop nudging them. |
| tristan-touched-panel-v1 | The same, for the control panel. |
| tristan-ctrl-mini | Panel size preference. |
| tristan-ctrl-used | Whether the panel has been opened before. |
Every read is wrapped in a try/catch and every one has a working default, so private browsing, cleared storage and a blocked storage API all degrade to a page that simply starts fresh.
The parts that are easy to skip
Reduced motion
The page respects prefers-reduced-motion in nineteen places. Under it, letter jolts and particle bursts do not fire, the scroll-reveal on the project links resolves immediately rather than sliding in, and the decorative line pulse is removed outright. The synth still runs — it is the content, not an animation — but nothing jumps at anyone.
Keyboard and screen readers
The wordmark carries an aria-label of the plain word, so it reads as "tristan" rather than seven separate letters. The shape knobs are sliders with proper value ranges. Every control has a visible focus ring, and the whole synth can be driven from the keyboard alone.
Weight
Nine display faces are self-hosted for the wordmark, which is expensive — until you notice they only ever set one word. Every one of them is subset down to the fourteen characters of tristan in both cases and re-encoded as WOFF2, taking the page from 4.17 MB to 1.13 MB with no visible change. The body face is left whole, because it has real text to set.
The trade this documents. A generative front door is a risk: it delays the work by one scroll and it asks a visitor to be curious. The mitigations are all in this page — it is fast, it is playable without instructions, it works with the keyboard, it calms down if you have asked your system to calm things down, and it remembers what you did.