HTML5游戏开发(十)

一、动画

1、动画循环

  在Canvas中这病动画效果很简单:只需要在播放动画时持续更新并绘制就行了。这种持续更新与重绘叫就动画循环。

(1)通过requestAnimationFrame()方法让浏览器来自行决定帧速率:

  使用window.setInterval()或window.setTimeout()制作出的动画,其效果可能并不如预期般流畅,而且还可能会占用额外的资源。这是因为setInterval()与setTimeout方法具有如下特征:
==它们都是通过法,并不是专为制作动画而用的。
即使向其传递以毫秒为单位的参数值,它们也达不到毫秒级的精确性。
没有对调用动画循环的机制作优化。
不考虑绘制动画的最佳时机,而只会一味地以某个大致的时间时隔来调用动画循环。==

(2)Ployfill
window.requestNextAnimationFrame =
   (function () {
      var originalWebkitRequestAnimationFrame = undefined,
          wrapper = undefined,
          callback = undefined,
          geckoVersion = 0,
          userAgent = navigator.userAgent,
          index = 0,
          self = this;

      //chrome浏览器
      if (window.webkitRequestAnimationFrame) {
         wrapper = function (time) {
           if (time === undefined) {
              time = +new Date();
           }
           self.callback(time);
         };
         //原生
         originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;    
         window.webkitRequestAnimationFrame = function (callback, element) {
            self.callback = callback;           
            originalWebkitRequestAnimationFrame(wrapper, element);
         }
      }
     //火狐浏览器
      if (window.mozRequestAnimationFrame) {
         index = userAgent.indexOf('rv:');
         if (userAgent.indexOf('Gecko') != -1) {
            geckoVersion = userAgent.substr(index + 3, 3);
            if (geckoVersion === '2.0') {
               //原生
               window.mozRequestAnimationFrame = undefined;
            }
         }
      }

      return window.requestAnimationFrame   ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame    ||
         window.oRequestAnimationFrame      ||
         window.msRequestAnimationFrame     ||
         //回调函数 
         function (callback, element) {
            var start,
                finish;
            window.setTimeout( function () {
               start = +new Date();
               callback(start);
               finish = +new Date();
               self.timeout = 1000 / 60 - (finish - start);
            }, self.timeout);
         };
      }
   )();

2.帧速率计算

 &ems;动画是由一系列叫做帧的图像组成的,这些图像的显示频率就叫做帧速率。通常来说,有必要计算一下帧速率。在基于时间的运动效果是地,可能会用到动画的帧速率,或是有时为了保证动画能够播放得足够流畅,我们也需要知道帧速率。 #### (1) 基本功能

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>帧速率计算</title>
        <style>
            body {
                background: #dddddd;
            }

            #canvas {
                position: absolute;
                left: 0px;
                top: 20px;
                margin: 20px;
                background: #ffffff;
                border: thin inset rgba(100, 150, 230, 0.5);
            }

            #controls {
                margin-top: 10px;
                margin-left: 15px;
            }
        </style>
    </head>

    <body>
        <div id='controls'>
            <input id='animateButton' type='button' value='开始' />
        </div>
        <canvas id='canvas' width='500' height='300'>
       </canvas>
        <script src='js/Frame.js'>
        </script>
    </body>

</html>

JS脚本

var canvas = document.querySelector('#canvas'),
    context = canvas.getContext('2d'),
    paused = true,
    //定义三个小球数据
    discs = [{
        x: 150,
        y: 50,
        lastX: 150,
        lastY: 50,
        velocityX: 3.2,
        velocityY: 3.5,
        radius: 25,
        innerColor: 'rgba(0,255,255,0.3)',
        middleColor: 'rgba(0,255,255,0.9)',
        outerColor: 'rgba(0,255,255,0.3)',
        strokeStyle: 'slateblue',
    }, {
        x: 75,
        y: 200,
        lastX: 75,
        lastY: 200,
        velocityX: 2.2,
        velocityY: 2.5,
        radius: 25,
        innerColor: 'rgba(225,225,225,0.1)',
        middleColor: 'rgba(225,225,225,0.9)',
        outerColor: 'rgba(225,225,225,0.3)',
        strokeStyle: 'gray'
    }, {
        x: 100,
        y: 150,
        lastX: 150,
        lastY: 50,
        velocityX: 1.2,
        velocityY: 1.5,
        radius: 25,
        innerColor: 'orange',
        middleColor: 'yellow',
        outerColor: 'gold',
        shadowColor: 'rgba(255,0,0,0.7)',
        strokeStyle: 'orange'
    }, ],
    //小球数量
    numDiscs = discs.length,
    //时间
    lastTime = 0,
    //
    lastFpsUpdateTime = 0,
    frameCount = 0,
    animateButton = document.querySelector('#animateButton');

