A canvas that fills its box with small OpenPeeps characters walking across the bottom on a continuous loop. Hover anywhere to raise a magnifier lens and zoom in on the crowd.
The crowd canvas fills its container with small OpenPeeps characters that walk across the bottom of the box on a continuous loop. Each character is one frame sampled from a sprite sheet; a set of animation timelines slides them left and right at varied speeds so the crowd never lines up. Hover anywhere over the box to raise a magnifier lens that samples and zooms the region under the cursor.
It is a decorative, ambient piece: a lively background for a hero, an empty state, or a section header. The component fills its nearest positioned parent, so size and clip it by styling the wrapper rather than the canvas.
Import the pattern from loosethreads:
import { CrowdCanvas } from "@pulp/loosethreads/patterns"Give the component a sprite sheet URL and the grid it is laid out on, then wrap it in a positioned, fixed-height box. It reads reduced-motion as a reducedMotion prop rather than checking the media query itself, so resolve the preference once and pass it down:
import { CrowdCanvas } from "@pulp/loosethreads/patterns"
import { useEffect, useState } from "react"
// The component takes reduced motion as a prop, so resolve it once and pass it in.
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false)
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
const update = () => setReduced(mq.matches)
update()
mq.addEventListener("change", update)
return () => mq.removeEventListener("change", update)
}, [])
return reduced
}
function WalkingCrowd() {
const reducedMotion = usePrefersReducedMotion()
return (
<div style={{ position: "relative", width: "100%", height: 520, overflow: "hidden" }}>
<CrowdCanvas
src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/open-peeps-sheet.png"
rows={15}
cols={7}
reducedMotion={reducedMotion}
/>
</div>
)
}| Prop | Type | Default | Description |
|---|---|---|---|
src | string | Required | URL of the sprite sheet to draw from. The canvas only draws the image and never reads its pixels, so a cross-origin URL works without crossOrigin. |
rows | number | Required | Number of character columns across the sheet (the sheet is divided into rows slices horizontally to find each frame width). |
cols | number | Required | Number of character rows down the sheet. Combined with rows this gives the total pool of unique characters. |
reducedMotion | boolean | Required | When true, the crowd renders a single static frame and the continuous animation loop never starts. Wire this to the user's prefers-reduced-motion setting. |
Two stacked canvases fill the wrapper. The base canvas draws the crowd: characters are placed on animation timelines driven by a shared ticker, and each one is redrawn every frame as it walks across the stage and recycles when it exits. The overlay canvas is the hover lens; while the cursor is over the box it samples a zoomed region of the base canvas each frame, so the magnified characters stay in sync with the ones underneath.
Off-screen the per-frame draw pauses. The base canvas skips its clear and redraw while it is scrolled out of view, so an ambient crowd low on a long page costs no main-thread time until it scrolls back in.
aria-hidden, so assistive tech skips the animation. Keep any real content out of the canvas.prefers-reduced-motion (WCAG 2.2.2): when reducedMotion is true the component paints one static frame and never starts the ticker. You must supply that prop for the gate to work.Next: Interactive robot. Back to Canvas & 3D.