Animation
ASCII Footer — Scroll Reveal + Mouse Parallax
GSAPLenisViteVanilla JS
Previews





About this build
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.
Prompt
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
