Chapter4 - Canvas背景动画(III)风中的花朵 今天再加码让动画更加自然的方法

一样先上图!

https://ithelp.ithome.com.tw/upload/images/20210929/20135197XptL4H3ws9.png
https://jerry-the-potato.github.io/Chapter4-demo3/

Staring(因为像星星一样绕行)

有了前两篇作为基础就更容易了!今天我们想做漩涡的动画,让一朵朵的花冒出并绕着画面中心旋转,那麽一样给予初始条件:

switch(animeName){
    case 'Falling':
        this.beginX = Math.random() * WIDTH;
        this.beginY = Math.random() * HEIGHT;
    break;
    case 'Floating':
        this.beginX = Math.random() * WIDTH;
        this.beginY = Math.random() * 0.1 * HEIGHT + 0.95 * HEIGHT;
    break;
    case 'Staring':
        this.beginX = Math.random() * 0.1 * WIDTH +  0.45 * WIDTH;
        this.beginY = Math.random() * 0.1 * HEIGHT + 0.45 * HEIGHT;
    break;
}

虽然希望在中间,但位置也要稍微有所不同,因此设置宽高0.45-0.55之间,一个0.1x0.1的方格为起始点

一样计算公转角度(总算是个很直观的简谐运动了!)

let lT = this.lifeTime;
let revolveNow = this.revolveTheta + this.revolveOmega * dT / lT;
let A = Math.sin(revolveNow);
let B = Math.cos(revolveNow);

接着给定一个半径,就可以用半径和角度取得座标并旋转了!

let radius = 0.3 * (WIDTH + HEIGHT);
this.pointX = this.beginX + radius * this.scaleX * (this.period * 0.5 * B - 0 * (dT / lT));
this.pointY = this.beginY - radius * this.scaleY * (this.period * 0.5 * A - 0 * (dT / lT));

数字0的部分一样可以自行修改,为整体动画添加线性轨迹

记得要去体验一下今天风中的花儿,搭配规律、多样、混乱、自由四种动画效果唷,每种感受都很不一样呢!

请大家想想看...

今天的部份真快结束!那就出个思考题吧,如果希望画面真的像漩涡一般,让花朵在旋转的时候渐渐吸往中心点,可以怎麽做呢?提示:思考radius和时间的关系。
(当初第三章就有偷偷放一个其实不是demo的上来,里面就有这个效果唷!)

开玩笑,怎麽能这样草草结束呢?

来聊聊图案大小的变化

仔细考量过後,还是来解释一下好了!过往我们作的动画都是基於sin和cos,其方便性大家应该都体会到了,唯一的问题就是说,如果图案的缩放想要使用三角函数,就变成图案大小变化的过程,只会在高峰值短暂停留,那使用者还没机会看清楚图案,图案马上又要消失了,这是我们不希望发生的,因此思路很简单,我们的目的就是:「使其在高峰值停留更久」,具体要多久,起初是采用三等分切法,三分之一变大、三分之一停留高峰值、三分之一变小。

为了要达成这个目的,便要采用数线的距离公式,基本的公式是什麽?还记得国中有一种题目,叫做数线上有两个点,分别是A和B,请试着取一个C点,使得C到AB距离的总和为最小值,这样的C点有几个?答案其实就是AB线段上的所有点,换句话说,只要C点位於AB线段时,其距离总和就是最小值,此时便是我们想要的稳定状态;另一方面,当C点离开AB线段,离得越远,C与AB的距离皆会增加,因此总和会一直无上限的增加。

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

映射後的结果(物件大小变化图)

上述也是前几天给大家示范的做法,缺点是到稳定状态後,那三分之一真的就停留在高峰值,大小完全不变,显得有些不自然,今天我们再进行改良,并包成更容易调整的函式吧!

数线的距离公式

我们话不多说,接着就从程序开始撰写,这次我们采用3个点的写法,相比之下可以让数线公式更接近於sin的平滑,又保留我们想要的稳定效果,公式为:
Math.abs(t - t1) + Math.abs(t - t2) + Math.abs(t - t3);

为了让大家好懂一些,我们先来套数字吧!
t表示输入的值,我们设定为dT/lT,是昨天提到的一次性周期,从动画开始0-1动画结束,这边我们分别设置对称的三个点(0.35, 0.5, 0.65),使得公式成为:

Math.abs(t - 0.35) + Math.abs(t - 0.5) + Math.abs(t - 0.65);

以同样的逻辑去推理,就可得知,在0~1之间,0和1是距离这三个点最远的位置,0.5则是距离这三个点最近的位置,因此,可以写成完整代码:

let maxT = 0.5; // 预期图案在此时最大 max
let minT = 0;   // 预期图案在此时最小 min
let t1 = 0.35;
let t2 = 0.5;
let t3 = 0.65;
let maxD = Math.abs(maxT - t1) + Math.abs(maxT - t2) + Math.abs(maxT - t3);
let minD = Math.abs(minT - t1) + Math.abs(minT - t2) + Math.abs(minT - t3);

