热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

canvasrotate累加旋转_用Canvas画一只会跟着鼠标走的小狗

点击上方程序员小乐关注,星标或置顶一起成长第一时间与你相约每日英文Thetruthmayhurtforalittlewhilebutaliehurtsforever.Somet

点击上方 "程序员小乐"关注, 星标或置顶一起成长

第一时间与你相约

每日英文

The truth may hurt for a little while but a lie hurts forever.Something is a knot when you reserve it,a scar when it's opened.

真相会让我们痛一阵,但谎言令我们痛一生。有些事情,不谈是个结,谈开了是个疤。

每日掏心话

生命最有趣的部分,正是它没有剧本、没有彩排、不能重来。时候执着是一种负担,放弃是一种解脱,人生没有完美,幸福也没有一百分。

来自:人人网FED | 责编:乐乐

链接:juejin.im/post/5a97bb3951882555867ecffc

73918cf1c9a50618119ca956ad676576.png

程序员小乐(ID:study_tech)第 688 次推文 图片来自网络

往日回顾:Git 从入门到放不下,看这篇就对了!

正文

87c0de5b3194f53ee6698b19fcea5ba1.png

以前经常看到这种效果:在网页右下角放一个人,然后他的眼珠会跟着鼠标转,效果如下:

6839d56b1d9d3209289decca2e728e9f.gif

这个例子来自于CodePen,它是根据鼠标的位置设置两个眼球的transform: rotate属性做的效果。

这种跟着鼠标移动的小交互一般都比较好玩,所以我突然想到,能不能做一只会跟着鼠标走的小狗,最后的效果如下所示:

404fc276ad397b7166e373ba79c96624.gif

我们一步步来实现这个效果。

1. 小狗走的动画

小狗走的动画应该怎么实现呢?如果用一张gif,然后根据鼠标的位置移动这张gif,那么当鼠标停下来小狗不动的效果就做不了,因为gif一直在循环播放代码控制不了这个行为。所以这种简单方案是不可行的。

然后又想到之前用CSS的animation做过这种逐帧动画:

45e4918f59c91377bea342ba4c344a1d.gif

所以就有思路了,小狗的动画也是使用逐帧的动画,并且用JS控制它的播放。

在网上搜罗了一番,还没有人做过类似的动画,不过找到了小狗的素材,这位老兄在教人怎么画行走的动物,刚好可以拿来当做我们的素材,把小狗抠出来:

86d77591f7b1f8c24f756992d0dd3c55.png

2. 画一只在原地踏步的小狗

动画的第一步先让小狗原地踏步,即先让这个动画能播放起来,然后再做移动的动画。所谓逐帧动画就是每隔一小会就播放一帧,这样连起来就是在动了。

写一个canvas标签,然后把它固定到页面的底部:

然后设置宽度为页面的100%:

  • let canvas = document.querySelector("#dog-walking");canvas.width = window.innerWidth;canvas.height = 200;

这样我们就有一个画布了。接着要把图片画让去,先要把图片加载下来,上面我们准备了9张png:0.png ~ 8.png,其中0.png是小狗停住不动的图片,1.png ~ 8.png是小狗在走的图片。

在JS里面怎么加载图片呢,用新建一个Image实例的方式,如下代码所示:

  • let img = new Image();img.onload = function() { beginDraw(img);};img.src = "dog/0.png";

由于图片比较多,我们用类的方式组织我们的代码,把数据当作类的属性,方便存取。如下代码所示:

  • class DogAnimation { constructor(canvas) { canvas.width = window.innerWidth; canvas.height = 200; // 存放加载后狗的图片 this.dogPictures = []; // 图片目录 this.RES_PATH = "./dog"; this.IMG_COUNT = 8; this.start(); } start() { this.loadResources(); } loadResources() {

  • }

  • }let canvas = document.querySelector("#dog-walking");let dogAnimation = new DogAnimation(canvas);dogAnimation.start();

