Greater than a yr of rebuilding, refining, and rethinking to get it good. What went dwell on March 23, 2026 was not one clear concept from begin to end, however the results of pushing the work till the visible path, movement, construction, and identification lastly clicked. Constructed round the concept doubt is dear, the portfolio was formed to make the usual apparent early, so the dialog can transfer straight to suit, scope, and execution.
A number of late nights for one thing that needed to really feel proper
I began designing younger and went by means of extra names than I’d prefer to admit. Falconcept, Paramor, and numerous different instructions all acquired shut in their very own means, however none of them totally carried the presence I used to be after. They both felt too protected, too indifferent, or an excessive amount of like I used to be hiding behind one thing that didn’t actually match my model.
R—Ok was the primary path that felt totally private whereas nonetheless carrying sufficient weight to talk to each manufacturers and businesses. Getting there was messy. A number of late nights, juggling different work, remodeling layouts, typography, pacing, and model language again and again, and loads of moments of considering **** this, this nonetheless isn’t it. However that was the method. I stored pushing till it stopped feeling like separate concepts and eventually began to really feel like one factor.
Someplace in all that remodeling, R—Ok stopped feeling like one other path and began feeling just like the path. The identification acquired sharper, the layouts extra deliberate, and the entire thing lastly started to hold the presence I had been after for a very long time. It began to really feel extra resonant, extra refined, much less certain by what a portfolio is meant to appear like, and far nearer to the type of readability that solely comes from actually understanding what the work must be. That first impression mattered quite a bit, which is precisely what formed the hero.
Technical Overview
Beneath that sits my very own framework, advanced from native Shopper-First by Finsweet. I additionally used Osmo Provide’s scaling system and Barba boilerplate as a base, then folded each into my very own setup so I can hold constructing on it in future tasks.
The positioning may be very visible, however the factor that retains it manageable is construction. I exploit attributes for mainly all the pieces. Each JavaScript file begins with notes explaining precisely which attributes it depends on, so I at all times know what talks to what. That issues if you work in Webflow and hold touching HTML/CSS. I by no means need some random visible edit to quietly break logic some place else.
Right here is an efficient instance of that, the construction and choices are outlined straight within the HTML, whereas the code reads them and wires up the behaviour:
const MENU = {
wrap: "[data-menu-wrap]",
toggle: "[data-menu-toggle]",
panel: "[data-menu-panel]",
objects: "[data-mi]"
}
const wrap = doc.querySelector(MENU.wrap)
const toggle = doc.querySelector(MENU.toggle)
const panel = doc.querySelector(MENU.panel)
const objects = panel?.querySelectorAll(MENU.objects)
const config =
Constructing the middle stage of the web site, the Unicorn Studio visible
The hero needed to do quite a bit, quick. It needed to pull individuals in earlier than the positioning actually explains something. The distorted portrait, the typography, the pacing, and the WebGL background all needed to work collectively with none one in all them overpowering the remaining.
For the background, I used Unicorn Studio, a software to create WebGL movement and interplay in minutes, with none code. The aim was to get the fitting visible weight and ambiance with out making the construct extra advanced than it wanted to be. I self-host the Unicorn undertaking file and mount it into the web page utilizing bunny.web
const SELECTOR = "[data-us-project], [data-us-project-src]";
const KEY_ATTR = "data-us-key";
async perform createSceneForElement(el, key) {
ensureElementId(el, key);
const cfg = buildSceneConfigFromElement(el);
const scene = await window.UnicornStudio.addScene(cfg);
sceneMap.set(key, scene);
mountedElementMap.set(key, el);
requestAnimationFrame(() => {
if (scene && typeof scene.resize === "perform") scene.resize();
});
return scene;
}
That alone is just not what makes the hero work, although. What makes it work is the way it sits inside all the pieces else. It isn’t some free visible on the prime of the web page. It’s a part of the positioning’s rhythm from the primary second on.
What retains all of it managed
A number of the positioning comes all the way down to grid. I exploit a 12-column format all through, which supplies me sufficient construction to push compositions round whereas retaining all the pieces managed. For many full-screen sections, I normally assume in six rows on a 100vh part, which helps place issues with intention with out making the layouts really feel stiff.
That carries throughout the entire web site. The homepage stays clear whereas nonetheless feeling authored, the undertaking pages lean into presentation, and the perception pages really feel extra editorial, nearer to spreads than customary weblog posts. The aim was to make every web page really feel associated, however not repetitive.
To maintain the hero secure on cell, I added a viewport-height helper. Cell browsers continually change the seen viewport when you scroll, which may make 100vh-based layouts resize unexpectedly. The helper writes a steadier worth to –vh which I then use in CSS for screen-based sizing.
perform setVh() {
const h = window.visualViewport?.top ?? window.innerHeight
const px = h * 0.01
if (minVhPx === null) minVhPx = px
minVhPx = Math.min(minVhPx, px)
doc.documentElement.type.setProperty("--vh", `${minVhPx}px`)
}
In observe, meaning I can outline screen-based sizing in CSS like this:
--vh: 1vh;
--near-screen-height: calc(var(--vh) * 80);
--full-screen-height: calc(var(--vh) * 100);
--overflow-screen-height: calc(var(--vh) * 120);
--double-screen-height: calc(var(--vh) * 200);
Sound, however solely if you’d like it
Sound was one thing I wished within the web site pretty early, as a result of it provides a bit extra weight to transitions, hover states, and menu interactions. On the identical time, I understand how annoying it’s when a web site simply begins enjoying music at you for no cause, so I stored it totally non-compulsory. Nothing begins on first click on. It’s important to allow it your self.
That gave me the perfect of each worlds: an additional layer for individuals who need it, with out forcing it into the expertise for everybody else.
perform bindSoundTargets(root = doc) {
root.querySelectorAll("[data-sound-hover]").forEach(bindHoverElement);
root.querySelectorAll("[data-sound-click]").forEach(bindClickElement);
root.querySelectorAll('[data-sound="mute"]').forEach(bindMuteElement);
}
perform bindHoverElement(el)
perform bindClickElement(el) {
if (!el || el._rkClickBound) return;
if (!el.hasAttribute("data-sound-click")) return;
el.addEventListener("click on", () => {
const soundName = el.getAttribute("data-sound-click");
playNamedSound(el, soundName);
});
el._rkClickBound = true;
}
Then the mute logic makes certain the entire thing stays user-controlled quite than computerized:
perform toggleMute() {
const wasUnlocked = audioUnlocked
if (!audioUnlocked) {
unlockAudio()
}
if (!wasUnlocked && audioUnlocked) {
isMuted = false
Howler.mute(false)
playBGM({ fromStart: !bgmEverStarted })
syncMuteUI(doc)
emitState()
return
}
isMuted = !isMuted
Howler.mute(isMuted)
if (isMuted) pauseBGM()
else playBGM({ fromStart: false })
syncMuteUI(doc)
emitState()
}
Preloader to set the stage
I didn’t need the preloader to really feel separate from the positioning. It needed to already really feel like a part of the expertise. As an alternative of simply protecting the web page whereas issues load, it makes use of an SVG clip path with an even-odd cutout, which lets me carve a gap out of a full-screen layer and develop that opening over time. That cutout turns into the reveal itself.
The timing is cut up into distinct phases. First, the emblem masks reveal in with a parallax ease. From there, the cutout timeline takes over, regrouping the emblem, resizing the wrappers, after which beginning the precise cutout sequence. The cutout begins as a small sq. within the centre, expands right into a wider rectangle, after which settles right into a full-screen opening. These steps are timed at 0.3s, 1.1s, and 0.8s, all utilizing the identical parallax ease, with a slight overlap between the rectangle and full-screen part so the movement by no means feels too segmented.
On the identical time, the Unicorn scene is triggered simply after the preliminary brand reveal, so it has sufficient time to get up beneath the masks earlier than the opening totally clears. That mixture is what makes the preloader really feel much less like a loading state and extra like a part of the hero itself.
perform buildEvenOddPath(w, h, holeW, holeH) {
const outer = `M0 0H${w}V${h}H0Z`;
if (!(holeW > 0 && holeH > 0)) return outer;
const x = (w - holeW) * 0.5;
const y = (h - holeH) * 0.5;
const interior = `M${x} ${y}H${x + holeW}V${y + holeH}H${x}Z`;
return `${outer}${interior}`;
}
This ends in:
+----------------------+
| |
| +--------+ |
| | gap | |
| +--------+ |
| |
+----------------------+
Barba web page transitions that tie all of it collectively
The transitions took a silly quantity of iteration. At one level, I had a a lot heavier WebGL model the place the web page nearly peeled up and rotated away. It sounded nice in principle, however in observe it was an excessive amount of — too heavy, too awkward, and too straightforward to overdo.
I finally moved to a clip-path based mostly transition as an alternative. It nonetheless gave me the sensation I wished, however it was cleaner, simpler to tune, and far simpler to sync with all the pieces else taking place on the web page.
That shift mattered, as a result of the transitions in R—Ok will not be simply there to brighten navigation. They carry rhythm by means of the positioning. They make the entire thing really feel like one steady world as an alternative of a stack of disconnected screens. The textual content reveals, masks, and enter animations all needed to land on the proper second — not simply vaguely after the brand new web page appeared.
const CLIP_1 = "polygon(0% 100%, 100% 40%, 100% 100%, 0% 100%)"
const CLIP_2 = "polygon(0% 60%, 100% 0%, 100% 100%, 0% 100%)"
const CLIP_3 = "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)"
tl.set(subsequent, { clipPath: CLIP_1, webkitClipPath: CLIP_1 }, "startEnter")
tl.to(subsequent, {
clipPath: CLIP_2,
webkitClipPath: CLIP_2,
period: 0.22,
ease: "parallax"
}, "startEnter")
tl.to(subsequent, {
clipPath: CLIP_3,
webkitClipPath: CLIP_3,
period: 0.76,
ease: "parallax"
}, "startEnter+=0.22")
Insights that really feel like actual journal spreads
This didn’t require a lot code. It was extra about enjoying with format, composition, and pacing, however I wished the Insights to really feel like precise journal spreads quite than customary articles. The aim was to make each really feel like a designed editorial web page, as in case you had been shifting from unfold to unfold, not simply scrolling by means of content material.
Smaller particulars matter
A number of what makes R—Ok really feel like R—Ok lives in smaller selections. The dock as an alternative of an ordinary navbar. The way in which it disappears across the hero and footer. The way in which the footer nonetheless feels alive as an alternative of merely turning into the tip of the web page. The way in which attributes hold all the pieces structured beneath, so I can hold pushing the visuals with out the entire thing turning into fragile.
One in all my favorite smaller touches is the animated swords within the footer. Not as a result of they’re some big technical flex, however as a result of they are saying quite a bit about how I take into consideration ending a web site. The footer ought to nonetheless really feel authored.
gsap.set(L, { transformOrigin: "0% 100%" })
gsap.set(R, { transformOrigin: "100% 100%" })
struggle.to(R, { x: 1.05, y: 0.1, rotation: 4.5, period: 0.4 }, 0)
struggle.to(L, { x: 0.15, y: -0.05, rotation: 1.2, period: 0.4 }, 0)
struggle.to(R, { x: -1.85, y: -0.55, rotation: -8, period: 0.34 }, ">-0.05")
struggle.to(L, { x: -0.55, y: 0.2, rotation: 7, period: 0.34 }, "<")
It’s a small factor, however it provides the footer a little bit of life as an alternative of constructing it really feel like the trouble stopped there.
Closing ideas
None of those items carry the positioning on their very own. Not the WebGL. Not the transitions. Not the preloader. Not the sound. Not the smaller particulars. The work was in getting them to help one another with out letting the entire thing collapse into noise.
R—Ok was by no means constructed to show what number of results I may match right into a portfolio. It was constructed to really feel thought-about from the primary body to the final — sharp, memorable, and clear sufficient to set the usual instantly.
Constructed for presence. Not quantity.









