Chapter2 - Canvas动画(III)让我们跳过微积分 用轻松的方式画落叶吧

接下来终於要谈谈,让我们更轻松的物件了

其实网路上有很多相关的文章,都可以带你更深入JS时,但常常问题在於,他们的举例都不够实际,并不是说不好,而是「需求的问题」,如果你和我一样,在早期只是单纯自我学习,而不是靠JS养活自己,那麽很容易没有方向,即使那些文章说的头头是道,仍然很无感,因为平常根本没有这样的用途!

而这次的系列文,透过一个很明确的目标,带大家实作,就是为了让大家有感,从一开始造轮子,渐渐会发现大大小小的轮子散落四方,开始越来越难整理,今天便是一个分歧点,当我们要进入复杂的programming,单纯的变数命名已经不符合我们的需求,那我们需求是什麽呢?我们要为游戏做动画,我们要让一个东西动起来,还要让无数个东西动起来,那要怎麽用有限的程序码,实现无限个动画呢?让我们从第二章节娓娓道来:

落叶归根

首先,还是先从实作面开始,就从一个落叶解释为什麽需要物件吧!要做一个"完整"的落叶动画效果,你觉得要至少准备几个参数呢?以下让我们实际演示一遍

首先我们定义原点:

let originX;
let originY;

接着我们定义初始时间、动画总生命周期:

let timestamp;
let lifeCycle; // 单位:毫秒(s)

然後设计落叶的初始角度、角速度和大小:

let rotateTheta; // 单位:rad
let rotateOmega; // 单位:rad/s
let size;

以为这样就结束了吗?不只这些,待会还要用到两个方程序:

  1. X 路径方程序
  2. Y 路径方程序

一般来说,为了得到路径方程序,需要用速度对时间做积分:

  1. (X 速度方程序) 对时间的积分
  2. (Y 速度方程序) 对时间的积分

但是这样的系统太复杂,说好要跳过微积分的!我们可以简化它,直接思考落叶是怎麽动的,印象中的叶子会左右摆动,缓慢飘弱,是不是很像钟摆的往复运动呢?这种周而复始的循环,用到的是三角函数,其最简单的形式是简谐运动,也就是说,只需要有公转角度、公转速度:

let revolveTheta; // 单位:rad
let revolveOmega; // 单位:rad/s

我们就可以跳过以上复杂的方程序,继续用国中数学画图了!

好了,材料备好了,用昨天的Demo改写,实作看看吧

第一步: 滑鼠点击时,取得初始条件

canvas.addEventListener('click', SetMouse);
function SetMouse(e){
    originX = (e.pageX - Rect.left) * RATIO;
    originY = (e.pageY - Rect.top) * RATIO;
    size = 200;
    rotateTheta = 0 / 180 * Math.PI;
    rotateOmega = 40 / 180 * Math.PI; // 每秒旋转40度
    revolveTheta = 0 / 180 * Math.PI;
    revolveOmega = 90 / 180 * Math.PI; // 每秒公转90度
    timestamp = Date.now();
    lifeCycle = 6; // 生命周期6秒
}

帮大家回忆一下,Rect是用canvas.getBoundingClientRect()原生方法取得画布在画面中位置

第二步: 计算当前时间,判断动画是否进行中

let deltaTS = (Date.now() - timestamp) / 1000;
if(lifeCycle > deltaTS){
    Clear(context);
    // 第三步 + 第四步写在这里面
}

Clear是接续前次设计的清除画布函式

第三步: 计算当前旋转角度、公转角度,并代进方程序

let rotateNow = rotateTheta + rotateOmega * deltaTS;
let revolveNow = revolveTheta + revolveOmega * deltaTS;
cursorX = originX + 500 * Math.sin(revolveNow);
cursorY = originY + 200 * Math.sin(1.5 * revolveNow)
                  + 100 * deltaTS;

设计让落叶水平来回摇摆、垂直落下则会遇到微风的影响,一下快一下慢

第四步: 进行座标的转换并绘制图案

if(leafImg.complete){
    let width = size;
    let height = size * (leafImg.height / leafImg.width);
    context.save();
    context.translate(cursorX, cursorY);
    context.rotate(rotateNow);
    context.drawImage(leafImg, -width/2, -height/2, width, height);
    context.restore();
}
  • save: 储存当前画布的状态(座标、平移、缩放、旋转等等),目前原点(0, 0)为最左上角
  • translate: 平移公式,将原点移到图案的正中心
  • rotate: 以原点为中心旋转,因为刚刚平移过了,因此会根据图案的中心旋转
  • restore: 返还已储存的画布状态,回到一开始以左上角为原点(0, 0)的状态

这样总算是完成一个落叶动画了!请看Demo

变数一大堆,维护程序码略显吃力

一开始你猜测会用到几个变数呢?除了一开始定义的9个变数,还有途中计算的当前时间等等参数,至少就用到了12个变数欸!更不用说原本就定义好的cursorX和cursorY以及其他我们事先准备好的架构,难道没有一个更好的方式去整理吗?

没错,答案就是物件XD,而现在的状况是,这个落叶的参数四散各处,我们想要的其实就是一种「可以将这个落叶视为一个整体的方法」,明天就带大家一起反向思考,我们先谈我们想要什麽,再谈物件是什麽,你就会发现,哇!JS的物件真的是设计得太好了,直接给JS的创始人一个大大的赞


<<:  Python turtle套件

>>:  第十一天:学习 Gradle 的第一个指令 - init

React.js 职场实战!图片 Infinite List

一天的开始 还记得吗?你是负责 Imager 的前端工程师,上次做了 Lazy Loading 改...

【Day24】人力资源篇-Time Off

#odoo #开源系统 #数位赋能 #E化自主 休假管理,在实务上又是另一门高深的学问。公司除了必须...

D8 - 如何用 Google Apps Script 将 Google Calendar 上的事件与更新全部列出到 Google Sheet 上?

来到了第 8 天。但一样先讲结论,如果你很急着用,可以直接使用这份 Add-On: Calendar...

Day23

函数指标只要参数与返回值相同是可以随时指向一个新的函数如前所说的max, min,当然C++作为那个...

day10: CSS style 规划 - utility CSS(Tailwind)-1

在前面章节我们介绍过 纯 CSS, CSS in JS 那接下来我们要来介绍 utility CSS...