//擦除背景
function eraseBackground() {
    context.clearRect(0, 0, canvas.width, canvas.height);
}

//更新小球位置,进行坐标计算
function update() {
    var i = numDiscs,
        disc = null;
    while(i--) {
        disc = discs[i];
//      坐标计算
        if(disc.x + disc.velocityX + disc.radius > context.canvas.width ||
            disc.x + disc.velocityX - disc.radius < 0)
            //横坐标
            disc.velocityX = -disc.velocityX;

        if(disc.y + disc.velocityY + disc.radius > context.canvas.height ||
            disc.y + disc.velocityY - disc.radius < 0)
            //纵坐标
            disc.velocityY = -disc.velocityY;
//      默认为相加 每次移动距离
        disc.x += disc.velocityX;
        disc.y += disc.velocityY;
    }
}
//绘制小球
function drawDisc(disc) {
    var gradient = context.createRadialGradient(disc.x, disc.y, 0,
        disc.x, disc.y, disc.radius);

    gradient.addColorStop(0.3, disc.innerColor);
    gradient.addColorStop(0.7, disc.middleColor);
    gradient.addColorStop(1.0, disc.outerColor);

    context.save();
    context.beginPath();
    context.arc(disc.x, disc.y, disc.radius, 0, Math.PI * 2, false);
    context.clip();

    context.fillStyle = gradient;
    context.strokeStyle = disc.strokeStyle;
    context.lineWidth = 2;
    context.fill();
    context.stroke();

    context.restore();
}
//绘制小球
function draw() {
    var i = numDiscs,
        disc;
    i = numDiscs;
    while(i--) {
        disc = discs[i];
        //小球绘制
        drawDisc(disc);
        disc.lastX = disc.x;
        disc.lastY = disc.y;
    }

    if(frameCount === 100) {
        frameCount = -1;
    }

    if(frameCount !== -1 && frameCount < 100) {
        frameCount++;
    }
}
//计算帧率
function calculateFps() {
    //每秒的播发帧数
    var now = (+new Date),
        fps = 1000 / (now - lastTime);
    lastTime = now;
    return fps;
}
//动画
function animate() {
    //获取当前时间
    var now = (+new Date),
        fps = 0;
    //启动动画
    if(!paused) {
        //擦除重绘
        eraseBackground();
        //更新
        update();
        //绘制
        draw();
        //计算帧率
        fps = calculateFps();
        //如果当前时间与最后更新时间大于1000
        if(now - lastFpsUpdateTime > 1000) {
            lastFpsUpdateTime = now;
            //最后帧率
            lastFpsUpdate = fps;
        }
        //设置帧率显示样式
        context.fillStyle = 'cornflowerblue';
        //四舍五入取值
        context.fillText(lastFpsUpdate.toFixed() + ' fps', 45, 50);
    }
    //chrome可以使用标准的requestAnimationFrame
    window.requestAnimationFrame(animate);
}

//加载就调用
window.requestAnimationFrame(animate);
(2)事件处理
//-------------------------------事件处理
canvas.onclick = function(e) {
    paused = paused ? false : true;
};

animateButton.onclick = function(e) {
    paused = paused ? false : true;
    if(paused) {
        animateButton.value = '开始';
    } else {
        animateButton.value = '停止';
    }
};

context.canvas.width = canvas.width;
context.canvas.height = canvas.height;
//设定字体
context.font = '48px Helvetica';

显示效果:

html5游戏编辑器 html5开发游戏_html5游戏编辑器

3.恢复动画背景

从来质上来看,恢复动画背景无外乎三种方法:
1、将所有内容擦除,并重绘制;
2、仅绘内容发生变化的区域;
3、从离屏缓冲区中将内容发生变化的那部分背景图像恢复到屏幕上。

4.利用剪辑区域来重绘背景

//重绘背景
function drawBackground(){
    context.fillStyle="#99CC99";
    context.fillRect(0,0,canvas.width,canvas.height);
}
//重绘小球区域
function drawDiscBackground(disc){
    //保存原来
    context.save();
    context.beginPath();
    context.arc(disc.lastX,disc.lastY,disc.radius+1,0,Math.PI*2,false);
    //剪辑
    context.clip();
    //擦除背景
    eraseBackground();
    //重绘背景
    drawBackground();
    //恢复
    context.restore();
}

