Chapter4 用音乐做动画 结合前三章学习的内容,一口气冲刺吧!

题外话

昨天没把树叶画上去,还是心痒痒的,所以动手简单装饰了一下这棵树:
https://jerry-the-potato.github.io/Chapter3-demo2-object/
不过灵感跟美感毕竟不太够,还是赶快前进到第四章要紧先
(进度快的话最後还能把树放到游戏中然後优化它)

差点忘了我们要做网页游戏!?

是时候结合1-3章的内容了,首先我们观察一下几首歌的音量变化:(音量增加图)

出自: 我的Demo
https://ithelp.ithome.com.tw/upload/images/20210926/201351976xXsT9FmZg.png
https://ithelp.ithome.com.tw/upload/images/20210926/20135197m7NBFNGKP6.png

当初没有教大家怎麽画这个,没关系这不难也不是重点,听结论就好

经过观察得知,每当音符/和弦出来的时候,都会有2-4侦的音量急遽增加,随後缓慢降低,因此第一个思路就很单纯,我们希望程序能懂得把连续的音量脉冲,当成完整一次的旋律,并且在第2侦时会接近高峰值,也是音乐正在传入耳朵时,因此将其脉冲大小作为参考,来决定要一次做出多少个动画。

接着看下一首,有钢琴和弦作为开场的音乐:
https://ithelp.ithome.com.tw/upload/images/20210926/20135197UvILWT4uVc.png

可以看到,音量的曲线应该接近於Ease-out达到高峰,所以音量的增加才会如图,到达高峰後陆续下降并趋於平缓,这种和弦带来的效果是拉长的听觉效果,先做个笔记,未来有时间可以再新增第二种动画模式,来呈现其绵延的旋律。

判读音量的状态

刚也有提到音量增加图表示了音量的差值,直接取名为volume,还记得我们用reduce方法的那篇写到:

// 阵列最大长度到 INDEX 为止
(dataIndex.volume > INDEX) ? dataIndex.volume = 0 : dataIndex.volume++;
let volume = dataArray.delta.reduce((a,b) => a+b, 0);
dataArray.volume.splice(dataIndex.volume, 1, volume);
// 计算最大值
let maxVolume = dataArray.volume.reduce((a,b) => Math.max(a,b), 1);

没印象的话可以回头去看这篇 https://ithelp.ithome.com.tw/articles/10268606

当初为了画图已经有做好这个阵列,这边我们就有很简单的方法,拿到最近三次的差值:

let v1 = dataArray.volume[dataIndex.volume - 1];
let v2 = dataArray.volume[dataIndex.volume - 2];
let v3 = dataArray.volume[dataIndex.volume - 3];
// 我们只要连续两次,连续三次以上都不算数,因此判断 v3 <= 0
if(v1 > 0 && v2 > 0 && v3 <= 0){
    let times = 100 * Math.max(v1, v2) / maxVolume; // 100乘上一个0~1之间的数
    animeList.push(new animeObject(times, 'Falling'));
}

animeObject 是我们第二章操作的落叶物件
animeList 是个把物件们放在一起,用来迭代的阵列

动画物件(建构式)

let animeList = new Array();
function animeObject(times, animeName='Falling',
                     imgNumber=Math.floor(Math.random()*4),
                     sizeMin=0.03,sizeMax=0.04,
                     lifeTime=5, timestamp=Date.now()){
    this.animeName = animeName;
    this.imgNumber = imgNumber;
    this.img = pngImg[this.imgNumber];
    if(animeName == "Falling" || animeName == "Staring"){
        this.beginX = Math.random() * WIDTH;
        this.beginY = Math.random() * HEIGHT;
    }
    this.size = Math.random() * (sizeMax - sizeMin) + sizeMin;
    this.timestamp = Date.now();
    this.lifeTime = lifeTime;
    this.period = 1 + Math.random() * 1;

    // 变化属性
    this.pointX = this.beginX;
    this.pointY = this.beginY;
    this.sizeNow = 0;
    this.rotateTheta = Math.random() * 360 / 180 * Math.PI;
    this.rotateOmega = 60 / 180 * Math.PI;
    this.revolveTheta = Math.random() * 360 / 180 * Math.PI;
    this.revolveOmega = 60 / 180 * Math.PI;
    if(times > 5) animeList.push(new animeObject(Math.pow(times, 0.9), 'Falling'));

    // times 的算法可以自行设计,就是把输入的参数转换成迭代的参数
    // 我是设计为5-100之间会进行迭代,然後每次开0.9次方根号
    // (100共会做十次、40会做八次、20会做六次、12会做四次、7会做两次)
}

