AtmosphèreGratuit

Liquid Metal Shader

Surface métallique liquide en WebGL : bruit simplex FBM 4 octaves, normales par différences finies, éclairage diffus-spéculaire et iridescence, 3 palettes.

WebGLShaderMetal
Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Atmosphère en contient 57 au total, dont 10 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →

Exemples d'usage

① Hero / Landing — Hero de marque technologique

QuandEn fond de section hero sur une landing B2B ou SaaS voulant signaler « technologie de pointe » sans image ni vidéo externe.
PourquoiLe shader silver animé remplace une vidéo HD : 0 requête réseau, ~1,5 Ko de JS, rendu 100 % GPU. Il retient l'attention sans distraire du CTA principal.
Réglagessettings.color = 'silver' (défaut), settings.speed = 0.7 pour un mouvement lent et premium, settings.lightAngle = 30 pour une lumière rasante dramatique.

② Composant produit — Carte offre Pro / pricing premium

QuandEn fond de la carte « Plan Pro » ou d'une tuile produit haut de gamme, pour la distinguer visuellement des offres standard.
PourquoiLa palette gold transforme un arrière-plan statique doré en surface vivante et renforce la perception de valeur sans asset externe.
Réglagessettings.color = 'gold', settings.distortion = 0.5 (ondulations discrètes), settings.speed = 0.8.

③ Fond plein-écran — Héro d'une marque horlogère ou tech luxe

QuandEn fond de section héro ou de page produit pour une marque positionnée sur le luxe, la précision ou la matière — horlogerie, audio haute-fidélité, carte bancaire métal, produit physique premium.
PourquoiLa surface métallique animée incarne le matériau de la marque : le métal liquide couvre 100 % du cadre, sans élément UI qui réduise son impact. C'est l'effet lui-même qui est le fond, pas une décoration superposée.
Réglagessettings.color = 'gold' pour une dominante chaude et précieuse, settings.speed = 0.5 (mouvement imperceptiblement lent), settings.distortion = 0.7 (ondulations visibles), settings.lightAngle = 20 pour une lumière rasante qui sculpte chaque relief.

Comment ça marche

Un quad plein-écran en WebGL 1 pilote l'effet. À l'initialisation, deux shaders GLSL (vertex minimal + fragment) sont compilés et liés ; un Float32Array de 4 sommets (−1/+1) forme le triangle-strip qui couvre tout le canvas. Les uniforms u_res et u_time sont mis à jour à chaque frame par requestAnimationFrame. Le canvas est dimensionné au vrai DPR physique (devicePixelRatio, plafonné à 2) via gl.viewport, et un écouteur resize le recalibre dynamiquement.

La surface est entièrement générée dans le fragment shader par du bruit simplex 2D classique (Ashima Arts, licence MIT). Une fonction FBM (fractal brownian motion) enchaîne 4 octaves : chaque octave double la fréquence spatiale et divise l'amplitude par deux, accumulant un champ de hauteur h = fbm(uv × 2 + vec2(t, t×0,7)). u_distortion module l'amplitude globale (0,6 par défaut), u_speed la vitesse de défilement temporel.

Le vecteur normal de surface est estimé par différences finies : deux échantillons FBM décalés de ε = 0,01 en x et y reconstruisent normal = normalize(vec3(Δh/Δx, Δh/Δy, 1)). La composante diffuse (produit scalaire normal × lightDir) et un spéculaire de puissance 32 (modèle Phong) sont mélangés avec un facteur d'iridescence0,5 + 0,5 × sin(h×8 + t×2) — pour interpoler les trois stops de couleur de la palette active. Un vignettage radial (1 − 0,4 × |uv|) assombrit les bords. Un IntersectionObserver suspend le rendu quand le canvas sort du viewport.

