A fixed-position footer sits behind a content section and is revealed as the user scrolls. GSAP ScrollTrigger drives three simultaneous animations: the two ASCII-rasterized hand images slide in from opposite sides of the viewport, the nav links and description paragraph lines slide upward, and the full name “Ebraheem Hetari” reveals character-by-character from opposite ends of each word. ADocumentation Index
Fetch the complete documentation index at: https://mintlify.com/hetari/creative-coding/llms.txt
Use this file to discover all available pages before exploring further.
useRafFn loop handles mouse parallax drift on the ASCII canvases every frame. The heading always spans the full viewport width at any breakpoint, achieved by measuring the text at a fixed 100 px reference size and scaling up to fill the available container width minus a responsive gap.
Registry Entry
This sketch is registered insrc/config/sketches.ts as:
Tech Stack
| Layer | Tool |
|---|---|
| ASCII rendering | useAsciiRasterizer composable |
| GSAP / ScrollTrigger | useGsap composable (gsap, useGsapContext) |
| Render loop | VueUse useRafFn |
| Responsive sizing | VueUse useBreakpoints (Tailwind breakpoints) |
| Resize observation | VueUse useResizeObserver |
| Event binding | VueUse useEventListener |
| Dark mode | VueUse useDark |
| Smooth scroll | LenisScroll component |
Key Constants
All tuning values are declared at the top of the component as module-level constants:| Constant | Purpose |
|---|---|
ASCII_CHARS | Character ramp from lightest (.) to darkest (9) used when mapping luminance to glyphs |
COLUMNS | Number of ASCII columns per canvas — determines horizontal resolution |
CELL_SIZE | Pixel size of each cell in the rasterizer grid |
FONT_SIZE | Font size (px) of each ASCII character drawn to the canvas |
PARALLAX_STRENGTH | Maximum pixel offset for parallax drift (doubled for ± range) |
PARALLAX_EASE | Lerp factor applied each frame — lower values = more lag / smoother drift |
HOVER_RADIUS | Radius in cells that a mouse hover highlights around the cursor |
HIGHLIGHT_LIFETIME | How long (ms) a hover-highlighted cell stays lit after the cursor leaves |
CLUSTER_SIZE | Number of cells grouped into a single highlight cluster |
Responsive Gap
VueUseuseBreakpoints with Tailwind breakpoints computes the horizontal gap between the two heading words. The gap widens on larger screens so the name is always letter-spaced across the full viewport:
Dynamic Font Sizing
updateFittedFontSizes() measures both heading words at a fixed reference size of 100px using off-screen <span> elements, then scales up to fill the available container width minus gapPx:
resize, useResizeObserver on both headingContainerRef and rootRef, and whenever gapPx changes.
Scroll-Triggered Animations
All three animation groups share the samescrollTrigger config: trigger: triggerRef, start: 'top 80%', toggleActions: 'play reverse play reverse'. They are set up inside useGsapContext(rootRef, ...) so all tweens are scoped to the component.
1 — ASCII Curtain Reveal
Thecurtain.offset reactive value starts at 125 (percentage). GSAP tweens it to 0 on scroll. The useRafFn loop reads this offset every frame and applies it as translateX to each canvas wrapper, so the hands slide in from ±125% simultaneously:
2 — Nav Links & Description Paragraph
All elements tagged[data-af-line] (nav link anchors and the paragraph) start with yPercent: 100 (clipped below their overflow-hidden parent). They slide up to yPercent: 0 with a stagger of 0.08s:
3 — Per-Character Heading Reveal
Each<h2> word is split into individual <span data-af-char> characters. Word 1 (“Ebraheem”) reveals from the last character inward (from: 'end'); Word 2 (“Hetari”) reveals from the first character outward (from: 'start'). Both run simultaneously at the same scroll trigger:
Parallax & Render Loop
Mouse position is normalised within the footer bounds, then each canvas wrapper is transformed every frame insideuseRafFn:
scale factor is derived from PARALLAX_STRENGTH to prevent the canvas edges from becoming visible when the parallax offsets are at their maximum extent.
Dark Mode
useDark() drives the character colours passed to both useAsciiRasterizer instances:
computed refs, changing the system colour scheme re-renders the ASCII canvases automatically on the next animation frame.
The ASCII canvases are rendered manually via
useRafFn(() => render()) rather than using an autoLoop: true option inside useAsciiRasterizer. This gives the sketch precise control over render-loop timing: the parallax drift lerp runs in the same callback as the canvas draw, so motion and ASCII rendering are always in sync with no extra frame of lag.