در این مقاله قصد دارم نحوه پیاده سازی کنترل حرکت در مرورگر را توضیح دهم. این بدان معناست که می‌توانید برنامه‌ای ایجاد کنید که در آن می‌توانید دست خود را حرکت دهید و ژست‌ها را انجام دهید و عناصر روی صفحه پاسخ دهند.

در اینجا یک مثال است:

قلم را ببینید [Magic Hand – Motion controls for the web [forked]](https://codepen.io/smashingmag/pen/vYrEEYw) توسط یافی.

قلم را ببینید دست جادویی – کنترل حرکت برای وب [forked] توسط یافی.

به هر حال، برای اینکه کنترل‌های حرکتی برای شما کار کنند، به چند عنصر اصلی نیاز دارید:

  1. داده های ویدئویی از یک وب کم؛
  2. یادگیری ماشینی برای ردیابی حرکات دست؛
  3. منطق تشخیص ژست.

توجه داشته باشید: این مقاله آشنایی کلی با HTML، CSS و جاوا اسکریپت را فرض می‌کند، بنابراین اگر آن را دارید، می‌توانیم شروع کنیم. همچنین توجه داشته باشید که در صورتی که پیش‌نمایش‌ها خالی به نظر می‌رسند (مجوزهای دوربین داده نشده است) ممکن است لازم باشد روی دموهای CodePen کلیک کنید.

مرحله 1: داده های ویدئویی را دریافت کنید

اولین قدم برای ایجاد کنترل های حرکتی، دسترسی به دوربین کاربر است. ما می توانیم این کار را با استفاده از مرورگر انجام دهیم getMediaDevices API.

در اینجا یک مثال آورده شده است که داده های دوربین کاربر را دریافت می کند و آن را به a می کشاند <canvas> هر 100 میلی ثانیه:

قلم را ببینید [Camera API test (MediaDevices) [forked]](https://codepen.io/smashingmag/pen/QWxwwbG) توسط یافی.

قلم را ببینید تست API دوربین (MediaDevices) [forked] توسط یافی.

از مثال بالا، این کد داده های ویدیو را به شما می دهد و آن را به بوم می کشد:

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
}

وقتی می دوی getUserMedia، مرورگر پس از درخواست اجازه از کاربر شروع به ضبط داده های دوربین می کند. را constraints پارامتر به شما امکان می‌دهد مشخص کنید که آیا می‌خواهید ویدیو و صدا را در آن قرار دهید یا خیر و اگر ویدیو دارید، وضوح آن چقدر باشد.

داده های دوربین به عنوان یک شی شناخته می شود MediaStream، که سپس می توانید آن را در یک HTML پرتاب کنید <video> عنصر از طریق آن srcObject ویژگی. پس از آماده شدن ویدیو، آن را راه‌اندازی می‌کنید و سپس هر کاری را که می‌خواهید با داده‌های فریم انجام می‌دهید. در این مورد، مثال کد هر 100 میلی ثانیه یک فریم ویدئو را روی بوم می کشد.

شما می توانید جلوه های بوم بیشتری با داده های ویدیویی خود ایجاد کنید، اما برای اهداف این مقاله، به اندازه کافی می دانید که به مرحله بعدی بروید.

بیشتر بعد از پرش! ادامه مطلب زیر ↓

مرحله 2: ردیابی حرکات دست

اکنون که می‌توانید به داده‌های فریم به فریم فید ویدیویی از وب‌کم دسترسی داشته باشید، گام بعدی در تلاش شما برای ایجاد کنترل‌های حرکتی این است که بفهمید دست کاربر کجاست. برای این مرحله، به یادگیری ماشین نیاز داریم.

برای انجام این کار، من از یک کتابخانه یادگیری ماشین منبع باز گوگل به نام استفاده کردم MediaPipe. این کتابخانه داده‌های فریم ویدیویی را می‌گیرد و مختصات چندین نقطه را به شما می‌دهد (همچنین به نام landmarks) روی دستان شما

این زیبایی کتابخانه های یادگیری ماشینی است: استفاده از فناوری پیچیده لازم نیست پیچیده باشد.

در اینجا کتابخانه در حال عمل است:

قلم را ببینید [MediaPipe Test [forked]](https://codepen.io/smashingmag/pen/XWYJJpY) توسط یافی.

قلم را ببینید تست MediaPipe [forked] توسط یافی.
مختصات دست بر روی بوم ارائه شده است
مختصات دست بر روی بوم ارائه شده است. (پیش نمایش بزرگ)

در اینجا تعدادی دیگ بخار برای شروع وجود دارد (اقتباس شده از MediaPipe مثال JavaScript API):

<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>

کد بالا کارهای زیر را انجام می دهد:

  • بارگذاری کد کتابخانه؛
  • شروع به ضبط فریم های ویدئویی کنید.
  • وقتی داده های دست وارد می شوند، نشانه های دست را روی بوم بکشید.

بیایید نگاهی دقیق تر به آن بیندازیم handData اعتراض کنید زیرا جادو در آنجا اتفاق می افتد. داخل handData است multiHandLandmarks، مجموعه ای از 21 مختصات برای قسمت های هر دست که در فید ویدیویی شناسایی شده است. در اینجا نحوه ساختار آن مختصات آمده است:

{
  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.
  ]
}

چند نکته:

  • دست اول لزوماً به معنای دست راست یا چپ نیست. این فقط هر کدام را که برنامه اول اتفاق می افتد تشخیص دهد. اگر می‌خواهید دست خاصی داشته باشید، باید بررسی کنید که با استفاده از کدام دست شناسایی می‌شود handData.multiHandedness[0].label و اگر دوربین شما آینه نشده باشد، به طور بالقوه مقادیر را عوض کنید.
  • به دلایل عملکرد، می‌توانید حداکثر تعداد دست‌ها را برای ردیابی محدود کنید، که قبلاً با تنظیم انجام دادیم maxNumHands: 1.
  • مختصات در یک مقیاس از تنظیم شده است 0 به 1 بر اساس اندازه بوم.

در اینجا یک نمایش تصویری از مختصات دست است:

نقشه ای از نقاط شماره گذاری شده روی یک دست
نقشه ای از نقاط شماره گذاری شده روی یک دست. (منبع: github.io) (پیش نمایش بزرگ)

اکنون که مختصات نقطه عطف دست را دارید، می توانید مکان نما را برای دنبال کردن انگشت اشاره خود بسازید. برای انجام این کار، باید مختصات انگشت اشاره را بدست آورید.

شما می توانید از آرایه به طور مستقیم مانند این استفاده کنید handData.multiHandLandmarks[0][5]، اما پیگیری آن برای من سخت است، بنابراین ترجیح می دهم مختصات را اینگونه برچسب گذاری کنم:

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 },
};

