TransitionsGratuit

Inertia Momentum

Un cube de 50 px qu'on saisit à la souris, au doigt ou au stylet, et qu'on lance : relâché, il garde la vitesse du geste, ralentit de 5 % à chaque image et rebondit à mi-vitesse sur les bords — 2,2 Ko de JavaScript, aucune dépendance, posé là où on le lâche si l'utilisateur réduit les animations.

JSPhysiqueDrag

Mis à jour le

Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Transitions en contient 52 au total, dont 9 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Soirée curling · Les SaisiesManche 3 · votre pierre

Saisissez la pierre, élancez-vous, lâchez avant la ligne rouge

① Lancer d'une pierre de curling — mini-jeu d'une station de sports d'hiver

QuandPendant la soirée curling, le joueur saisit sa pierre sur la glace, prend son élan et la lâche avant la ligne rouge ; elle glisse et ralentit jusqu'à s'arrêter dans la maison.
PourquoiLa force du geste décide de la distance : l'inertie fait tout le jeu, sans jauge de puissance à régler.
RéglagesPiste de glace de 512 × 266 px, cible de 150 px ; objet en pierre ronde de 42 px avec poignée rouge ; friction et rebond d'origine (0,95, moitié de la vitesse) ; lancer de démonstration à 13 px par évènement, arrêt à 40 cm du bouton.
Studio, 4e étage · 24 m²Déménagement le 14 nov.

Saisissez le canapé et lancez-le vers un mur

② Canapé glissé contre un mur — appli de plan de déménagement

QuandSur le plan du studio, l'utilisateur attrape le canapé et le pousse d'un geste vers le mur est ; le canapé glisse, touche le mur et s'y arrête, et l'appli indique le passage restant vers le lit.
PourquoiLe meuble glisse et freine comme sur un parquet ; le rebond sur le bord montre la limite de la pièce sans message d'erreur.
RéglagesPièce de 420 × 260 px aux murs de 7 px, parquet en lames ; objet de 100 × 42 px en forme de canapé vu de dessus ; physique d'origine, les bords calculés sur la taille réelle du canapé ; lancer réglé pour toucher le mur presque à l'arrêt.
Quai 4 · exercice de triScore 1 240
NORD SUD

Colis N-4417 : lancez-le dans son couloir

③ Colis lancé vers son couloir — jeu de formation au quai logistique

QuandPendant l'exercice de tri, le cariste en formation saisit le colis N-4417 sur le tapis et le lance vers le haut, dans le couloir Nord ; le score monte quand le colis s'arrête dans la bonne zone.
PourquoiLe lancer avec inertie rend le geste de tri ludique et mémorisable : on vise, on dose, on voit le colis arriver.
RéglagesQuai de 512 × 270 px, deux couloirs de 150 px en pointillés, tapis rayé en bas ; colis de 54 × 42 px ; lancer vertical (déplacement de −9 px par évènement), arrêt contre le bord haut du couloir.

Comment ça marche

