Chapter4 - Canvas背景动画(IV)把纷飞的落叶,通通抓回来当作收藏吧!

今天挑战半小时写完一篇文章(被打,其实我写完程序了,把文章撰写出来就好噜。

https://ithelp.ithome.com.tw/upload/images/20210930/20135197NvhXlIYh72.png

https://jerry-the-potato.github.io/Chapter4-demo4/

什麽互动?

很多框架或引擎的教学在建造物理世界时,都是从物体的碰撞开始讲起,因为他们会设计重力加速度、风力、对流等概念进去,一般来说,如果想干扰一个「系统」,放一个东西进去就好,比如说把阿基米德弄进浴缸、把碳酸氢钠丢进水里、把jojo的宠物犬丢到焚化炉阿透漏年纪了,总之水会满溢出来、不同物体放进去还会产生化学变化,这种紮实的回馈感,给予了我们不断观察并实验,并乐此不疲的琢磨着背後的原理,对於真实世界的五感便是如此直接自然,应该很难想像阿基米德直接掉进水里,一点儿水花都没溅起来吧,这种情况,就很「平面」,或者说不真实。

因此,我们可以思考如何与画面产生互动,比方说我们前几天设定的一种动画模式自由,你说他算不算互动呢?说实在,若以上面的定义来讲,我应该要能清楚的掌握着把东西抓着、放下、掉进水里一系列的过程,然而自由模式似乎就还不够符合这一标准,有时候它是直观的,能让使用者想操纵画面怎麽动就怎麽动,有时候它是令人挫折的,使用者不太能明白这个自由模式,我移动滑鼠到底意味着什麽?为什麽画面会产生些微的变化?就好比说,试着把手举高到头顶,结果却看到手指举到了你的眼前,这种失控感,无法完全肯定这只手是自己在控制。

有意义的行动

综上所述,互动即「让使用者透过回馈感受到自主的行动具有意义」,这不是维基百科写的,这是我所认为的定义,尤其是在和电子产品介面的沟通上,并且也值得我们做去做研究。

那麽,我们今天到底要做什麽呢?对於路上纷飞的落叶、乱飘的羽毛等等,不知道大家直觉想做什麽呢?
A. 很想抓住它们
B. 很想收集起来
C. 很想控制风的流向
D. 其他想法?欢迎留言

我想,大家想法都会不同,对我来说最直觉的就是把他们捉下来了,如同把星星摘下来那般,等摘到一定的数量,便能收集成一群,然後再一次抛出去!好不快乐,那麽,今天就来完成「补捉的动作检查」和「收集的动画」吧!

碰撞检测

这个领域有个词叫collision,不过今天也不想着墨太多,我们就把眼光放在「我以滑鼠控制着某物去捕捉物件」,其实就是要判断会不会碰触到,只要去比较两点的距离两物件半径的相加大小,那麽,我会这麽写:

  1. 计算两个物件中心点距离的平方: dx^2 + dy^2
  2. 计算两个物件半径相加的平方: (r1 + r2)^2

一般算距离都会用毕氏定理来开根号,不过我没有开根号,这算我的迷信吧,我认为乘法的效能比Math.sqrt好多了!一样写在我们用来跑动画的NextFrame方法中

animeObject.prototype.NextFrame = function(){

    // 检查是否与滑鼠碰撞
    let distanceP2 = Math.pow(this.pointX - myMouse.pointX, 2) + Math.pow(this.pointY - myMouse.pointY, 2);
    let mouseWidth = WIDTH * 0.05;
    let thisWidth = this.sizeNow;
    if(distanceP2 < Math.pow((mouseWidth * 0.4 + thisWidth * 0.5), 2)){
        // 触发收集的动画
        // ......
    }
    
    // 以下略
    // ......
}

let myMouse = new mouseTrail(0, 0, true);滑鼠几乎每篇都有用到,这边还是提醒一下,它是将第二章的学的滑鼠动画,以第三章的观念改写成物件建构式,因为觉得没有很重要,就没拿来水文章了,若觉得困惑可以留言询问。

为求方便性,会把物件当作一个圆,用半径来检测碰撞,因此一个方形的png图片,将其宽度乘上0.5,表示一个内切圆的半径,这边demo我用的是一个爱心当作滑鼠游标,比内切圆来的更小,因此乘上0.4。

另外暂时预设这个爱心(滑鼠)的宽度是整个画面宽度的0.05倍,且先前为了节省记忆体,没有存下this.width、this.height,而是直接计算後使用,所以才再定义一个thisWidth变数,帮大家回忆一下:

let width = this.sizeNow;
let height = this.sizeNow * this.img.height / this.img.width;
context.drawImage(this.img, -width/2, -height/2, width, height);

以上,应该没啥问题吧!那滑鼠碰到了物件要干嘛呢?收集起来罗!

收集的动画

我们可以先假定有一个收集点在正中间,思考的方向是要如何让这个动画物件,中断原本的动画,接着开始移动前往正中间的位置,这是一个从起点到终点的动画,基本的思路有:

  1. 改变这个物件本身的状态
  2. 提供这个物件一股推力

然而,以上两个各有缺点,第一个想法是改变物件的动画名称,之前原本是这样写的:

animeObject.prototype.NextFrame = function(){
    // 以上略
    if(this.animeName == "Floating") this.Floating(dT);
    else if(this.animeName == "Falling") this.Falling(dT);
    else if(this.animeName == "Staring") this.Staring(dT);
    // 以下略
}

由於我们的动画物件,是透过this.animeName来作为在NextFrame方法中分别演算座标的判断依据,那麽,今天只要落叶再也不是落下的状态,就不会动了,换言之,可以改成另一种状态,是不是就能轻松完成目标了呢?

这边要考量到原本设计该动画的性质,在我们第四章节所制作的这几种动画特性,它的形式较接近於粒子系统中所具有的随机性不可预测性,虽然我们有给定三角函数作为移动的公式,不过我们无法确保物件能够移动到一个确切的位置,这也是我们赋予这些物件灵魂和自由的重要因素,因此,若要在这里面添加新的prototype,反倒会使物件越来越肥大,随时都携带着不必要的变数。

第二个想法希望的是,可以在不使动画中断的情况下,让物件很自然的转弯被送往目的地,却是一种过度自信的展现,相似的例子有是有,在现实中,我们运动的时候,会在投篮时瞄准篮框、会在击球时看好方向,透过给予该物体势能,来让它跑到指定的位置并且得分。但是我请大家想想欧,如果有一种新篮球,要求必须由队友传球,然後你在不接球的前提下,把他传的球给送进篮框,这样容易吗?

这已经不是在打篮球了!这是在算数学,要你算入射角、反射角哪!同理,在程序面,当然也是能去思考,在已知出发点、终点、和当前速度後,要怎麽取得一个平滑的路径,这却是一个相当深入的主题,通常用在路径演算法Pathfinding,又称寻路问题,那是与自动控制、车辆有关的领域,虽然可以结合作讨论,不过有点偏门,网页主流的css动画都讲求简单好写,要是搞这麽复杂,未来要应用也难呀!(至於真的对车子有兴趣想做那类型的游戏的话......可以去Unity 3D丰富世界发展)

折衷的方案

答案只要一说,大家立刻就懂了,如果可以还是想让大家思考看看,上面的第一种方法,有更好的实现方案吗?

我们要先抽离对於叶子这个型态的想像,叶子之所以为叶子,是因为它会往下掉吗?还是我们判断它是一片叶子呢?对於使用者来说,其实也就是有个叶子的图形在画面上,只是因为看到图片,就把它想像成一个物体,所以我们才很认真地操作,使叶子的动画尽量自然,大小还要淡出淡入,诱导使用者判断「这是一片叶子」,不过反过来说,却也无比的随性,只要图片没有改成花朵、羽毛什麽的,对使用者来说,还是同一片叶子。

「摁?你在工沙ㄒ...」别急着喷我!如果,今天是两个看起来一模一样的叶子出现在同一个位置,使用者分得出来吗?叮咚!接下来我们要做的就是,复制一个看起来相同的叶子,来取代原本的叶子,如此一来,刚刚掉落的那片叶子就可以直接删掉回收,也不用去动到原本我们设计的动画物件了!

第二章学到的路径动画

因此,我们来设计另外一个动画物件的建构式,目前没想到合适名称,那就叫做animeObject2吧!

// 路径动画物件
function animeObject2(img, size, originX, originY, targetX, targetY, frames = 120){
    this.img = img;
    this.sizeNow = size;
    this.pointX = originX;
    this.pointY = originY;
    this.originX = originX;
    this.originY = originY;
    this.targetX = targetX;
    this.targetY = targetY;
    this.period = frames;
    this.timer = frames;
}

概念和第二章一模一样,可以回去复习

并且,在刚刚做好的碰撞检测中,执行如下步骤:

  1. 制作一个新的叶子仿冒品
  2. 删掉原本的叶子
  3. 将新的叶子放入原本的动画清单中
if(distance2p < Math.pow((mouseWidth * 0.4 + thisWidth * 0.5), 2)){
    // 用另一个物件取代该物件
    let newObject = new animeObject2(this.img, this.sizeNow,
                                     this.pointX, this.pointY,
                                     WIDTH / 2, HEIGHT / 2, 120);
    let index = animeList.indexOf(this);
    delete animeList[index];
    animeList[index] = newObject;
}

收集点设在正中央(WIDTH/2, HEIGHT/2)

复制这片叶子的时候,只需要有它的图案、大小、和当前位置即可,原本比较占空间、有着许多变数的那片叶子,就用delete语法删除掉,接着让新的物件取代它,就大功告成拉!

这边细节上要注意的就是,原本的动画框架是透过animeList来绘制一系列的动画,并呼叫每个物件的NextFrame方法,如下所示:

function Redraw(){
    clear(context);
    AudioProcess();
    MouseAnime();
    animeList.forEach(obj => obj.NextFrame());
}

因此我们也要替今天这个物件建构式,设计一个NextFrame方法:

animeObject2.prototype.NextFrame = function(){
    if(this.timer > 0){
        // 1. 计算座标
        // 2. 渲染图形
    }
    else{
        // 3. 把动画物件删掉
    }
}

这里就相对简单很多了,因为呢,这个骨架也是在第二章学过的,步骤2、3跟先前的动画物件为相同逻辑,步骤1又是第二章的内容,几乎没变:

if(this.timer > 0){
    // 1. 计算座标
    let dX = this.targetX - this.originX;
    let dY = this.targetY - this.originY;
    let t = this.timer;
    let p = this.period;
    let linear = 1/p;
    let easeout = Math.pow(t/p, 2) - Math.pow((t-1)/p, 2);
    let easein = Math.pow(1 - (t-1)/p, 2) - Math.pow(1 - t/p, 2);
    let a = 1;
    let b = 1;
    let c = -3;
    this.pointX+= (a * linear + b * easein + c * easeout) / (a+b+c) * dX;
    this.pointY+= (a * linear + b * easein + c * easeout) / (a+b+c) * dY;
    this.timer--;
}

应该有着莫名的熟悉感吧!不过还是补充一下,当初不是有说abc参数可以设定负值,并且造成回弹效果吗?如果开头的demo大家有注意到的话,就是来自於我这边设定c为-3,算是相当有趣的效果。

今天的文章就到这罗!喜欢的话请帮我按个赞呗

半小时挑战失败XD,别急,让我把code慢慢贴上来...

Yeah整理完了,结果花了两小时撰写文章、20分钟校稿www


<<:  【领域展开 15 式】 裸窥 WordPress Soledad 主题客制栏位

>>:  RISC-V: Memory Store 指令

JavaScript Day11 - 回圈

for、while 可参考:Day13 - 回圈(01) 还蛮常搭配阵列使用的 for (初始值 ;...

[Day 23]从零开始学习 JS 的连续-30 Days---箭头函式

函式陈述式与函式表达式差异 函式陈述式 : function num(x) { return x *...

11. STM32-SPI Nokia 5110 LCD 实作

Nokia 5110 LCD 介绍 刚好手边有块Nokia 5110 LCD 就拿它来做测试吧~虽然...

Day 05 GPIO peripherals

Control GPIO peripherals using digital input/outpu...

Day10 休息是为了走更长远的路

连续30天都看演算法的东西有点心累,目前计画第10、20、30天来做个轻松一点的东西XD。 发文真的...