Fermer

octobre 28, 2022

Contrôles de mouvement dans le navigateur

Contrôles de mouvement dans le navigateur


Si vous avez toujours voulu créer une application Web que vous pouvez contrôler avec des gestes de la main comme par magie, cet article est pour vous. Avec quelques API et du JavaScript, vous pouvez créer des applications qui se comportent comme de la sorcellerie.

Dans cet article, je vais vous expliquer comment implémenter les commandes de mouvement dans le navigateur. Cela signifie que vous serez capable de créer une application où vous pouvez déplacer votre main et faire des gestes, et les éléments à l’écran répondront.

Voici un exemple :

Voir le stylo [Magic Hand – Motion controls for the web [forked]](https://codepen.io/smashingmag/pen/vYrEEYw) par .

Voir le stylo Magic Hand – Commandes de mouvement pour le Web [forked] par .

Quoi qu’il en soit, il y a quelques ingrédients principaux dont vous aurez besoin pour que les commandes de mouvement fonctionnent pour vous :

  1. Données vidéo d’une webcam ;
  2. Apprentissage automatique pour suivre les mouvements de la main ;
  3. Logique de détection de geste.

Noter: Cet article suppose une familiarité générale avec HTML, CSS et JavaScript, donc si vous avez cela, nous pouvons commencer. Notez également que vous devrez peut-être cliquer sur les démos CodePen au cas où des aperçus apparaîtraient vides (autorisations de caméra non accordées).

Étape 1 : Obtenir les données vidéo

La première étape de la création de commandes de mouvement consiste à accéder à la caméra de l’utilisateur. Nous pouvons le faire en utilisant le navigateur getMediaDevices API.

Voici un exemple qui récupère les données de l’appareil photo de l’utilisateur et les dessine dans un <canvas> toutes les 100 millisecondes :

Voir le stylo [Camera API test (MediaDevices) [forked]](https://codepen.io/smashingmag/pen/QWxwwbG) par .

Voir le stylo Test de l’API de caméra (MediaDevices) [forked] par .

Dans l’exemple ci-dessus, ce code vous donne les données vidéo et les dessine sur le canevas :

const constraints = {
  audio: false, video: { width, height }
};

navigator.mediaDevices.getUserMedia(constraints)
  .then(function(mediaStream) {
    video.srcObject = mediaStream;
    video.onloadedmetadata = function(e) {
      video.play();
      setInterval(drawVideoFrame, 100);
    };
  })
  .catch(function(err) { console.log(err); });

function drawVideoFrame() {
  context.drawImage(video, 0, 0, width, height);
  // or do other stuff with the video data
}

Quand tu cours getUserMedia, le navigateur commence à enregistrer les données de la caméra après avoir demandé l’autorisation à l’utilisateur. La constraints Le paramètre vous permet d’indiquer si vous souhaitez inclure de la vidéo et de l’audio et, si vous avez de la vidéo, quelle doit être sa résolution.

Les données de la caméra se présentent sous la forme d’un objet connu sous le nom de MediaStreamque vous pouvez ensuite insérer dans un code HTML <video> élément via son srcObject propriété. Une fois que la vidéo est prête à être lancée, vous la démarrez, puis faites ce que vous voulez avec les données d’image. Dans ce cas, l’exemple de code dessine une image vidéo sur le canevas toutes les 100 millisecondes.

Vous pouvez créer plus d’effets de canevas avec vos données vidéo, mais pour les besoins de cet article, vous en savez assez pour passer à l’étape suivante.

Plus après saut! Continuez à lire ci-dessous ↓

Étape 2 : Suivre les mouvements de la main

Maintenant que vous pouvez accéder aux données image par image d’un flux vidéo à partir d’une webcam, la prochaine étape de votre quête pour créer des commandes de mouvement consiste à déterminer où se trouvent les mains de l’utilisateur. Pour cette étape, nous aurons besoin de machine learning.

Pour que cela fonctionne, j’ai utilisé une bibliothèque d’apprentissage automatique open source de Google appelée MediaPipe. Cette bibliothèque prend des données d’images vidéo et vous donne les coordonnées de plusieurs points (également appelés landmarks) sur vos mains.

C’est la beauté des bibliothèques d’apprentissage automatique : une technologie complexe n’a pas besoin d’être complexe à utiliser.

Voici la bibliothèque en action :

Voir le stylo [MediaPipe Test [forked]](https://codepen.io/smashingmag/pen/XWYJJpY) par .

Voir le stylo Test MediaPipe [forked] par .
Coordonnées de la main rendues sur une toile
Coordonnées de la main rendues sur une toile. (Grand aperçu)

Voici un passe-partout pour commencer (adapté de MediaPipe’s Exemple d’API JavaScript):

<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script>

<video class="input_video"></video>
<canvas class="output_canvas" width="1280px" height="720px"></canvas>

<script>
const videoElement = document.querySelector('.input_video');
const canvasElement = document.querySelector('.output_canvas');
const canvasCtx = canvasElement.getContext('2d');

function onResults(handData) {
  drawHandPositions(canvasElement, canvasCtx, handData);
}

function drawHandPositions(canvasElement, canvasCtx, handData) {
  canvasCtx.save();
  canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
  canvasCtx.drawImage(
      handData.image, 0, 0, canvasElement.width, canvasElement.height);
  if (handData.multiHandLandmarks) {
    for (const landmarks of handData.multiHandLandmarks) {
      drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS,
                     {color: '#00FF00', lineWidth: 5});
      drawLandmarks(canvasCtx, landmarks, {color: '#FF0000', lineWidth: 2});
    }
  }
  canvasCtx.restore();
}

const hands = new Hands({locateFile: (file) => {
  return `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`;
}});
hands.setOptions({
  maxNumHands: 1,
  modelComplexity: 1,
  minDetectionConfidence: 0.5,
  minTrackingConfidence: 0.5
});
hands.onResults(onResults);

const camera = new Camera(videoElement, {
  onFrame: async () => {
    await hands.send({image: videoElement});
  },
  width: 1280,
  height: 720
});
camera.start();

</script>

Le code ci-dessus fait ce qui suit :

  • Chargez le code de la bibliothèque ;
  • Commencez à enregistrer les images vidéo ;
  • Lorsque les données de la main arrivent, dessinez les repères de la main sur une toile.

Regardons de plus près le handData objet puisque c’est là que la magie opère. À l’intérieur handData est multiHandLandmarks, une collection de 21 coordonnées pour les parties de chaque main détectées dans le flux vidéo. Voici comment ces coordonnées sont structurées :

{
  multiHandLandmarks: [
    // First detected hand.
    [
      {x: 0.4, y: 0.8, z: 4.5},
      {x: 0.5, y: 0.3, z: -0.03},
      // ...etc.
    ],

    // Second detected hand.
    [
      {x: 0.4, y: 0.8, z: 4.5},
      {x: 0.5, y: 0.3, z: -0.03},
      // ...etc.
    ],

    // More hands if other people participate.
  ]
}

Quelques remarques :

  • La première main ne signifie pas nécessairement la main droite ou la main gauche ; c’est juste celui que l’application détecte en premier. Si vous souhaitez obtenir une main spécifique, vous devrez vérifier quelle main est détectée à l’aide de handData.multiHandedness[0].label et éventuellement échanger les valeurs si votre caméra n’est pas en miroir.
  • Pour des raisons de performances, vous pouvez limiter le nombre maximum de mains à suivre, ce que nous avons fait précédemment en définissant maxNumHands: 1.
  • Les coordonnées sont fixées sur une échelle de 0 à 1 en fonction de la taille de la toile.

Voici une représentation visuelle des coordonnées de la main :

Une carte de points numérotés sur une main
Une carte de points numérotés sur une main. (La source: github.io) (Grand aperçu)

Maintenant que vous avez les coordonnées du point de repère de la main, vous pouvez créer un curseur pour suivre votre index. Pour ce faire, vous devrez obtenir les coordonnées de l’index.

Vous pouvez utiliser le tableau directement comme ceci handData.multiHandLandmarks[0][5]mais je trouve cela difficile à suivre, donc je préfère étiqueter les coordonnées comme ceci :

const handParts = {
  wrist: 0,
  thumb: { base: 1, middle: 2, topKnuckle: 3, tip: 4 },
  indexFinger: { base: 5, middle: 6, topKnuckle: 7, tip: 8 },
  middleFinger: { base: 9, middle: 10, topKnuckle: 11, tip: 12 },
  ringFinger: { base: 13, middle: 14, topKnuckle: 15, tip: 16 },
  pinky: { base: 17, middle: 18, topKnuckle: 19, tip: 20 },
};

Et puis vous pouvez obtenir les coordonnées comme ceci :

const firstDetectedHand = handData.multiHandLandmarks[0];
const indexFingerCoords = firstDetectedHand[handParts.index.middle];

J’ai trouvé le mouvement du curseur plus agréable à utiliser avec la partie médiane de l’index plutôt qu’avec la pointe car le milieu est plus stable.

Vous devez maintenant créer un élément DOM à utiliser comme curseur. Voici le balisage :

<div class="cursor"></div>

Et voici les styles :

.cursor {
  height: 0px;
  width: 0px;
  position: absolute;
  left: 0px;
  top: 0px;
  z-index: 10;
  transition: transform 0.1s;
}

.cursor::after {
  content: '';
  display: block;
  height: 50px;
  width: 50px;
  border-radius: 50%;
  position: absolute;
  left: 0;
  top: 0;
  transform: translate(-50%, -50%);
  background-color: #0098db;
}

Quelques notes sur ces styles :

  • Le curseur est positionné de manière absolue afin qu’il puisse être déplacé sans affecter le flux du document.
  • La partie visuelle du curseur est dans le ::after pseudo-élément, et le transform s’assure que la partie visuelle du curseur est centrée autour des coordonnées du curseur.
  • Le curseur a un transition pour adoucir ses mouvements.

Maintenant que nous avons créé un élément curseur, nous pouvons le déplacer en convertissant les coordonnées de la main en coordonnées de page et en appliquant ces coordonnées de page à l’élément curseur.

function getCursorCoords(handData) {
  const { x, y, z } = handData.multiHandLandmarks[0][handParts.indexFinger.middle];
  const mirroredXCoord = -x + 1; /* due to camera mirroring */
  return { x: mirroredXCoord, y, z };
}

function convertCoordsToDomPosition({ x, y }) {
  return {
    x: `${x * 100}vw`,
    y: `${y * 100}vh`,
  };
}

function updateCursor(handData) {
  const cursorCoords = getCursorCoords(handData);
  if (!cursorCoords) { return; }
  const { x, y } = convertCoordsToDomPosition(cursorCoords);
  cursor.style.transform = `translate(${x}, ${y})`;
}

function onResults(handData) {
  if (!handData) { return; }
  updateCursor(handData);
}

Notez que nous utilisons le CSS transform propriété pour déplacer l’élément plutôt que left et top. C’est pour des raisons de performances. Lorsque le navigateur affiche une vue, il passe par une séquence d’étapes. Lorsque le DOM change, le navigateur doit redémarrer à l’étape de rendu appropriée. La transform La propriété répond rapidement aux modifications car elle est appliquée à la dernière étape plutôt qu’à l’une des étapes intermédiaires, et par conséquent le navigateur a moins de travail à répéter.

Maintenant que nous avons un curseur fonctionnel, nous sommes prêts à passer à autre chose.

Étape 3 : Détecter les gestes

La prochaine étape de notre voyage consiste à détecter les gestes, en particulier gestes de pincement.

Tout d’abord, qu’entendons-nous par un pincement? Dans ce cas, nous définirons un pincement comme un geste où le pouce et l’index sont suffisamment proches l’un de l’autre.

Pour désigner une pincée de code, nous pouvons regarder quand le x, yet z les coordonnées du pouce et de l’index ont une différence suffisamment petite entre eux. « Assez petit » peut varier en fonction du cas d’utilisation, alors n’hésitez pas à expérimenter différentes gammes. Personnellement, j’ai trouvé 0.08, 0.08et 0.11 être à l’aise pour x, yet z coordonnées, respectivement. Voici à quoi cela ressemble :

function isPinched(handData) {
  const fingerTip = handData.multiHandLandmarks[0][handParts.indexFinger.tip];
  const thumbTip = handData.multiHandLandmarks[0][handParts.thumb.tip];
  const distance = {
    x: Math.abs(fingerTip.x - thumbTip.x),
    y: Math.abs(fingerTip.y - thumbTip.y),
    z: Math.abs(fingerTip.z - thumbTip.z),
  };
  const areFingersCloseEnough = distance.x < 0.08 && distance.y < 0.08 && distance.z < 0.11;

  return areFingersCloseEnough;
}

Ce serait bien si c’était tout ce que nous avions à faire, mais hélas, ce n’est jamais aussi simple.

Que se passe-t-il lorsque vos doigts sont au bord d’une position de pincement ? Si nous ne faisons pas attention, la réponse est le chaos.

Avec de légers mouvements des doigts ainsi que des fluctuations dans la détection des coordonnées, notre programme peut rapidement alterner entre les états pincés et non pincés. Si vous essayez d’utiliser un geste de pincement pour « saisir » un élément à l’écran, vous pouvez imaginer à quel point il serait chaotique que l’élément alterne rapidement entre être ramassé et lâché.

Afin d’éviter que nos gestes de pincement ne causent le chaos, nous devrons introduire un léger délai avant d’enregistrer un changement d’un état pincé à un état non pincé ou vice versa. Cette technique s’appelle une debounceet la logique est la suivante :

  • Lorsque les doigts entrent dans un état pincé, démarrez une minuterie.
  • Si les doigts sont restés dans l’état pincé sans interruption pendant assez longtemps, enregistrez un changement.
  • Si l’état pincé est interrompu trop tôt, arrêtez le chronomètre et n’enregistrez pas de changement.

L’astuce est que le délai doit être suffisamment long pour être fiable mais suffisamment court pour se sentir rapide.

Nous reviendrons bientôt sur le code anti-rebond, mais nous devons d’abord nous préparer en suivant l’état de nos gestes :

const OPTIONS = {
  PINCH_DELAY_MS: 60,
};

const state = {
  isPinched: false,
  pinchChangeTimeout: null,
};

Ensuite, nous préparerons quelques événements personnalisés pour faciliter la réponse aux gestes :

const PINCH_EVENTS = {
  START: 'pinch_start',
  MOVE: 'pinch_move',
  STOP: 'pinch_stop',
};

function triggerEvent({ eventName, eventData }) {
  const event = new CustomEvent(eventName, { detail: eventData });
  document.dispatchEvent(event);
}

Nous pouvons maintenant écrire une fonction pour mettre à jour l’état pincé :

function updatePinchState(handData) {
  const wasPinchedBefore = state.isPinched;
  const isPinchedNow = isPinched(handData);
  const hasPassedPinchThreshold = isPinchedNow !== wasPinchedBefore;
  const hasWaitStarted = !!state.pinchChangeTimeout;

  if (hasPassedPinchThreshold && !hasWaitStarted) {
    registerChangeAfterWait(handData, isPinchedNow);
  }

  if (!hasPassedPinchThreshold) {
    cancelWaitForChange();
    if (isPinchedNow) {
      triggerEvent({
        eventName: PINCH_EVENTS.MOVE,
        eventData: getCursorCoords(handData),
      });
    }
  }
}

function registerChangeAfterWait(handData, isPinchedNow) {
  state.pinchChangeTimeout = setTimeout(() => {
    state.isPinched = isPinchedNow;
    triggerEvent({
      eventName: isPinchedNow ? PINCH_EVENTS.START : PINCH_EVENTS.STOP,
      eventData: getCursorCoords(handData),
    });
  }, OPTIONS.PINCH_DELAY_MS);
}

function cancelWaitForChange() {
  clearTimeout(state.pinchChangeTimeout);
  state.pinchChangeTimeout = null;
}

Voici ce que updatePinchState() fait:

  • Si les doigts ont dépassé le seuil de pincement en démarrant ou en arrêtant un pincement, nous allons démarrer une minuterie pour attendre et voir si nous pouvons enregistrer un changement d’état de pincement légitime.
  • Si l’attente est interrompue, cela signifie que le changement n’était qu’une fluctuation, nous pouvons donc annuler la minuterie.
  • Cependant, si la minuterie est ne pas interrompu, nous pouvons mettre à jour l’état pincé et déclencher l’événement de changement personnalisé correct, à savoir, pinch_start ou pinch_stop.
  • Si les doigts n’ont pas dépassé le seuil de changement de pincement et sont actuellement pincés, nous pouvons envoyer un message personnalisé pinch_move un événement.

Nous pouvons courir updatePinchState(handData) chaque fois que nous obtenons des données manuelles afin que nous puissions les mettre dans notre onResults fonctionner comme ceci :

function onResults(handData) {
  if (!handData) { return; }
  updateCursor(handData);
  updatePinchState(handData);
}

Maintenant que nous pouvons détecter de manière fiable un changement d’état de pincement, nous pouvons utiliser nos événements personnalisés pour définir le comportement que nous voulons lorsqu’un pincement est démarré, déplacé ou arrêté. Voici un exemple :

document.addEventListener(PINCH_EVENTS.START, onPinchStart);
document.addEventListener(PINCH_EVENTS.MOVE, onPinchMove);
document.addEventListener(PINCH_EVENTS.STOP, onPinchStop);

function onPinchStart(eventInfo) {
  const cursorCoords = eventInfo.detail;
  console.log('Pinch started', cursorCoords);
}

function onPinchMove(eventInfo) {
  const cursorCoords = eventInfo.detail;
  console.log('Pinch moved', cursorCoords);
}

function onPinchStop(eventInfo) {
  const cursorCoords = eventInfo.detail;
  console.log('Pinch stopped', cursorCoords);
}

Maintenant que nous avons expliqué comment réagir aux mouvements et aux gestes, nous avons tout ce dont nous avons besoin pour créer une application pouvant être contrôlée avec des mouvements de la main.

Voici quelques exemples:

Voir le stylo [Beam Sword – Fun with motion controls! [forked]](https://codepen.io/smashingmag/pen/WNybveM) par .

Voir le stylo Beam Sword – Amusez-vous avec les commandes de mouvement ! [forked] par .

Voir le stylo [Magic Quill – Air writing with motion controls [forked]](https://codepen.io/smashingmag/pen/OJEPVJj) par .

Voir le stylo Magic Quill – Écriture aérienne avec commandes de mouvement [forked] par .

J’ai également rassemblé d’autres démos de contrôle de mouvement, y compris cartes à jouer mobiles Et un plan d’appartement avec des images mobiles des meubles, et je suis sûr que vous pouvez penser à d’autres façons d’expérimenter cette technologie.

Conclusion

Si vous êtes arrivé jusqu’ici, vous avez vu comment implémenter des commandes de mouvement avec un navigateur et une webcam. Vous avez lu les données de la caméra à l’aide des API du navigateur, vous avez obtenu les coordonnées de la main via l’apprentissage automatique et vous avez détecté les mouvements de la main avec JavaScript. Avec ces ingrédients, vous pouvez créer toutes sortes d’applications contrôlées par le mouvement.

Quels cas d’utilisation allez-vous proposer ? Faites-moi savoir dans les commentaires!

Éditorial fracassant(yk, il)






Source link