live - 60fps 1920 x 1080
<!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. Related