Saisissez la pierre, élancez-vous, lâchez avant la ligne rougeDans la maison ! à 40 cm du bouton · 2 points
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.
Mis à jour le
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
Saisissez le canapé et lancez-le vers un murContre le mur est · reste 1,20 m de passage vers le lit
Colis N-4417 : lancez-le dans son couloirCouloir Nord ✓ +50 points
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
divsans rôle ni texte. Annoncez le résultat du geste en texte, dans un élémentrole="status"(« Pierre dans la maison, à 40 cm du bouton »), comme le font les exemples. - Toucher :
touch-action: nones'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.
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.
<div class="md-inertia-stage">
<div class="md-inertia">
<div class="md-inertia-box"></div>
</div>
</div>
.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;
}
(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éfaut | Effet |
|---|---|---|
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.