Accessibilité

  • prefers-reduced-motion : réduitinitMetal() retourne immédiatement ; le canvas reste vide et aucune boucle ne tourne. Le commentaire du shader mentionne un « fallback CSS gradient via @media » mais aucune règle @media (prefers-reduced-motion) n'est définie dans le CSS livré : à ajouter selon votre contexte.
  • Le canvas fourni dans le HTML n'a ni aria-hidden ni role : ajoutez aria-hidden="true" si l'effet est purement décoratif, ou enveloppez-le dans un role="img" aria-label="Surface métallique animée" s'il est porteur de sens.
  • L'effet est purement passif (aucune interaction pointeur ou clavier) — aucune gestion de focus n'est requise.
  • Fallback WebGL : si WebGL n'est pas disponible, le canvas est masqué (style.display = 'none') sans message alternatif — le contenu texte superposé reste visible.

Compatibilité navigateur

Nécessite WebGL 1 (aucune extension optionnelle) — présent dans tous les navigateurs modernes depuis 2015. Zéro dépendance externe.

Chrome 56+✓ Complet
Firefox 51+✓ Complet
Safari 15+✓ Complet
Edge 79+✓ Complet
Mobile iOS✓ WebGL OK
Android Chrome✓ Complet

Sans WebGL, le canvas est masqué silencieusement (display:none) — aucune erreur JS fatale. Le contenu texte ou UI superposé reste intégralement visible.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="ps-canvas-wrap">
  <canvas class="ps-canvas" id="psLiquidMetal" width="393" height="280" style="width: 392.672px; height: 280px;">
  </canvas>
</div>
CSS
.ps-canvas  {
   width: 100%;
   height: 280px;
   display: block;
   border-radius: 0px;
   }

.ps-canvas-wrap  {
   position: relative;
   width: 100%;
   height: 280px;
   }

.ps-canvas-wrap canvas  {
   position: absolute;
   top: 0px;
   left: 0px;
   width: 100%;
   height: 100%;
   }
JavaScript (fx-0783)
var c=document.createElement('div');

var b=document.querySelector('.nav-theme-toggle');

const canvas = document.getElementById('psStarfield');