Le cadre .md-inertia-stage (250 px de haut, fond #1a1a24) contient la piste .md-inertia, qui occupe tout le cadre, et l'objet .md-inertia-box : 50 × 50 px, coins de 10 px, dégradé indigo vers cyan (#6366f1 → #06b6d4), curseur en main ouverte. Il est centré par le CSS (left et top à calc(50% - 25px)), donc à la bonne place même si le bloc est masqué au chargement et affiché plus tard. touch-action: none laisse le doigt saisir l'objet au lieu de faire défiler la page, user-select: none évite de sélectionner du texte en le traînant.

Le script parcourt chaque .md-inertia de la page et écoute les évènements pointer sur l'objet, qui couvrent souris, doigt et stylet. Au pointerdown, il lit la position réelle de l'objet (offsetLeft, offsetTop) et capture le pointeur (setPointerCapture) : l'objet suit même si le geste sort de lui. À chaque pointermove, la vitesse est le dernier déplacement, en pixels. Au pointerup, une boucle requestAnimationFrame prend le relais : la vitesse est multipliée par 0,95 à chaque image, l'objet avance d'autant, et s'arrête sous 0,1 px par image. Sur un bord, la vitesse s'inverse et tombe de moitié (vx *= -0.5), et la position est ramenée dans la piste — les limites se calculent avec la taille réelle de l'objet, quel qu'il soit. La glissade parcourt environ 19 fois la vitesse du lâcher : lâché à 12 px par image, l'objet repart d'environ 230 px.

Mesuré dans Chromium 151, Firefox 153 et WebKit 26, lancer de 72 px vers la droite à la souris : l'objet est lâché 72 px plus loin et glisse encore 226 px. Au doigt (évènements tactiles réels dans Chromium), le même lancer s'arrête exactement au même endroit. La distance ne dépend pas de la fréquence de l'écran ; la durée, si : environ 1,5 s à 60 images par seconde pour un lâcher à 12 px, moitié moins sur un écran à 120 Hz. Pendant la saisie, l'objet n'est pas retenu dans la piste : tiré au-delà du bord, il y revient au lâcher, y compris sous mouvement réduit, où il est replacé contre le bord sans glisser. Le cadre a sa propre classe, préfixée comme le reste de l'effet : deux effets de la bibliothèque collés sur la même page ne se réécrivent pas.

Accessibilité

  • Mouvement réduit : sous prefers-reduced-motion: reduce, l'objet suit toujours la main pendant la saisie (le mouvement vient de l'utilisateur), mais ne glisse plus au lâcher : il reste où on l'a posé, ou contre le bord s'il a été tiré hors de la piste. Mesuré dans Chromium 151, Firefox 153 et WebKit 26 : lâché à 447 px, toujours à 447 px deux secondes plus tard ; tiré 50 px sous le bas de la piste, replacé contre ce bord dès le lâcher.
  • Clavier : l'effet ne répond qu'au pointeur. Si le lancer compte (un jeu, un placement de meuble), donnez une autre voie : des boutons « lancer fort / doux », ou des flèches qui déplacent l'objet et la touche Entrée qui le lâche avec une vitesse fixée.
  • Lecteurs d'écran : l'objet est un div sans rôle ni texte. Annoncez le résultat du geste en texte, dans un élément role="status" (« Pierre dans la maison, à 40 cm du bouton »), comme le font les exemples.
  • Toucher : touch-action: none s'applique seulement à l'objet ; le reste de la piste laisse défiler la page. Gardez l'objet assez grand pour un doigt (44 px au moins).

Compatibilité navigateur

CSS + 2,2 Ko de JavaScript (Pointer Events, setPointerCapture, requestAnimationFrame), aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26 : même lancer, même glissade, même rebond, même arrêt net sous mouvement réduit ; geste au doigt vérifié dans Chromium avec de vrais évènements tactiles. Les Pointer Events existent depuis Chrome 55, Firefox 59 et Safari 13.

Chrome 55+✓ Complet
Firefox 59+✓ Complet
Safari 13+✓ Complet
Edge 79+✓ Complet
Mobile iOS 13+✓ Complet (au doigt)
Android Chrome 55+✓ Complet (au doigt)

Sans JavaScript, l'objet reste immobile au centre de sa piste : il se voit, mais ne se saisit pas. Sans Pointer Events (navigateurs antérieurs à 2019), même chose : prévoyez dans ce cas une action par bouton si le geste est indispensable.

Le code

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

HTML
<div class="md-inertia-stage">
  <div class="md-inertia">
    <div class="md-inertia-box"></div>
  </div>
</div>
CSS
.md-inertia-stage,
.md-inertia-stage *,
.md-inertia-stage *::before,
.md-inertia-stage *::after {
  box-sizing: border-box
}