و سپس می توانید مختصات را به صورت زیر بدست آورید:

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

به نظر من استفاده از حرکت مکان نما با قسمت میانی انگشت اشاره به جای نوک آن خوشایندتر بود زیرا وسط ثابت تر است.

اکنون باید یک عنصر DOM برای استفاده به عنوان مکان نما بسازید. این نشانه گذاری است:

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

و در اینجا سبک ها وجود دارد:

.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;
}

چند نکته در مورد این سبک ها:

  • مکان نما کاملاً در موقعیتی قرار دارد که می توان آن را بدون تأثیر بر جریان سند جابجا کرد.
  • قسمت بصری مکان نما در ::after شبه عنصر، و transform اطمینان حاصل می کند که قسمت بصری مکان نما حول مختصات مکان نما متمرکز شده است.
  • مکان نما دارای یک است transition برای صاف کردن حرکاتش

اکنون که یک عنصر مکان نما ایجاد کرده ایم، می توانیم با تبدیل مختصات عقربه به مختصات صفحه و اعمال آن مختصات صفحه به عنصر مکان نما، آن را جابه جا کنیم.

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);
}

توجه داشته باشید که ما از CSS استفاده می کنیم transform ویژگی برای جابجایی عنصر به جای left و top. این به دلایل عملکردی است. هنگامی که مرورگر یک نما را ارائه می دهد، از طریق a عبور می کند توالی مراحل. هنگامی که DOM تغییر می کند، مرورگر باید دوباره در مرحله رندر مربوطه شروع به کار کند. را transform ویژگی به سرعت به تغییرات پاسخ می دهد زیرا در آخرین مرحله به جای یکی از مراحل میانی اعمال می شود و بنابراین مرورگر کار کمتری برای تکرار دارد.

اکنون که یک مکان نما داریم، آماده حرکت هستیم.

مرحله 3: تشخیص حرکات

گام بعدی در سفر ما تشخیص ژست ها است، به ویژه حرکات نیشگون گرفتن.

اول، منظور ما از یک نیشگون گرفتن چیست؟ در این مورد، نیشگون گرفتن را به عنوان حرکتی تعریف می کنیم که در آن شست و سبابه به اندازه کافی به هم نزدیک هستند.

برای تعیین یک خرج کردن در کد، می‌توانیم به زمانی نگاه کنیم x، y، و z مختصات انگشت شست و سبابه به اندازه کافی تفاوت کمی بین آنها دارند. “به اندازه کافی کوچک” بسته به مورد استفاده می تواند متفاوت باشد، بنابراین با خیال راحت دامنه های مختلف را آزمایش کنید. من شخصا پیدا کردم 0.08، 0.08، و 0.11 برای راحت بودن x، y، و z مختصات، به ترتیب. در اینجا به نظر می رسد:

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;
}

خوب است اگر این تنها کاری باشد که باید انجام می دادیم، اما افسوس که هرگز به این سادگی نیست.

چه اتفاقی می‌افتد وقتی انگشتان شما در لبه موقعیت نیشگون قرار دارند؟ اگر مراقب نباشیم، پاسخ هرج و مرج است.

