Chapter5 - 当一个勤劳的园丁,来修剪我们美丽的树(III)Canvas动画 让树随着读取画面长大

题外话

补充昨天忘记下的结论:不管要绘制的图案多大,都建议画(储存)在一个和原图一样大的canvas上,取代原本的图案,当作未来的绘制来源。

let leafImg = new Array();
let pngImg = new Array();
for(let N = 0; N < 4; N++){ // 准备了四种树叶素材
    leafImg[N] = new Image();
    // 等待每一张图片读取好
    leafImg[N].onload = () => {
        // 每张图片创建一个对应的画布
        pngImg[N] = document.createElement('canvas');
        pngImg[N].width = leafImg[N].width;
        pngImg[N].height = leafImg[N].height;
        let ctx = pngImg[N].getContext("2d");
        // 画一次就可以了,以後就拿pngImg[N]来当图片(N为0~3之间)
        ctx.drawImage(leafImg[N], 0, 0, leafImg[N].width, leafImg[N].height);
    }
}
// 依序设置图片的src(略)

也就是把昨天我们设定大小为30x30的那些画布,改成和图片相同的大小,即使如此,仍然可以减轻绘图的负担,实测都是100x100px的情况下:

  • 以leafImg(原图)做来源,用了13~15秒绘制树叶
  • 以pngImg (画布)做来源,用了10~12秒绘制树叶

相当推荐大家使用这个方法,先把要用到的图片转存一次!

开头画面

因为只是要做这个游戏的原型,先有个主体就好,就先不设定背景,一般游戏档案载入都会有一定时间,那麽我们就先假装我们正在载入,设计一下这个页面吧!

// 获取开始按钮的宽度
let Start = document.querySelector("#Start");
let Start_CSS = window.getComputedStyle(Start);
let minWidth = Math.floor(Start_CSS.width.substring(0, Start_CSS.width.lastIndexOf("p")));

// 动画框架
requestAnimationFrame(LoadingScreen);
function LoadingScreen(){
    try{
        Resize("#game-box", canvas, context, '#000');
        clear(context);
        
        // 1. 让树长出来
        treeGrowth.NextFrame(1, -1, 3);
        myTree.Transform();
        myTree.Draw();
        
        // 2. 读取进度条
        if(loading.timer > 0){
            loading.NextFrame(1, 0, 2);
            let percent = Math.floor(loading.pointX * 100);
            Start.style.width = minWidth + percent + "px";
            Start.textContent = percent + "%";
        }
        else Start.textContent = "Start";
    }catch(e){
        console.log(e);
        return;
    }
    requestAnimationFrame(LoadingScreen);
}

今天时间有点赶可能没办法一一解释,主要是把之前的技术整合起来,分别说明两大项:

缓冲函式

第二章所设计的缓冲函式,稍微改了个名字和微调,在建构式中先设定起始点,再用NewTarget方法来设定终点,同时也把Restore方法的定义写在当中,便可用来持续重置动画:

let Trail = function(x = 0, y = 0, visibility = false){
    this.pointX = x;
    this.pointY = y;
    this.originX = x;
    this.originY = y;
    this.period = 1;
    this.timer = 0;
    this.timestamp = Date.now();

    this.NewTarget = function(targetX = 1, targetY = 1, frames = 60){
        this.targetX = targetX;
        this.targetY = targetY;
        this.originX = this.pointX;
        this.originY = this.pointY;
        this.timer = frames;
        this.period = frames;
        this.Restore = function(){
            this.pointX = x;
            this.pointY = y;
            this.originX = x;
            this.originY = y;
            this.timer = frames;
            this.period = frames;
        };
    };
    // 以下略
    // NextFrame方法
}

把Restore包在NewTarget,是因为在输入参数时,比如说frames = 90,会把Restore中的this.timer设定成frames当下的值,也就是说无须另外设定一个变数存取该值。

举实际例子,接下来我会分别为树的成长和画面读取写入:

let treeGrowth = new Trail(0, 0, false);
let loading = new Trail(0, 0, false);
treeGrowth.NewTarget(1, 0, 90);
loading.NewTarget(1, 0, 90);

我们只需要一个变数X,由0到1,而Y皆设为0,两个动画侦数设为90

new Traail(x, y, visibility)设置为(0, 0, false),Restore函式就会变成:

this.Restore = function(){
    this.pointX = 0;
    this.pointY = 0;
    this.originX = 0;
    this.originY = 0;
    this.timer = frames;
    this.period = frames;
};

NewTarget(targetX, targetY, frames)设置为(1, 0, 90),Restore函式就会变成:

this.Restore = function(){
    this.pointX = 0;
    this.pointY = 0;
    this.originX = 0;
    this.originY = 0;
    this.timer = 90;
    this.period = 90;
};

那麽让树长出来应该就不难理解了,把昨天的Transform方法中grow变数的代码处修改成:

Stick.prototype.grow = treeGrowth.pointX;

就可以顺利使用一开始贴上的代码:

// 1. 让树长出来
treeGrowth.NextFrame(1, -1, 3);
myTree.Transform();
myTree.Draw();

