• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

The way to Construct Cinematic 3D Scroll Experiences with GSAP

Admin by Admin
November 19, 2025
Home Coding
Share on FacebookShare on Twitter



On this tutorial, we’ll discover two examples on how GSAP can act as a cinematic director for 3D environments. By connecting scroll movement to digital camera paths, lighting, and shader-driven results, we’ll rework static scenes into fluid, story-like sequences.

The primary demo focuses on shader-based depth — a rotating WebGL cylinder surrounded by reactive particles — whereas the second turns a 3D scene right into a scroll-controlled showcase with transferring cameras and animated typography.

By the tip, you’ll discover ways to orchestrate 3D composition, easing, and timing to create immersive, film-inspired interactions that reply naturally to consumer enter.


Free GSAP 3 Express Course


Be taught trendy internet animation utilizing GSAP 3 with 34 hands-on video classes and sensible initiatives — excellent for all ability ranges.


Test it out

Cylindrical Movement: Shader-Pushed Scroll Dynamics

1. Establishing GSAP and customized easings

We’ll import and register ScrollTrigger, ScrollSmoother, and CustomEase.

Customized easing curves are important for controlling how the scroll feels — small variations in acceleration dramatically have an effect on the visible rhythm.

import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { ScrollSmoother } from "gsap/ScrollSmoother"
import { CustomEase } from "gsap/CustomEase"

if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger, ScrollSmoother, CustomEase)

  CustomEase.create("cinematicSilk",   "0.45,0.05,0.55,0.95")
  CustomEase.create("cinematicSmooth", "0.25,0.1,0.25,1")
  CustomEase.create("cinematicFlow",   "0.33,0,0.2,1")
  CustomEase.create("cinematicLinear", "0.4,0,0.6,1")
}

2. Web page format and ScrollSmoother setup

ScrollSmoother works with a wrapper and a content material container.

The WebGL canvas sits fastened within the background, whereas the sleek content material scrolls above it.



{/* overlay textual content synchronized with scroll */}

We initialize the smoother:

const smoother = ScrollSmoother.create({
  wrapper:  smoothWrapperRef.present,
  content material:  smoothContentRef.present,
  {smooth}:   4,
  smoothTouch: 0.1,
  results:  false
})

3. Constructing the WebGL scene

We’ll use OGL to arrange a renderer, digital camera, and scene. The cylinder shows an picture atlas (a canvas that stitches a number of photographs horizontally). This enables us to scroll by means of a number of photographs seamlessly by rotating a single mesh.

const renderer = new Renderer({
  canvas: canvasRef.present,
  width:  window.innerWidth,
  top: window.innerHeight,
  dpr:    Math.min(window.devicePixelRatio, 2),
  alpha:  true
})
const gl = renderer.gl
gl.clearColor(0.95, 0.95, 0.95, 1)
gl.disable(gl.CULL_FACE)

const digital camera = new Digicam(gl, { fov: 45 })
digital camera.place.set(0, 0, 8)

const scene = new Remodel()
const geometry = createCylinderGeometry(gl, cylinderConfig)

We create the picture atlas dynamically:

const canvas = doc.createElement("canvas")
const ctx = canvas.getContext("second")!
canvas.width  = imageConfig.width * photographs.size
canvas.top = imageConfig.top

photographs.forEach((img, i) => {
  drawImageCover(ctx, img, i * imageConfig.width, 0, imageConfig.width, imageConfig.top)
})

const texture = new Texture(gl, { minFilter: gl.LINEAR, magFilter: gl.LINEAR })
texture.picture = canvas
texture.needsUpdate = true

Then connect the feel to the cylinder shader:

Cylinder shaders
The cylinder’s shaders deal with the UV mapping of the picture atlas and refined floor shade modulation.

// cylinderVertex.glsl
attribute vec3 place;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

various vec2 vUv;

void most important() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
}
// cylinderFragment.glsl
precision highp float;

uniform sampler2D tMap;
various vec2 vUv;

void most important() {
  vec4 shade = texture2D(tMap, vUv);
  gl_FragColor = shade;
}
const program = new Program(gl, {
  vertex:   cylinderVertex,
  fragment: cylinderFragment,
  uniforms: { tMap: { worth: texture } },
  cullFace: null
})

const cylinder = new Mesh(gl, { geometry, program })
cylinder.setParent(scene)
cylinder.rotation.y = 0.5

4. Scroll-driven cinematic timeline

