Chapter5 - 轻松用Canvas实现转场动画和运镜处理

接下来时间真的很紧,也顾不上结构了,只能就目前想到的功能,先以直觉的方式编写了,如果讲不太清楚还多多包涵!昨天的一定要先看唷,今天是接着作下去的

转场

玩家从进到网页开始,一直到游戏结束,总共会经历四个场景:

  1. 读取画面+主画面(LoadingScreen)
    1. 树的生长动画(treeGrowth)
    2. 读取进度条(loading)
    3. 选单淡入(opacity)
  2. 游戏开场画面(OpeningScreen)
    1. 选单淡出(opacity)
    2. 运镜(camera)
  3. 游戏进行画面(GamingScreen)
    1. 树叶生长动画
    2. 树叶掉落动画
    3. 树叶收集动画
  4. 游戏结算画面(EndingScreen)

1. 读取画面+主画面 LoadingScreen

调整一下昨天设计的:「点击画面任一处,重新长出一颗新的树」取名为MakeTree函式,方便我们在离开读取画面後,可以移除这个功能(事件):

startScreen.addEventListener('click', MakeTree);
function MakeTree(){
    treeGrowth.Restore();
    myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/6, 90, maxTimes);
}

树的生长1-1和读取进度条1-2昨天都完成了,这边继续添加选单淡入1-3的功能,是在按下开始键後始选单1.5秒(90帧)内淡入,覆盖整个画面,初始设定:

let opacity = new Trail(0, 0, false);

按下Start按钮时,移除MakeTree函式,接着设定转场效果:

Start.addEventListener("click", function(event){
    // 设定
    event.stopPropagation();

    // 执行
    startScreen.removeEventListener('click', MakeTree);
    Start.style.display = "none";
    opacity.NewTarget(1, 0, 90);
});

由於昨天有设置整个画面的click作为让树重新长出来的互动,如果点了Start按钮也会触发则显得奇怪,因此在点击事件中做阻挡冒泡的设定,这样点击时,这样就不算点击到整个startScreen

然後在昨天设计的动画框架中,添加1-3选单淡入,设定header的opacity:

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 if(Start.disabled == true){
            Start.textContent = "Start";
            Start.disabled = false;
        }
         
        // 3. 选单淡入
        if(opacity.timer > 0){
            opacity.NextFrame(1, 0, 2);
            let header = document.getElementsByTagName("header")[0];
            header.style.opacity = opacity.pointX;
        }
    }catch(e){
        console.log(e);
        return;
    }
    loadingAnime = requestAnimationFrame(LoadingScreen);
}

NextFrame内部会自行判断timer是否已归零(动画结束),这边的if判断式在於限制dom的操作在90次(帧数frames=90),为了应该会尝试把它整合到NextFrame的原型方法中,今天没这个时间啦~~

2. 游戏开场画面(OpeningScreen)

2-1是衔接1-3,让选单淡入後,等待玩家点击Play,就让选单淡出,此过程从场景1换到场景2,因此在这边:

Play.addEventListener("click", function(event){
    // 让玩家发现,刚刚开场的那颗树,已经默默地成长为参天大树
    myTree = new Tree(WIDTH/2, 0.8 * HEIGHT, HEIGHT/2, 90, maxTimes);
    
    // 设定淡出和运镜
    opacity.NewTarget(0, 0, 90);
    camera.NewTarget(0.1, 0.3, 120); // 第一次运镜到左下角
    
    // 切换场景 1>2
    cancelAnimationFrame(loadingAnime);
    openingAnime = requestAnimationFrame(OpeningScreen);
});

直接创建了一颗新的树,严谨一点的作法可以在使用原型方法Transform,并在其中修改树根长度的参数,这样可以保证是同一棵树,且形状相同,不过今天没时间去修改了><

运镜起始点:

let camera = new Trail(0, 0, false);

