# Advanced Animation Patterns Three.js integration, WebGL, Canvas effects, and advanced SVG animations. ## Table of Contents 1. [Three.js + GSAP](#threejs--gsap) 2. [WebGL Shaders](#webgl-shaders) 3. [Canvas Effects](#canvas-effects) 4. [Image Sequences](#image-sequences) 5. [SVG Advanced](#svg-advanced) 6. [View Transitions API](#view-transitions-api) ## Three.js + GSAP ### Setup ```bash npm install three @types/three @react-three/fiber @react-three/drei ``` ### Basic Integration ```tsx 'use client' import { Canvas, useFrame, useThree } from '@react-three/fiber' import { useRef, useEffect } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' import * as THREE from 'three' gsap.registerPlugin(ScrollTrigger) function AnimatedMesh() { const meshRef = useRef(null) useEffect(() => { if (!meshRef.current) return // GSAP can animate Three.js objects directly gsap.to(meshRef.current.rotation, { x: Math.PI * 2, y: Math.PI * 2, scrollTrigger: { trigger: '#canvas-container', start: 'top top', end: 'bottom bottom', scrub: 1, } }) gsap.to(meshRef.current.position, { z: 2, scrollTrigger: { trigger: '#canvas-container', start: 'top top', end: 'bottom bottom', scrub: 1, } }) }, []) return ( ) } export function ThreeScene() { return (
) } ``` ### Material Animation ```tsx function AnimatedMaterial() { const materialRef = useRef(null) useEffect(() => { if (!materialRef.current) return gsap.to(materialRef.current, { opacity: 0.5, metalness: 1, roughness: 0, scrollTrigger: { trigger: '#scene', start: 'top top', end: 'bottom bottom', scrub: 1, } }) }, []) return ( ) } ``` ### Camera Animation ```tsx function CameraRig() { const { camera } = useThree() useEffect(() => { gsap.to(camera.position, { x: 5, y: 2, z: 3, scrollTrigger: { trigger: '#scene', start: 'top top', end: 'bottom bottom', scrub: 1, onUpdate: () => camera.lookAt(0, 0, 0) } }) }, [camera]) return null } ``` ### Scroll-Linked Animation with useFrame ```tsx function ScrollLinkedMesh() { const meshRef = useRef(null) const scrollProgress = useRef(0) useEffect(() => { ScrollTrigger.create({ trigger: '#canvas-container', start: 'top top', end: 'bottom bottom', onUpdate: (self) => { scrollProgress.current = self.progress } }) }, []) useFrame(() => { if (meshRef.current) { meshRef.current.rotation.y = scrollProgress.current * Math.PI * 2 meshRef.current.position.y = Math.sin(scrollProgress.current * Math.PI) * 2 } }) return ( ) } ``` ## WebGL Shaders ### Custom Shader Material with GSAP ```tsx 'use client' import { useRef, useEffect } from 'react' import { Canvas, useFrame } from '@react-three/fiber' import * as THREE from 'three' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' const vertexShader = ` varying vec2 vUv; uniform float uTime; uniform float uProgress; void main() { vUv = uv; vec3 pos = position; pos.z += sin(pos.x * 10.0 + uTime) * 0.1 * uProgress; gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); } ` const fragmentShader = ` varying vec2 vUv; uniform float uProgress; void main() { vec3 color = mix(vec3(0.0), vec3(1.0, 0.5, 0.0), uProgress); gl_FragColor = vec4(color, 1.0); } ` function ShaderPlane() { const materialRef = useRef(null) const uniforms = useRef({ uTime: { value: 0 }, uProgress: { value: 0 } }) useEffect(() => { gsap.to(uniforms.current.uProgress, { value: 1, scrollTrigger: { trigger: '#shader-scene', start: 'top top', end: 'bottom bottom', scrub: 1, } }) }, []) useFrame(({ clock }) => { if (materialRef.current) { materialRef.current.uniforms.uTime.value = clock.getElapsedTime() } }) return ( ) } ``` ### Image Distortion Shader ```tsx const distortionFragment = ` uniform sampler2D uTexture; uniform float uProgress; uniform float uTime; varying vec2 vUv; void main() { vec2 uv = vUv; // Distortion based on progress float distortion = sin(uv.y * 10.0 + uTime) * 0.1 * uProgress; uv.x += distortion; vec4 color = texture2D(uTexture, uv); gl_FragColor = color; } ` function DistortedImage({ src }: { src: string }) { const materialRef = useRef(null) const texture = useLoader(TextureLoader, src) const uniforms = useRef({ uTexture: { value: texture }, uProgress: { value: 0 }, uTime: { value: 0 } }) useEffect(() => { gsap.to(uniforms.current.uProgress, { value: 1, duration: 1, ease: 'power2.out' }) }, []) useFrame(({ clock }) => { if (materialRef.current) { materialRef.current.uniforms.uTime.value = clock.getElapsedTime() } }) return ( ) } ``` ## Canvas Effects ### Particle System on Scroll ```tsx 'use client' import { useRef, useEffect } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger) interface Particle { x: number y: number vx: number vy: number size: number } export function ParticleCanvas() { const canvasRef = useRef(null) const particles = useRef([]) const scrollProgress = useRef(0) useEffect(() => { const canvas = canvasRef.current! const ctx = canvas.getContext('2d')! // Setup canvas const resize = () => { canvas.width = window.innerWidth canvas.height = window.innerHeight } resize() window.addEventListener('resize', resize) // Create particles for (let i = 0; i < 100; i++) { particles.current.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, size: Math.random() * 3 + 1 }) } // ScrollTrigger ScrollTrigger.create({ trigger: '#particle-section', start: 'top top', end: 'bottom bottom', onUpdate: (self) => { scrollProgress.current = self.progress } }) // Animation loop const animate = () => { ctx.fillStyle = 'rgba(0, 0, 0, 0.1)' ctx.fillRect(0, 0, canvas.width, canvas.height) particles.current.forEach(p => { // Move particles faster based on scroll p.x += p.vx * (1 + scrollProgress.current * 5) p.y += p.vy * (1 + scrollProgress.current * 5) // Wrap around if (p.x < 0) p.x = canvas.width if (p.x > canvas.width) p.x = 0 if (p.y < 0) p.y = canvas.height if (p.y > canvas.height) p.y = 0 // Draw ctx.fillStyle = `rgba(255, 255, 255, ${0.5 + scrollProgress.current * 0.5})` ctx.beginPath() ctx.arc(p.x, p.y, p.size * (1 + scrollProgress.current), 0, Math.PI * 2) ctx.fill() }) requestAnimationFrame(animate) } animate() return () => window.removeEventListener('resize', resize) }, []) return (
) } ``` ## Image Sequences ### Scroll-Driven Image Sequence ```tsx 'use client' import { useRef, useEffect, useState } from 'react' import gsap from 'gsap' import { ScrollTrigger } from 'gsap/ScrollTrigger' gsap.registerPlugin(ScrollTrigger) export function ImageSequence({ frameCount = 120, basePath }: { frameCount?: number basePath: string }) { const canvasRef = useRef(null) const containerRef = useRef(null) const images = useRef([]) const [loaded, setLoaded] = useState(false) useEffect(() => { // Preload images let loadedCount = 0 for (let i = 0; i < frameCount; i++) { const img = new Image() img.src = `${basePath}/frame_${i.toString().padStart(4, '0')}.jpg` img.onload = () => { loadedCount++ if (loadedCount === frameCount) setLoaded(true) } images.current.push(img) } }, [frameCount, basePath]) useEffect(() => { if (!loaded) return const canvas = canvasRef.current! const ctx = canvas.getContext('2d')! const container = containerRef.current! // Set canvas size canvas.width = images.current[0].width canvas.height = images.current[0].height // Draw first frame ctx.drawImage(images.current[0], 0, 0) // Animate frame const frameObj = { frame: 0 } gsap.to(frameObj, { frame: frameCount - 1, snap: 'frame', ease: 'none', scrollTrigger: { trigger: container, start: 'top top', end: 'bottom bottom', scrub: 0.5, pin: true, }, onUpdate: () => { ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.drawImage(images.current[Math.round(frameObj.frame)], 0, 0) } }) }, [loaded, frameCount]) return (
{!loaded &&
Loading frames...
}
) } ``` ## SVG Advanced ### Path Morphing with Anime.js ```tsx 'use client' import { useRef, useEffect } from 'react' import anime from 'animejs' export function MorphingSVG() { const pathRef = useRef(null) useEffect(() => { anime({ targets: pathRef.current, d: [ { value: 'M50,10 L90,90 L10,90 Z' }, // Triangle { value: 'M50,10 A40,40 0 1,1 50,90 A40,40 0 1,1 50,10' }, // Circle { value: 'M10,10 L90,10 L90,90 L10,90 Z' }, // Square ], duration: 3000, easing: 'easeInOutQuad', loop: true, direction: 'alternate', }) }, []) return ( ) } ``` ### Motion Path Animation ```tsx import { MotionPathPlugin } from 'gsap/MotionPathPlugin' gsap.registerPlugin(MotionPathPlugin) function MotionPathAnimation() { const ballRef = useRef(null) useGSAP(() => { gsap.to(ballRef.current, { motionPath: { path: '#motion-path', align: '#motion-path', alignOrigin: [0.5, 0.5], autoRotate: true, }, duration: 5, ease: 'none', scrollTrigger: { trigger: '#motion-container', start: 'top center', end: 'bottom center', scrub: 1, } }) }) return (
) } ``` ## View Transitions API ### Native Page Transitions (Chrome) ```tsx // For simple transitions without libraries 'use client' import { useRouter } from 'next/navigation' export function TransitionLink({ href, children }: { href: string children: React.ReactNode }) { const router = useRouter() const handleClick = async (e: React.MouseEvent) => { e.preventDefault() if (!document.startViewTransition) { router.push(href) return } document.startViewTransition(() => { router.push(href) }) } return ( {children} ) } ``` ```css /* View Transition CSS */ ::view-transition-old(root), ::view-transition-new(root) { animation-duration: 0.5s; } ::view-transition-old(root) { animation: fade-out 0.5s ease-out; } ::view-transition-new(root) { animation: fade-in 0.5s ease-in; } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } ``` ### Named View Transitions ```tsx // Give elements view-transition-name for morphing
``` ```css ::view-transition-old(hero-image), ::view-transition-new(hero-image) { animation-duration: 0.3s; } ```