//绘制小球
function draw() {
    var i = numDiscs,
        disc;
    i = numDiscs;
    while(i--) {
        drawDiscBackground(discs[i]);
        disc = discs[i];
        //小球绘制
        drawDisc(disc);
        disc.lastX = disc.x;
        disc.lastY = disc.y;
    }

    if(frameCount === 100) {
        frameCount = -1;
    }

    if(frameCount !== -1 && frameCount < 100) {
        frameCount++;
    }
}

5.利用图块复制技术重绘背景

将整个背景一次性地绘制到离屏canvas中,稍后从离屏canvas中只将修复动画背景所需的那一块图像复制到屏幕上即可。

//创建离屏对象
var offscreenCanvas = document.createElement('canvas');
offscreenContext = offscreenCanvas.getContext('2d');
//离屏的大小设定
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
//将原画布复制给离屏
function drawBackground() {
    //将当前画布给离屏
    offscreenContext.drawImage(canvas, 0, 0,
                              canvas.width, canvas.height);
}
//重绘小球区域
function drawDiscBackground(context, disc) {
    var x = disc.lastX,
        y = disc.lastY,
        r = disc.radius,
        w = r * 2,
        h = r * 2;
    //保存原来
    context.save();
    context.beginPath();
    context.arc(x, y, r + 1, 0, Math.PI * 2, false);
    //剪辑
    context.clip();
    //擦除背景
    eraseBackground();
    //重绘背景--使用离屏技术
    context.drawImage(offscreenCanvas,x-r,y-r,w,h,x-r,y-r,w,h);
    //恢复
    context.restore();
}

默认情况下:浏览器使用了双缓冲技术绘制动画

6.基于时间的运动

例:每秒移动100个像素,那么每毫秒移动的像素为:100/1000=0.1px,计算每一帧移动多少像素:假设这一帧对于上一帧过去的时间为200ms,那么这一帧移动的像素即为:200ms*0.1px=20px

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>帧速率计算</title>
        <style>
            body {
                background: #dddddd;
            }

            #canvas {
                position: absolute;
                left: 0px;
                top: 20px;
                margin: 20px;
                background: #ffffff;
                border: thin inset rgba(100, 150, 230, 0.5);
            }

            #controls {
                margin-top: 10px;
                margin-left: 15px;
            }
        </style>
    </head>

    <body>
        <div id='controls'>
            <input id='animateButton' type='button' value='开始' />
        </div>
        <canvas id='canvas' width='500' height='300'>
       </canvas>
        <script src='js/TimeFrame.js'>
        </script>
    </body>

</html>

JS脚本

var canvas = document.querySelector('#canvas'),
    context = canvas.getContext('2d'),
    paused = true,
    discs = [{
            x: 215,
            y: 175,
            lastX: 150,
            lastY: 75,
            velocityX: -3.2,
            velocityY: 4.5,
            radius: 25,
            innerColor: 'rgba(255,0,0,1.0)',
            middleColor: 'rgba(255,0,0,0.7)',
            outerColor: 'rgba(255,0,0,0.5)',
            shadowColor: 'rgba(255,0,0,0.7)',
            strokeStyle: 'orange'
        }, {
            x: 250,
            y: 150,
            lastX: 150,
            lastY: 80,
            velocityX: 2.2,
            velocityY: -4.5,
            radius: 25,
            innerColor: 'rgba(255,255,0,1)',
            middleColor: 'rgba(255,255,0,0.7)',
            outerColor: 'rgba(255,255,0,0.5)',
            shadowColor: 'rgba(175,175,175,0.7)',
            strokeStyle: 'gray',
        },

        {
            x: 150,
            y: 75,
            lastX: 50,
            lastY: 150,
            velocityX: 2.2,
            velocityY: -1.5,
            radius: 25,
            innerColor: 'rgba(100,145,230,1.0)',
            middleColor: 'rgba(100,145,230,0.7)',
            outerColor: 'rgba(100,145,230,0.5)',
            shadowColor: 'rgba(100,145,230,0.8)',
            strokeStyle: 'blue'
        },

        {
            x: 100,
            y: 100,
            lastX: 150,
            lastY: 75,
            velocityX: -5.9,//X轴速度
            velocityY: -0.2,//Y轴速度
            radius: 25,
            innerColor: 'rgba(255,0,0,1.0)',
            middleColor: 'rgba(255,0,0,0.7)',
            outerColor: 'rgba(255,0,0,0.5)',
            shadowColor: 'rgba(255,0,0,0.7)',
            strokeStyle: 'orange'
        },
    ],
    numDiscs = discs.length,
    startTime = 0, //开始时间
    lastTime = 0, //结束时间
    elapsedTime = 0, //运行时间,每帧时间差
    fps = 0, //帧率
    lastFpsUpdate = {
        time: 0,
        value: 0
    },
    animateButton = document.querySelector('#animateButton');
//擦除背影
function eraseBackground() {
    context.clearRect(0, 0, canvas.width, canvas.height);
}
//更新小球
function update() {
    var i = numDiscs,
        disc = null;

    while(i--) {
        disc = discs[i];

        if(disc.x + disc.velocityX + disc.radius > context.canvas.width ||
            disc.x + disc.velocityX - disc.radius < 0)
            disc.velocityX = -disc.velocityX;

        if(disc.y + disc.velocityY + disc.radius > context.canvas.height ||
            disc.y + disc.velocityY - disc.radius < 0)
            disc.velocityY = -disc.velocityY;

        disc.x += disc.velocityX;
        disc.y += disc.velocityY;
    }
}
//基于时间更新
function updateTimeBased(time) {
    var i = numDiscs,
        disc = null;

    if(fps == 0)
        return;

    while(i--) {
        disc = discs[i];
        deltaX = disc.velocityX
        //elapsedTime为每帧时间差,速度(像素)X每帧秒数,移动坐标像素
        deltaX = disc.velocityX * (elapsedTime / 1000);
        deltaY = disc.velocityY * (elapsedTime / 1000);

        if(disc.x + deltaX + disc.radius > context.canvas.width ||
            disc.x + deltaX - disc.radius < 0) {
            disc.velocityX = -disc.velocityX;
            deltaX = -deltaX;
        }

        if(disc.y + deltaY + disc.radius > context.canvas.height ||
            disc.y + deltaY - disc.radius < 0) {
            disc.velocityY = -disc.velocityY;
            deltaY = -deltaY;
        }

        disc.x = disc.x + deltaX;
        disc.y = disc.y + deltaY;
    }
}
//绘制小球
function draw() {
    var i = numDiscs,
        disc = discs[i];

    while(i--) {
        disc = discs[i];
        //
        gradient = context.createRadialGradient(disc.x, disc.y, 0,
            disc.x, disc.y, disc.radius);

        gradient.addColorStop(0.3, disc.innerColor);
        gradient.addColorStop(0.5, disc.middleColor);
        gradient.addColorStop(1.0, disc.outerColor);

        context.beginPath();
        context.arc(disc.x, disc.y, disc.radius, 0, Math.PI * 2, false);

        context.save();

        context.fillStyle = gradient;
        context.strokeStyle = disc.strokeStyle;
        context.fill();
        context.stroke();
        context.restore();

    }
}
//计算帧率
function calculateFps(now) {
    //第一帧所用时间单位为毫秒
    elapsedTime = now - lastTime;
    //计算帧率,每一秒多少帧
    fps = 1000 / elapsedTime;
    lastTime = now;
}
//更新帧率
function updateFps() {
    var now = (+new Date);
    //计算帧率
    calculateFps(now);

    if(now - startTime < 2000) {
        return;
    }

    if(now - lastFpsUpdate.time > 1000) {
        lastFpsUpdate.time = now;
        lastFpsUpdate.value = fps;
    }
    if(!paused) {
        context.fillStyle = 'cornflowerblue';
        context.fillText(lastFpsUpdate.value.toFixed() + ' fps', 50, 48);
    }
}
//基于时间运行动画
function animate(time) {
    if(time === undefined) {
        time = +new Date;
    }
    //console.log(time)
    if(!paused) {
        //擦除重绘背景
        eraseBackground();
        //drawBackground();
        //基于时间
        updateTimeBased(time);
        //非基于时间
        //update();
        draw();
    }
    //更新帧率
    updateFps();
}
//按钮事件
animateButton.addEventListener('click', function(e) {
    paused = paused ? false : true;
    if(paused) {
        animateButton.value = '开始';
    } else {
        for(var i = 0; i < discs.length; ++i) {
            discs[i].velocityX *= 20;
            discs[i].velocityY *= 50;
        }
        animateButton.value = '停止';
    }
});

context.font = '36px Helvetica';
//获取开始是境
startTime = +new Date;
//计算每一帧所用时长为多少毫秒,以下为每秒60帧动画
setInterval(animate, 1000 / 60);

6.视差动画

视差动画,就是动画制作者让各个动画图层以不同的速度滚动,这样就实现了视差效果。近的移动快,远的移动慢。