Now we’ll join scroll to digital camera motion and cylinder rotation utilizing ScrollTrigger.
The container’s top: 500vh provides us sufficient room to area out a number of “pictures.”

const cameraAnim = { x: 0, y: 0, z: 8 }

const tl = gsap.timeline({
  scrollTrigger: {
    set off: containerRef.present,
    begin:   "prime prime",
    finish:     "backside backside",
    scrub:   1
  }
})

tl.to(cameraAnim, { x: 0, y: 0, z: 8,   period: 1,   ease: "cinematicSilk" })
  .to(cameraAnim, { x: 0, y: 5, z: 5,   period: 1,   ease: "cinematicFlow" })
  .to(cameraAnim, { x: 1.5, y: 2, z: 2, period: 2,   ease: "cinematicLinear" })
  .to(cameraAnim, { x: 0.5, y: 0, z: 0.8, period: 3.5, ease: "power1.inOut" })
  .to(cameraAnim, { x: -6, y: -1, z: 8,  period: 1,   ease: "cinematicSmooth" })

tl.to(cylinder.rotation, { y: "+=28.27", period: 8.5, ease: "none" }, 0)

Render loop:

const animate = () => {
  requestAnimationFrame(animate)

  digital camera.place.set(cameraAnim.x, cameraAnim.y, cameraAnim.z)
  digital camera.lookAt([0, 0, 0])

  const vel = cylinder.rotation.y - lastRotation
  lastRotation = cylinder.rotation.y

  renderer.render({ scene, digital camera })
}
animate()

5. Typographic overlays

Every title part fades out and in in sync with the scroll, dividing the journey into visible chapters.

views.forEach((perspective, i) => {
  const textEl = textRefs.present[i]
  if (!textEl) return

  const part = 100 / views.size
  const begin   = `${i * part}% prime`
  const finish     = `${(i + 1) * part}% prime`

  gsap.timeline({
    scrollTrigger: {
      set off: containerRef.present,
      begin, finish,
      scrub: 0.8
    }
  })
  .fromTo(textEl, { opacity: 0 }, { opacity: 1, period: 0.2, ease: "cinematicSmooth" })
  .to(textEl,      { opacity: 1, period: 0.6, ease: "none" })
  .to(textEl,      { opacity: 0, period: 0.2, ease: "cinematicSmooth" })
})

6. Particles with rotational inertia

To intensify movement, we’ll add refined line-based particles orbiting the cylinder.
Their opacity will increase when the cylinder spins and fades because it slows down.

for (let i = 0; i < particleConfig.numParticles; i++) {
  const { geometry, userData } = createParticleGeometry(gl, particleConfig, i, cylinderConfig.top)

  const program = new Program(gl, {
    vertex:   particleVertex,
    fragment: particleFragment,
    uniforms: { uColor: { worth: [0,0,0] }, uOpacity: { worth: 0.0 } },
    clear: true,
    depthTest:   true
  })

  const particle = new Mesh(gl, { geometry, program, mode: gl.LINE_STRIP })
  particle.userData = userData
  particle.setParent(scene)
  particles.push(particle)
}

Contained in the render loop:

const inertiaFactor = 0.15
const decayFactor   = 0.92
momentum = momentum * decayFactor + vel * inertiaFactor

const isRotating = Math.abs(vel) > 0.0001
const velocity      = Math.abs(vel) * 100

particles.forEach(p => {
  const goal = isRotating ? Math.min(velocity * 3, 0.95) : 0
  p.program.uniforms.uOpacity.worth += (goal - p.program.uniforms.uOpacity.worth) * 0.15

  const rotationOffset = vel * p.userData.velocity * 1.5
  p.userData.baseAngle += rotationOffset

  const positions = p.geometry.attributes.place.information as Float32Array
  for (let j = 0; j <= particleConfig.segments; j++) {
    const t = j / particleConfig.segments
    const angle = p.userData.baseAngle + p.userData.angleSpan * t
    positions[j*3 + 0] = Math.cos(angle) * p.userData.radius
    positions[j*3 + 1] = p.userData.baseY
    positions[j*3 + 2] = Math.sin(angle) * p.userData.radius
  }
  p.geometry.attributes.place.needsUpdate = true
})

Particle shaders

Every particle line is outlined by a vertex shader that positions factors alongside an arc and a fraction shader that controls shade and opacity.

// particleVertex.glsl
attribute vec3 place;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

void most important() {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
}
// particleFragment.glsl
precision highp float;

uniform vec3 uColor;
uniform float uOpacity;

void most important() {
  gl_FragColor = vec4(uColor, uOpacity);
}

Scene Path: Scroll-Managed Storytelling in Three.js

1. GSAP setup

Register the plugins as soon as on the shopper. We’ll use ScrollTrigger, ScrollSmoother, and SplitText to orchestrate digital camera strikes and textual content beats.

import gsap from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"
import { ScrollSmoother } from "gsap/ScrollSmoother"
import { SplitText } from "gsap/SplitText"

if (typeof window !== "undefined") {
  gsap.registerPlugin(ScrollTrigger, ScrollSmoother, SplitText)
}

2. Web page format + ScrollSmoother

We preserve the 3D canvas fastened behind, overlay UI on prime (scroll trace + progress), and wrap the lengthy content material space with #smooth-wrapper / #smooth-content to allow smoothing.

...

{/* Left scroll trace and backside progress bar overlays right here */}

Activate smoothing:

ScrollSmoother.create({
  wrapper:  smoothWrapperRef.present!,
  content material:  smoothContentRef.present!,
  {smooth}:   4,
  results:  false,
  smoothTouch: 2,
})

3. The 3D scene (R3F + drei)

We mount a PerspectiveCamera that we are able to replace each body, add fog for depth, and lightweight the constructing. The constructing mannequin is loaded with useGLTF and calmly remodeled.

perform CyberpunkBuilding() {
  const { scene } = useGLTF("/cyberpunk_skyscraper.glb")
  useEffect(() => {
    if (scene) {
      scene.scale.set(3, 3, 3)
      scene.place.set(0, 0, 0)
    }
  }, [scene])
  return 
}

perform AnimatedCamera({ cameraAnimRef, targetAnimRef }: any) {
  const cameraRef = useRef(null)
  const { set } = useThree()
  useEffect(() => {
    if (cameraRef.present) set({ digital camera: cameraRef.present })
  }, [set])
  useFrame(() => {
    if (cameraRef.present) {
      cameraRef.present.place.set(
        cameraAnimRef.present.x,
        cameraAnimRef.present.y,
        cameraAnimRef.present.z
      )
      cameraRef.present.lookAt(
        targetAnimRef.present.x,
        targetAnimRef.present.y,
        targetAnimRef.present.z
      )
    }
  })
  return 
}

function Scene({ cameraAnimRef, targetAnimRef }: any) {
  const { scene } = useThree()
  useEffect(() => {
    if (scene) {
      const fogColor = new THREE.Color("#0a0a0a")
      scene.fog = new THREE.Fog(fogColor, 12, 28)
      scene.background = new THREE.Color("#0a0a0a")
    }
  }, [scene])
  return (
    <>
      
      
      
      
      
      
    >
  )
}

As the scene takes shape, the lighting and scale help establish depth, but what truly brings it to life is motion. The next step is to connect the scroll to the camera itself — transforming simple input into cinematic direction.

4. Camera timeline driven by scroll

We keep two mutable refs: cameraAnimRef (camera position) and targetAnimRef (look-at). A single timeline maps scene segments (from a scenePerspectives config) to scroll progress.

const cameraAnimRef = useRef({ x: -20, y: 0,  z: 0 })
const targetAnimRef = useRef({ x:   0, y: 15, z: 0 })

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: containerRef.current,
    start: "top top",
    end:   "bottom bottom",
    scrub: true,
    onUpdate: (self) => {
      const progress = self.progress * 100
      setProgressWidth(progress)      // quickSetter for width %
      setProgressText(String(Math.round(progress)).padStart(3, "0") + "%")
    }
  }
})

scenePerspectives.forEach((p) => {
  const start = p.scrollProgress.start / 100
  const end   = p.scrollProgress.end   / 100
  tl.to(cameraAnimRef.current, { x: p.camera.x, y: p.camera.y, z: p.camera.z, duration: end - start, ease: "none" }, start)
  tl.to(targetAnimRef.current, { x: p.target.x, y: p.target.y, z: p.target.z, duration: end - start, ease: "none" }, start)
})

5. SplitText chapter cues

For each perspective, we place a text block in a screen position derived from its config and animate chars in/out with small staggers.