这边可能有人会感到疑惑,摁?你刚刚不是说0.5是最近的吗?怎麽变成maxT了!别紧张,这里指的是图案的最大值(所映射的时间0.5),同理,maxD也是图案的最大值(所映射的距离总和),什麽意思呢?

映射

精确地说,这里应该叫座标转换,试想,如果今天输入值是1到0,希望把输出值改成0到1,可以怎麽映射呢?简单,就写:newX = 1 - oldX,就可以从旧的x座标转换成新的x座标了,在这个例子中,既是把X轴作水平翻转,又向右平移了1个单位,不过,映射厉害的地方在於,我们不一定要完全懂其中的原理,也可以轻松映射。

首先将目前的时间dT/lT输入到公式,接着得到距离总和dist:

let dist = Math.abs(dT/lT - t1) + Math.abs(dT/lT - t2) + Math.abs(dT/lT - t3);
let transSize = (dist - minD) / (maxD - minD);

而原本dist介於minD到maxD之间,我们希望能作转换,使其介於0-1之间,因此:

  1. 将其先减去最小值,使得dist介於0到(maxD-minD)之间
  2. 然後再除以(maxD-minD),就能使其介於0到1之间

代数很难懂吧qq,来我们代入数字,计算後得知minD为1.5、maxD为0.3,因此:

  1. 将其先减去1.5(midD),使距离介於0到-1.2之间
  2. 然後除以-1.2(maxD-minD),使距离介於0到1之间

然後就没有然後了,我们的功能就神奇的做完了!

包成函式

但是,我们还是可以把公式写得更好维护一些,如下:

let Distance = function(t, t1 = 0.35, t2 = 0.5 ,t3 = 0.65){
    if(!isNaN(t)) 
        return Math.abs(t - t1) + Math.abs(t - t2) + Math.abs(t - t3);
}

判断输入值t是否"不"为NaN或数字以外的值,并给予t1、t2、t3预设值,可输入、可不输入

animeObject.prototype.GetSize = function(dT, lT){
    let maxD = Distance(0.5);
    let minD = Distance(0);
    let distance = Distance(dT/lT);
    let transSize = (distance - minD) / (maxD - minD);
    return transSize;
}
animeObject.prototype.Staring = function(dT){
    // ......
    // 以上省略
    const popSize = 0.1;
    this.sizeNow = WIDTH * this.size * (popSize + (1 - popSize) * this.GetSize(dT, lT));
}

说明:
图片完整大小设为 WIDTH * this.size
popSize是初始的参数0.1
再加上0.9 * 刚刚公式回传的transSize

这麽一来,就可以得到我们想要的曲线了:
https://ithelp.ithome.com.tw/upload/images/20210929/20135197VJyI8IwEnF.png

可以试着到geogebra复制这一行函数,即可看到上图
f(x)=((abs(x-0.5)+abs(x-0.35)+abs(x-0.65)-1.5)/(-1.2))

不过,毕竟是折线图,如果觉得变大变小的过程过於线性,可以给transSize次方项:
return Math.pow(transSize, 0.6);
我蛮推荐0.6的,可以让花朵快速长出来,并且在最大时停留更久,如图:
https://ithelp.ithome.com.tw/upload/images/20210929/20135197z53HW5CYgM.png

反过来说,也可以设置1.5,会使得花朵出来时,短暂的静止不变,接着才快速长大,造成延迟效果,到最大时的变化也相对剧烈一些,是另一种感觉。

最後还是放一下这三篇动画的程序码吧!

经过这次铁人赛逐步整理,变得好简洁(感动)
https://ithelp.ithome.com.tw/upload/images/20210929/20135197cL4gZGNXmX.jpg


<<:  【D29】模组化#4:讯号灯

>>:  Day14 - this&Object Prototypes Ch3 Objects - Contents - Existence - Enumeration 作者建议

[Day16]What is Merkle tree?

这篇会分成4个部分,分别是介绍merkle tree以及各种待会会用到的名词、实际看merkle ...

只要三分钟就能上手的Markdown语法~ 让你沉浸於笔记

#题外话 : 只要三分钟就能上手的Markdown语法~ 让你沉浸於笔记 ~在进入主题之前,回想一下...

DAY20聚类演算法(DBSCAN)

昨天介绍完kmeans演算法程序,今天就要介绍DBSCAN演算法: 基本上他是根据资料点的密度进行聚...

[Day27] 沟通之术 - 开发工程师篇

撑过了双十连假啦~~这也是铁人赛接近尾声的倒数第 4 篇~今天就来讲讲跟开发工程师的沟通之术吧! 前...

[Day 14]从零开始学习 JS 的连续-30 Days---forEach回圈

阵列 forEach 资料处理方法 语法:宣告阵列的名称+( . )+forEach( + func...