>_htmlhub
library/text & typography/kinetic-typography-swarm

Kinetic Swarm

Live previewPerformance varies by device
kinetic-typography-swarm.html

No account needed to copy or download. Check dependencies and thelicense status before publishing.

<!DOCTYPE html>
<html>
<head>
<title>Kinetic Typography Swarm</title>
<style>
    body {
        background-color: #1a1a1a;
        color: white;
        margin: 0;
        overflow: hidden;
        font-family: 'Courier New', Courier, monospace;
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
        height: 100vh;
    }
    canvas {
        position: absolute;
        top: 0;
        left: 0;
        z-index: 1;
    }
    h1 {
        z-index: 2;
        font-size: 4rem;
        color: #fff;
    }
</style>
</head>
<body>
    <h1>KINETIC</h1>
    <canvas id="swarmCanvas"></canvas>
<script>
    const canvas = document.getElementById('swarmCanvas');
    const ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    let particles = [];
    const chars = ['$', '#', '@', '&', '%', '}', '{', ']', '['];
    const mouse = {
        x: undefined,
        y: undefined,
    };
    window.addEventListener('mousemove', (event) => {
        mouse.x = event.clientX;
        mouse.y = event.clientY;
    });
    class Particle {
        constructor(x, y, char, color, size) {
            this.x = x;
            this.y = y;
            this.char = char;
            this.color = color;
            this.size = size;
            this.baseX = this.x;
            this.baseY = this.y;
            this.density = (Math.random() * 30) + 1;
            // Cache the canvas font once, quantised to 0.5px steps: building
            // a unique float font string and assigning ctx.font for every
            // particle on every frame was the frame bottleneck. Particles
            // are sorted by this font in init() so animate() only assigns
            // ctx.font when the size step actually changes (~31 distinct
            // fonts instead of 1000 unique ones).
            this.fontSize = Math.round(size * 2) / 2;
            this.font = this.fontSize + 'px Courier New';
        }
        draw() {
            // fillStyle and font are set by animate() once per font group;
            // every particle in a group shares the same colour and font.
            ctx.fillText(this.char, this.x, this.y);
        }
        update() {
            let dx = mouse.x - this.x;
            let dy = mouse.y - this.y;
            let distance = Math.sqrt(dx * dx + dy * dy);
            let forceDirectionX = dx / distance;
            let forceDirectionY = dy / distance;
            let maxDistance = 200;
            let force = (maxDistance - distance) / maxDistance;
            if (distance < maxDistance) {
                this.x -= forceDirectionX * this.density * 0.5;
                this.y -= forceDirectionY * this.density * 0.5;
            } else {
                if (this.x !== this.baseX) {
                    let dx = this.x - this.baseX;
                    this.x -= dx / 10;
                }
                if (this.y !== this.baseY) {
                    let dy = this.y - this.baseY;
                    this.y -= dy / 10;
                }
            }
        }
    }
    function init() {
        particles = [];
        for (let i = 0; i < 1000; i++) {
            let x = Math.random() * canvas.width;
            let y = Math.random() * canvas.height;
            let char = chars[Math.floor(Math.random() * chars.length)];
            let color = 'white';
            let size = Math.random() * 15 + 5;
            particles.push(new Particle(x, y, char, color, size));
        }
        // Sort by cached font so animate() can set ctx.font once per size
        // step instead of once per particle. Draw order does not affect
        // the image: every glyph is the same opaque colour.
        particles.sort((a, b) => a.fontSize - b.fontSize);
    }
    const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
    function animate() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        if (particles.length) ctx.fillStyle = particles[0].color;
        let currentFont = null;
        for (let i = 0; i < particles.length; i++) {
            particles[i].update();
            if (particles[i].font !== currentFont) {
                ctx.font = particles[i].font;
                currentFont = particles[i].font;
            }
            particles[i].draw();
        }
        if (!prefersReduced) requestAnimationFrame(animate);
    }
    init();
    animate();
    window.addEventListener('resize', function(){
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        init();
    });
</script>
</body>
</html>

About this animation

Letters swarming and forming shapes.

Under the hood

Canvas Character-Swarm Repulsion

One thousand particles are spawned at random canvas positions, each a single glyph from a small symbol set ($ # @ & % } { ] [) drawn white with fillText() at sizes of 5–20px, swarming beneath a static DOM KINETIC heading. Within 200px of the mouse a particle is pushed away with a force scaled by its random density; otherwise it eases back toward its base position by a tenth of the remaining distance each frame.

Source Code
Copied to clipboard