<!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>
<div id="container"></div>
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;
}
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();