efectos visuales creativos

Efecto romántico de nieve cayendo

Un escenario de nieve a pantalla completa en Canvas con cantidad de nieve y dirección del viento ajustables.

HTMLCanvasJavaScriptCopos de nieve
Tras activar JavaScript, puedes editar, ejecutar y guardar este ejemplo. El código fuente fijado es el siguiente:
<!doctype html>
<html lang="es" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Efecto romántico de nieve cayendo</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>Nieve silenciosa</h1><p>Ajusta la cantidad de nieve y la dirección del viento para crear tu propia noche de invierno. El lienzo se adapta automáticamente a la ventana y a las pantallas de alta densidad.</p></section></main>
  <aside class="controls" aria-label="Configuración de nieve">
    <label class="control"><span>Cantidad de nieve</span><input id="amount" type="range" min="30" max="220" value="120"><output id="amountValue">120</output></label>
    <label class="control"><span>Dirección del viento</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>
Comentarios de la página¿Has encontrado un problema o tienes sugerencias de mejora?
Las funciones de retroalimentación deben usarse en la versión en línea

Envía tras activar JavaScript en la versión en línea; las funciones locales de la herramienta o del ejemplo no se verán afectadas.

Ir a la versión en línea para dar feedback