# Reactive Face Mesh

> Pull any part of a portrait out like taffy and the face reacts: eyes follow your hand, widen in surprise, blink as it snaps back.

Canonical: https://gsapvault.com/effects/reactive-face-mesh
Live demo: https://gsapvault.com/demos/reactive-face-mesh/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £10 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, mesh-distortion, elastic, drag-interaction, pointer-tracking, morphing, generated-photography, touch-gestures |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 9 October 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

A portrait that notices you. Grab any part of the face and pull: the skin stretches out beyond the photograph in a tapering cone that stays glued to the pointer, and snaps back with an elastic twang when you let go.

The face reacts the whole time. Her eyes follow the cursor around the page, glance at your hand as you start to pull, and widen in surprise while staring at it as the stretch grows; her mouth drops open, then grimaces and blinks as the skin snaps home. At rest she breathes, blinks and makes the small eye movements a real face never stops making.

## Features

- Taffy stretch: any point of the photograph can be pulled far beyond its frame, with the grabbed skin fixed under the pointer and a tapering cone behind it
- Elastic snap-back with overshoot, and a stretch that can be caught again mid-spring
- Eyes that follow the cursor in nine directions, drifting smoothly near the face and jumping like real saccades further out
- Surprise that looks where you are pulling: wide eyes aimed at the stretched tip, an open mouth, a grimace and blink on release
- Optical-flow morphing between expression frames, so lids, brows and lips travel through every in-between instead of cross-fading
- Idle life: blinks, fixation jitter, a slight head turn towards the pointer and slow breathing, each switchable off
- Works with fewer frames: any expression not supplied falls back to its nearest relative, and missing motion maps fall back to a cross-fade
- One WebGL draw per frame, rendered only while on screen, with a 2D canvas fallback and the plain photograph as the no-JS state

## Use Cases

- Playful About pages and team portraits that reward a visitor for touching them
- Creative studio and agency heroes where the portrait is the joke
- Campaign microsites, character mascots and interactive editorial covers
- Portfolio pieces that need one memorable, tactile interaction

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

The portrait is drawn as a WebGL triangle mesh. Each pull drags the skin under the pointer with it, the surrounding skin following less and less with distance, so a long pull tapers into a cone that lies over the face it passes; on release GSAP springs it home with an elastic overshoot.

Expressions are photographs of the same face that differ only inside an eye band or a mouth band, shipped as just those bands. For each one a small motion map records how every pixel moves from the neutral face. The fragment shader warps the neutral face part of the way along that map while pulling the expression back the rest of the way, so changes morph rather than dissolve; below the halfway point it is a pure warp, which is how the eyes drift after the cursor without ever showing two images. GSAP times every expression change, the reactions and the idle behaviour; the render loop runs on GSAP's ticker and only while the portrait is visible.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

_Code snippet omitted: it ships with the download._

**2. Add the portrait anywhere in your `<body>`:**

_Code snippet omitted: it ships with the download._

The demo's `index.html` lists every expression the effect knows (21 of them); copy its attributes for the full set.

**3. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

### Using It With Your Own Design

**What the effect needs from your markup:** an element with `data-face-pull` (the stage) containing one `img.face-pull__img`. The image's on-screen box is the photograph; the script lays a WebGL canvas over the whole stage, so a stretched feature can travel beyond the photo onto the stage around it. Expression frames and their motion maps are URLs in `data-face-*` attributes on the stage. Every instance on a page is independent.

**What is only the demo's styling:** the full-viewport stage, the grey ground, the "Grab her face and pull" pill (`.face-cue`) and the Mona Sans font. Size the stage to whatever space you want the stretch to be able to reach; the photo can be any size inside it.

**CSS the effect depends on:**

- The stage must be a positioning context (`position: relative`); the canvas is `position: absolute; inset: 0; pointer-events: none`.
- `.is-live .face-pull__img { opacity: 0; touch-action: none; }` hides the photo once the canvas has drawn it, while keeping it in place to catch touches. Until then (no JavaScript, a blocked CDN, no WebGL and no canvas) the plain photo shows.
- Keep the image's aspect ratio: size it with `max-width` / `max-height` rather than forcing both dimensions.

#### Using your own portrait

The reactions are photographs of the same face, so a new face needs its own frames:

