Trong bài viết này, tôi sẽ giải thích cách triển khai điều khiển chuyển động trong trình duyệt. Điều đó có nghĩa là bạn sẽ có thể tạo một ứng dụng cho phép bạn di chuyển tay và thực hiện cử chỉ, và các thành phần trên màn hình sẽ phản hồi.
Đây là một ví dụ:
Xem Bút [Bàn tay ma thuật - Điều khiển chuyển động cho web [phân nhánh]](https://codepen.io/smashingmag/pen/vYrEEYw) của Yaphi.
Xem Bút Bàn tay ma thuật - Điều khiển chuyển động cho web [phân nhánh] của Yaphi.
Dù sao đi nữa, có một số thành phần chính mà bạn sẽ cần để tạo điều khiển chuyển động cho mình:
Sau đây là một ví dụ lấy dữ liệu camera của người dùng và vẽ nó vào
Xem Bút [Kiểm tra API camera (MediaDevices) [phân nhánh]](https://codepen.io/smashingmag/pen/QWxwwbG) của Yaphi.
Xem Bút Kiểm tra API Camera (MediaDevices) [phân nhánh] của Yaphi.
Từ ví dụ trên, mã này cung cấp cho bạn dữ liệu video và vẽ nó vào canvas:
Khi bạn chạy
Dữ liệu camera xuất hiện dưới dạng đối tượng được gọi là
Bạn có thể tạo thêm nhiều hiệu ứng canvas hơn bằng dữ liệu video của mình, nhưng đối với mục đích của bài viết này, bạn đã biết đủ để chuyển sang bước tiếp theo.
Để thực hiện được điều này, tôi đã sử dụng một thư viện máy học nguồn mở từ Google có tên là MediaPipe. Thư viện này lấy dữ liệu khung hình video và cung cấp cho bạn tọa độ của nhiều điểm (còn được gọi là
Xem Bút [Kiểm tra MediaPipe [phân nhánh]](https://codepen.io/smashingmag/pen/XWYJJpY) của Yaphi.
Xem Bút Kiểm tra MediaPipe [phân nhánh] của Yaphi.

Dưới đây là một số mẫu để bắt đầu (được chuyển thể từ ví dụ về API JavaScript của MediaPipe):
Đoạn mã trên thực hiện các thao tác sau:
Một vài lưu ý:

Bây giờ bạn đã có tọa độ điểm mốc của bàn tay, bạn có thể tạo con trỏ để theo dõi ngón trỏ của mình. Để làm được điều đó, bạn sẽ cần lấy tọa độ của ngón trỏ.
Bạn có thể sử dụng mảng trực tiếp như thế này
Sau đó, bạn có thể lấy tọa độ như thế này:
Tôi thấy chuyển động của con trỏ dễ chịu hơn khi sử dụng phần giữa của ngón trỏ thay vì phần đầu ngón vì phần giữa ổn định hơn.
Bây giờ, bạn sẽ cần tạo một phần tử DOM để sử dụng làm con trỏ. Đây là mã đánh dấu:
Và đây là các kiểu:
Một vài lưu ý về các kiểu này:
Lưu ý rằng chúng ta đang sử dụng thuộc tính
Bây giờ chúng ta đã có con trỏ hoạt động, chúng ta đã sẵn sàng để tiếp tục.
Trước tiên, chúng ta muốn nói gì khi nói đến chụm? Trong trường hợp này, chúng ta sẽ định nghĩa chụm là cử chỉ mà ngón cái và ngón trỏ ở đủ gần nhau.
Để chỉ định chụm trong mã, chúng ta có thể xem khi tọa độ
Đây là một ví dụ:
Xem Bút [Bàn tay ma thuật - Điều khiển chuyển động cho web [phân nhánh]](https://codepen.io/smashingmag/pen/vYrEEYw) của Yaphi.
Xem Bút Bàn tay ma thuật - Điều khiển chuyển động cho web [phân nhánh] của Yaphi.
Dù sao đi nữa, có một số thành phần chính mà bạn sẽ cần để tạo điều khiển chuyển động cho mình:
- Dữ liệu video từ webcam;
- Máy học để theo dõi chuyển động của tay;
- Logic phát hiện cử chỉ.
Bước 1: Lấy dữ liệu video
Bước đầu tiên để tạo điều khiển chuyển động là truy cập vào máy ảnh của người dùng. Chúng ta có thể thực hiện điều đó bằng cách sử dụnggetMediaDevices
API của trình duyệt.Sau đây là một ví dụ lấy dữ liệu camera của người dùng và vẽ nó vào
sau mỗi 100 mili giây:Xem Bút [Kiểm tra API camera (MediaDevices) [phân nhánh]](https://codepen.io/smashingmag/pen/QWxwwbG) của Yaphi.
Xem Bút Kiểm tra API Camera (MediaDevices) [phân nhánh] của Yaphi.
Từ ví dụ trên, mã này cung cấp cho bạn dữ liệu video và vẽ nó vào canvas:
Mã:
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); // hoặc thực hiện các thao tác khác với dữ liệu video}
getUserMedia
, trình duyệt sẽ bắt đầu ghi lại dữ liệu camera sau khi yêu cầu người dùng cấp quyền. Tham số constraints
cho phép bạn chỉ định xem bạn có muốn bao gồm video và âm thanh hay không và nếu có video, bạn muốn độ phân giải của video là bao nhiêu.Dữ liệu camera xuất hiện dưới dạng đối tượng được gọi là
MediaStream
, sau đó bạn có thể đưa vào phần tử HTML
thông qua thuộc tính srcObject
của nó. Khi video đã sẵn sàng, bạn khởi động video và sau đó làm bất cứ điều gì bạn muốn với dữ liệu khung hình. Trong trường hợp này, ví dụ mã sẽ vẽ một khung video vào canvas sau mỗi 100 mili giây.Bạn có thể tạo thêm nhiều hiệu ứng canvas hơn bằng dữ liệu video của mình, nhưng đối với mục đích của bài viết này, bạn đã biết đủ để chuyển sang bước tiếp theo.
Bước 2: Theo dõi chuyển động của tay
Bây giờ bạn đã có thể truy cập dữ liệu từng khung của nguồn cấp video từ webcam, bước tiếp theo trong hành trình tạo điều khiển chuyển động của bạn là tìm ra vị trí tay của người dùng. Đối với bước này, chúng ta sẽ cần máy học.Để thực hiện được điều này, tôi đã sử dụng một thư viện máy học nguồn mở từ Google có tên là MediaPipe. Thư viện này lấy dữ liệu khung hình video và cung cấp cho bạn tọa độ của nhiều điểm (còn được gọi là
điểm mốc
) trên tay bạn.Đây là thư viện đang hoạt động:
Xem Bút [Kiểm tra MediaPipe [phân nhánh]](https://codepen.io/smashingmag/pen/XWYJJpY) của Yaphi.
Xem Bút Kiểm tra MediaPipe [phân nhánh] của Yaphi.

Dưới đây là một số mẫu để bắt đầu (được chuyển thể từ ví dụ về API JavaScript của MediaPipe):
Mã:
const videoElement = document.querySelector('.input_video');const canvasElement = document.querySelector('.output_canvas');const canvasCtx = canvasElement.getContext('2d');hàm onResults(handData) { drawHandPositions(canvasElement, canvasCtx, handData);}hàm 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 landmark của handData.multiHandLandmarks) { drawConnectors(canvasCtx, landmark, HAND_CONNECTIONS, {color: '#00FF00', lineWidth: 5}); drawLandmarks(canvasCtx, landmark, {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();
- Tải mã thư viện;
- Bắt đầu ghi các khung hình video;
- Khi dữ liệu bàn tay xuất hiện, hãy vẽ các điểm mốc bàn tay trên một khung vẽ.
handData
vì đó là nơi phép thuật xảy ra. Bên trong handData
là multiHandLandmarks
, một tập hợp 21 tọa độ cho các phần của mỗi bàn tay được phát hiện trong nguồn cấp dữ liệu video. Sau đây là cách các tọa độ đó được cấu trúc:
Mã:
{ multiHandLandmarks: [ // Bàn tay đầu tiên được phát hiện. [ {x: 0,4, y: 0,8, z: 4,5}, {x: 0,5, y: 0,3, z: -0,03}, // ...v.v. ], // Bàn tay thứ hai được phát hiện. [ {x: 0,4, y: 0,8, z: 4,5}, {x: 0,5, y: 0,3, z: -0,03}, // ...v.v. ], // Nhiều bàn tay hơn nếu những người khác tham gia. ]}
- Bàn tay đầu tiên không nhất thiết có nghĩa là bàn tay phải hoặc bàn tay trái; chỉ là bất kỳ ứng dụng nào phát hiện ra đầu tiên. Nếu bạn muốn có một bàn tay cụ thể, bạn sẽ cần kiểm tra bàn tay nào đang được phát hiện bằng cách sử dụng
handData.multiHandedness[0].label
và có khả năng hoán đổi các giá trị nếu máy ảnh của bạn không được phản chiếu. - Vì lý do hiệu suất, bạn có thể hạn chế số lượng bàn tay tối đa để theo dõi, điều mà chúng tôi đã thực hiện trước đó bằng cách đặt
maxNumHands: 1
. - Các tọa độ được đặt theo thang điểm từ
0
đến1
dựa trên kích thước của canvas.

Bây giờ bạn đã có tọa độ điểm mốc của bàn tay, bạn có thể tạo con trỏ để theo dõi ngón trỏ của mình. Để làm được điều đó, bạn sẽ cần lấy tọa độ của ngón trỏ.
Bạn có thể sử dụng mảng trực tiếp như thế này
handData.multiHandLandmarks[0][5]
, nhưng tôi thấy khó theo dõi nên tôi thích dán nhãn tọa độ như thế này:
Mã:
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 },};
Mã:
const firstDetectedHand = handData.multiHandLandmarks[0];const indexFingerCoords = firstDetectedHand[handParts.index.middle];
Bây giờ, bạn sẽ cần tạo một phần tử DOM để sử dụng làm con trỏ. Đây là mã đánh dấu:
Mã:
Mã:
.cursor { height: 0px; width: 0px; position: absolute; trái: 0px; trên cùng: 0px; chỉ số z: 10; chuyển đổi: biến đổi 0,1 giây;}.cursor::after { nội dung: ''; hiển thị: khối; chiều cao: 50px; chiều rộng: 50px; bán kính đường viền: 50%; vị trí: tuyệt đối; trái: 0; trên cùng: 0; biến đổi: dịch chuyển (-50%, -50%); màu nền: #0098db;}
- Con trỏ được định vị tuyệt đối để có thể di chuyển mà không ảnh hưởng đến luồng của tài liệu.
- Phần trực quan của con trỏ nằm trong phần tử giả
::after
vàtransform
đảm bảo phần trực quan của con trỏ được căn giữa xung quanh tọa độ của con trỏ. - Con trỏ có
chuyển tiếp
để làm mượt các chuyển động của nó.
Mã:
function getCursorCoords(handData) { const { x, y, z } = handData.multiHandLandmarks[0][handParts.indexFinger.middle]; const mirroredXCoord = -x + 1; /* do phản chiếu camera */ trả về { x: mirroredXCoord, y, z };}hàm convertCoordsToDomPosition({ x, y }) { trả về { x: `${x * 100}vw`, y: `${y * 100}vh`, };}hàm updateCursor(handData) { const cursorCoords = getCursorCoords(handData); if (!cursorCoords) { trả về; } const { x, y } = convertCoordsToDomPosition(cursorCoords); cursor.style.transform = `translate(${x}, ${y})`;}hàm onResults(handData) { nếu (!handData) { trả về; } updateCursor(handData);}
transform
của CSS để di chuyển phần tử thay vì left
và top
. Điều này là vì lý do hiệu suất. Khi trình duyệt hiển thị chế độ xem, nó sẽ trải qua chuỗi các bước. Khi DOM thay đổi, trình duyệt phải bắt đầu lại ở bước hiển thị có liên quan. Thuộc tính transform
phản hồi nhanh với các thay đổi vì nó được áp dụng ở bước cuối cùng chứ không phải một trong các bước ở giữa, do đó trình duyệt có ít công việc phải lặp lại hơn.Bây giờ chúng ta đã có con trỏ hoạt động, chúng ta đã sẵn sàng để tiếp tục.
Bước 3: Phát hiện cử chỉ
Bước tiếp theo trong hành trình của chúng ta là phát hiện cử chỉ, cụ thể là cử chỉ chụm.Trước tiên, chúng ta muốn nói gì khi nói đến chụm? Trong trường hợp này, chúng ta sẽ định nghĩa chụm là cử chỉ mà ngón cái và ngón trỏ ở đủ gần nhau.
Để chỉ định chụm trong mã, chúng ta có thể xem khi tọa độ
x
, y
và z
của ngón cái và ngón trỏ có sự khác biệt đủ nhỏ giữa chúng. “Đủ nhỏ” có thể thay đổi tùy theo trường hợp sử dụng, vì vậy hãy thoải mái thử nghiệm với các phạm vi khác nhau. Cá nhân tôi thấy 0.08
, 0.08
và 0.11
tương ứng với các tọa độ x
, y
và z
. Sau đây là cách nó trông như thế nào:
Mã:
hàm 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