这个章节呢,同样会以实作为主,在解决问题中带大家学习,逐渐引入JS的语言特性,前面一样会从简单的开始,後面八成会开始越讲越快,若有不明白的欢迎留言询问!
身为程序人,要懂得拆解和观察,可以先想想看"什麽叫做动画",一幅会动的画?还是一个移动中的图像?在Canvas上画画我们已经学会了,上个章节透过不断更新频谱数据,画出来的直方图,也是一种持续变化的动画,因此要思考的是"动"这件事,要满足动,也就是位移,只要能掌握路径,便能掌握动画,咦?最长在动的不就是咱们的滑鼠吗,那今天就用滑鼠带大家学动画吧!
参数回顾:
让我们沿用上个章节的架构:
let mouseImg = new Image();
mouseImg.src = "../mouse.png";
let mouseX = 0, mouseY = 0;
canvas.addEventListener('mousemove', function (e) {
let Rect = canvas.getBoundingClientRect();
mouseX = (e.pageX - Rect.left) * RATIO;
mouseY = (e.pageY - Rect.top) * RATIO;
}, false);
function MouseAnime(){
context.drawImage(MouseImg, mouseX, mouseY);
}
pageX, pageY 对应到的实际画面大小,计算其在画布上的相对位置,要乘上我们当时设计的 RATIO
若考量到RWD所用到的touchmove,给大家参考:
if(window.innerWidth < 992)
canvas.addEventListener('touchmove', function (e) {
let Rect = canvas.getBoundingClientRect();
mouseX = (e.touches[0].pageX - Rect.left) * RATIO;
mouseY = (e.touches[0].pageY - Rect.top) * RATIO;
}, false);
用breakpoint来判断,不是mousemove就是touchmove,才不会在移动端重覆执行
这个坑实在有点不想踩,又要拿一些进阶的名词,是後面章节才会提到的,简单来说就是 Image 实例化的物件(mouseImg)并不会在你赋予它图片路径後,就直接读取图片,要是图片太大读取太久,那程序不就会卡住吗?因此它用到的是非同步的方式载入,等到图片载入完毕後,就会触发 load 事件,我们可以自行定义其内容,来决定「当图片载入後,要做什麽反应」。
如果你正心想:「什麽是类名?什麽是实例化?什麽是非同步?」这些不知道没关系,我们未来有机会再娓娓道来,这边只需要知道「如果在设定图片路径後,立刻呼叫drawImage,则浏览器会抛出错误,彷佛mouseImg不存在一样」,因此这边最直觉的解决方案便是:
mouseImg.onload = () => {
AnimationLoop();
}
在图片载入完毕後,再呼叫动画循环
这样解决虽然很直白,但把整个动画循环的开始,完全依赖於一个图片的读取,实在是不太靠谱,万一这个档案就丢了,读取不到怎麽办呢?因此较好的做法应该是以下这两种:
function MouseAnime(){
if(MouseImg.complete) context.drawImage(mouseImg, mouseX, mouseY);
}
图片物件身上有complete属性,图片载入时为false,完成後为true,若MouseImg未读取完毕,则暂时不进行绘制
不过有一种状况例外,就是当图片的路径有问题时,由於是非同步载入,这个路径的错误并不会中断整个网页的运行,这固然是一个优点(毕竟网路上的图片总会经常弄丢),只是问题在於由於载入的过程被中断了,MouseImg.complete
仍然会是预设值true,并没有进到载入中的状态,直接用上面的代码就会让人有「摁?complete == true
已经载入完成了,却还是没有图片」的错觉,於是我们还可以这麽做:
function MouseAnime(){
if(mouseImg.width) return; // 若宽度为0,表示图片来源未正确设定,直接中断
if(MouseImg.complete) context.drawImage(mouseImg, mouseX, mouseY);
else context.drawImage(MouseImg, mouseX, mouseY);
}
图片物件建立之初,会预设宽高属性为0,因此我们可以选用width来检查代码
聪明的你是不是也想到了什麽呢?没错,上个章节用到的HTMLMediaElement(aduio),其实也是需要读取的!相比load事件,它的事件名称长了些,叫做loadeddata。那当初为什麽程序没出错?可以说我们运气好,也可以说我们的架构设计的还不错,这些细节的部份在最後的章节,将会一口气对整个架构进行细节的调整。
於是乎,这样就有一个基本的图形会随时在滑鼠的位置了,不过它的贴图位置是以坐标点为左上角,因此会发现滑鼠总是在图案的左上角,因此若要置中,可以用第二种写法来设定宽高,并把图片偏移左上,使中心刚好为滑鼠座标,这边为了方便,以长宽1:1的图形举例:
function MouseAnime(){
let size = WIDTH * 0.1;
context.drawImage(mouseImg, mouseX-size/2, mouseY-size/2, size, size);
}
考量到RWD,大小也是用WIDTH决定(画布总宽度)
context.drawImage(mouseImg, 0, 0, mouseImg.width, mouseImg.height,
mouseX-size/2, mouseY-size/2, size, size);
↑这边也同步提供第三种格式给大家参考,前面四个参数可以让你对图片进行裁切,不过我个人是不建议把事情做得这麽复杂,最好的方式是在制作素材的时候就先设计好宽高的比例,然後取得宽高比,这样,不论是什麽图片,都不用重新做设定:
function MouseAnime(){
let imgRatio = mouseImg.height / mouseImg.width;
let w = WIDTH * 0.1;
let h = WIDTH * 0.1 * imgRatio;
context.drawImage(mouseImg, mouseX-w/2, mouseY-h/2, w, h);
}
那麽,终於让滑鼠总是待在图片的正中心了,不过,单纯只有这样的话,你会发现,随着鼠标的移动,图片有非常明显的瞬移现象,动画的品质是低劣的,因此我们要来设计一个缓冲的方案了,一开始谈数学公式太复杂了,我们就先思考「希望图案不要立刻到滑鼠的位置」这件事,那是不是就不要马上冲过去,可以让位移变少?
这几种方案是不是就马上浮现脑袋了,接下来分析他们会遇到的问题,用减法的话,比方说缓冲设为100,那是不是图案最後只会停在距离滑鼠100的地方?那显然不行,如果用固定每次移动5格呢?欸这办法似乎挺不错的,只是当距离超过500,那是不是要移动100次才够呢?似乎再稍微修改一下,就可以得到答案了。
因应上面的问题,我们需要一个方案:「在距离小的时候,移动变少;在距离大的时候,移动变多」,巧了,这意思不就是距离和位移呈正比吗,并且位移要比距离还小,那麽只要这麽设计就大功告成了:
位移 = 距离 / 缓冲
function MouseAnime(){
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
如果有格式控觉得不太好看的话,还有另一种相同效果的版本
cursorX = (cursorX * 4 + mouseX) / 5;
cursorY = (cursorY * 4 + mouseY) / 5;
此时我们发现,图案就像是跟随着滑鼠一样,有自己的运动轨迹,看起来也更加顺畅了,严格来说,它已经不是鼠标了,就是个小跟班
接下来就可以练习尝试做个函式,在加入时间这个概念前,先来制作一个基本的互动模型,让滑鼠点击後,让动画暂停,释放滑鼠後,再让动画继续进行:
let isPathing = false;
canvas.addEventListener('mousedown', () => isPathing = true);
canvas.addEventListener('mousemove', GetMouse);
canvas.addEventListener('mouseup', () => isPathing = false);
canvas.addEventListener('mouseout', () => isPathing = false);
function GetMouse(e){
let Rect = canvas.getBoundingClientRect();
mouseX = (e.pageX - Rect.left) * RATIO;
mouseY = (e.pageY - Rect.top) * RATIO;
}
function MouseAnime(){
if(!isPathing){
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
}
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
在滑鼠按下的时候,先锁住游标图案的座标,让它保持不变,等放开滑鼠时,再一次释放它!有没有很像要喊三二一让赛车冲出去的感觉呢?
接下来我们给它一定的时间,单位为侦数,60侦大约为1秒
let period = 0, timer = 0;
let distanceX = 0, distanceY = 0;
canvas.addEventListener('mouseup', GetDistance);
canvas.addEventListener('mouseout', GetDistance);
function GetDistance(){
distanceX = (mouseX - cursorX);
distanceY = (mouseY - cursorY);
period = 90;
timer = 90;
isPathing = false;
}
function MouseAnime(){
if(!isPathing){
if(timer > 0){
cursorX+= distanceX / period;
cursorY+= distanceY / period;
timer--;
}
else{
cursorX+= (mouseX - cursorX) / 5;
cursorY+= (mouseY - cursorY) / 5;
}
}
let size = WIDTH * 0.1;
context.drawImage(mouseImg, cursorX-size/2, cursorY-size/2, size, size);
}
这样就完成一个等速动画了,可以参考 Demo,接下来几天会陆续更新!
时间也不早了,出个思考题吧
如果想要让图案启动时,慢慢出发,一边加速一边抵达鼠标的位置,可以怎麽做呢?
<<: [DAY 1] _ ARM-M0架构MCU之韧体开发教学规划
高级持久威胁(APT) 多向量多态攻击 拒绝服务 缓冲区溢出 流动码 恶意软件(恶意软件) 偷渡式...
今日我们将要介绍ES官方提供go-elasticsearch客户端的基本操作。 go-elastic...
Python 内建的数值类函式 数值类函式 执行结果 功能 abs(-10) 10 取绝对值 min...
特性 使用记忆体进行操作 所有资料都透过Key-Value的方式存放在记忆体,在找寻所需资料透过Ha...
按照前一篇的程序安装完成并重开机後,即可开始正式使用 Proxmox VE 系统,请以浏览器连接至...