用姿态检测的方式为自己塑造骨架 2021-11-04 默认分类 暂无评论 2493 次阅读 **在这篇文章中,我们将向你展示如何通过用TensorFlow.js和姿势检测分析网络摄像头的画面,将你的身体实时转化为一个骨架。为了达到最大的幽灵效果,我们建议在10月31日的夜深人静时学习这个教程。** 我们最近做了一个有趣的万圣节实验,我们使用一种叫做姿势检测的东西,用你的身体来实时塑造你的身体的骨架。 你可以在这里自己尝试一下。[Trick or Treat][1] 在这篇文章中,我们将向你展示如何使用TensorFlow.js和姿势检测模型创建类似的东西。请看这里,看看你将建立什么:[https://skeletoniser.netlify.app][2] 如果你喜欢这类视频格式的指南,我们也会为你提供: 视频地址:https://youtu.be/CIG-1MMUZqE 什么是姿态检测? -------- 姿势检测到底是什么,为什么我可能想使用它?简单地说,它是拍摄图像或视频的过程,在画面中定位任何身体,然后找出它们的身体部位。 姿势检测有什么用?嗯,它可能会有大量的现实用途 你可以用它来创建一个瑜伽应用程序,检测你的姿势是否正确。你可以用它来分析你的跑步技术并提供反馈。你可以用它来创造互动艺术装置。在我们的案例中,我们决定在可能是最重要的用例上测试它......当然是把你变成一副骨架!"。 在我们开始之前 ------- 你可以在这里找到本教程的成品代码:https://github.com/pixelhop/skeletoniser 我们将使用Vite来捆绑我们的项目,并运行一个开发服务器,所以用以下方式启动一个新项目。 npm init vite@latest skeletoniser -- --template vanilla-ts 启动网络摄像头 ------- 因此,在有任何机会进行镂空之前,我们需要一些东西来进行镂空 我们需要做的第一件事是抓取网络摄像头的信号,以看到你身体上的幽灵。 创建HTML来显示网络摄像头的画面 ----------------- 为了看到网络摄像头,我们首先需要添加一个能够显示它的HTML元素。`<video>`元素是这项工作的完美工具。更新你的`index.html`以包含一个视频元素,如下所示。(删除现有的`<div id="app"></div>`) ```html Vite App ``` 让我们快速谈论一下我们添加到视频元素中的一些属性。首先,我们添加了`playsinline`。这个属性告诉移动浏览器,视频应该在页面上内联播放,而不是在全屏中打开的默认行为。 第二个属性是`autoplay="true"`。这个属性如其所言,意味着一旦有视频可用,视频元素就会自动播放,而不是默认的暂停。 现在我们有了一个视频元素,让我们引用它,以便在以后的代码中使用它。修改 src/main.ts,使其看起来像下面这样。 // src/main.ts import './style.css' const videoEl = document.querySelector('video')!; 你可能会注意到在querySelector调用之后的!。这告诉Typescript,视频元素肯定会存在,所以我们不需要考虑它可能在我们以后编写的代码中丢失的可能性。 **请求访问网络摄像头并附加到视频元素上** 我们的视频元素都已经设置好了,而且我们有一个对它的引用。下一步是让一些媒体在它上面播放。要做到这一点,我们需要请求一个MediaStream。 // src/main.ts async function initCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 }, }, audio: false, }); let onVideoReady: (ready: boolean) => void; const videoReadyPromise = new Promise((resolve) => onVideoReady = resolve); videoEl.onloadedmetadata = () => onVideoReady(true); videoEl.srcObject = stream; return videoReadyPromise; } async function start() { await initCamera(); } start(); 当我们调用 `initCamera` 函数时,用户将被提示允许访问他们的网络摄像头。我们向 `getUserMedia` 函数传递一个选项对象。 在视频对象中指定 `facingMode` 属性可以让浏览器知道,如果有多个摄像头,它应该选择面向用户的摄像头。在移动设备上,这应该意味着默认选择自拍相机。 我们还指定了 `width` 和 `height` 参数。这告诉浏览器我们想要接收的视频的理想尺寸。在这种情况下,如果可能的话,我们选择将其限制在640x480,这样姿势检测将是有效的。视频越大,姿势检测的资源就越密集,所以640x480是一个很好的媒介。 在获得数据流后,我们可以将其分配给我们先前创建的视频元素。在这之前,我们做一个新的承诺,允许我们在解决之前等待视频完全加载。在为视频分配了一个媒体流后,需要时间来初始化一切并实际加载该流。通过向解决我们承诺的视频元素添加一个 `onloadmetadata` 事件监听器,我们可以确保在视频准备好之前不会使用它。一旦所有这些都设置好了,就可以通过将流分配给视频元素的 `srcObject` 属性来将其添加到视频中。 希望到了这一步,你应该看到你的脸出现在页面上了! **用画布在视频上作画** 显示网络摄像头画面非常好,但我们需要开始考虑对其进行操作和添加。我们的目标是在画面上叠加一个骨架,所以我们需要在上面绘制图像。 画布是完成这项工作的完美工具。我们可以添加一个新的画布元素,并将其精确地定位在视频的顶部。 修改`index.html`,使其看起来像下面这样。 ```html Vite App ``` 我们在视频元素周围添加了一个包裹性的div。这是为了让我们能够将画布元素绝对地放在视频的顶部。要做到这一点,我们需要添加一些css ```css // style.css .container { position: relative; width: 640px; height: 480px; } canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } ``` 最后,让我们获得一个对canvas元素的引用,并获得一个渲染上下文,以便我们以后可以使用它。 ```javascript // main.ts const videoEl = document.querySelector('video'); const canvasEl = document.querySelector('canvas'); const ctx = canvasEl?.getContext('2d'); async function initCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 }, }, audio: false, }); let onVideoReady: (ready: boolean) => void; const videoReadyPromise = new Promise((resolve) => onVideoReady = resolve); videoEl.onloadedmetadata = () => onVideoReady(true); videoEl.srcObject = stream; return videoReadyPromise; } async function start() { await initCamera(); } start(); ``` 用TensorFlow.js和BlazePose进行姿势检测 ------------------------------ 好了,有趣的部分来了! 现在,我们的网络摄像头已经启动并运行,而且我们有办法在视频资料的顶部作画,现在是时候使用人工智能来分析视频资料并检测视频中的任何姿势骨架了。 在这一点上,你可能在想。"Jozef,我只是一个低级的前端开发员。难道我不需要成为天才的人工智能、机器学习大师来做姿势检测吗?" 你真是太谦虚了,但不要这样贬低自己 TensorFlow.js使所有这些非常聪明的人工智能的东西对任何前端开发人员来说都是非常方便的 TensorFlow.js是一个在浏览器中实现机器学习的库。有大量的预建模型可以实现各种任务,从物体检测到语音识别,对我们来说感谢的是,还有姿势检测。你可以看一下这里的可用模型:https://www.tensorflow.org/js/models 除了预建模型,你还可以训练你自己的模型,但让我们把这个问题留到另一篇文章中讨论吧 TensorFlow.js的姿势检测模型可以在这里找到:https://github.com/tensorflow/tfjs-models/tree/master/pose-detection 正如你所看到的,他们有3个模型可供选择,每一个都有优点和缺点。我们决定使用BlazePose,因为我们发现它有良好的性能,而且它提供了额外的跟踪点,可能很有用。 让我们安装TensorFlow.js的依赖项,以便我们可以在我们的项目中使用它。 ```shell npm i @tensorflow-models/pose-detection@0.0.6 @tensorflow/tfjs-backend-webgl@3.8.0 @tensorflow/tfjs-converter@3.8.0 ``` 注意,我们已经将TensorFlow的依赖关系锁定为3.8.0,因为后面的版本有问题。 现在我们已经安装了TensforFlow.js,让我们更新src/main.ts,以利用新的软件包,并开始进行姿势检测! ```javascript // main.ts import * as poseDetection from '@tensorflow-models/pose-detection'; import '@tensorflow/tfjs-backend-webgl'; import './style.css'; const videoEl = document.querySelector('video'); const canvasEl = document.querySelector('canvas'); const ctx = canvasEl?.getContext('2d'); async function initCamera() { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 }, }, audio: false, }); let onVideoReady: (ready: boolean) => void; const videoReadyPromise = new Promise((resolve) => onVideoReady = resolve); videoEl.onloadedmetadata = () => onVideoReady(true); videoEl.srcObject = stream; return videoReadyPromise; } async function initPoseDetection() { const model = poseDetection.SupportedModels.BlazePose; const detector = await poseDetection.createDetector(model, { runtime: 'tfjs', modelType: 'lite', maxPoses: 1, } as poseDetection.BlazePoseTfjsModelConfig); return detector; } async function start() { await initCamera(); const detector = await initPoseDetection(); async function render() { const poses = await detector.estimatePoses(videoEl!); console.log(poses); requestAnimationFrame(render); } } start(); ``` 让我们来谈谈这里的变化。首先在文件的顶部,你可以看到新的导入。我们为TensorFlow.js导入姿势检测库和WebGL后端。这允许`TensorFlow.js`在图形处理器上运行,从而使其性能比在CPU上运行快很多。 下一个新增功能是`initPoseDetection`函数。在这里,我们选择了BlazePose作为首选模型,然后我们使用`BlazePose`模型实例化了一个新的检测器。这样做的时候,我们会传递一个配置对象,让我们自定义模型的运行方式。 我们将`runtime`设置为`"tfjs"`。这告诉检测器使用`TensorFlow.js`(你也可以使用称为`MediaPipe`的东西,但不同浏览器的性能差异很大)。 `model`被设置为`"lite"`。有三种类型的BlazePose模型可供选择:"lite"、"full "和 "heavy"。这些改变了检测模型的质量。Lite的检测精度最低,但性能最好,而 "heavy "的检测精度最好,但性能非常密集,而 "full "则处于中间位置。我们选择了 "精简版",因为我们希望它能够实时工作,并尽可能在较弱的设备上工作。 如果你没有猜到,`maxPoses`指定了检测器应该搜索多少个姿势。但是,同样,为了性能,我们把它限制为一个。 `start`函数已经更新,以便在摄像机初始化后创建我们的姿势骨架检测器,我们还添加了一个新的`render`函数。我们将在每一帧视频上检测姿势,并最终将它们绘制到画布上。就目前而言,我们只是用线来检测姿势。 `const poses = await detector.improvePoses(videoEl!)。` 这是多简单的事啊! 你只需将你的视频元素传递给 `estimatePoses` 函数,很多 AI 魔术就会在后台发生,在你知道之前,你会有一个漂亮的姿势数组。 如果你现在看一下你的控制台,你应该看到有源源不断的姿势数组登录到你的控制台。如果你打开其中一个,看看里面,它应该有点像这样。 ```json [ { "score": 0.9999910593032837, "keypoints": [ { "x": 362.1851530148483, "y": 235.49307254371956, "z": 729053.3069949468, "score": 0.9998758783456222, "name": "nose" }, { "x": 381.78655583117063, "y": 195.58328362686387, "z": 684314.8261948047, "score": 0.9996265026574452, "name": "left_eye_inner" }, { "x": 396.3415603433863, "y": 198.3687794162295, "z": 684276.2656579157, "score": 0.9997904934464069, "name": "left_eye" }, { "x": 408.91519818260184, "y": 201.09340840610764, "z": 683932.6845901452, "score": 0.999563694033746, "name": "left_eye_outer" }, { "x": 336.5293852396535, "y": 189.06441746316608, "z": 694288.547797726, "score": 0.9996265177033095, "name": "right_eye_inner" }, { "x": 316.81846770951233, "y": 188.4412867665395, "z": 693936.2406338064, "score": 0.9997704015296288, "name": "right_eye" } ] } ] ``` 最高级别的`score`属性是对一个姿势确实在画面中的信心。在这之后,我们有`keypoints`(关键点)。这些包含了你身体所有部位的坐标。让我们停下来想一想,你可以完全在浏览器中达到这一点,这是多么令人惊奇的事情啊 不久前,像这样的姿势跟踪需要那种可笑的摇摆服和昂贵的摄像机设置,就像你在电影中看到的CGI场景的录制。现在,你可以用你的自拍镜和谷歌浏览器获得相当好的效果 绘制检测到的姿态 -------- 现在是时候让我们开始使用这些聪明的人工智能,并绘制我们的骨架了。作为简单的第一步,我们将尝试 "骨架化 "的美好世界,用简单的点和线来标记我们头部的组成部分。 为了做到这一点,我们可以创建一个新的 "骨架 "类,它将处理每个身体部分的绘制。目前,我们将只绘制头部。下面是我们的 "骨架 "类的开头。 **绘制检测到的姿态** 现在是时候让我们开始使用这些聪明的人工智能,并绘制我们的骨架了。作为简单的第一步,我们将尝试 "骨架化 "的美好世界,用简单的点和线来标记我们头部的组成部分。 为了做到这一点,我们可以创建一个新的 "骨架 "类,它将处理每个身体部分的绘制。目前,我们将只绘制头部。下面是我们的 "骨架 "类的开头。 ```javascript // src/skeleton.ts import { Pose } from '@tensorflow-models/pose-detection'; export class Skeleton { constructor(private ctx: CanvasRenderingContext2D) {} private drawHead(pose: Pose) { const leftEye = pose.keypoints.find((keypoint) => keypoint.name === 'left_eye'); const rightEye = pose.keypoints.find((keypoint) => keypoint.name == 'right_eye'); const leftMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_left'); const rightMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_right'); const nose = pose.keypoints.find((keypoint) => keypoint.name === 'nose'); this.ctx.fillStyle = 'red'; this.ctx.strokeStyle = 'red'; this.ctx.lineWidth = 5; if (leftEye) { this.ctx.beginPath(); this.ctx.arc(leftEye.x - 10, leftEye.y - 10, 10, 0, 2 * Math.PI); this.ctx.fill(); } if (rightEye) { this.ctx.beginPath(); this.ctx.arc(rightEye.x - 10, rightEye.y - 10, 10, 0, 2 * Math.PI); this.ctx.fill(); } if (nose) { this.ctx.beginPath(); this.ctx.arc(nose.x - 10, nose.y - 10, 10, 0, 2 * Math.PI); this.ctx.fill(); } if (leftMouth && rightMouth) { this.ctx.beginPath(); this.ctx.moveTo(leftMouth.x, leftMouth.y); this.ctx.lineTo(rightMouth.x, rightMouth.y); this.ctx.stroke(); } } public draw(pose: Pose) { this.drawHead(pose); } } ``` 所以在这里你可以看到我们有一个有两个方法的类。当我们实例化一个新的Skeleton时,我们将通过构造函数把它传递给canvas渲染上下文。这将允许Skeleton以后使用它,这样它就可以画出骨架部分。 接下来,我们有`drawHead`方法。正如你可能已经猜到的,这将负责渲染头部。最终,我们将为身体的每个部分都设置一个函数,但头部已经足够让事情开始了。 每次调用`drawHead`方法时,我们将把BlazePose的当前姿势对象传给它。在函数的开头,我们找到构成头部的每个关键点。 一旦我们有了关键点,我们就会去画它们。对于眼睛和鼻子,我们在它们的坐标上画一个简单的圆。对于嘴,我们在嘴的左右两边画一条线。 我们现在可以更新`main.ts`来创建一个新的骨架,并在渲染时调用其`draw`函数。 ```javascript // main.ts async function start() { await initCamera(); const detector = await initPoseDetection(); const skeleton = new Skeleton(ctx); async function render() { const poses = await detector.estimatePoses(videoEl!, { maxPoses: 1, flipHorizontal: false, scoreThreshold: 0.4, }); ctx.clearRect(0, 0, 640, 480); if (poses[0]) { skeleton.draw(poses[0]); } requestAnimationFrame(render); } render(); } ``` 你现在应该有一些东西,看起来有点像这样: ![33.png][3] 姿势检测开始工作了! 当然,我们刚刚写的绘制头部的代码将被替换,但看到姿势检测工作正常的视觉证明是很好的。 将图像映射到身体部位 ---------- 所以你可以看到,在姿势关键点上画点和线是非常容易的。不幸的是,点和线使得万圣节的服装非常糟糕,所以用插图的骨架片来代替它们会更好。 我们得到了一张骨架插图,并把所有的身体部位分开,然后把它们单独从Figma中导出。这就是我们的骨架。 ![44.png][4] 为了将图像映射到正确的位置,我们需要解决一些问题。 图像的位置。它的x和y坐标。 图像的角度。我们是否需要旋转它以适应正确的位置? 高度和宽度。我们是否需要在水平或垂直方向上对其进行缩放? 对于头部,我们可以使用鼻子的关键点作为中心坐标。为了计算出旋转,我们可以计算出两个眼睛关键点之间的角度。最后,为了算出水平比例,我们可以计算出两眼之间的距离,然后与头部图像中两眼之间的实际距离进行比较。 我们可以通过测量嘴巴和眼睛之间的高度,对水平比例做同样的处理。 以上所有这些都可以用矢量数学计算出来。我们已经创建了一个小的辅助文件来完成这个工作。 ```javascript // src/vectors.ts export interface Vector { x: number; y: number; } /** * Find the middle point between two vectors */ export function getMiddle(a: Vector, b: Vector): Vector { return { x: a.x + (b.x - a.x) * 0.50, y: a.y + (b.y - a.y) * 0.50, }; } /** * Find the distance between two vectors * @param a * @param b */ export function getDistance(a: Vector, b: Vector): number { const x = a.x - b.x; const y = a.y - b.y; return Math.sqrt(x * x + y * y); } /** * Get the angle in degrees between two vectors * @param a * @param b */ export function getAngle(a: Vector, b: Vector) { return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI; } 我们的矢量辅助文件现在可以在骨架绘制功能中使用,以弄清图像的定位。 import { Pose } from '@tensorflow-models/pose-detection'; import { getAngle, getDistance, getMiddle } from './vectors'; // Skeleton parts import Head from './assets/skeleton/Head.png'; // Load the skeleton parts const head = new Image(); head.src = Head; export class Skeleton { constructor(private ctx: CanvasRenderingContext2D) {} private drawHead(pose: Pose) { const leftEye = pose.keypoints.find((keypoint) => keypoint.name === 'left_eye'); const rightEye = pose.keypoints.find((keypoint) => keypoint.name == 'right_eye'); const leftMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_left'); const rightMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_right'); const nose = pose.keypoints.find((keypoint) => keypoint.name === 'nose'); // The real dimensions of keypoints in our head image const eyeWidth = 205; const eyesToMouth = 220; // If we are missing any parts return early if (!leftEye || !rightEye || !leftMouth || !rightMouth || !nose) { return; } const eyeAngleDeg = getAngle(leftEye, rightEye); const distance = getDistance(leftEye, rightEye); const xScale = distance / eyeWidth; const middleEye = getMiddle(leftEye, rightEye); const middleMouth = getMiddle(leftMouth, rightMouth); const mouthToEyeDistance = getDistance(middleEye, middleMouth); const yScale = mouthToEyeDistance / eyesToMouth; this.drawImage({ image: head, x: nose.x, y: nose.y, height: head.height * yScale, width: head.width * xScale, rotation: eyeAngleDeg, offsetX: 0.55, offsetY: 0.8, }); } private drawImage(options: { image: HTMLImageElement, x: number, y: number, height: number, width: number, rotation: number, offsetX: number, offsetY: number, }): void { const { image, x, y, height, width, rotation, offsetX, offsetY, } = options; // save the unrotated context of the canvas so we can restore it later // the alternative is to untranslate & unrotate after drawing this.ctx.save(); // move to the center of the canvas this.ctx.translate(x, y); // rotate the canvas to the specified degrees this.ctx.rotate(((180 + rotation) * Math.PI) / 180); // draw the image // since the ctx is rotated, the image will be rotated also this.ctx.drawImage(image, 0 - (width * offsetX), 0 - (height * offsetY), width, height); // we’re done with the rotating so restore the unrotated ctx this.ctx.restore(); } public draw(pose: Pose) { this.drawHead(pose); } } ``` 这里有相当多的变化,所以让我们来看看它们。从文件的顶部开始,我们导入我们闪亮的新矢量辅助工具。我们还导入了将用于头部的图像,并创建了一个新的图像并设置了它的src。这将使我们能够在以后用`drawImage`把它画到画布上。 ```javascript import { getAngle, getDistance, getMiddle } from './vectors'; // Skeleton parts import Head from './assets/skeleton/Head.png'; // Load the skeleton parts const head = new Image(); head.src = Head; ``` 接着是`drawHead`函数。 ```javascript private drawHead(pose: Pose) { const leftEye = pose.keypoints.find((keypoint) => keypoint.name === 'left_eye'); const rightEye = pose.keypoints.find((keypoint) => keypoint.name == 'right_eye'); const leftMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_left'); const rightMouth = pose.keypoints.find((keypoint) => keypoint.name === 'mouth_right'); const nose = pose.keypoints.find((keypoint) => keypoint.name === 'nose'); // The real dimensions of keypoints in our head image const eyeWidth = 205; const eyesToMouth = 220; // If we are missing any parts return early if (!leftEye || !rightEye || !leftMouth || !rightMouth || !nose) { return; } const eyeAngleDeg = getAngle(leftEye, rightEye); const distance = getDistance(leftEye, rightEye); const xScale = distance / eyeWidth; const middleEye = getMiddle(leftEye, rightEye); const middleMouth = getMiddle(leftMouth, rightMouth); const mouthToEyeDistance = getDistance(middleEye, middleMouth); const yScale = mouthToEyeDistance / eyesToMouth; this.drawImage({ image: head, x: nose.x, y: nose.y, height: head.height * yScale, width: head.width * xScale, rotation: eyeAngleDeg, offsetX: 0.55, offsetY: 0.8, }); } ``` 在这里,你可以看到我们已经摆脱了快速的点/线绘制,取而代之的是大量的数学计算 我们有两个常数来表示眼睛的宽度和眼睛到嘴的高度。 这些是`Head.png`中眼睛和眼睛到嘴之间的实际像素距离,我们可以用这些来缩放我们的图像相对于检测姿势中的长度。 我们通过得到两个眼睛关键点之间的角度来计算头部图像的旋转。 ```javascript const eyeAngleDeg = getAngle(leftEye, rightEye); ``` X轴的刻度是通过测量两个眼睛关键点之间的距离,然后除以真实的像素宽度来计算的。 ```javascript const distance = getDistance(leftEye, rightEye); const xScale = distance / eyeWidth; ``` 然后我们可以做一些类似的事情来计算出Y轴的比例。 ```javascript const middleEye = getMiddle(leftEye, rightEye); const middleMouth = getMiddle(leftMouth, rightMouth); const mouthToEyeDistance = getDistance(middleEye, middleMouth); const yScale = mouthToEyeDistance / eyesToMouth; ``` 这里唯一的区别是,我们找到眼睛之间的中间点和嘴巴两边的中心点,然后计算出高度。 现在所有的数学运算都完成了,是时候在画布上绘制头部图像了。 ```javascript this.drawImage({ image: head, x: nose.x, y: nose.y, height: head.height * yScale, width: head.width * xScale, rotation: eyeAngleDeg, offsetX: 0.55, offsetY: 0.8, }); ``` 我们在这里使用了骨架类上的另一个新函数,叫做`drawImage,`我们稍后会讲到它。不过,就目前而言,你可以看到我们正在使用上面计算的变量来正确定位、缩放和旋转图像。 现在回到Skeleton的`drawImage`函数。 ```javascript private drawImage(options: { image: HTMLImageElement, x: number, y: number, height: number, width: number, rotation: number, offsetX: number, offsetY: number, }): void { const { image, x, y, height, width, rotation, offsetX, offsetY, } = options; // save the unrotated context of the canvas so we can restore it later this.ctx.save(); // move to the center of the canvas this.ctx.translate(x, y); // rotate the canvas to the specified angle this.ctx.rotate(((180 + rotation) * Math.PI) / 180); // draw the image because ctx is rotated, the image will be rotated also this.ctx.drawImage(image, 0 - (width * offsetX), 0 - (height * offsetY), width, height); // restore the unrotated ctx this.ctx.restore(); } ``` 这个函数允许我们绘制一幅图像,并围绕我们选择的锚点进行旋转。默认情况下,canvas drawImage函数将从图像的左上角坐标定位。不幸的是,这使得我们在定位和旋转图像以匹配姿势时变得很尴尬。 在绘制图像之前使用画布的平移和旋转函数,可以让我们按需要进行定位和旋转。 希望在这一点上,你已经开始像我一样看起来有点傻了。 ![55.png][5] 我的身体在哪里? -------- 我们已经有了一个看起来很可爱的骷髅头骨,但我们仍然缺少很多骨头。幸运的是,我们现在已经具备了绘制身体其他部分的一切条件。我们需要为身体的其他部分创建绘图函数。我不会在这里复制所有的绘制函数,因为它们有很多,但如果你想看到它们,可以在repo中查看:https://github.com/pixelhop/skeletoniser/blob/main/src/skeleton.ts 万圣节快乐 -------- 我们希望你喜欢这篇文章,并能因此而充分地塑造自己的骨架!我们喜欢那些有点古怪的项目。我们喜欢这样有趣的项目,并迫不及待地想继续尝试。 一如既往,如果你喜欢这篇文章或有任何问题,请随时在Twitter上与我们联系。@pixelhopio 或订阅我们的新闻简报,以便了解我们接下来要做的任何疯狂的事情。 [1]: https://trick-or-treat.pixelhop.io/ [2]: https://skeletoniser.netlify.app [3]: http://guobacai.com/usr/uploads/2021/11/341311889.png [4]: http://guobacai.com/usr/uploads/2021/11/2473773286.png [5]: http://guobacai.com/usr/uploads/2021/11/3046992252.png 标签: 浏览器, css, npm, 文件, init, 移动, const, 功能, 类型, 处理, height, width, 姿势, righteye
评论已关闭