运镜这边用偷吃步的作法,把四个运镜接起来,逻辑就是每次timer归零,就会进入载入下一次的运镜,然後把帧数当作编号,依序用120、121、122、123表示四种运镜,因此当编号120的运镜结束(timer归零)时,就会设定新的路径编号为121,以此类推,当然这样写有点占空间,未来可以再多花时间去思考能怎麽样包成更简单的函式:

function OpeningScreen(){
    try{
        Resize("#game-box", canvas, context, undefined);
        clear(context);

        // 1. 选单淡出
        if(opacity.timer > 0){
            opacity.NextFrame(1, 0, 2);
            let header = document.getElementsByTagName("header")[0];
            header.style.opacity = opacity.pointX;
        }

        // 2. 运镜
        let x = camera.pointX * WIDTH;
        let y = camera.pointY * HEIGHT * 1;
        context.translate(x, y);
        myTree.Transform();
        myTree.Draw(context);
        context.translate(-x, -y);
        
        if(camera.timer > 0){
            if(camera.period == 120){
                camera.NextFrame(1, 1.5, 0);
            }
            else if(camera.period == 121){
                camera.NextFrame(1, 0, 1);
            }
            else if(camera.period == 122){
                camera.NextFrame(1, 1.5, 0);
            }
            else if(camera.period == 123){
                camera.NextFrame(1, 0, 1.5);
            }
        }
        else if(camera.period == 120){
            camera.NewTarget(0, 1, 121); // 第二次运镜到正上方
        }
        else if(camera.period == 121){
            camera.NewTarget(-0.3, 0.2, 122); // 第三次运镜到右下角
        }
        else if(camera.period == 122){
            camera.NewTarget(0, 0.6, 123); // 第四次运镜到中间
        }
        else{
            // 运镜结束,直接进入第三个场景:游戏进行画面
            GamingAnime = requestAnimationFrame(GamingScreen);
            // 直接在此处中断即可结束该开场动画
            return;
        }
    }catch(e){
        console.log(e);
        return;
    }
    openingAnime = requestAnimationFrame(OpeningScreen);
}

以camera的座标去换算,运镜分别设定四个点,让镜头顺时针绕一圈,呈螺旋状(底部>左下>正上方>右下>中间)

3. 游戏进行画面(GamingScreen)

做到这边我发现一个致命问题,接下来的第三个场景需要一个静态画布+动态画布,而我原先设计的动画框架,没有考虑到使用两个以上的画布的情况,因此有点难以扩充,搞了一下子发现事情不太对劲,要重新编修整个框架,包含Resize动态调整宽高的方式,这部份明天继续!

今日DEMO (上传完毕)

https://jerry-the-potato.github.io/Chapter5/
运镜的部分稍微有点粗糙,主要是因为这四个运镜都是直线,如果之後有把第四章附录的贝兹曲线继续做完,就可以应用在这上面,便再也不用担心运镜看起来卡卡的罗!


<<:  [第二十一只羊] 迷雾森林舞会XV 建立村庄 游戏角色设定

>>:  DAY 21 - 四足战车 (2)

【Day4】:来使用STM32CubeIDE吧!

程序码导读 点开我们的main.c档案,可以看到里面密密麻麻的注解,第一次看到还真令人害怕,但其实他...

IOS、Python自学心得30天 Day-5 TensorFlow 建立和训练模型

前言: 再来就是建立和训练模型 程序码: 方案一 model = tf.keras.Sequenti...

Day25 - 透过 Rake 自动下载处理台湾证券交易所的资料

前言 已经能从「台湾证券交易所」抓资料、存入 DB,接下来要做自动化处理 说明 由於我电脑没有 24...

Day14 javascript 错误

今天要来看的是JavaScript 错误 - throw、try 和 catch: 1.try 语句...

成员 14 人:如何养好一池鲨鱼水族箱

「干部不强,我身上尽是汗水味;  干部太强,我身旁满是血腥味。」 年轻时候 待过的公司,共有三个部门...