با حرکات جزئی انگشت و همچنین نوسانات در تشخیص مختصات، برنامه ما می‌تواند به سرعت بین حالت‌های نیشگون و نیشگون نشده تغییر کند. اگر می‌خواهید از یک حرکت نیشگون برای “برداشتن” یک آیتم روی صفحه استفاده کنید، می‌توانید تصور کنید که چقدر آشفته است که آیتم به سرعت بین برداشته شدن و انداختن متناوب شود.

برای جلوگیری از ایجاد هرج و مرج حرکات نیشگون گرفتن ما، باید قبل از ثبت تغییر از حالت نیشگون به حالت نیشگون گرفتن یا برعکس، کمی تأخیر ایجاد کنیم. این تکنیک الف نامیده می شود debounceو منطق به این صورت است:

  • هنگامی که انگشتان وارد حالت فشرده شدند، یک تایمر راه اندازی کنید.
  • اگر انگشتان به مدت کافی بدون وقفه در حالت نیشگون باقی مانده اند، تغییر را ثبت کنید.
  • اگر حالت فشرده خیلی زود قطع شد، تایمر را متوقف کنید و تغییری را ثبت نکنید.

ترفند این است که تاخیر باید آنقدر طولانی باشد که قابل اعتماد باشد اما آنقدر کوتاه باشد که سریع احساس شود.

به زودی به کد بازپرداخت خواهیم رسید، اما ابتدا باید با ردیابی وضعیت حرکات خود آماده شویم:

const OPTIONS = {
  PINCH_DELAY_MS: 60,
};

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

در ادامه مقداری را آماده می کنیم رویدادهای سفارشی برای راحت کردن پاسخ دادن به حرکات:

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);
}

اکنون می توانیم یک تابع برای به روز رسانی حالت pinched بنویسیم:

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;
}

در اینجا چیست updatePinchState() انجام می دهد:

  • اگر انگشتان با شروع یا توقف یک پینچ از آستانه پینچ عبور کرده باشند، ما یک تایمر را شروع می کنیم تا منتظر بمانیم و ببینیم آیا می توانیم تغییر حالت پینچ قانونی را ثبت کنیم.
  • اگر انتظار قطع شد، به این معنی است که تغییر فقط یک نوسان بوده است، بنابراین می‌توانیم تایمر را لغو کنیم.
  • با این حال، اگر تایمر باشد نه قطع شد، می‌توانیم حالت پینچ شده را به‌روزرسانی کنیم و رویداد تغییر سفارشی صحیح را راه‌اندازی کنیم، یعنی، pinch_start یا pinch_stop.
  • اگر انگشتان از آستانه تغییر نیشگون گرفتن عبور نکرده اند و در حال حاضر گیر کرده اند، می توانیم سفارشی ارسال کنیم. pinch_move رویداد.

می توانیم بدویم updatePinchState(handData) هر بار که داده های دستی به دست می آوریم تا بتوانیم آنها را در خود قرار دهیم onResults عملکرد مانند این:

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

اکنون که می‌توانیم به‌طور قابل‌اعتماد تغییر حالت پینچ را تشخیص دهیم، می‌توانیم از رویدادهای سفارشی خود برای تعریف هر رفتاری که می‌خواهیم هنگام شروع، جابه‌جایی یا توقف آن استفاده کنیم. در اینجا یک مثال است:

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);
}

اکنون که نحوه پاسخ دادن به حرکات و ژست‌ها را توضیح دادیم، همه چیزهایی که برای ساختن یک برنامه کاربردی که با حرکات دست قابل کنترل باشد، در اختیار داریم.

در اینجا چند نمونه آورده شده است:

قلم را ببینید [Beam Sword – Fun with motion controls! [forked]](https://codepen.io/smashingmag/pen/WNybveM) توسط یافی.

قلم را ببینید شمشیر پرتو – سرگرم کننده با کنترل های حرکت! [forked] توسط یافی.

قلم را ببینید [Magic Quill – Air writing with motion controls [forked]](https://codepen.io/smashingmag/pen/OJEPVJj) توسط یافی.

قلم را ببینید Magic Quill – نوشتن هوا با کنترل حرکت [forked] توسط یافی.

من همچنین چند دمو کنترل حرکت دیگر از جمله کارت های بازی متحرک و یک پلان طبقه آپارتمان با تصاویر متحرک مبلمان، و من مطمئن هستم که می توانید راه های دیگری برای آزمایش این فناوری در نظر بگیرید.

نتیجه

اگر تا اینجا پیش رفته اید، نحوه پیاده سازی کنترل های حرکتی با مرورگر و وب کم را دیده اید. شما داده‌های دوربین را با استفاده از APIهای مرورگر خوانده‌اید، مختصات دست را از طریق یادگیری ماشین دریافت کرده‌اید، و حرکات دست را با جاوا اسکریپت شناسایی کرده‌اید. با استفاده از این مواد، می توانید انواع برنامه های کاربردی با کنترل حرکت ایجاد کنید.

چه موارد استفاده ای خواهید داشت؟ در نظرات به من اطلاع دهید!

سرمقاله Smashing
(yk, il)