把狗的图片放到dogPictures数组里面,在loadResources里面进行加载,如下代码所示:

  • // 加载图片loadResources() { let imagesPath &#61; []; // 准备图片的src for (let i &#61; 0; i <&#61; this.IMG_COUNT; i&#43;&#43;) { imagesPath.push(&#96;${this.RES_PATH}/${i}.png&#96;); }

  • let works &#61; []; imagesPath.forEach(imgPath &#61;> { // 图片加载完之后触发Promise的resolve works.push(new Promise(resolve &#61;> { let img &#61; new Image(); img.onload &#61; () &#61;> resolve(img); img.src &#61; imgPath; })); });

  • return new Promise(resolve &#61;> { // 借助Promise.all知道了所有图片都加载好了 Promise.all(works).then(dogPictures &#61;> { this.dogPictures &#61; dogPictures; resolve(); }); }); // 这里再套一个Promise是为了让调用者能够知道处理好了}

这段加载图片的代码借助了Promise&#xff0c;把每张图片的加载都当作一个Promise的任务&#xff0c;统一放到一个数组里面&#xff0c;然后再借助Promise.all就知道所有的任务都完成了。这样就拿到了所有已onload的img对象&#xff0c;然后就可以拿来画了。

在start函数里面添加一个画的函数walk的执行&#xff1a;

  • async start() { // 等待资源加载完 await this.loadResources(); this.walk();}walk() {

  • }

实际上为了画逐帧动画&#xff0c;我们要使用window.requestAnimationFrame&#xff0c;这个函数在浏览器画它自己的动画的下一帧之前会先调一下这个函数&#xff0c;理想情况下&#xff0c;1s有60帧&#xff0c;即帧率为60 fps。因为不管是播放视频还是浏览网页它们都是逐帧的&#xff0c;例如往下滚动网页的时候就是一个滚动的动画&#xff0c;所以浏览器本身也是在不断地在画动画&#xff0c;只是当你的网页停止不动时(且页面没有动画元素)&#xff0c;它可能会降低帧率减少资源消耗。

所以代码改成这样&#xff1a;

  • async start() { await this.loadResources(); // 给下一帧注册一个函数 window.requestAnimationFrame(this.walk.bind(this));}walk() { // 绘制狗的图片 // 继续给下一帧注册一个函数 window.requestAnimationFrame(this.walk.bind(this));}

我们使用了一个bind(this)&#xff0c;它的作用是让walk函数的执行上下文还是指向当前类的实例。

现在怎么让狗动起来呢&#xff1f;最简单的我们可以每隔0.1s就画一帧&#xff0c;这样就会连起来&#xff0c;形成一个动画&#xff0c;为此我们需要记录上一次画的时间&#xff0c;然后判断当前时间与上一次的时间是否大于0.1s&#xff0c;如果是的话就画下一帧&#xff0c;否则什么也不用干。因为上文提过&#xff0c;1s最多有60帧&#xff0c;每一帧间隔 1s / 60 &#61; 16.67ms。如下代码所示&#xff0c;先在constructor添加几个变量&#xff0c;包括一个记录上一帧时间的变量&#xff1a;

  • constructor(canvas) { this.canvas &#61; canvas; this.ctx &#61; canvas.getContext("2d"); // 记录上一帧的时间 this.lastWalkingTime &#61; Date.now(); // 记录当前画的图片索引 this.keyFrameIndex &#61; -1; this.start();}

然后在walk函数里面进行绘制&#xff0c;在画的时候每次画都取下张图片&#xff0c;即下一帧的图片&#xff0c;不断循环&#xff1a;

  • walk() { // 绘制狗的图片&#xff0c;每过100ms就画一张 let now &#61; Date.now(); if (now - this.lastWalkingTime > 100) { // 先清掉上一次画的内容 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 获取下一张图片的索引 let keyFrameIndex &#61; &#43;&#43;this.keyFrameIndex % this.IMG_COUNT; let img &#61; this.dogPictures[keyFrameIndex &#43; 1]; // img, sx, sy, swidth, sheight this.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, // dx &#61; 20, dy, dwidth, dheight 20, 20, 186, 162); this.lastWalkingTime &#61; now; } // 继续给下一帧注册一个函数 window.requestAnimationFrame(this.walk.bind(this));}

这样我们就有了一只在原地踏步的小狗&#xff1a;

819c888507eba21df2dcbeff6707d0de.gif

然后让它往前走。

3. 让小狗往前走

上面在drawImage的传参固定dx &#61; 20&#xff0c;如果不断加大这个dx&#xff0c;那么它就往前走了。为此在构造函数里面添加一个变量记录当前的位移&#xff0c;并设置小狗的速度&#xff1a;

  • constructor(canvas) { // 小狗的速度 this.dogSpeed &#61; 0.1; // 小狗当前的位移 this.currentX &#61; 0;}

然后在walk函数里面计算当前累加的位移&#xff1a;

  • // 计算位移 &#61; 时间 * 速度let distance &#61; (now - this.lastWalkingTime) * this.dogSpeed;this.currentX &#43;&#61; distance;this.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, // dx, dy, dwidth, dheight this.currentX, 20, 186, 162);

但是这样我们发现小狗走起路来一卡一卡的&#xff0c;不是很连贯&#xff1a;

0e99e18afe443361517e02f5d9c91790.gif

这个是因为每0.1s画一帧&#xff0c;帧率只有10fps&#xff0c;所以一走起来就不太行了。方法一是让它走慢点&#xff0c;这样可以减缓&#xff0c;但是如果想保持速度甚至提高速度的话&#xff0c;我们得想办法优化一下。

4. 算法优化

考虑到狗的控制参数比较集中&#xff0c;把它们写到一个dog的Object里面&#xff1a;

  • constructor (canvas) { this.dog &#61; { // 一步10px stepDistance: 10, // 狗的速度 speed: 0.15, // 鼠标的x坐标 mouseX: -1 };}

主要有两个参数&#xff0c;一个是狗的速度另一个是每一步走的位移&#xff0c;然后计算距离方式变成&#xff1a;

  • let now &#61; Date.now();let distance &#61; (now - this.lastWalkingTime) * this.dog.speed;if (distance

每一步至少走10px&#xff0c;如果小于这个数的话就不走了。通过每步的位移和速度这两个参数可以很方便地控制狗走的快慢和帧率&#xff0c;例如把stepDistance改小点&#xff0c;speed提高就会走得比较频繁&#xff0c;能提高帧率&#xff0c;上面设置的帧率是14 fps. 不过帧率低的根本原因还是在于小狗走路的图片较少。

5. 走到鼠标的位置停下

给小狗添加一个停留的位置&#xff0c;包括往前走和往后走的&#xff0c;因为一个是鼠标在图片前面&#xff0c;一个是鼠标在图片的后面&#xff0c;需要区分&#xff1a;

  • this.dog &#61; { // 往前走停留的位置 frontStopX: -1, // 往回走停留的位置, backStopX: window.innerWidth,};

然后添加一个记录鼠标位置的函数&#xff0c;主要是监听mousemove事件&#xff1a;

  • async start () { await this.loadResources(); this.pictureWidth &#61; this.dogPictures[0].naturalWidth / 2; this.recordMousePosition(); window.requestAnimationFrame(this.walk.bind(this));}// 记录鼠标位置recordMousePosition() { window.addEventListener("mousemove




推荐阅读
  • Html5-Canvas实现简易的抽奖转盘效果
    本文介绍了如何使用Html5和Canvas标签来实现简易的抽奖转盘效果,同时使用了jQueryRotate.js旋转插件。文章中给出了主要的html和css代码,并展示了实现的基本效果。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 本文整理了常用的CSS属性及用法,包括背景属性、边框属性、尺寸属性、可伸缩框属性、字体属性和文本属性等,方便开发者查阅和使用。 ... [详细]
  • angular.element使用方法及总结
    2019独角兽企业重金招聘Python工程师标准在线查询:http:each.sinaapp.comangularapielement.html使用方法 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了腾讯最近开源的BERT推理模型TurboTransformers,该模型在推理速度上比PyTorch快1~4倍。TurboTransformers采用了分层设计的思想,通过简化问题和加速开发,实现了快速推理能力。同时,文章还探讨了PyTorch在中间层延迟和深度神经网络中存在的问题,并提出了合并计算的解决方案。 ... [详细]
  • 本文介绍了如何使用n3-charts绘制以日期为x轴的数据,并提供了相应的代码示例。通过设置x轴的类型为日期,可以实现对日期数据的正确显示和处理。同时,还介绍了如何设置y轴的类型和其他相关参数。通过本文的学习,读者可以掌握使用n3-charts绘制日期数据的方法。 ... [详细]
  • 本文讨论了在dva中引入antd组件table时没有显示样式的问题。提供了.roadhogrc文件的配置,包括环境和import的设置。同时介绍了extraBabelPlugins和transform-runtime的使用方法,并解释了libraryName和css的含义。 ... [详细]
author-avatar
宋安武_375
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有