(function() {
  var settings = { color: 'silver', speed: 1.0, distortion: 0.6, lightAngle: 45 };
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function initMetal(canvas) {
  // Reduced motion : fallback CSS gradient déjà en place via @media
  if (reduceMotion) return;

  const gl = canvas.getContext('webgl', { antialias: false, alpha: false }) ||
             canvas.getContext('experimental-webgl');

  // WebGL not supported → CSS gradient fallback (handled in CSS @media query)
  if (!gl) {
    canvas.style.display = 'none';
    return;
  }

  // === Vertex shader : fullscreen quad ===
  const VS = `
    attribute vec2 a_pos;
    void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
  `;

  // === Fragment shader : liquid metal via Perlin noise + raymarching simplifié ===
  const FS = `
    precision highp float;
    uniform vec2  u_res;
    uniform float u_time;
    uniform float u_speed;
    uniform float u_distortion;
    uniform float u_light_angle;
    uniform vec3  u_color_a;
    uniform vec3  u_color_b;
    uniform vec3  u_color_c;

    // Classic 2D simplex noise (Ashima Arts, MIT)
    vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
    vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
    vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

    float snoise(vec2 v){
      const vec4 C = vec4(0.211324865405187, 0.366025403784439,
                         -0.577350269189626, 0.024390243902439);
      vec2 i  = floor(v + dot(v, C.yy));
      vec2 x0 = v - i + dot(i, C.xx);
      vec2 i1 = (x0.x > x0.y) ? vec2(1.0,0.0) : vec2(0.0,1.0);
      vec4 x12 = x0.xyxy + C.xxzz;
      x12.xy -= i1;
      i = mod289(i);
      vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0))
                              + i.x + vec3(0.0, i1.x, 1.0));
      vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
      m = m*m; m = m*m;
      vec3 x = 2.0 * fract(p * C.www) - 1.0;
      vec3 h = abs(x) - 0.5;
      vec3 ox = floor(x + 0.5);
      vec3 a0 = x - ox;
      m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
      vec3 g;
      g.x  = a0.x  * x0.x  + h.x  * x0.y;
      g.yz = a0.yz * x12.xz + h.yz * x12.yw;
      return 130.0 * dot(m, g);
    }

    // Octave fbm for richer surface
    float fbm(vec2 p) {
      float v = 0.0;
      float a = 0.5;
      for (int i = 0; i < 4; i++) {
        v += a * snoise(p);
        p *= 2.0;
        a *= 0.5;
      }
      return v;
    }

    void main() {
      vec2 uv = (gl_FragCoord.xy - 0.5 * u_res) / min(u_res.x, u_res.y);
      float t = u_time * u_speed * 0.3;

      // Surface height via fbm
      float h = fbm(uv * 2.0 + vec2(t, t * 0.7)) * u_distortion;

      // Surface normal via gradient (finite differences)
      float eps = 0.01;
      float hx = fbm((uv + vec2(eps,0.0)) * 2.0 + vec2(t, t*0.7)) * u_distortion;
      float hy = fbm((uv + vec2(0.0,eps)) * 2.0 + vec2(t, t*0.7)) * u_distortion;
      vec3 normal = normalize(vec3((h - hx)/eps, (h - hy)/eps, 1.0));

      // Light direction
      float angle = radians(u_light_angle);
      vec3 lightDir = normalize(vec3(cos(angle), sin(angle), 0.6));

      // Specular metal reflection
      float diffuse = max(dot(normal, lightDir), 0.0);
      vec3 viewDir = vec3(0.0, 0.0, 1.0);
      vec3 reflectDir = reflect(-lightDir, normal);
      float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);

      // Iridescence factor (used by gold/iridescent presets)
      float irid = 0.5 + 0.5 * sin(h * 8.0 + t * 2.0);

      // Mix three colour stops based on diffuse + iridescence
      vec3 base = mix(u_color_a, u_color_b, diffuse);
      base = mix(base, u_color_c, irid * 0.5);

      // Metal gloss : add specular highlight
      vec3 finalColor = base + vec3(spec * 1.2);

      // Vignette subtle
      float vig = 1.0 - 0.4 * length(uv);
      finalColor *= vig;

      gl_FragColor = vec4(finalColor, 1.0);
    }
  `;

  // === Compile + link program ===
  function compile(type, src) {
    const sh = gl.createShader(type);
    gl.shaderSource(sh, src);
    gl.compileShader(sh);
    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
      console.error('Shader compile error:', gl.getShaderInfoLog(sh));
      gl.deleteShader(sh);
      return null;
    }
    return sh;
  }

  const vs = compile(gl.VERTEX_SHADER, VS);
  const fs = compile(gl.FRAGMENT_SHADER, FS);
  if (!vs || !fs) return;

  const prog = gl.createProgram();
  gl.attachShader(prog, vs);
  gl.attachShader(prog, fs);
  gl.linkProgram(prog);
  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    console.error('Program link error:', gl.getProgramInfoLog(prog));
    return;
  }
  gl.useProgram(prog);

  // === Fullscreen quad ===
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
  const aPos = gl.getAttribLocation(prog, 'a_pos');
  gl.enableVertexAttribArray(aPos);
  gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

  // === Uniforms ===
  const uRes = gl.getUniformLocation(prog, 'u_res');
  const uTime = gl.getUniformLocation(prog, 'u_time');
  const uSpeed = gl.getUniformLocation(prog, 'u_speed');
  const uDist = gl.getUniformLocation(prog, 'u_distortion');
  const uLight = gl.getUniformLocation(prog, 'u_light_angle');
  const uColA = gl.getUniformLocation(prog, 'u_color_a');
  const uColB = gl.getUniformLocation(prog, 'u_color_b');
  const uColC = gl.getUniformLocation(prog, 'u_color_c');

  // === Color presets ===
  const PALETTES = {
    silver:     { a: [0.10, 0.11, 0.13], b: [0.85, 0.88, 0.92], c: [0.55, 0.60, 0.70] },
    gold:       { a: [0.20, 0.13, 0.05], b: [0.95, 0.78, 0.30], c: [0.70, 0.45, 0.15] },
    iridescent: { a: [0.10, 0.05, 0.20], b: [0.30, 0.85, 0.95], c: [0.95, 0.40, 0.85] }
  };

  function syncCanvasSize() {
    const rect = canvas.parentElement.getBoundingClientRect();
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = Math.round(rect.width * dpr);
    canvas.height = Math.round(rect.height * dpr);
    canvas.style.width = rect.width + 'px';
    canvas.style.height = rect.height + 'px';
    gl.viewport(0, 0, canvas.width, canvas.height);
  }
  syncCanvasSize();
  window.addEventListener('resize', syncCanvasSize);

  // === Pause when offscreen (perf) ===
  let visible = true;
  if ('IntersectionObserver' in window) {
    new IntersectionObserver((entries) => {
      entries.forEach(e => visible = e.isIntersecting);
    }, { threshold: 0.01 }).observe(canvas);
  }

  // === Render loop ===
  const start = performance.now();
  function render() {
    if (visible) {
      const t = (performance.now() - start) / 1000;
      const pal = PALETTES[settings.color] || PALETTES.silver;
      gl.uniform2f(uRes, canvas.width, canvas.height);
      gl.uniform1f(uTime, t);
      gl.uniform1f(uSpeed, settings.speed);
      gl.uniform1f(uDist, settings.distortion);
      gl.uniform1f(uLight, settings.lightAngle);
      gl.uniform3fv(uColA, pal.a);
      gl.uniform3fv(uColB, pal.b);
      gl.uniform3fv(uColC, pal.c);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }
    requestAnimationFrame(render);
  }
  render();
  } // fin initMetal

  var el = document.getElementById('psLiquidMetal');
  if (el) initMetal(el);
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
settings.color 'silver' Palette active — 'silver' (gris-bleu métallique), 'gold' (doré chaud), 'iridescent' (violet → cyan → magenta). Modifier la constante JS dans l'IIFE.
settings.speed 1.0 Vitesse de défilement du champ FBM. 0.5 = très lent et premium ; 2.0 = agité et dynamique.
settings.distortion 0.6 Amplitude des ondulations. 0.2 = surface quasi-plate ; 1.0 = métal fortement perturbé.
settings.lightAngle 45 Direction de la lumière en degrés (passé à radians() dans le shader). 0 = rasante horizontale ; 90 = zénithale ; 180 = contre-jour.
PALETTES (objet JS) 3 presets Ajoutez une palette custom : PALETTES.copper = { a:[0.25,0.10,0.03], b:[0.80,0.45,0.20], c:[0.60,0.28,0.10] }, puis settings.color = 'copper'. Valeurs en linéaire [0,1] par canal.
Octaves FBM (ligne shader) 4 Dans le FS, la boucle for (int i=0;i<4;i++) — changer 4 par 2 (plus rapide, texture grossière) ou 6 (détail fin, GPU légèrement plus sollicité).

FAQ

La description parle de « Perlin » mais le code dit « simplex » — quelle est la différence ?

Le fragment shader implémente le bruit simplex 2D de Ken Perlin (variante Ashima Arts, MIT), pas le bruit de Perlin classique à grille carrée. Le simplex utilise une grille triangulaire : il élimine les artefacts directionnels des axes et est environ 2× moins coûteux en 2D. La carte catégorie emploie « Perlin » comme raccourci courant.

L'effet fonctionne-t-il sur GPU intégré (Intel Iris, Apple M-series) ?

Oui. Le shader est volontairement léger : highp float, 4 octaves FBM, un seul draw call, pas d'extension GL. Il tient 60 fps confortablement sur GPU intégré. L'IntersectionObserver suspend le rendu hors viewport pour ne pas consommer de ressources inutilement.

Peut-on animer <code>lightAngle</code> en temps réel pour un effet de rotation de lumière ?

settings.lightAngle est lu à chaque frame dans la boucle render. Un simple RAF externe qui fait settings.lightAngle += 0.3 produit une rotation continue de la lumière. Combiné à settings.color = 'gold', ça simule un métal doré qui tourne sous un projecteur.