Week 2: Bringing 3D & Narrative to the Scroll Experience
Week 2 Schedule:
This week, the focus shifts from pure motion experiments to merging those effects with more meaningful storytelling and personal touches. By the end, I want the site to feel truly reflective of me, in terms of imagery, flow, and tone, while beginning to layer in 3D elements that add depth and immersion.
✅ Day 1: Content pass + scroll animation refinement
✅ Day 2: Render basic shapes in R3F; add lighting, controls, shadows
✅ Day 3: Build a simple 3D scene; animate camera/object with scroll
✅ Day 4: Embed 3D into the existing site
🔜 Day 5: Try a Spline → R3F export; wrap with a short reflection
Built Using:
The site’s foundation is largely unchanged from Week 1, but this week’s updates bring in more personal photography and a stronger visual identity. My aim is to evolve the technical base I’ve already built, not reinvent it from scratch.
Framework: Next.js
Animation: Framer Motion, Lenis
3D: react-three-fiber (starting this week)
Creative Coding: Vanilla Canvas API
Visual Assets: Personal photography (SF, Paris, etc.), AI imagery (Runway)
IDE: VS Code
Version Control: Git + GitHub
Day 1 : Content Polish & Scroll refinement
Today’s Goals
Today was meant to be about storytelling as much as polish. I wanted to make the site feel like mine by replacing placeholder content, adjusting layouts, and ensuring each scroll-triggered animation served a purpose rather than existing for novelty’s sake. I also hoped to start weaving a subtle narrative thread between sections.
What I built
Most of my time went into visual refinements that pushed the design closer to my own style.
I swapped the generic gallery images with personal shots from mostly of San Francisco, instantly making the page more authentic.
I refined gradient backgrounds, tweaked section spacing, and adjusted animation offsets to reduce awkward gaps and better highlight key moments in the scroll.
What I Learned
Working on content and motion at the same time revealed just how intertwined they are: a change to one often disrupts the other.
I also confirmed that personal imagery transforms the tone of the site far more than I expected, even without a fully formed narrative.
Finally, I took stock of all the scroll animation patterns I can now implement confidently, creating a baseline skill inventory.
Scroll animations now in my toolkit:
Fade-in on scroll (
whileInView
with opacity)Slide-in from sides (x-axis translate + opacity)
Scale-up reveal (scale + opacity tied to scroll)
Continuous, scroll-linked motion (
useScroll()
+useTransform()
)Scroll-driven color shifts (Canvas visuals via scrollRef)
Layered parallax backgrounds (low-opacity imagery)
Staggered element entrances (
staggerChildren
)
Challenges
What I envisioned as a content-first day quickly shifted into a visual and motion refinement session. While these updates improved the site’s look and feel, they came at the cost of progressing the narrative.
Because I’m still new to many of these tools, understanding the why behind certain code changes was often as challenging as making the changes themselves. This slowed decision-making and made it harder to evaluate whether each tweak was truly moving the project toward my bigger goals.
Even after today’s refinements, the site’s sections still feel more like visually related moments than parts of a single, continuous story; a gap I’ll need to close in the coming days.
Reflections
The site now feels far more personal than it did yesterday, yet without a clear narrative arc, it still comes across as a polished demo rather than an expressive self-portrait.
Replacing generic imagery with my own photography was a meaningful step toward authenticity, but the storytelling gap is still evident. My next priority is to fully design the story for one section and let motion serve that story, instead of spreading my attention across the entire site at once.
At the same time, I’m beginning to explore embedding and animating a 3D scene within one of the sections; a move that could add depth, intrigue, and another layer of personal expression to the overall experience.
Days 2 & 3: Building My First Interactive 3D Scene
My Goals
For these two days, my aim was to go beyond 2D and step fully into a 3D world. I wanted to render basic shapes in React Three Fiber, experiment with lighting, shadows, and camera controls, and then build a small interactive scene where the camera and objects respond to scroll.
This wasn’t just about adding novelty; it was about learning how to bring depth, movement, and personality to the generative section so it felt seamlessly integrated into the rest of the site’s flow and visual rhythm.
What i Built
I started by installing and configuring Three, React Three Fiber, and Drei, then created a dedicated ThreeScene component. The first step was a simple hot-pink cube without texture or motion. Just something simple to confirm the scene was rendering correctly.
From there, I added ambient and directional lighting, a basic spin animation, and orbit controls so I could freely rotate around the object.
Once I had the basics working, I swapped the solid color for a meshNormalMaterial, which added a dynamic rainbow effect that changed as the cube rotated, making it easier to visualize depth and lighting without spending time fine-tuning materials.
I experimented with size, rotation speed, and camera position until the cube felt more “alive” and responsive. Adding soft contact shadows grounded the object in space, and I tested scroll-linked rotation so the cube subtly reacted as the user moved through the page.
These steps gave me a much clearer sense of how objects, light, and motion interact in a 3D environment.
With that foundation in place, I turned to the generative section. The old version was a flat HTML canvas filled with 2D circles that pulsed and changed color (see week 1 for more).
I rebuilt it in 3D using spheres (and occasionally cubes for testing), scattered across the scene with varied positions, scales, and colors.
I introduced gentle floating motion and tied both rotation and color changes to scroll position and elapsed time.
The WebGL canvas was made transparent so it could blend with the site’s gradients, and I extended the section so nothing was clipped at the edges.
Finally, I added a scroll-linked camera dolly for subtle depth changes, and began experimenting with cursor-driven lines connecting shapes to the mouse for an interactive touch.
Updated Toolkit ( skills from Days 2 & 3)
Fade-in on scroll (
whileInView
with opacity)Slide-in from sides (x-axis translate + opacity)
Scale-up reveal (scale + opacity tied to scroll)
Continuous, scroll-linked motion (
useScroll()
+useTransform()
)Scroll-driven color shifts (Canvas visuals via scrollRef)
Layered parallax backgrounds (low-opacity imagery)
Staggered element entrances (
staggerChildren
)
What I learned
A cube with no light is flat. A cube with light but no shadow floats without context. A cube with shadow suddenly feels real. Each addition built on the last, and I realized how critical it is to treat motion, lighting, and materials as a system rather than separate steps.
I also learned that trial and error is part of the process. Most changes didn’t work the first time, and understanding why something broke was often more valuable than the fix itself.
3D design in code is less about individual features and more about layering fundamentals.
Challenges
Working in 3D for the first time quickly exposed the gap between what I imagined and what I could actually build. The cube went through several awkward stages before I figured out how to use light, color, and animation in a way that felt intentional.
I also experienced friction when trying to balance visual experimentation with technical understanding. Adding shadows, materials, or color transitions often worked through trial and error rather than confidence, which made progress slower than expected. Each change introduced new bugs, which forced me to troubleshoot before I could move forward.
Finally, fitting the 3D experiments into the existing scroll-based flow proved difficult. While the cube worked on its own, integrating it so it felt like part of the larger narrative is still an open challenge… One I’ll need to resolve as I move toward a more cohesive, story-driven site.
Reflections
These two days pushed me out of the comfort zone I was in with scroll-based animations. Even though the final result was just a cube, the process showed me how much potential there is in layering dimension and interactivity into my site. It also reminded me that learning new tools is messy; progress doesn’t always feel smooth, but every small breakthrough adds up.
The site is beginning to hint at new depth, even if the integration isn’t seamless yet. My next step will be figuring out how to blend these 3D experiments into the scroll narrative so they feel less like a demo and more like an intentional piece of the story.
Day 4: Make the 3d cube travel and dock
My Goals
Today’s goal was to make the cube more than decoration. I want it to follow me as I scroll, adapt to each section’s mood, and finally dock into a bay like it belonged there. My inspiration was motion.zajno.com, where 3D elements feel woven into the story of the page and especially by the little ball that follows the user throughout their visit.
What I Built
A CubeConductor system that renders a single cube overlayed above all sections.
The cube now:
Travels between
data-motif
sections, inheriting color, rotation, and scale.Smoothly interpolates toward section anchors, so it’s never stuck in one corner.
Docks in the Docking Bay (
ThreeScene
) and stays fixed until you scroll past.
Sections without
data-motif
no longer hijack the cube — the cube only responds where it’s meant to.
What I Learned
Anchors (
[data-motif-anchor]
) are the secret to fine-grained control of cube placement
Cube behavior requires clear states: Traveling → Docked → Idle. Without strict separation, it glitches.
R3F’s viewport math keeps movement consistent across screen sizes — avoiding distortion nightmares.
Challenges
This day was the definition of vibe coding pain
Something as “simple” as dock the cube here took hours of false starts.
Tried multiple approaches: direct transforms, scroll offsets, section-relative math, and finally a global conductor that reads from DOM sections.
The cube turned into a rectangular prism at one point (fixed by forcing
boxGeometry args={[1,1,1]}
):
It slid into the wrong sections. Fixed by simply removing the
data-motif
attribute so the cube ignores it:
Sometimes it docked but then refused to leave. Fixed by separating the docking check from normal travel:
The process was equal parts debugging, re-architecting, and stubbornly refusing to give up.
Reflections
Finally getting the cube to dock and resume its journey felt like a huge win. It was really the kind of moment where hours of frustration suddenly became worth it. This is the nature of vibe coding: you chase a feeling, the implementation fights you, and when it finally clicks, it feels magical.
The cube now feels like part of the story, not just a floating gimmick. This taught me (again) that the logic of transitions is the real design challenge.