library / celebration / movingpeeps
live - 60fps 1920 x 1080
movingpeeps.html - self-contained
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Walking Peeps Animation</title>
    
    <!-- 1. CSS goes inside <style> tags -->
    <style>
        html, body {
            height: 100%;
        }
        body {
            margin: 0;
            background-color: #f0f0f0; /* Added a light background for visibility */
            overflow: hidden; /* Prevents scrollbars */
        }
        #canvas {
            width: 100%;
            height: 100%;
            display: block;
        }
    </style>

    <!-- 2. Import External Libraries (GSAP is required for your code) -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
</head>
<body>

    <!-- 3. HTML Content -->
    <canvas id="canvas"></canvas>

    <!-- 4. JavaScript goes inside <script> tags -->
    <script>
        console.clear();
        console.log('Animation Initialized');

        const config = {
            src: 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/open-peeps-sheet.png',
            rows: 15,
            cols: 7
        };

        // UTILS
        const randomRange = (min, max) => min + Math.random() * (max - min);
        const randomIndex = (array) => randomRange(0, array.length) | 0;
        const removeFromArray = (array, i) => array.splice(i, 1)[0];
        const removeItemFromArray = (array, item) => removeFromArray(array, array.indexOf(item));
        const removeRandomFromArray = (array) => removeFromArray(array, randomIndex(array));
        const getRandomFromArray = (array) => (
            array[randomIndex(array) | 0]
        );

        // TWEEN FACTORIES
        const resetPeep = ({ stage, peep }) => {
            const direction = Math.random() > 0.5 ? 1 : -1;
            // using an ease function to skew random to lower values to help hide that peeps have no legs
            const offsetY = 100 - 250 * gsap.parseEase('power2.in')(Math.random());
            const startY = stage.height - peep.height + offsetY;
            let startX;
            let endX;
            
            if (direction === 1) {
                startX = -peep.width;
                endX = stage.width;
                peep.scaleX = 1;
            } else {
                startX = stage.width + peep.width;
                endX = 0;
                peep.scaleX = -1;
            }
            
            peep.x = startX;
            peep.y = startY;
            peep.anchorY = startY;
            
            return {
                startX,
                startY,
                endX
            };
        };

        const normalWalk = ({ peep, props }) => {
            const {
                startX,
                startY,
                endX
            } = props;

            const xDuration = 10;
            const yDuration = 0.25;
            
            const tl = gsap.timeline();
            tl.timeScale(randomRange(0.5, 1.5));
            tl.to(peep, {
                duration: xDuration,
                x: endX,
                ease: 'none'
            }, 0);
            tl.to(peep, {
                duration: yDuration,
                repeat: xDuration / yDuration,
                yoyo: true,
                y: startY - 10
            }, 0);
                
            return tl;
        };

        const walks = [
            normalWalk,
        ];

        // CLASSES
        class Peep {
            constructor({
                image,
                rect,
            }) {
                this.image = image;
                this.setRect(rect);
                
                this.x = 0;
                this.y = 0;
                this.anchorY = 0;
                this.scaleX = 1;
                this.walk = null;
            }
            
            setRect (rect) {
                this.rect = rect;
                this.width = rect[2];
                this.height = rect[3];
                
                this.drawArgs = [
                    this.image,
                    ...rect,
                    0, 0, this.width, this.height
                ];  
            }
            
            render (ctx) {
                ctx.save();
                ctx.translate(this.x, this.y);
                ctx.scale(this.scaleX, 1);
                ctx.drawImage(...this.drawArgs);
                ctx.restore();
            }
        }

        // MAIN
        const img = document.createElement('img');
        img.onload = init;
        img.src = config.src;

        const canvas = document.querySelector('#canvas');
        const ctx = canvas.getContext('2d');

        const stage = {
            width: 0,
            height: 0,
        };

        const allPeeps = [];
        const availablePeeps = [];
        const crowd = [];

        function init () {  
            createPeeps();
            
            // resize also (re)populates the stage
            resize();

            gsap.ticker.add(render);
            window.addEventListener('resize', resize);
        }

        function createPeeps () {
            const {
                rows,
                cols
            } = config;
            const {
                naturalWidth: width,
                naturalHeight: height
            } = img;
            const total = rows * cols;
            const rectWidth = width / rows;
            const rectHeight = height / cols;
            
            for (let i = 0; i < total; i++) {
                allPeeps.push(new Peep({
                    image: img,
                    rect: [
                        (i % rows) * rectWidth,
                        (i / rows | 0) * rectHeight,
                        rectWidth,
                        rectHeight,
                    ]
                }));
            }  
        }

        function resize () {
            stage.width = canvas.clientWidth;
            stage.height = canvas.clientHeight;
            canvas.width = stage.width * devicePixelRatio;
            canvas.height = stage.height * devicePixelRatio;
            
            crowd.forEach((peep) => {
                peep.walk.kill();
            });
            
            crowd.length = 0;
            availablePeeps.length = 0;
            availablePeeps.push(...allPeeps);
            
            initCrowd();
        }

        function initCrowd () {
            while (availablePeeps.length) {
                // setting random tween progress spreads the peeps out
                addPeepToCrowd().walk.progress(Math.random());
            }
        }

        function addPeepToCrowd () {
            const peep = removeRandomFromArray(availablePeeps);
            const walk = getRandomFromArray(walks)({
                peep,
                props: resetPeep({
                    peep,
                    stage,
                })
            }).eventCallback('onComplete', () => {
                removePeepFromCrowd(peep);
                addPeepToCrowd();
            });
            
            peep.walk = walk;
            
            crowd.push(peep);
            crowd.sort((a, b) => a.anchorY - b.anchorY);
            
            return peep;
        }

        function removePeepFromCrowd (peep) {
            removeItemFromArray(crowd, peep);
            availablePeeps.push(peep);
        }

        function render () {
            canvas.width = canvas.width;
            ctx.save();
            ctx.scale(devicePixelRatio, devicePixelRatio);
            
            crowd.forEach((peep) => {
                peep.render(ctx);
            });
            
            ctx.restore();
        }
    </script>
</body>
</html>

About this animation

Animated characters walking across the screen.

Under the hood

CSS Sprite Sheet Animation

To create highly complex illustrative frame-by-frame animations (like a moving car or walking character) without loading a heavy video file, this handles it via CSS Sprite Sheets. A large image containing all frames is loaded, and the steps() CSS timing function shifts the background-position instantly frame-by-frame, creating a perfectly looping character animation.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger