Romantic Snowfall Effect
A full-screen Canvas snowfall scene with adjustable snowfall and wind direction.
With JavaScript enabled, you can edit, run, and save this example. The fixed source is as follows:
<!doctype html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Romantic Snowfall Effect</title>
<style>
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; overflow: hidden; color: #f6fbff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: linear-gradient(#07162b, #173354 58%, #6285a2); }
canvas { position: fixed; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.moon { position: fixed; top: 9vh; right: 12vw; width: clamp(70px, 12vw, 130px); aspect-ratio: 1; border-radius: 50%; background: #fff7d3; box-shadow: 0 0 55px rgba(255,247,211,.45); }
.mountains { position: fixed; inset: 38% -5% 0; background: #17304b; clip-path: polygon(0 58%, 11% 25%, 20% 48%, 34% 7%, 49% 52%, 62% 22%, 73% 56%, 87% 11%, 100% 43%, 100% 100%, 0 100%); }
.mountains::after { content: ""; position: absolute; inset: 18% 0 0; background: #0d253d; clip-path: polygon(0 58%, 15% 29%, 28% 61%, 45% 20%, 58% 58%, 74% 27%, 88% 60%, 100% 38%, 100% 100%, 0 100%); }
.cabin { position: fixed; left: 16vw; bottom: 10vh; width: 118px; height: 80px; background: #3d2a28; box-shadow: inset 0 -16px rgba(0,0,0,.18); }
.cabin::before { content: ""; position: absolute; left: -18px; top: -43px; border-left: 77px solid transparent; border-right: 77px solid transparent; border-bottom: 48px solid #241d22; }
.cabin::after { content: ""; position: absolute; right: 20px; bottom: 24px; width: 24px; height: 25px; background: #ffd77a; box-shadow: 0 0 24px #f6aa42; }
main { position: relative; z-index: 2; width: min(1120px, calc(100% - 36px)); min-height: 100vh; margin: auto; padding-top: clamp(36px, 10vh, 100px); }
.copy { max-width: 560px; text-shadow: 0 3px 16px rgba(3,12,25,.65); }
.eyebrow { color: #b9dfff; font-size: 11px; font-weight: 800; letter-spacing: .22em; }
h1 { margin: 12px 0; font: 500 clamp(46px, 10vw, 92px)/.92 "STKaiti", "KaiTi", serif; }
.copy p { color: #c2d5e6; line-height: 1.7; }
.controls { position: fixed; z-index: 3; right: 22px; bottom: 22px; width: min(310px, calc(100% - 44px)); padding: 18px; border: 1px solid rgba(255,255,255,.2); border-radius: 18px; background: rgba(7,22,43,.72); backdrop-filter: blur(12px); }
.control { display: grid; grid-template-columns: 58px 1fr 40px; gap: 9px; align-items: center; color: #cadbea; font-size: 12px; }
.control + .control { margin-top: 14px; }
input { width: 100%; accent-color: #fff7d3; }
output { text-align: right; color: #fff7d3; font: 700 11px/1 "Courier New", monospace; }
@media (max-width: 600px) { .copy p { max-width: 75%; } .cabin { left: 10vw; bottom: 26vh; transform: scale(.72); } }
</style>
</head>
<body>
<div class="moon" aria-hidden="true"></div><div class="mountains" aria-hidden="true"></div><div class="cabin" aria-hidden="true"></div>
<canvas id="snow" aria-hidden="true"></canvas>
<main><section class="copy"><span class="eyebrow">A QUIET WINTER NIGHT</span><h1>Silent snowfall</h1><p>Adjust the snowfall and wind direction to create your own winter night. The canvas automatically adapts to the window and high-resolution displays.</p></section></main>
<aside class="controls" aria-label="Snowfall Settings">
<label class="control"><span>Snowfall Amount</span><input id="amount" type="range" min="30" max="220" value="120"><output id="amountValue">120</output></label>
<label class="control"><span>Wind direction</span><input id="wind" type="range" min="-12" max="12" value="2"><output id="windValue">+0.2</output></label>
</aside>
<script>
const canvas = document.querySelector('#snow');
const context = canvas.getContext('2d');
const amountInput = document.querySelector('#amount');
const windInput = document.querySelector('#wind');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
let width = 0, height = 0, flakes = [];
function makeFlake(randomY = true) {
return { x: Math.random() * width, y: randomY ? Math.random() * height : -12, radius: 1 + Math.random() * 3.2, speed: .35 + Math.random() * 1.25, drift: Math.random() * Math.PI * 2, opacity: .35 + Math.random() * .65 };
}
function syncAmount() {
const amount = Number(amountInput.value);
while (flakes.length < amount) flakes.push(makeFlake());
flakes.length = amount;
document.querySelector('#amountValue').value = String(amount);
draw();
}
function resize() {
const ratio = Math.min(devicePixelRatio || 1, 2);
width = innerWidth; height = innerHeight;
canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
canvas.style.width = `${width}px`; canvas.style.height = `${height}px`;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
flakes = Array.from({ length: Number(amountInput.value) }, () => makeFlake());
draw();
}
function draw() {
context.clearRect(0, 0, width, height);
context.fillStyle = '#ffffff';
flakes.forEach(flake => { context.globalAlpha = flake.opacity; context.beginPath(); context.arc(flake.x, flake.y, flake.radius, 0, Math.PI * 2); context.fill(); });
context.globalAlpha = 1;
}
function animate() {
const wind = Number(windInput.value) / 10;
flakes.forEach(flake => {
flake.y += flake.speed;
flake.x += wind + Math.sin(flake.drift += .012) * .25;
if (flake.y > height + 10 || flake.x < -20 || flake.x > width + 20) Object.assign(flake, makeFlake(false));
});
draw(); requestAnimationFrame(animate);
}
amountInput.addEventListener('input', syncAmount);
windInput.addEventListener('input', () => { const wind = Number(windInput.value) / 10; document.querySelector('#windValue').value = `${wind >= 0 ? '+' : ''}${wind.toFixed(1)}`; });
addEventListener('resize', resize); resize(); if (!reduceMotion) requestAnimationFrame(animate);
</script>
</body>
</html>