Effet de chute de neige romantique
Scène de chute de neige en plein écran avec quantité de neige et direction du vent réglables, Canvas.
Après activation de JavaScript, ce cas peut être modifié, exécuté et enregistré. Code source fixe ci-dessous :
<!doctype html>
<html lang="fr" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Effet de chute de neige romantique</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>La neige tombe en silence</h1><p>Ajustez la quantité de neige et la direction du vent pour créer votre propre nuit d’hiver. Le canevas s’adapte automatiquement à la fenêtre et aux écrans haute définition.</p></section></main>
<aside class="controls" aria-label="Paramètres de chute de neige">
<label class="control"><span>Quantité de neige</span><input id="amount" type="range" min="30" max="220" value="120"><output id="amountValue">120</output></label>
<label class="control"><span>Direction du vent</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>