← Library

Animation

ASCII Footer — Scroll Reveal + Mouse Parallax

GSAPLenisViteVanilla JS

Animated footer with ASCII art hands rendered on canvas, mouse parallax, and GSAP scroll reveal. Hands slide in from the sides, heading chars rise from below (SplitText), nav links appear line by line. Hover over the hands to light up random orange character clusters.

Build an animated footer using Vanilla JS, Vite, GSAP (ScrollTrigger + SplitText) and Lenis.                                                         
                                                                                                                                                          
  ## Setup                                                                                                                                                
  const lenis = new Lenis();                                                                                                                              
  lenis.on("scroll", ScrollTrigger.update);                                                                                                               
  gsap.ticker.add((time) => lenis.raf(time * 1000));                                                                                                      
  gsap.ticker.lagSmoothing(0);                                                                                                                            
                                                                                                                                                          
  ## HTML Structure                                                                                                                                       
  3 full-height sections (.one, .two, .three) above a .footer-revealer div, then a <footer> containing:                                                   
  - .footer-images: two .footer-hand-img divs, each with an <img class="ascii-hand"> (hand-left.jpg / hand-right.jpg)                                     
  - .footer-content: nav.footer-links (4 links) + .footer-text > p                                                                                        
  - .footer-header: two <h1> tags ("Blank" / "Canvas")                                                                                                    
                                                                                                                                                          
  ## ASCII Art Hands (Canvas)                                                                                                                             
  Constants: ASCII_CHARS="........:::=+xX#0369", FONT_SIZE=18, CELL_SIZE=20, ASCII_COLUMNS=80, DPR=2                                                      
  Colors: CHAR_COLOR="#803500", HOVER_COLOR="#ff6a00", HOVER_CHAR_COLOR="#0f0f0f"                                                                         
  HOVER_RADIUS=8, CLUSTER_SIZE=10, HIGHLIGHT_LIFETIME=300                                                                                                 
                                                                                                                                                          
  For each .ascii-hand image:                                                                                                                             
  - Create a canvas, append to .footer-hand-img                                                                                                           
  - Sample image at ASCII_COLUMNS × rows via offscreen canvas (rows = round(ASCII_COLUMNS / aspectRatio))                                                 
  - Map brightness to char index: Math.floor((1 - brightness) * ASCII_CHARS.length)                                                                       
  - Skip cells at or below background threshold (lastIndexOf("."))                                                                                        
  - Store cells in Map<"col,row", {col,row,char,highlightEndTime}>                                                                                        
  - RAF render loop: for each cell, if highlightEndTime > Date.now() fill HOVER_COLOR rect then draw HOVER_CHAR_COLOR char, else draw CHAR_COLOR char     
  - On mousemove: find nearest cell within HOVER_RADIUS (euclidean), then highlightCluster: walk CLUSTER_SIZE random neighbours, each gets                
  highlightEndTime = now + HIGHLIGHT_LIFETIME + step*10                                                                                                   
                                                                                                                                                          
  ## Mouse Parallax                                                                                                                                       
  PARALLAX_STRENGTH=20, PARALLAX_EASE=0.05
  - Scale both wrappers by (1 + PARALLAX_STRENGTH*2/200) to hide overflow                                                                                 
  - Lerp drift {x,y} toward pointer position relative to footer center (pointer = ((clientX - rect.left)/rect.width - 0.5) * PARALLAX_STRENGTH*2)         
  - Left hand: translate(+drift.x, -drift.y), right hand: translate(-drift.x, -drift.y)                                                                   
  - Loop via requestAnimationFrame                                                                                                                        
                                                                                                                                                          
  ## Scroll Reveal                                                                                                                                        
  SplitText heading chars: type="chars", charsClass="char" → gsap.set(chars, {position:"relative", yPercent:125})                                         
  SplitText content lines: type="lines", mask="lines", linesClass="line" → gsap.set(lines, {yPercent:100})                                                
  Reveal state: {left:-125, right:125} (% translateX applied to hand wrappers)                                                                            
                                                                                                                                                          
  animateIn: gsap.to(reveal, {left:0, right:0, duration:1, ease:"power3.out"}) + chars yPercent→0 stagger {each:0.04, from:"center"} + lines yPercent→0   
  stagger 0.08                                                                                                                                            
  animateOut: same targets back to initial values, duration:0.4, ease:"power2.in", stagger each:0.01 / 0.02                                               
                                                                                                                                                          
  ScrollTrigger: trigger=".footer-revealer", start="top 50%", onEnter=animateIn                                                                           
  ScrollTrigger: trigger=".footer-revealer", start="top 85%", onLeaveBack=animateOut