加上事件监听,就可以利用Restore让玩家在开头画面有些许的互动:「点击画面任一处」重新长出一颗新的树。

let myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/6, 90, maxTimes)
let startScreen = document.querySelector("#startScreen");
startScreen.addEventListener('click', () =>{
    treeGrowth.Restore();
    myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/6, 90, maxTimes)
});

2. 读取进度条

在HTML设计一个ID为Start的按钮後,内文设"0%",上面我们在制作树的时候,把树根的Y座标设置在高度的80%,同样CSS就是设计水平置中垂直80%,来跟树相接:

<div id="startScreen">
    <button id="Start">0%</button>
</div>

接着用window.getComputedStyle方法取得目前该按钮的宽度,做字串处理来取得数值的部分:

// 获取开始按钮的宽度
let Start = document.querySelector("#Start");
let Start_CSS = window.getComputedStyle(Start);
let minWidth = Math.floor(Start_CSS.width.substring(0, Start_CSS.width.lastIndexOf("p")));

接着在动画框架中就可以利用缓冲函式loading的X座标映射到百分比percent,然後使按钮的宽度逐次增加,直到动画结束(也是loading.timer == 0为True的时候),就把该按钮的内文改成Start。

// 2. 读取进度条
if(loading.timer > 0){
    loading.NextFrame(1, 0, 2);
    let percent = Math.floor(loading.pointX * 100);
    Start.style.width = minWidth + percent + "px";
    Start.textContent = percent + "%";
}
else if(Start.disabled == true){ // 增设一个条件,避免重复设定Dom的属性
    Start.textContent = "Start";
    Start.disabled = false; // 让按钮从disabled状态回到可使用状态
}

最後别忘了在玩家点击开始按钮後,要隐藏开始按钮并且使该出现的东西出现,比如这边我们在CSS替header隐藏了起来,那就要记得把display改回block,并且用cancelAnimationFrame方法取消读取画面的动画:

Start.addEventListener("click", function(){
    Start.style.display = "none";
    let header = document.getElementsByTagName("header")[0];
    header.style.display = "block";
    cancelAnimationFrame(loadingAnime);
});

这边我们用loadingAnime变数来指向动画,才符合该方法的格式,那loadingAnime指向的动画是谁呢?让我们修改原本的动画框架:

let loadingAnime = requestAnimationFrame(LoadingScreen);
function LoadingScreen(){
    // ......
    // 以上略
    loadingAnime = requestAnimationFrame(LoadingScreen);
}

就是requestAnimationFrame方法所回传的值,暂存在loadingAnime里面,拿去console查看会发现,他实际上只是一个数字、一个表示动画的编号,似乎是从0开始累加,很可能表示了一连串的阵列

附录: 缓冲函式复习

还记得参数(a, b, c)吗?这次有稍作修改,作为可输入的三个参数,若无输入的情况,同样能使用input作为预设值,刚刚树的成长和进度条读取分别用了不同的参数,不过frames皆设为90,因此视觉上仍然能保有一致性:

let Trail = function(x = 0, y = 0, visibility = false){
    // ......
    // 以上略
    this.NextFrame = function(a=input.linear, b=input.easein, c=input.easeout){
        if(this.timer > 0){
            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);
            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--;
        if(visibility){
            let width = WIDTH * 0.05;
            let height = WIDTH * 0.05 * mouseImg.height / mouseImg.width;
            context.save();
            context.translate(this.pointX, this.pointY);
            context.drawImage(mouseImg, -width/2, -height/2, width, height);
            context.restore();
        }
    };
}

唯一的不同处在於多了一个专为滑鼠设计的visibility,在作滑鼠追踪时,不管是映射真实座标或是虚拟座标都很好用,上一个章节的爱心鼠标就是把这个设为true来绘制真实座标的

载入中

https://ithelp.ithome.com.tw/upload/images/20211006/20135197JsMS37qomD.png

载入完毕

https://ithelp.ithome.com.tw/upload/images/20211006/20135197IGKAfCMMoS.png

後记

接下来两天拚一下,等游戏完成在来个大一点的demo吧!


<<:  [经典回顾]过旧的作业系统事故纪录

>>:  DAY 20 - 四足战车 (1)

【Day 17】Google Apps Script - API 篇 - Spreadsheet Service - 电子试算表服务介绍

Spreadsheet(电子试算表) Service API 可以让你完整的控制 Google s...

【Day 02】认识演算法 Algorithm ( 使用 JavaScript )

一、什麽是演算法 ( Algorithm ) ? 演算法是一组 step by step 用来解决问...

使用python 模拟使用者输入 for Win

故事是这样的 ... 有个专案需要在执行过程中输入某些文字, 但不能使用按键精灵之类的软件去使用. ...

DAY 17 Big Data 5Vs – Variety(速度) Glue Data Brew

目前为止Glue的三个工具,可以依使用者的开发习惯与技术背景来选用,而AWS是以客户为导向的公司,对...

虾皮串接实作笔记-Access Token

前言 目标:串接虾皮订单、标签资讯,目前串接虾皮 OpenAPI 2.0 版本,串接手册 串接步骤:...