scenePerspectives.forEach((p, index) => {
  const textEl = textRefs.current[index]
  if (!textEl) return
  if (p.hideText) { gsap.set(textEl, { opacity: 0, pointerEvents: "none" }); return }

  const titleEl = textEl.querySelector("h2")
  const subtitleEl = textEl.querySelector("p")
  if (titleEl && subtitleEl) {
    const titleSplit = new SplitText(titleEl, { kind: "chars" })
    const subtitleSplit = new SplitText(subtitleEl, { kind: "chars" })
    splitInstancesRef.present.push(titleSplit, subtitleSplit)

    const textTl = gsap.timeline({
      scrollTrigger: {
        set off: containerRef.present,
        begin: `${p.scrollProgress.begin}% prime`,
        finish:   `${p.scrollProgress.finish}% prime`,
        scrub: 0.5,
      }
    })

    const isFirst = index === 0
    const isLast  = index === scenePerspectives.size - 1

    if (isFirst) {
      gsap.set([titleSplit.chars, subtitleSplit.chars], { x: 0, opacity: 1 })
      textTl.to([titleSplit.chars, subtitleSplit.chars], {
        x: 100, opacity: 0, period: 1, stagger: 0.02, ease: "power2.in"
      })
    } else {
      textTl
        .fromTo([titleSplit.chars, subtitleSplit.chars],
          { x: -100, opacity: 0 },
          {
            x: 0, opacity: 1,
            period: isLast ? 0.2 : 0.25,
            stagger:  isLast ? 0.01 : 0.02,
            ease: "power2.out"
          }
        )
        .to({}, { period: isLast ? 1.0 : 0.5 })
        .to([titleSplit.chars, subtitleSplit.chars], {
          x: 100, opacity: 0, period: 0.25, stagger: 0.02, ease: "power2.in"
        })
    }
  }
})

6. Overlay UI: scroll trace + progress

A minimal scroll trace on the left and a centered progress bar on the backside. We use gsap.quickSetter to replace width and label effectively from ScrollTrigger’s onUpdate.

const setProgressWidth = gsap.quickSetter(progressBarRef.present, "width", "%")
const setProgressText  = gsap.quickSetter(progressTextRef.present, "textContent")

// ... used inside ScrollTrigger's onUpdate() above

Conclusion

That’s it for this tutorial. You’ve seen how scroll movement can form a scene, how timing and easing can recommend rhythm, and the way digital camera motion can flip a static format into one thing that feels intentional and cinematic. With GSAP, all of it stays versatile and fluid — each movement turns into simpler to regulate and refine.

The strategies listed here are simply a place to begin. Strive shifting the main target, slowing issues down, or exaggerating transitions to see the way it modifications the temper. Deal with the scroll as a director’s cue, guiding the viewer’s consideration by means of area, mild, and movement.

In the long run, what makes these experiences participating isn’t the complexity of the code, however the sense of circulation you create. Maintain experimenting, keep curious, and let your subsequent venture inform its story one scroll at a time.

Tags: BuildcinematicexperiencesGSAPScroll
Admin

Admin

Next Post
9 Finest In-Recreation Radio Stations And Music Gamers

9 Finest In-Recreation Radio Stations And Music Gamers

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

Rockstar’s GTA VI poised to change into the most important gaming launch of all time

Rockstar’s GTA VI poised to change into the most important gaming launch of all time

July 19, 2025
Finest Legendary Weapon For Every Nightfarer

Finest Legendary Weapon For Every Nightfarer

June 24, 2025

Trending.

How you can open the Antechamber and all lever places in Blue Prince

How you can open the Antechamber and all lever places in Blue Prince

April 14, 2025
The most effective methods to take notes for Blue Prince, from Blue Prince followers

The most effective methods to take notes for Blue Prince, from Blue Prince followers

April 20, 2025
Exporting a Material Simulation from Blender to an Interactive Three.js Scene

Exporting a Material Simulation from Blender to an Interactive Three.js Scene

August 20, 2025
AI Girlfriend Chatbots With No Filter: 9 Unfiltered Digital Companions

AI Girlfriend Chatbots With No Filter: 9 Unfiltered Digital Companions

May 18, 2025
Constructing a Actual-Time Dithering Shader

Constructing a Actual-Time Dithering Shader

June 4, 2025

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

Easy and painless productiveness | Seth’s Weblog

Take heed to your self | Seth’s Weblog

January 10, 2026
Complete Wi-fi Promo Codes & Offers: 50% Off Choose Plans

Complete Wi-fi Promo Codes & Offers: 50% Off Choose Plans

January 10, 2026
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved