Joonas Virtanen — Aesthetic Principles

Taste

Codified from the work. A living reference for design decisions.

Technology should feel closer to human things — bodies, moods, weather, time, each other. The question isn't how to maximize engagement, but what this helps you feel.

Pro-human, playful, honest. Art history is a real resource — in conversation with Rothko, Constable, Monet, not just last year's Dribbble. Restraint is earned, not assumed. Settling for good enough is not good enough.

Paper, not screen
Warm cream backgrounds (#f4eae6, #faf8f5), not pure white. Warm ink (#1c1814), not #000. The material register matters.
4–6 colors, earned
Terracotta, ochre, sage, soft blue — tones drawn from natural materials and art history. More than 6 and nothing has weight. Contradictory tones create tension that makes palettes memorable.
Dark: deep blacks
#0a0a0a, #131316 — not muddy grays. Liquid glass and iridescent overlays belong here. Warm/cool gradient pairs (orange + blue). Not rainbow.
Tokens, not hex
CSS color tokens, not hardcoded values scattered through files. One gray scale. Dark mode must be consistent — a component that breaks in dark mode ships broken.
Display serif
Edict Display, Kalice, EB Garamond for headlines — personality, warmth, humanity. Tighten at large sizes (letter-spacing: -0.02em or tighter). Not Inter Bold.
Small foundries
Schick-Toikka, Klim, independent type foundries — not just Google Fonts. Trial during prototyping; licensed when shipping. The typeface is never neutral.
Technical hygiene
-webkit-font-smoothing: antialiased always. font-variant-numeric: tabular-nums on changing numbers. text-wrap: balance on headlines. Sizes are generous, never apologetic.
120–180ms Presses, hovers
180–260ms Small state changes
≤300ms Larger transitions
400ms Feels broken
Springs vs. curves
Springs for user-driven motion (drag, flick, press) — they survive interruption. Easing curves for system changes. Ease-out for entrances. Ease-in for exits. Ease-in-out for transitions. Linear only for time itself.
Motion earns its place
Default is no animation. Add one when it clarifies state or causality. Never animate to perform. "You don't need animations" is the starting question.
Physics over flatness
Hover states are elaborate (dim siblings, saturate active). Cards rotate, stack, fan, fling. Drag = momentum. Snap = place. Inertia is information. Never transition: all.

Real refraction, not frosted blur. A displacement map encodes per-pixel optical direction — WebGL2 bends the source image on the GPU. The lens is weightless: only its position changes at runtime.

Same displacement map as the rest — but the QR is painted to its own canvas, so a WebGL shader refracts the code's own pixels. Move your cursor over it.

The map generator
How it works
A small PNG is generated per lens shape — its R channel encodes horizontal displacement, G encodes vertical. feDisplacementMap reads that map and bends the pixels of the underlying content. Outside the lens: neutral values (128, 128) = no displacement. Only the quadrant you generate needs to be real; the rest mirrors.
Key parameters
Scale — displacement intensity (0.10 typical). Depth — refraction strength. Curvature — light-bending curve. Chroma — color fringe at edges (0.20 typical). Specular angle — highlight direction (45°). Edge highlight — rim lighting (0.25).
Safari workarounds
Safari caches SVG filter output by filter ID — assign a fresh ID on every map update or it serves stale output. Safari also has a ceiling on source-graphic size; keep DOM complexity conservative when glass is active.
Two render paths
SVG filter path for live DOM (text, UI, interactive elements). WebGL shader path for canvas/video content — Safari can't apply SVG filters to <video> elements at all. Both share the same displacement map PNG.
Performance
The displacement map only regenerates when the lens shape changes, not when it moves. Moving the glass is just shifting the filter region — the expensive map computation stays cached. Four-fold symmetry: generate the top-left quadrant only, mirror into all four.
When to use
Interactive controls that travel over rich content — switches, sliders, toggle groups, video scrubbers. Glass should amplify what's underneath, not obscure it. Dark backgrounds with color underneath are the natural habitat. Never use it just for frosted-blur vibes — that's backdrop-filter: blur() dressed up.
Fitts's Law
Minimum 44×44px touch targets. Expand hit areas with invisible ::before padding. Tiny interactive elements are neglected, not designed.
Hick's Law
Every added option raises decision time logarithmically. Progressive disclosure over density. A curated menu is more useful than a full one.
Doherty Threshold
Under 100ms = extension of your hand. Over 400ms = feels broken. When actual speed is slow, use optimistic UI and skeletons to shift perceived speed.
Miller's Law
~7 items in working memory at once. Chunk everything. "415-867-5309" vs "4158675309" — same information, completely different cognitive load.

"Fuck it we ball" in a display serif on cream. Not edgy — accurate. The best content doesn't hedge.

Specific and honest
Write to the specific human moment. Art, weather, mood, time are legitimate subjects. Not every project needs a task or a dashboard.
Banned words
Synergy. Streamline. Seamless. Delightful (noun). Leverage (verb). User-centric. Placeholder text in a final design is a hard failure.
Muddy mid-grays
Purple-to-pink gradients
Glow effects on text
transition: all
Ease-in for entrances
Blur over 20px
Pill buttons by default
Generic two-column hero
Non-concentric border radius
Placeholder copy in finals
Two filled CTAs
Inter Bold for headlines
Lottie animations
Glassmorphism for vibes
Explained interactions
Baked-in drop shadows
Rothko — Color as emotion, mood as data
Early iOS — Physics as information
Constable, Monet — Atmosphere as subject
Teenage Engineering — Tools as objects
Donald Judd — Restraint + material
are.na — Curation as interface
Dieter Rams — Less but better
Linear — Density done right
Müller-Brockmann — Grid as philosophy
Schick-Toikka, Klim — Type with character