kaanses.com · Making of

My portfolio is a toy.

kaanses.com is one interactive 3D scene: a toon vinyl figure of me, holding a MacBook and a coffee, inside an open-window collector box. The box's printed sides are the navigation. You can grab my arms and stretch them, slap me, and turn the box around to read my projects. It's built with Three.js, TypeScript and Vite. No framework, no physics engine.

The 11-second promo, recorded from the real site by a script. More on that below.

A collector box instead of a menu

Most portfolios ask you to read. I wanted one you'd play with and remember, so the whole site is a toy box. The front is a wrap-around corner window with the figure inside. The ABOUT sticker turns the box to the side with my bio, stack and contact links. SEE HIS WORK turns it to the back, where the projects sit in "collect them all" shelf rows. Drag anywhere to spin it yourself.

The figure: photo to rigged toy

The figure started as a photo. An image model turned it into an A-pose caricature reference, Meshy turned that into a textured mesh, retextured it and auto-rigged it: about 31k triangles on a 24-bone skeleton. The look is a toon toy: flat cel bands, an inverted-hull ink outline, and one small hard highlight that only light surfaces get, so the black hoodie and hair stay flat ink.

An asset script prepares the GLB once at build time. It drops tangents, bakes creased normals and fixes skin weights, so the browser doesn't have to. One trap: the model is in centimetres, so bones carry a 0.01 world scale and anything attached to a hand bone needs a local scale of 100.

Rubber limbs

Grab a hand, a foot or the head and drag. Each limb is a CCD IK chain with per-link rotation limits plus a stretch bone, so pulling past arm's reach stretches the arm like rubber instead of popping the shoulder. Limbs are clamped to the inside of the box.

Self-collision is cheap and good enough. Every bone gets one capsule, fitted from the vertices that bone dominates. When a dragged limb's target sinks into another capsule, it's pushed out and the chain is solved again. Pairs already touching at rest, like the upper arm and the torso, are ignored.

Holding things without clipping

Hands holding props are where 3D characters usually fall apart. The holding pose and the laptop and mug placement are baked offline by a script, and a Vitest test fails the build if a single body vertex ends up inside a prop. The check even pads the mug by the outline's width: the gripping hand's ink hull was once cutting a black notch into the cup.

Slaps, bruises and spilled coffee

Slap him and his head snaps away from the hit, overshoots once and squashes, with the chest following. Each slap leaves worse damage on that cheek: a red blush, then a lump, then a black eye, then a band-aid, healing in reverse after about five seconds. The damage is a shader step that runs after skinning and is shared by the skin and its ink hull, so the outline swells with the lump. Yank the mug arm hard enough and coffee flies off the rim, leaving splats on the box floor that shrink away. An idle sip never gets that fast.

When you leave him alone he breathes, turns his head to follow your cursor, sips, glances around and shifts his weight.

A box printed with canvas

Every face of the box is drawn at runtime onto a 2D canvas and used as a texture: the band, the halftone dots, the project rows with their logos, and the fitted text, in English or Turkish. Each face is laid out in its own 0–1 UV space, and the same numbers both draw the print and hit-test taps. Move a sticker and its tap area moves with it. The tech stack is a sheet of 3D die-cut stickers with a glint that ripples across them, and tapping one pops it.

Pressing the box tips it toward your finger, up to about 7°. That broke taps at first: a raycast on release could land beside the link you pressed. So taps now pick their target on press.

Loading fast

A 3D page is heavy, so the first thing you see is a WebP poster of the exact rest view. That poster is the page's largest paint. The model and box downloads start only once it has loaded, behind a packing-tape loader that shows real progress. Heavy startup work is split into chunks that yield to the main thread. With the CPU throttled 4×, that cut the longest freeze from about 780 ms to about 150 ms.

Search engines, screen readers and browsers without WebGL get the same content as real HTML. If loading stalls for 12 seconds, the page falls back to that version.

A promo film shot by the site itself

The trailer isn't a screen recording. A script drives the real site and swaps its clocks, timers, Math.random and CSS animations for a virtual clock that steps exactly 1/60 s per captured frame. That makes every capture smooth and identical however slow the machine is, and ffmpeg cuts it to 16:9, 9:16 and 4:5.

Stack

← Go play with the box · GitHub · LinkedIn