Custom Code Experiment •
Drifting Clouds
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Drifting Clouds</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
body {
background: #0a2f5a;
overflow: hidden;
height: 100vh;
width: 100vw;
}
canvas {
display: block;
width: 100%;
height: 100%;
cursor: crosshair;
}
.info {
position: fixed;
bottom: 16px;
right: 20px;
color: rgba(255,255,255,0.7);
background: rgba(0,0,0,0.3);
backdrop-filter: blur(4px);
padding: 6px 14px;
border-radius: 40px;
font-size: 13px;
font-family: monospace;
pointer-events: none;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="skyCanvas"></canvas>
<div class="info">☁️ clouds drifting · infinite horizon</div>
<script>
(function() {
const canvas = document.getElementById('skyCanvas');
if (!canvas) {
console.error('❌ Canvas element with id "skyCanvas" not found!');
return;
}
let ctx = canvas.getContext('2d');
let clouds = [];
let animationId = null;
let isAnimating = true;
// helper
function randomRange(min, max) {
return min + Math.random() * (max - min);
}
// create a fluffy cloud from overlapping circles
function createCloud(x, y, baseSize, speed) {
const template = [
{ dx: 0.0, dy: 0.0, rFactor: 1.0 },
{ dx: -0.42, dy: -0.28, rFactor: 0.72 },
{ dx: 0.48, dy: -0.22, rFactor: 0.78 },
{ dx: 0.22, dy: 0.35, rFactor: 0.68 },
{ dx: -0.35, dy: 0.30, rFactor: 0.64 },
{ dx: 0.52, dy: 0.08, rFactor: 0.60 },
{ dx: -0.50, dy: 0.05, rFactor: 0.58 },
{ dx: 0.05, dy: -0.48, rFactor: 0.55 },
{ dx: -0.15, dy: 0.55, rFactor: 0.52 }
];
const circles = [];
for (let t of template) {
let dxJitter = (Math.random() - 0.5) * 0.1;
let dyJitter = (Math.random() - 0.5) * 0.1;
let rJitter = 0.92 + Math.random() * 0.16;
let rFactor = Math.min(1.05, Math.max(0.48, t.rFactor * rJitter));
circles.push({
dx: t.dx + dxJitter,
dy: t.dy + dyJitter,
radiusFactor: rFactor
});
}
// compute actual radius and max extent
let maxExtent = 0;
const actualCircles = circles.map(c => {
const rad = baseSize * c.radiusFactor;
const distFromCenter = Math.hypot(c.dx * baseSize, c.dy * baseSize);
const furthest = distFromCenter + rad;
if (furthest > maxExtent) maxExtent = furthest;
return { dx: c.dx, dy: c.dy, radius: rad };
});
return {
x, y, baseSize, speed,
circles: actualCircles,
maxExtent: maxExtent,
alpha: 0.92 + Math.random() * 0.07
};
}
// draw one cloud
function drawCloud(ctx, cloud) {
ctx.save();
ctx.globalAlpha = cloud.alpha;
ctx.fillStyle = '#FFFFFF';
for (let circ of cloud.circles) {
ctx.beginPath();
ctx.arc(cloud.x + circ.dx, cloud.y + circ.dy, circ.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// draw sky gradient
function drawSky(ctx, w, h) {
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#4a90e2');
grad.addColorStop(0.4, '#6bb5ff');
grad.addColorStop(0.7, '#8fcbff');
grad.addColorStop(1, '#b9dcff');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// optional faint haze
ctx.fillStyle = 'rgba(255,255,255,0.05)';
ctx.fillRect(0, h * 0.7, w, h * 0.3);
}
// update positions and wrap around
function updateClouds(canvasW, canvasH) {
for (let c of clouds) {
c.x += c.speed;
// wrap right → left
if (c.x > canvasW + c.maxExtent) {
c.x = -c.maxExtent;
// slight random vertical shift for variety
let minY = c.maxExtent;
let maxY = canvasH - c.maxExtent;
if (maxY > minY) {
let delta = (Math.random() - 0.5) * 30;
c.y = Math.min(maxY, Math.max(minY, c.y + delta));
} else {
c.y = canvasH / 2;
}
}
// vertical clamping
let minY = c.maxExtent;
let maxY = canvasH - c.maxExtent;
if (maxY > minY) {
c.y = Math.min(maxY, Math.max(minY, c.y));
} else {
c.y = canvasH / 2;
}
}
}
// generate a set of clouds for current canvas size
function generateClouds(w, h) {
let count = Math.min(14, Math.max(7, Math.floor(6 + w / 400 + Math.random() * 3)));
let newClouds = [];
for (let i = 0; i < count; i++) {
let baseSize = randomRange(48, 105);
let speed = randomRange(0.35, 0.85) * (w / 1300);
speed = Math.min(1.2, Math.max(0.3, speed));
let randX = randomRange(-baseSize * 1.5, w + baseSize);
// estimate Y bounds
let approxExtent = baseSize * 1.3;
let minY = approxExtent + 10;
let maxY = h - approxExtent - 10;
if (maxY <= minY) {
minY = 30;
maxY = h - 30;
}
let randY = randomRange(minY, maxY);
let cloud = createCloud(randX, randY, baseSize, speed);
// re-clamp Y with exact maxExtent
let exactMin = cloud.maxExtent;
let exactMax = h - cloud.maxExtent;
if (exactMax > exactMin) {
cloud.y = Math.min(exactMax, Math.max(exactMin, randY));
} else {
cloud.y = h / 2;
}
newClouds.push(cloud);
}
return newClouds;
}
// resize handler – reposition clouds proportionally
function resizeCanvasAndClouds() {
const newW = window.innerWidth;
const newH = window.innerHeight;
if (newW === 0 || newH === 0) return;
const oldW = canvas.width;
const oldH = canvas.height;
canvas.width = newW;
canvas.height = newH;
ctx = canvas.getContext('2d'); // refresh context (safe)
if (clouds.length > 0 && oldW > 0 && oldH > 0) {
const ratioX = newW / oldW;
const ratioY = newH / oldH;
for (let c of clouds) {
c.x *= ratioX;
c.y *= ratioY;
// keep inside vertical bounds
let minY = c.maxExtent;
let maxY = newH - c.maxExtent;
if (maxY > minY) c.y = Math.min(maxY, Math.max(minY, c.y));
else c.y = newH / 2;
// horizontal wrap safety
if (c.x > newW + c.maxExtent) c.x = -c.maxExtent;
if (c.x < -c.maxExtent) c.x = newW + c.maxExtent;
}
} else {
clouds = generateClouds(newW, newH);
}
// force a redraw
if (ctx) {
drawSky(ctx, newW, newH);
for (let c of clouds) drawCloud(ctx, c);
}
}
// animation loop
function animate() {
if (!isAnimating) return;
if (!ctx || !canvas) return;
const w = canvas.width;
const h = canvas.height;
if (w === 0 || h === 0) {
animationId = requestAnimationFrame(animate);
return;
}
drawSky(ctx, w, h);
for (let c of clouds) drawCloud(ctx, c);
updateClouds(w, h);
animationId = requestAnimationFrame(animate);
}
// debounced resize
let resizeTimer;
function onResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resizeCanvasAndClouds();
}, 100);
}
// initialization
function init() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx = canvas.getContext('2d');
clouds = generateClouds(canvas.width, canvas.height);
isAnimating = true;
animate();
window.addEventListener('resize', onResize);
console.log('✅ Animation started | Canvas size:', canvas.width, 'x', canvas.height);
}
// start when DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// optional cleanup
window.addEventListener('beforeunload', () => {
if (animationId) cancelAnimationFrame(animationId);
window.removeEventListener('resize', onResize);
});
})();
</script>
</body>
</html>