.md-inertia-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.md-inertia {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

/* Centré par le CSS (et non mesuré au chargement) : juste même si le bloc est masqué au départ.
   touch-action: none laisse le doigt saisir le cube au lieu de faire défiler la page. */
.md-inertia-box {
  width: 50px;
  height: 50px;
  border-radius: 10px;
  background: linear-gradient(135deg, #6366f1, #06b6d4);
  cursor: grab;
  position: absolute;
  left: calc(50% - 25px);
  top: calc(50% - 25px);
  touch-action: none;
  user-select: none;
  transition: none;
}
JavaScript (fx-0729)
(function() {
  // Inertia Momentum : saisir, lancer, l'élément glisse puis ralentit (souris, doigt, stylet)
  var friction = 0.95;
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.md-inertia').forEach(function(container) {
    var box = container.querySelector('.md-inertia-box');
    if (!box) return;

    var x = 0,
      y = 0,
      vx = 0,
      vy = 0;
    var isDragging = false,
      lastX = 0,
      lastY = 0,
      raf = 0;

    function place() {
      box.style.left = x + 'px';
      box.style.top = y + 'px';
    }

    box.addEventListener('pointerdown', function(e) {
      isDragging = true;
      cancelAnimationFrame(raf);
      x = box.offsetLeft; // position réelle au moment de la saisie
      y = box.offsetTop;
      lastX = e.clientX;
      lastY = e.clientY;
      vx = 0;
      vy = 0;
      try { box.setPointerCapture(e.pointerId); } catch (err) {}
      box.style.cursor = 'grabbing';
      e.preventDefault();
    });

    box.addEventListener('pointermove', function(e) {
      if (!isDragging) return;
      vx = e.clientX - lastX;
      vy = e.clientY - lastY;
      x += vx;
      y += vy;
      place();
      lastX = e.clientX;
      lastY = e.clientY;
    });

    function release() {
      if (!isDragging) return;
      isDragging = false;
      box.style.cursor = 'grab';
      if (reduceMotion.matches) { // mouvement réduit : pas de glissade, l'élément est seulement ramené dans la piste
        x = Math.max(0, Math.min(container.clientWidth - box.offsetWidth, x));
        y = Math.max(0, Math.min(container.clientHeight - box.offsetHeight, y));
        place();
        return;
      }
      raf = requestAnimationFrame(animate);
    }
    box.addEventListener('pointerup', release);
    box.addEventListener('pointercancel', release);

    function animate() {
      if (isDragging) return;
      if (Math.abs(vx) < 0.1 && Math.abs(vy) < 0.1) return;
      vx *= friction;
      vy *= friction;
      x += vx;
      y += vy;
      var maxX = container.clientWidth - box.offsetWidth;
      var maxY = container.clientHeight - box.offsetHeight;
      if (x < 0 || x > maxX) vx *= -0.5;
      if (y < 0 || y > maxY) vy *= -0.5;
      x = Math.max(0, Math.min(maxX, x));
      y = Math.max(0, Math.min(maxY, y));
      place();
      raf = requestAnimationFrame(animate);
    }
  });
})();

Personnaliser

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

Option / propriétéDéfautEffet
Friction (JS — var friction = 0.95) 0,95 : la vitesse perd 5 % par image 0,98 donne une glace (distance ≈ 49 fois la vitesse du lâcher) ; 0,9 un tapis épais (≈ 9 fois). Au-delà de 0,99, l'objet traverse la piste au moindre geste.
Rebond sur les bords (JS — vx *= -0.5, vy *= -0.5) repart à la moitié de sa vitesse -0.8 donne un palet qui rebondit franchement ; 0 colle l'objet au bord, comme un meuble contre un mur.
Seuil d'arrêt (JS — 0.1) arrêt sous 0,1 px par image 0,5 arrête l'objet plus tôt, sans la longue fin presque immobile ; utile pour déclencher le résultat (score, message) plus vite.
Taille et habillage de l'objet (CSS — .md-inertia-box) 50 × 50 px, dégradé indigo-cyan Toute taille marche : le script lit offsetWidth et offsetHeight pour les bords. Si vous changez la taille, mettez à jour le centrage (calc(50% - moitié)).
Taille de la piste (CSS — .md-inertia-stage / .md-inertia) toute la largeur, 250 px de haut La piste peut être un plan, un terrain, une étagère : donnez-lui ses dimensions, l'objet reste dedans à la fin de chaque lancer, mouvement réduit compris.
Plusieurs objets un objet par .md-inertia Dupliquez le bloc entier : chaque piste a sa propre physique. Plusieurs objets dans une même piste demandent d'adapter le script (une boucle par objet), sans collision entre eux.

FAQ

Est-ce que ça marche au doigt ?

Oui. Le script écoute les évènements pointer, communs à la souris, au doigt et au stylet, et l'objet porte touch-action: none : posé sur lui, le doigt le saisit au lieu de faire défiler la page. Mesuré dans Chromium avec de vrais évènements tactiles : un lancer de 72 px finit au même endroit qu'à la souris. Ailleurs sur la piste, le doigt fait défiler normalement.

Comment savoir où l'objet s'est arrêté ?

La glissade s'arrête quand la vitesse passe sous 0,1 px par image : c'est la ligne if (Math.abs(vx) < 0.1 && Math.abs(vy) < 0.1) return;. Juste avant ce return, émettez un évènement (box.dispatchEvent(new CustomEvent('md-stop'))) et lisez box.offsetLeft / offsetTop dans votre page : c'est là que vous comptez les points ou affichez le message.

Pourquoi l'objet glisse-t-il plus vite sur certains écrans ?

La friction s'applique à chaque image : sur un écran à 120 Hz, il y a deux fois plus d'images par seconde, donc la glissade dure deux fois moins longtemps. La distance parcourue, elle, ne change pas (c'est la somme d'une suite qui ne dépend pas du rythme). Pour une durée identique partout, multipliez la vitesse par Math.pow(0.95, dt / 16.7), où dt est le temps écoulé depuis l'image précédente.