1. **A neutral portrait.** Head-and-shoulders, straight on, eyes and brows clear of hair, plain background.
2. **Expression variants**, made by editing only the eyes and brows (or only the mouth) of that exact photo, for example in an image editor or an AI image-editing tool. Paste each edit back onto the neutral photo through a soft-edged mask so that **everything outside the band is pixel-identical**. Any expression you skip falls back to its nearest relative (`look-up-left` to `look-left`, `wide-right` to `wide`) or is left out.
3. **Bands and motion maps.** Run the included tool:

_Code snippet omitted: it ships with the download._

   It writes each expression as just its band plus a lossless `flow-*.webp` motion map. Use the same rectangles in `data-face-band` and `data-face-mouth-band`, and set `data-face-eyes` to the point between the pupils. Without motion maps the frames still work, but cross-fade instead of morphing.

Expression names: `look-<dir>` and `wide-<dir>` for each of `left`, `right`, `up`, `down`, `up-left`, `up-right`, `down-left`, `down-right`; plus `wide`, `wince`, `blink`, `mouth-oh` and `mouth-grimace`.

### Options

Set these on the `data-face-pull` element.

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-face-<name>` | URL | none | An expression frame: its band, or the whole photo |
| `data-face-<name>-flow` | URL | none | That frame's motion map from `tools/make-frames.py` |
| `data-face-band` | `x y w h` | whole photo | Eye/brow band in source-image pixels |
| `data-face-mouth-band` | `x y w h` | none | Mouth band in source-image pixels |
| `data-face-eyes` | `x y` | `50% 40%` of the photo | Point between the pupils, in source-image pixels; gaze is aimed from here |
| `data-face-radius` | `0.02` to `0.3` | `0.07` | Grab radius as a fraction of the photo width |
| `data-face-reach` | `0.1` to `1` | `0.6` | Furthest pull as a fraction of the stage width |
| `data-face-cell` | pixels | `10` | Mesh cell size; smaller is smoother and heavier |
| `data-face-turn` | pixels | `5` | How far the head turns towards the pointer |
| `data-face-intro` | `true`, `false` | `true` | One automatic cheek tug after load |
| `data-face-idle` | `true`, `false` | `true` | Blinks, eye drift and breathing at rest |
| `data-face-renderer` | `webgl`, `2d` | `webgl` | Force the 2D canvas fallback (slower; no morphing or idle motion) |

### Accessibility

- **Keyboard**: the stage is focusable; Left and Right arrows tug a cheek, Enter or Space tugs a random side. Give the stage a visible focus style.
- **Screen readers**: the canvas is `aria-hidden`; the stage's `aria-label` and the image's `alt` text describe it.
- **Touch**: a drag that starts on the face pulls it; elsewhere the page scrolls normally.
- **No JavaScript or no WebGL**: the plain photograph is shown (the 2D canvas fallback still allows pulling where WebGL is missing).

#### Reduced Motion Behavior

When `prefers-reduced-motion: reduce` is set:

- No intro tug, head turn, eye drift or breathing
- Released features return with a short ease instead of an elastic overshoot
- Pulling and the face's reactions still work, more gently

### Dependencies

**Required:**
- GSAP 3.12+ (the demo is pinned to 3.15.0). GSAP times every expression change, the reactions, the elastic snap-back and the idle behaviour, and runs the render loop on its ticker. The stretch and the morphing are drawn by WebGL.
- A browser with WebGL 1 for the full effect

**For your own portrait only:**
- Python 3 with `opencv-python-headless` and `numpy`, to run `tools/make-frames.py`

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £10 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Reactive Face Mesh](https://gsapvault.com/effects/reactive-face-mesh)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Hover Underline](https://gsapvault.com/effects/hover-underline.md): Four material link underlines (an exit-through line, marker sweep, hand-drawn wave, and an endlessly travelling wave) with coordinated type and active-index responses.
- [3D Card Flip Gallery](https://gsapvault.com/effects/3d-card-flip.md): Tactile GSAP 3D cards with deep perspective, reactive edge lighting and shifting shadows. Flip on hover, keyboard focus or tap, with grouped auto-close.
- [Tailwind Component Remixer](https://gsapvault.com/effects/tailwind-class-playground.md): Generate fresh Tailwind component recipes with procedural SVG artwork, coordinated colour palettes and animated GSAP layout transitions.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