开头传入参数时,若无值传入,则预设为Falling模式、图片以4个一组随机抽签etc...
整体跟落叶物件那篇相去不远,主要添加了递回的观念
以及一个随机的周期参数period,使动画物件彼此拥有不同的周期,范围是1~2倍

动画方法

接着搭配第三章画树时学到的prototype改写方法:

animeObject.prototype.NextFrame = function(){
    
    // 计算下一侦的位置
    let dT = (Date.now() - this.timestamp) / 1000;
    if(this.animeName == "Floating") this.Floating(dT);
    else if(this.animeName == "Falling") this.Falling(dT);
    else if(this.animeName == "Staring") this.Staring(dT);

    if(dT < this.lifeTime){
        // 画出下一侦的位置
        if(this.img.complete){
            let width = this.sizeNow;
            let height = this.sizeNow * this.img.height / this.img.width;
            let rotateNow = this.rotateTheta + this.rotateOmega * dT;
            context.save();
            context.translate(this.pointX, this.pointY);
            context.rotate(rotateNow);
            context.drawImage(this.img, -width/2, -height/2, width, height);
            context.restore();
        }
    }
    else{
        // 把动画物件删掉
        let index = animeList.indexOf(this);
        delete animeList[index];
        animeList.splice(index, 1);
    }
}

计算下一侦的位置

计算下一侦的位置,根据不同种类的动画公式去做计算,以落下为例子:

animeObject.prototype.Falling = function(dT){
    let revolveNow = this.revolveTheta + this.revolveOmega * dT;
    let A = Math.sin(revolveNow);
    let B = Math.cos(revolveNow);
    let C = Math.sin(revolveNow * this.period);
    let D = Math.cos(revolveNow * this.period);
    this.pointX = this.beginX + WIDTH * 0.04 * A;
    this.pointY = this.beginY + HEIGHT * 0.01 * C + HEIGHT * 0.05 * dT;
    const popSize = 0.2;
    let lT = this.lifeTime;
    let distanceT = (Math.abs(dT - lT*0.35) + Math.abs(dT - lT*0.65)) / lT;
    this.sizeNow = WIDTH * this.size * (popSize + (1 - distanceT));
}

在第二章的动画基础上,添加几个部分:

  1. 有两种三角函数,一种是基本的、另一种是乘上周期倍速period的变速版本
  2. 添加popSize,使得动画一开始出现时以只有0.2倍大(从远方渐入的效果)
  3. distanceT是一个国中学过的线性函式,用来计算数线0-1之间,任一点与0.35和0.65的距离总和,结果就不用解释...了吧?XD

後记

接下来第四章(包括这篇)都是重头戏,先前铺陈那麽久,就是为了能让大家循序渐进好上手,如果前面的章节你都看过,想必这一篇并不会太难吧!也是不断精简过後的内容了,这段code是我重写第三次了,第一次大概是2倍的行数,第二次是1.5倍,现在就如各位看到的。

话说,明天上个流程图吧!


<<:  离职倒数5天:「你是中国人吗」应该是我在日本生活最不自在的瞬间之一

>>:  Day11:开发 MVP

CSS文字样式相关属性(DAY11)

今天这篇文章会介绍CSS文字大小、文字粗细、字体和字型,这些都是有关文字样式的相关属性: 文字大小 ...

.NET、托管代码(managed code)、反射

托管代码(managed code) 微软特定用语 简单来说 managed code 就是由一个 ...

[Day14] TS:什麽!TypeScript 中还有回圈的概念 - 用 Mapped Type 操作物件型别

) 上面这个是今天会提到的内容,如果你已经可以轻松看懂,欢迎直接左转去看我队友们的精彩文章! Ind...

Day 2 python简易语法

在开始学习机器学习之前,我们得先准备好环境,我们使用python来当我们的程序语言,稍微介绍一下py...

LeetCode 刷题的只是写好程序的第一哩路

什麽是 LeetCode? 「什麽是 LeetCode?」是整个铁人赛系列文章的第一个主题,你现在...