JavaScript Obtenir les Coordonnees de la Souris
Chaque tutoriel vous dit que clientX existe. Presque aucun ne vous montre pourquoi il donne un nombre different de offsetX -- avec la preuve juste devant vous.
Demo en Direct : Quatre Systemes de Coordonnees a la Fois
Bougez la souris sur la zone grilleciulee ci-dessous. Les quatre systemes de coordonnees se mettent a jour simultanement. C'est le moyen le plus rapide de comprendre la difference entre eux -- vous pouvez litteralement voir screenX rester plus grand que clientX parce qu'il compte depuis le coin de votre moniteur physique, pas du viewport du navigateur.
Bougez la souris sur cette zone
pageY augmente mais clientY reste le meme.Les Bases : mousemove en Une Minute
Chaque coordonnee de souris en JavaScript commence par l'evenement mousemove. Vous attachez un ecouteur, lisez l'objet d'evenement et vous obtenez des valeurs X et Y. C'est tout le fondement :
document.addEventListener('mousemove', function(e) {
console.log(e.clientX, e.clientY);
});
L'objet d'evenement e transporte une pile de proprietes de coordonnees. Les quatre que vous utiliserez vraiment sont offsetX, clientX, pageX et screenX (plus leurs contreparties Y). Toutes decrivent la meme position physique de la souris, mais mesurees depuis differents points de reference.
L'erreur que font la plupart des tutoriels : ils vous montrent clientX et s'arretent la. Mais quand vous construisez un vrai glisser-deposer, un outil de dessin sur canvas ou un systeme de positionnement d'infobulle, choisir la mauvaise propriete de coordonnees cause des bugs subtils et frustrants. La demo ci-dessus existe pour que vous n'ayez plus jamais a deviner.
offsetX vs clientX vs pageX vs screenX : Lequel ?
Voici le tableau de comparaison qui aurait du etre sur chaque page de tutoriel mais ne l'etait pas. Chaque propriete mesure la meme position du curseur depuis un point d'origine different :
| Propriete | Point de Reference | Mieux Pour | Change au Scroll ? |
|---|---|---|---|
offsetX / offsetY | Le bord de padding de l'element cible (l'element sous le curseur) | Effets de survol, interactions locales a l'element | Non |
clientX / clientY | Le coin superieur gauche du viewport du navigateur | Positionner infobulles, menus deroulants, modales relatif a la page visible | Non |
pageX / pageY | Le coin superieur gauche du document complet (y compris la zone defilee) | Dessiner sur canvas, ancrer des elements qui doivent rester au scroll | Oui |
screenX / screenY | Le coin superieur gauche de votre ecran physique (le moniteur) | Applications multi-fenetres, extensions de navigateur, rarement en web normal | Non |
Ce qui perturbe les gens : pageX inclut l'offset de defilement, clientX non. Si votre page fait 3000px de haut et que vous defilez de 1000px vers le bas, alors pageY sera environ 1000 de plus que clientY a la meme position du curseur. La demo en direct ci-dessus le prouve -- defilez et observez l'ecart.
Regle generale : utilisez clientX/clientY pour 90% des taches de positionnement d'UI. Utilisez offsetX quand vous ne vous souciez que de l'element sous le curseur. Utilisez pageX quand vous avez besoin de coordonnees ancrees au document (dessin canvas, elements en position absolue). N'utilisez presque jamais screenX en developpement web normal.
getBoundingClientRect() : le Couteau Suisse dont vous avez Besoin
Voici quelque chose que les tutoriels couvrent a peine : la fonction de coordonnees la plus utile en JavaScript n'est pas du tout une propriete d'evenement de souris. C'est Element.getBoundingClientRect(). Elle renvoie la taille et la position de n'importe quel element relatif au viewport :
var rect = element.getBoundingClientRect();
// rect.left, rect.top -- distance from viewport top-left
// rect.right, rect.bottom -- opposite edges
// rect.width, rect.height -- element dimensions
// All values include CSS border but NOT margin
Le vrai pouvoir : soustrayez rect.left de e.clientX pour obtenir la position de la souris relative a cet element, peu importe ou il se trouve sur la page. C'est ainsi que vous construisez des cibles de clic fiables, du dessin canvas et des zones de glissement :
element.addEventListener('mousemove', function(e) {
var rect = this.getBoundingClientRect();
var localX = e.clientX - rect.left; // X relative to this element
var localY = e.clientY - rect.top; // Y relative to this element
console.log('Mouse inside element:', localX, localY);
});
Ceci est plus fiable que offsetX dans les cas limites. Quand votre element a des elements enfants, offsetX peut soudainement sauter parce que la reference passe a l'enfant. L'approche getBoundingClientRect() vous donne toujours des coordonnees relatives a l'element qui vous interesse vraiment.
Transforms CSS : Ou les Coordonnees Cassent Silencieusement
Cette section est nee de vrais rapports de bug. Si vous appliquez transform: scale() ou transform: rotate() a un element, les proprietes de coordonnees commencent a vous mentir -- et la documentation le mentionne a peine.
transform: scale() fausse offsetX
Quand vous mettez a l'echelle un element a 50%, le navigateur reduit la taille visuelle mais offsetX rapporte des coordonnees dans l'espace de coordonnees mis a l'echelle. Si votre element fait 200px de large et est mis a l'echelle a 0.5, deplacer la souris au centre visuel donne offsetX = 50, pas 100. getBoundingClientRect() renvoie egalement des dimensions mises a l'echelle.
// WRONG: offsetX under scale() gives scaled coordinates
// If element is scaled to 0.5, offsetX is halved
element.style.transform = 'scale(0.5)';
element.addEventListener('mousemove', function(e) {
console.log(e.offsetX); // Reports in scaled space
});
// CORRECT: Use getBoundingClientRect for true visual position
element.addEventListener('mousemove', function(e) {
var rect = element.getBoundingClientRect();
var visualX = e.clientX - rect.left;
var visualY = e.clientY - rect.top;
// To get the unscaled coordinate:
var scaleX = element.offsetWidth / rect.width;
var realX = visualX * scaleX;
console.log('Unscaled X:', realX);
});
Elements SVG : offsetX se comporte differemment
En SVG, offsetX/offsetY peuvent referencer la racine SVG ou la forme enfant specifique selon le navigateur. L'approche fiable est getBoundingClientRect() ou createSVGPoint() :
// SVG: use createSVGPoint for accurate coordinates
var svg = document.querySelector('svg');
var pt = svg.createSVGPoint();
svg.addEventListener('pointermove', function(e) {
pt.x = e.clientX;
pt.y = e.clientY;
// Convert to SVG user space:
var svgPoint = pt.matrixTransform(
svg.getScreenCTM().inverse()
);
console.log('SVG coords:', svgPoint.x, svgPoint.y);
});
Pointer Events : Arretez d'Utiliser mousemove Seul
Si vous ecrivez encore des gestionnaires separes pour mousemove, touchmove et touchstart, vous faites trois fois le travail pour de moins bons resultats. Les Pointer Events unifient souris, tactile et stylet en une seule API :
// Old way: three event types, three handlers
element.addEventListener('mousemove', handleMouse);
element.addEventListener('touchmove', handleTouch); // different event shape!
element.addEventListener('touchstart', handleTouchStart);
// Modern way: one event type
element.addEventListener('pointermove', function(e) {
// Same properties as MouseEvent:
console.log(e.clientX, e.clientY, e.pointerType);
// pointerType: 'mouse', 'touch', or 'pen'
});
L'avantage cle : pointermove se declenche pour la souris, le doigt et le stylet avec la meme interface clientX/clientY. Fini le mappage de touches[0] vers un faux evenement de souris.
| Evenement | Souris | Tactile | Stylet | Notes |
|---|---|---|---|---|
mousemove | Oui | Non (emule) | Non | Legacy, le tactile emule mousemove avec delai |
touchmove | Non | Oui | Non | Utilise le tableau touches, API de forme differente |
pointermove | Oui | Oui | Oui | API unifiee, meme interface de coordonnees |
Le support des Pointer Events dans les navigateurs est desormais universel (97%+ mondial). Sauf si vous supportez de tres anciens navigateurs, pointermove est le bon choix. Ajoutez touch-action: none en CSS pour empecher le navigateur de detourner les gestes tactiles pour le defilement.
Performance : Throttle vs requestAnimationFrame
Un evenement mousemove peut se declencher plus de 100 fois par seconde sur un ordinateur rapide. Si votre gestionnaire fait des mises a jour DOM, des calculs de layout ou des redessins de canvas, vous allez detruire les performances. Deux solutions : le throttling et requestAnimationFrame.
Throttling (limiter les appels par seconde)
// Throttle: max one call per 16ms (~60fps)
var lastTime = 0;
element.addEventListener('mousemove', function(e) {
var now = Date.now();
if (now - lastTime < 16) return;
lastTime = now;
doExpensiveWork(e.clientX, e.clientY);
});
requestAnimationFrame (synchroniser avec le repaint du navigateur)
// rAF: batch updates to the next frame
var pendingEvent = null;
element.addEventListener('mousemove', function(e) {
pendingEvent = e; // Store latest, drop intermediates
});
function update() {
if (pendingEvent) {
doExpensiveWork(pendingEvent.clientX, pendingEvent.clientY);
pendingEvent = null;
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Lequel est meilleur ? requestAnimationFrame est l'approche superieure pour les mises a jour visuelles car il se synchronise avec le taux de rafraichissement de l'ecran et se met en pause automatiquement quand l'onglet n'est pas visible. Le throttling est plus simple pour les taches non visuelles comme le logging analytique. Pour tout ce qui deplace des pixels a l'ecran, utilisez rAF.
Impact mesure
Sur une page type avec un redraw de canvas dans le gestionnaire : le mousemove brut tombe a 12-15fps sous un mouvement intense de souris. Throttle a 60fps : fluide mais fait encore du travail redondant entre les frames. Avec rAF : reste a 60fps avec zero rendus gaspilles. La difference est visible dans les applications reelles.
Code de Production : Glisser-Deposer avec Limites
Voici l'implementation de glisser-deposer que nous aurions voulu avoir en debutant. Elle utilise pointermove (souris/tactile unifie), restreint l'element dans un conteneur et nettoie proprement les ecouteurs :
function makeDraggable(element, container) {
var isDragging = false;
var startX, startY, origLeft, origTop;
element.addEventListener('pointerdown', function(e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
var rect = element.getBoundingClientRect();
origLeft = rect.left;
origTop = rect.top;
element.setPointerCapture(e.pointerId);
e.preventDefault();
});
element.addEventListener('pointermove', function(e) {
if (!isDragging) return;
var deltaX = e.clientX - startX;
var deltaY = e.clientY - startY;
var newLeft = origLeft + deltaX;
var newTop = origTop + deltaY;
// Boundary clamping: keep element inside container
var cRect = container.getBoundingClientRect();
var eRect = element.getBoundingClientRect();
var maxLeft = cRect.right - eRect.width;
var maxTop = cRect.bottom - eRect.height;
newLeft = Math.max(cRect.left, Math.min(newLeft, maxLeft));
newTop = Math.max(cRect.top, Math.min(newTop, maxTop));
element.style.left = newLeft + 'px';
element.style.top = newTop + 'px';
});
element.addEventListener('pointerup', function(e) {
isDragging = false;
element.releasePointerCapture(e.pointerId);
});
}
Deux choses la rendent prete pour la production : setPointerCapture() assure que l'element continue de recevoir les evenements meme si le curseur le quitte en plein glissement, et la limitation des frontieres utilise getBoundingClientRect() pour respecter les tailles rendues reelles plutot que de deviner avec les valeurs CSS.
Mapping de Coordonnees Canvas
Dessiner sur un canvas necessite de mapper les coordonnees de la souris vers l'espace de pixels du canvas. Quand le canvas a une taille CSS differente de sa resolution interne (ce qui devrait etre le cas pour les ecrans HiDPI), l'approche naive casse :
var canvas = document.getElementById('my-canvas');
var ctx = canvas.getContext('2d');
// Set internal resolution for HiDPI
var dpr = window.devicePixelRatio || 1;
var rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
canvas.addEventListener('pointermove', function(e) {
var rect = canvas.getBoundingClientRect();
// CSS pixel coordinates (what you draw with):
var cssX = e.clientX - rect.left;
var cssY = e.clientY - rect.top;
// If you need raw canvas pixels:
var pixelX = cssX * dpr;
var pixelY = cssY * dpr;
ctx.fillRect(cssX - 2, cssY - 2, 4, 4); // Draw at cursor
});
La mise a l'echelle dpr (device pixel ratio) est essentielle sur les ecrans Retina/HiDPI. Sans elle, votre dessin canvas apparait flou et les clics de coordonnees sont legerement decales. Nous couvrons cela en profondeur dans notre guide des pixels physiques vs logiques.
FAQ : Pieges Courants
Pourquoi offsetX saute quand la souris croise un element enfant ?
offsetX est relatif a l'element directement sous le curseur. Quand vous passez d'un parent a un enfant, le point de reference saute au coin de l'enfant. Utilisez e.clientX - element.getBoundingClientRect().left pour des coordonnees stables relatives a l'element.
Les coordonnees de la souris fonctionnent-elles dans un iframe ?
Oui, mais screenX/screenY rapportent des valeurs relatives a la position de l'iframe dans la page parente, pas du vrai moniteur. clientX et pageX fonctionnent normalement dans le propre document de l'iframe. La traduction de coordonnees entre cadres necessite une communication postMessage.
Qu'arrive-t-il aux coordonnees dans Shadow DOM ?
Les evenements de souris sont recibles en traversant les frontieres de Shadow DOM. e.target peut pointer vers le shadow host plutot que vers l'element interne. event.composedPath() vous donne le vrai chemin de l'evenement a travers les frontieres shadow. offsetX/offsetY referencent le shadow host, pas l'element interne.
Les coordonnees sont-elles affectees par les transforms CSS sur les elements parents ?
Oui. getBoundingClientRect() renvoie la position visuellement transformee, donc si un parent a transform: rotate(45deg), le rect refletera le layout pivote. clientX/clientY sont toujours dans l'espace du viewport et ne sont pas affectes par les transforms, mais la position de l'element par rapport a ces coordonnees oui.
Qu'est-ce que movementX/movementY ?
Ces proprietes rapportent le delta (le changement) depuis le dernier evenement mousemove, pas une position absolue. Elles sont utiles pour implementer des choses comme le pointer lock (controles de camera style FPS) ou vous vous souciez de la distance parcourue par la souris, pas de sa position.
Dois-je utiliser pageX ou clientX ?
Utilisez clientX pour la plupart des positionnements d'UI (infobulles, modales, popovers) car il ignore l'offset de defilement. Utilisez pageX quand vous devez retenir une position a travers des changements de defilement (comme marquer un point sur un long document). La difference est seulement l'offset de defilement : pageX = clientX + window.scrollX.
Continuer: Maintenant que vous comprenez le cote JavaScript, voyez ces coordonnees en action dans differents contextes :
Detecteur de Position de Souris — Outil en direct montrant les quatre systemes de coordonnees avec capture par clic
Trouver les Coordonnees de Pixel — Methodes par plateforme pour Windows, macOS, Linux et navigateur
Pixels Physiques vs Logiques — Pourquoi vos coordonnees different entre pixels CSS et de peripherique
Guide des Coordonnees d'Ecran — Reference complete des concepts de coordonnees d'ecran