(1)初始设置
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>视差动画</title>
    <style> 
           body {
                background: #dddddd;
            }
            #canvas {
            position: absolute;
            left: 20px;
            top: 30px;
            background: #ffffff;
            margin-left: 10px;
            margin-top: 10px;
            box-shadow: 4px 4px 8px rgba(0,0,0,0.5);
            }
            margin-left: 15px;
            }

        </style>
   </head>

  <body>
    <input id='animateButton' type='button' value='动画'/>
    <canvas id='canvas' width='1000' height='440'>
    </canvas>
    <script src='js/requestNextAnimationFrame.js'></script>
    <script src='js/Parallax.js'></script>
  </body>
</html>

JS脚本

var canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    animateButton = document.getElementById('animateButton'),
    //树
    tree = new Image(),
    //大数
    nearTree = new Image(),
    //草
    grass = new Image(),
    grass2 = new Image(),
    //天空
    sky = new Image(),

    paused = true,
    lastTime = 0,
    lastFpsUpdate = { time: 0, value: 0 },
    fps=60,
    //天空偏移
    skyOffset = 0,
    //草地偏移
    grassOffset = 0,
    //树偏移
    treeOffset = 0,
    //大树偏移
    nearTreeOffset = 0,
    //速度
    TREE_VELOCITY = 20,
    FAST_TREE_VELOCITY = 40,
    SKY_VELOCITY = 8,
    GRASS_VELOCITY = 75;

//----------------------初始设置
context.font = '48px Helvetica';

tree.src = 'img/smalltree.png';
nearTree.src = 'img/tree-twotrunks.png';
grass.src = 'img/grass.png';
grass2.src = 'img/grass2.png';
sky.src = 'img/sky.png';


 //擦除背景   
function erase() {
   context.clearRect(0,0,canvas.width,canvas.height);
}
//绘制图像
function draw() {
   //天空
   skyOffset = skyOffset < canvas.width ?
               skyOffset + SKY_VELOCITY/fps : 0;
   //草地
   grassOffset = grassOffset < canvas.width ?
                 grassOffset +  GRASS_VELOCITY/fps : 0;
    //树
   treeOffset = treeOffset < canvas.width ?
                treeOffset + TREE_VELOCITY/fps : 0;
   //大树
   nearTreeOffset = nearTreeOffset < canvas.width ?
                    nearTreeOffset + FAST_TREE_VELOCITY/fps : 0;
    //保存画布 
   context.save();
   //平移
   context.translate(-skyOffset, 0);
   //绘制两个天空,实现背景滚动
   context.drawImage(sky, 0, 0);
   context.drawImage(sky, sky.width-2, 0);
   //恢复画布
   context.restore();

    //保存画布 
   context.save();
   context.translate(-treeOffset, 0);
   context.drawImage(tree, 100, 240);
   context.drawImage(tree, 1100, 240);
   context.drawImage(tree, 400, 240);
   context.drawImage(tree, 1400, 240);
   context.drawImage(tree, 700, 240);
   context.drawImage(tree, 1700, 240);
   //恢复画布
   context.restore();

   context.save();
   context.translate(-nearTreeOffset, 0);
   context.drawImage(nearTree, 250, 220);
   context.drawImage(nearTree, 1250, 220);
   context.drawImage(nearTree, 800, 220);
   context.drawImage(nearTree, 1800, 220);
   context.restore();

   context.save();
   context.translate(-grassOffset, 0);
   context.drawImage(grass, 0, canvas.height-grass.height);
   context.drawImage(grass, grass.width-5,
                     canvas.height-grass.height);
   context.drawImage(grass2, 0, canvas.height-grass2.height);
   context.drawImage(grass2, grass2.width,
                     canvas.height-grass2.height);
   context.restore();

}
(2)动画实现
//--------------------帧率
//帧率
function calculateFps(now) {
   var fps = 1000 / (now - lastTime);
   lastTime = now;
   return fps; 
}
//动画
function animate(now) {
   if (now === undefined) {
      now = +new Date;
   }
   fps = calculateFps(now);
   if (!paused) {
      erase();
       draw();
   }
   requestNextAnimationFrame(animate);
}

animateButton.onclick = function (e) {
   paused = paused ? false : true;
   if (paused) {
      animateButton.value = '动画';
   }
   else {
      animateButton.value = '暂停';
   }
};


//加载绘制
sky.onload = function (e) {
   draw();
};
//动画加载
requestNextAnimationFrame(animate);

显示效果:

html5游戏编辑器 html5开发游戏_重绘_02