Effets visuels créatifs

Effet de traînée de particules

Particules lumineuses colorées qui se déplacent en suivant le curseur de la souris ou le toucher.

HTMLCanvasJavaScriptParticule
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 traînée de particules</title>
  <style>
    * { box-sizing: border-box; }
    :root { color-scheme: dark; }
    body { min-height: 100vh; margin: 0; overflow: hidden; color: #f3f5ff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: #05050d; }
    canvas { position: fixed; inset: 0; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
    .noise { position: fixed; inset: 0; pointer-events: none; opacity: .18; background-image: radial-gradient(rgba(255,255,255,.22) .7px, transparent .7px); background-size: 7px 7px; mask-image: linear-gradient(to bottom, black, transparent 85%); }
    header { position: fixed; z-index: 2; top: clamp(28px, 8vh, 78px); left: clamp(24px, 7vw, 80px); max-width: 620px; pointer-events: none; }
    .eyebrow { color: #71f5d0; font-size: 10px; font-weight: 900; letter-spacing: .28em; }
    h1 { margin: 12px 0 14px; font: 500 clamp(46px, 10vw, 96px)/.9 Georgia, "STSong", serif; letter-spacing: -.05em; }
    p { max-width: 470px; margin: 0; color: #848aa3; line-height: 1.75; }
    .palette { position: fixed; z-index: 3; right: 24px; bottom: 24px; display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid #24263a; border-radius: 999px; color: #8e94ad; font-size: 11px; background: rgba(10,10,22,.78); backdrop-filter: blur(10px); }
    button { width: 30px; height: 30px; border: 2px solid #fff; border-radius: 50%; background: linear-gradient(135deg, #71f5d0, #6a78ff, #ff6fae); cursor: pointer; box-shadow: 0 0 15px rgba(113,245,208,.35); }
    button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
  </style>
</head>
<body>
  <canvas id="particles" aria-label="Génération de trajectoire de particules par déplacement du pointeur"></canvas><div class="noise" aria-hidden="true"></div>
  <header><span class="eyebrow">INTERACTIVE LIGHT FIELD</span><h1>Toucher un faisceau de lumière</h1><p>Déplacez la souris ou le doigt, et les particules colorées se développeront le long de la trajectoire, dériveront puis se dissiperont lentement.</p></header>
  <div class="palette"><span>Cliquer pour basculer la teinte</span><button id="palette" type="button" aria-label="Changer la teinte des particules"></button></div>
  <script>
    const canvas = document.querySelector('#particles');
    const context = canvas.getContext('2d');
    const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
    const palettes = [[160, 250], [280, 350], [25, 90]];
    let paletteIndex = 0, width = 0, height = 0, particles = [], pointer = { x: 0, y: 0, active: false };

    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);
      context.setTransform(ratio, 0, 0, ratio, 0, 0);
    }
    function emit(x, y, amount = 7) {
      const range = palettes[paletteIndex];
      for (let index = 0; index < (reduceMotion ? Math.min(amount, 2) : amount); index += 1) {
        const angle = Math.random() * Math.PI * 2, speed = .3 + Math.random() * 1.8;
        particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: 1.5 + Math.random() * 4, life: 1, decay: .012 + Math.random() * .018, hue: range[0] + Math.random() * (range[1] - range[0]) });
      }
      if (particles.length > 650) particles.splice(0, particles.length - 650);
    }
    function movePointer(event) {
      const rect = canvas.getBoundingClientRect(); pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top, active: true }; emit(pointer.x, pointer.y);
    }
    function frame() {
      context.fillStyle = 'rgba(5, 5, 13, .16)'; context.fillRect(0, 0, width, height);
      context.globalCompositeOperation = 'lighter';
      particles = particles.filter(particle => {
        particle.x += particle.vx; particle.y += particle.vy; particle.vx *= .985; particle.vy = particle.vy * .985 - .008; particle.life -= particle.decay;
        if (particle.life <= 0) return false;
        context.globalAlpha = particle.life;
        context.fillStyle = `hsl(${particle.hue} 90% 65%)`;
        context.beginPath(); context.arc(particle.x, particle.y, particle.size * particle.life, 0, Math.PI * 2); context.fill(); return true;
      });
      context.globalAlpha = 1; context.globalCompositeOperation = 'source-over'; requestAnimationFrame(frame);
    }
    canvas.addEventListener('pointermove', movePointer);
    canvas.addEventListener('pointerdown', event => { movePointer(event); emit(pointer.x, pointer.y, 28); });
    canvas.addEventListener('pointerleave', () => { pointer.active = false; });
    document.querySelector('#palette').addEventListener('click', () => { paletteIndex = (paletteIndex + 1) % palettes.length; emit(width * .75, height * .65, 40); });
    addEventListener('resize', resize); resize(); emit(width * .72, height * .64, 55); requestAnimationFrame(frame);
  </script>
</body>
</html>
Commentaires sur la pageVous avez découvert un problème ou avez une suggestion d’amélioration ?
Les fonctions de retour nécessitent l’utilisation de la version en ligne

Veuillez activer JavaScript dans la version en ligne avant de soumettre ; les fonctionnalités locales de l’outil ou du cas ne sont pas affectées.

Aller vers la version en ligne pour envoyer des commentaires