library / celebration / envato-bouncing-balloons
live - 60fps 1920 x 1080
envato-bouncing-balloons.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>Bouncing SVG Balloons</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #f8fafc;
            font-family: sans-serif;
            cursor: pointer;
            /* Interaction hint */
        }

        #container {
            width: 100vw;
            height: 100vh;
            position: relative;
        }

        .balloon {
            position: absolute;
            transform-origin: bottom center;
            will-change: transform, top, left;
        }

        .string {
            stroke: #cbd5e1;
            stroke-width: 2;
            fill: none;
        }
    </style>
</head>

<body>
    <div id="container"></div>

    <script>
        const container = document.getElementById('container');
        const colors = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899'];
        let balloons = [];

        const config = {
            count: 15,
            gravity: 0.15,
            bounce: -0.6,
            friction: 0.98
        };

        class Balloon {
            constructor(x, y, isClick = false) {
                this.x = x || Math.random() * window.innerWidth;
                this.y = y || Math.random() * (window.innerHeight / 2) - window.innerHeight; // Start offscreen top
                this.vx = (Math.random() - 0.5) * 4;
                this.vy = isClick ? -5 - Math.random() * 5 : 0;
                this.radiusX = 30 + Math.random() * 15;
                this.radiusY = this.radiusX * 1.3;
                this.color = colors[Math.floor(Math.random() * colors.length)];
                this.element = this.createSVG();
                container.appendChild(this.element);
            }

            createSVG() {
                const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                const width = this.radiusX * 2.5;
                const height = this.radiusY * 2.5 + 60; // Extra for string
                svg.setAttribute('width', width);
                svg.setAttribute('height', height);
                svg.setAttribute('class', 'balloon');

                // Definitions for gradient
                const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
                const gradient = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient");
                const id = 'grad-' + Math.random().toString(36).substr(2, 9);
                gradient.setAttribute('id', id);
                gradient.setAttribute('cx', '30%');
                gradient.setAttribute('cy', '30%');
                gradient.setAttribute('r', '50%');

                const stop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
                stop1.setAttribute('offset', '0%');
                stop1.setAttribute('stop-color', '#ffffff');
                stop1.setAttribute('stop-opacity', '0.6');

                const stop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop");
                stop2.setAttribute('offset', '100%');
                stop2.setAttribute('stop-color', this.color);

                gradient.appendChild(stop1);
                gradient.appendChild(stop2);
                defs.appendChild(gradient);
                svg.appendChild(defs);

                // String
                const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
                path.setAttribute('d', `M ${width / 2} ${this.radiusY * 2 + 5} Q ${width / 2 + 10} ${this.radiusY * 2 + 30} ${width / 2 - 5} ${height}`);
                path.setAttribute('class', 'string');
                svg.appendChild(path);

                // Tie
                const tie = document.createElementNS("http://www.w3.org/2000/svg", "polygon");
                tie.setAttribute('points', `${width / 2},${this.radiusY * 2} ${width / 2 - 6},${this.radiusY * 2 + 8} ${width / 2 + 6},${this.radiusY * 2 + 8}`);
                tie.setAttribute('fill', this.color);
                svg.appendChild(tie);

                // Balloon Body
                const ellipse = document.createElementNS("http://www.w3.org/2000/svg", "ellipse");
                ellipse.setAttribute('cx', width / 2);
                ellipse.setAttribute('cy', this.radiusY);
                ellipse.setAttribute('rx', this.radiusX);
                ellipse.setAttribute('ry', this.radiusY);
                ellipse.setAttribute('fill', `url(#${id})`);
                svg.appendChild(ellipse);

                return svg;
            }

            update() {
                this.vy += config.gravity;
                this.vx *= config.friction;

                this.x += this.vx;
                this.y += this.vy;

                const ground = window.innerHeight - (this.radiusY * 2.5 + 60);

                // Floor collision
                if (this.y > ground) {
                    this.y = ground;
                    this.vy *= config.bounce;
                    this.vx *= 0.9; // Extra floor friction
                }

                // Wall collision
                if (this.x < 0) {
                    this.x = 0;
                    this.vx *= -1;
                } else if (this.x > window.innerWidth - this.radiusX * 2.5) {
                    this.x = window.innerWidth - this.radiusX * 2.5;
                    this.vx *= -1;
                }

                // Squeeze effect based on velocity
                const scaleY = 1 - Math.min(Math.abs(this.vy) * 0.02, 0.3);
                const scaleX = 1 + Math.min(Math.abs(this.vy) * 0.01, 0.15);
                const rotation = this.vx * 2;

                this.element.style.transform = `translate(${this.x}px, ${this.y}px) rotate(${rotation}deg) scale(${scaleX}, ${scaleY})`;
            }
        }

        function init() {
            container.innerHTML = '';
            balloons = [];
            for (let i = 0; i < config.count; i++) {
                setTimeout(() => {
                    balloons.push(new Balloon());
                }, i * 200); // Staggered spawn
            }
        }

        function animate() {
            for (let i = 0; i < balloons.length; i++) {
                balloons[i].update();
            }
            requestAnimationFrame(animate);
        }

        window.addEventListener('click', (e) => {
            // Spawn 3 balloons on click
            for (let i = 0; i < 3; i++) {
                balloons.push(new Balloon(e.clientX - 30, window.innerHeight, true));
            }
            // Keep array size manageable
            if (balloons.length > config.count + 15) {
                const old = balloons.shift();
                old.element.remove();
            }
        });

        init();
        animate();
    </script>
</body>

</html>

About this animation

Gravity-affected SVG balloons that bounce and settle interactively on the bottom of the screen.

Under the hood

Gravity and Elastic Collision Dynamics

This script simulates basic Newtonian physics on dynamically generated SVG elements. Variables for gravity (0.15) and friction pull the nodes downward until they hit the bottom of the window innerHeight, at which point an inverse bounce scalar ricochets them realistically back upwards.

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