前端特效,用Canvas画一只会跟着鼠标走的小狗,文末有福利分享

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

请点击此处输入图片描述

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

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

请点击此处输入图片描述

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

1. 小狗走的动画

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

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

请点击此处输入图片描述

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

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

请点击此处输入图片描述

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

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

请点击此处输入图片描述

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

请点击此处输入图片描述

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

在start函数里面添加一个画的函数walk的执行:

async start() {

    // 等待资源加载完

    await this.loadResources();

    this.walk(); 

}

walk() {

}

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

所以代码改成这样:

async start() {

    await this.loadResources();

    // 给下一帧注册一个函数

    window.requestAnimationFrame(this.walk.bind(this));

}

walk() {

    // 绘制狗的图片 

    // 继续给下一帧注册一个函数

    window.requestAnimationFrame(this.walk.bind(this));

}

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

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

constructor(canvas) {

    this.canvas = canvas;

    this.ctx = canvas.getContext("2d");

    // 记录上一帧的时间

    this.lastWalkingTime = Date.now(); 

    // 记录当前画的图片索引

    this.keyFrameIndex = -1; 

    this.start();

}

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

请点击此处输入图片描述

这样我们就有了一只在原地踏步的小狗:

请点击此处输入图片描述

然后让它往前走。

3. 让小狗往前走

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

constructor(canvas) {

    // 小狗的速度

    this.dogSpeed = 0.1;

    // 小狗当前的位移

    this.currentX = 0;

}

然后在walk函数里面计算当前累加的位移:

// 计算位移 = 时间 * 速度

let distance = (now - this.lastWalkingTime) * this.dogSpeed;

this.currentX += distance;

this.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight,

        // dx, dy, dwidth, dheight

        this.currentX, 20, 186, 162)

但是这样我们发现小狗走起路来一卡一卡的,不是很连贯:

请点击此处输入图片描述

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

4. 算法优化

考虑到狗的控制参数比较集中,把它们写到一个dog的Object里面:

constructor (canvas) {

    this.dog = {

        // 一步10px

        stepDistance: 10,

        // 狗的速度

        speed: 0.15,

        // 鼠标的x坐标

        mouseX: -1

    };

}

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

let now = Date.now(); 

let distance = (now - this.lastWalkingTime) * this.dog.speed;

if (distance < this.dog.stepDistance) {

    window.requestAnimationFrame(this.walk.bind(this));

    return;

}

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

5. 走到鼠标的位置停下

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

this.dog = {

    // 往前走停留的位置

    frontStopX: -1,

    // 往回走停留的位置,

    backStopX: window.innerWidth,

};

然后添加一个记录鼠标位置的函数,主要是监听mousemove事件:.

请点击此处输入图片描述

然后在walk函数里面用一个变量stopWalking表示小狗是否停下来,和一个direct表示小狗的方向:​

请点击此处输入图片描述

如果小狗没有停,计算位置的时候乘以direct:

// 计算位置

if (!stopWalking) {

    this.dog.mouseX += this.dog.stepDistance * direct;

}

如果小狗停了,则mouseX还是上次的值。

鼠标停留在小狗位置的那段代码可以做个优化,如果鼠标在小狗中间的右边,则方向调整为正,否则为负:

// 如果鼠标在狗在位置

else {

    stopWalking = true;

    // 如果鼠标在小狗图片中间的右边,则direct为正,否则为负

    direct = this.dog.backStopX - this.dog.mouseX 

                    > this.pictureWidth / 2 ? 1 : -1; 

    this.keyFrameIndex = -1;

}

这样鼠标在小狗左右来回移动时,小狗会转头。

得到小狗的位置和方向之后就是画上去,正方向的还好,反方向的由于没图片,我们通过canvas的翻转flip进行绘制,如下代码所示:

请点击此处输入图片描述

这样基本上就完成了,最后一个问题是小狗初始化位置的摆放,如果你要把它摆在右边的话,那需要把它的方向反转一下,摆在最左边也需要。不然你会发现小狗摆在左边,但它的头朝左了,需要转一下放在右边。

图片的素材和绘制过程已说得很详细,读者可以自行实现,或者想其它一些跟着鼠标动的交互效果。

对想学习前端的小伙伴,小编给你们准备了全套前端电子版书籍,包含了目前大部分前端开发的书

领取方式,加前端学习群 330336289 获取 邀请码 寂静



​​前端 特效 开发 编程语言  互联网  微信 代码 程序员

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容