H5 canvas基础、进阶笔记

代码仓库

主要是读书笔记,里面物理知识都是通用,记录一下,里面的示例做小游戏时会经常遇到很实用。

1Canvas元素

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script type="text/javascript">
              window.onload = function () {
                  //1、获取canvas对象
                  var cnv = document.getElementById("canvas");
                  //2、获取上下文环境对象context
                  var cxt = cnv.getContext("2d");
                  //3、开始绘制图形
                  cxt.moveTo(50, 100);
                  cxt.lineTo(150, 50);
                  cxt.stroke();
              }
        </script>
    </head>
    <body>
        <canvas id="canvas" width="200" height="150" style="border:1px dashed
gray; "></canvas>
    </body>
    </html>

注意:width和height分别定义Canvas的宽度和高度。默认情况下,Canvas的宽度为300px,高度为150px。(css 设置无效)

2 直线

2.1 画直线

    cxt.moveTo(x1, y1);
    cxt.lineTo(x2, y2);
    cxt.stroke();

2.2 描边

    cxt.strokeStyle = 属性值;
    cxt.strokeRect(x, y, width, height);

strokeStyle属性 取值有三种,即颜色值、渐变色、图案。

strokeRect()方法 用于确定矩形的坐标,其中x和y为矩形最左上角的坐标。

2.3 填充

    cxt.fillStyle=属性值;
    cxt.fillRect(x, y, width, height);

fillStyle属性跟strokeStyle属性一样,取值也有三种,即颜色值、渐变色、图案。fillRect()方法跟strokeRect()方法一样,用于确定矩形的坐标,其中x和y为矩形最左上角的坐标,width表示矩形的宽度,height表示矩形的高度。跟描边矩形一样,填充矩形的fillStyle属性也必须在fillRect()方法之前定义,否则fillStyle属性无效。

2.4 rect() 方法

2.5 清空矩形

cxt.clearRect(x, y, width, height);

3. 曲线

图形、弧线、二次贝塞尔曲线、三次贝塞尔曲线。

3.1 圆

        cxt.beginPath();
    cxt.arc(x, y,半径,开始角度,结束角度,anticlockwise); //anticlockwise:默认false 顺时针
    cxt.closePath();、三次贝塞尔曲线。

上面这个语法仅仅是对圆形的一个“状态描述”,我们还需要对圆形进行“描边”或“填充”,才会有实际效果。这一点跟绘制矩形是一样的道理,大家别忘了对比一下

    //状态描述
    cxt.beginPath();
    cxt.arc(x, y,半径,开始角度,结束角度,anticlockwise);
    cxt.closePath();
    //描边
    cxt.strokeStyle = "颜色值";
    cxt.stroke();
    
    //状态描述
    cxt.beginPath();
    cxt.arc(x, y,半径,开始角度,结束角度,anticlockwise);
    cxt.closePath();
    //填充
    cxt.fillStyle = "颜色值";
    cxt.fill();

3.2 圆弧

  • arc()画弧线不使用closePath()来关闭路径。这一点大家一定要区分开,因为弧线不是一个闭合图形。而closePath()是用来绘制“封闭图形”的(作用在于关闭路径、连接起点与终点)

  • cxt.arcTo(cx, cy, x2, y2, radius);

    image.png

3.3 二次贝塞尔曲线

cxt.quadraticCurveTo(cx , cy , x2, y2);
image.png

3.4 三次贝塞尔曲线

cxt.bezierCurveTo(cx1 , cy1 , cx2 , cy2 , x , y);
image.png

4 线条操作

4.1 lineWidth属性

lineWidth属性:取值为整数,默认值为1,默认单位为px。

lineWidth属性不仅可以用于直线图形,也可以用于曲线图形。此外需要注意的是,假设线条宽度为lineWidth,则strokeRect()方法绘制的矩形实际宽度是(width+lineWidth),实际高度是(height+lineWidth)。而arc()方法绘制的圆形实际半径为(radius+lineWidth)。

4.2 lineCap属性

image.png

注意,round和square值会使线条稍微变长一点,因为它们给线条增加了线帽部分。

我们必须使用beginPath()来开始新的路径,才可以使用一个新的lineCap属性值。此外还需要注意,round和square值会使线条稍微变长一点,因为它们给线条增加了线帽部分。

image.png

4.3 LineJoin属性

cxt.lineJoin = "属性值";

image.png

4.4 setLineDash()方法

setLineDash()方法

image.png

5. 文本操作

image.png

5.1strokeText()方法

strokeText(text , x , y , maxWidth) 

参数maxWidth为可选参数,表示允许的最大文本的宽度(单位为px)。如果文本宽度超出maxWidth值,文本会被压缩至maxWidth值的宽度。

5.2 fillText()方法

fillText(text , x , y , maxWidth)

5.3 measureText()方法

var length = cxt.measureText(text).width;

参数text表示一个字符串文本,measureText().width返回文本的长度,单位为px。

5.4 textBaseline属性

image.png

6. 图片操作

6.1 绘制图片

cxt.drawImage(image , dx , dy);

参数image,表示页面中的图片。该图片可以是页面中的img元素,也可以是JavaScript创建的Image对象

function $$(id) {
   return document.getElementById(id);
}
window.onload = function () {
  var cnv = $$("canvas");
  var cxt = cnv.getContext("2d");

  //创建image对象
  var image = new Image();
  image.src = "images/princess.png";
  
  //必须在图片载入完成后才能将图片绘制到Canvas上
  image.onload = function () {
    cxt.drawImage(image, 40, 20);
  }
}
2. cxt.drawImage(image , dx , dy , dw , dh)
image.png
  1. cxt.drawImage(image , sx , sy , sw, sh , dx , dy , dw , dh)

    image.png

6.2 平铺照片

var pattern = cxt.createPattern(image , type);    

cxt.fillStyle = pattern;    

cxt.fillRect();
image.png

6.3 切割图片

cxt.clip();

想要使用clip()方法切割一张图片,一般需要以下三步。

(1)绘制基本图形。(2)使用clip()方法。(3)绘制图片。

//第1步,绘制基本图形,用来切割
cxt.beginPath();
cxt.arc(70, 70, 50, 0, 360 * Math.PI / 180, true);
cxt.closePath();
cxt.stroke();

//第2步,使用clip()方法,使得切割区域为上面绘制的基本图形
cxt.clip();

//第3步,绘制一张图片
var image = new Image();
image.src = "images/princess.png";
image.onload = function () {
cxt.drawImage(image, 10, 20);
}

7 变形操作

image.png

7.1 平移

cxt.translate(x, y);  //“状态”都必须在“动作”之前定义

7.2 清空画布

cxt.clearRect(0,0, cnv.width, cnv.height); //等价 cxt.clearRect();

7.3 缩放

cxt.scale(x, y);

scale()方法会改变图形的以下几点。(1)左上角坐标。(2)宽度或高度。(3)线条宽度。

7.4 图形旋转

cxt.rotate(angle); //“度数*Math.PI/180”

默认情况下,rotate()方法的旋转中心就是原点坐标(0,0)

  1. 改变旋转中心: 如果我们想要以某一点(x, y)为旋转中心,可以先使用translate(x, y),然后再使用rotate()方法。(配合save restore方法)

    cxt.clearRect(0, 0, cnv.width, cnv.height);
    cxt.save();
    cxt.translate(cnv.width / 2, cnv.height / 2); //将坐标移
    动到中心
    cxt.rotate(Math.PI * (i / 100));            //累进旋转
    cxt.fillStyle = "HotPink";
    cxt.fillRect(-rectWidth / 2, -rectHeight / 2, rectWidth,
    rectHeight); //填充矩形
    cxt.restore();
    

7.5 transform()

cxt.transform(a, b, c, d, e, f);
image.png
image.png
image.png

7.6 setTransform()方法

setTransform()方法会重置图形的状态,然后再进行变换

变形操作除了可以用于图形,还可以用于文字和图片

8 像素操作

8.1 getImageData()方法

var imgData = cxt.getImageData(x , y , width , height);    

var data = imgData.data;

说明:x、y表示所选图片区域的坐标,width、height表示所选图片区域的宽度和高度。getImageData()方法返回的是一个canvasPixelArray对象,我们可以用一个变量(如imgData)来保存这个对象。canvasPixelArray对象有一个data属性,这个data属性是一个保存了这一张图片像素数据的数组,数组取值如[r1, g1, b1, a1, r2,g2, b2, a2, ……]。也就是说,在imgData.data这个数组中每四个数存储着1个像素的rgba颜色值,这四个数分别是该像素的红(r)、绿(g)、蓝(b)、透明度(a)

8.2 putImageData()方法

cxt.putImageData(image , x , y);

说明:image表示重新绘制的图形,也就是使用getImageData()方法获取的canvasPixelArray对象。x、y表示重新绘制图形左上角的横坐标、纵坐标。getImageData()和putImageData()都是配合使用的,一般我们都是先用getImageData()获取像素数据,然后利用一定的算法进行像素操作,最后再使用putImageData()方法输出像素数据(即在Canvas中显示一张图片)。上面只是简单介绍了一下getImageData()和putImageData()这两个方法的语法,在后面这几节里,我们结合相应例子并经常回过头来看看这一节,就很容易理解了

[图片上传失败...(image-5f588d-1677137124942)]

  1. 颜色反转
    for (var i = 0; i < data.length; i += 4)
    {
        data[i + 0] = 255- data[i + 0];
        data[i + 1] = 255- data[i + 1];
        data[i + 2] = 255- data[i + 2];
    }
  1. 黑白效果

        for (var i = 0; i < data.length; i += 4)
        {
            //var average = (data[i + 0] + data[i + 1] + data[i + 2] + data[i + 3]) / 3;
            var grayscale = data[i] * 0.3 + data[i + 1] * 0.6 + data[i + 2] * 0.1;
            data[i + 0] = average;   //红
            data[i + 1] = average;   //绿
            data[i + 2] = average;   //蓝
        }
    
       
    
  2. 亮度效果

        for (var i = 0; i < data.length; i += 4)
        {
            var a = 50;
            data[i + 0] += a;
            data[i + 1] += a;
            data[i + 2] += a;
        }
    
  1. 复古效果

        for (var i = 0; i < data.length; i += 4) {
            var r = data[i + 0];
            var g = data[i + 1];
            var b = data[i + 2];
            data[i + 0] = r * 0.39 + g * 0.76 + b * 0.18;
            data[i + 1] = r * 0.35 + g * 0.68 + b * 0.16;
            data[i + 2] = r * 0.27 + g * 0.53 + b * 0.13;
        }
    
  1. 红色蒙版

        for (var i = 0; i < data.length; i += 4)
        {
            var r = data[i + 0];
            var g = data[i + 1];
            var b = data[i + 2];
            
            var average = (r + g + b) / 3;
            data[i + 0] = average;
            data[i + 1] = 0;
            data[i + 2] = 0;
        }
    
  2. 透明处理

        for (var i = 0; i < data.length; i += 4)
        {
            data[i + 3] = data[i + 3]*n;
        }
    

8.3 createImageData()方法

cxt.createImageData(sw, sh);       //第1种格式    

cxt.createImageData(imageData);    //第2种格式

(1)第一种格式:接收两个参数,sw和sh分别表示要创建区域的宽度和高度。

(2)第二种格式:接收一个像素对象,表示要“创建区域”的宽度和高度与“这个像素对象”的宽度和高度相等。

createImageData(sw, sh) 和createImageData(imageData)这两个方法说白了都是用来创建一个区域而已。只不过一个是我们自己定宽高,另外一个是“复制”别人的宽高。

9. 渐变与阴影

9.1 线性渐变

    var gnt = cxt.createLinearGradient(x1, y1, x2, y2);
    gnt.addColorStop(value1, color1);
    gnt.addColorStop(value2, color2);
    cxt.fillStyle = gnt;
    cxt.fill();

参数value表示渐变位置的偏移量,取值为0~1之间任意值。value1,表示渐变开始位置,value2表示渐变结束位置。

9.2 径向渐变

    var gnt = cxt.createRadialGradient(x1, y1, r1, x2, y2, r2);
    gnt.addColorStop(value1, color1);
    gnt.addColorStop(value2, color2);
    cxt.fillStyle = gnt;
    cxt.fill();

(1)调用createLinearGradient()方法创建一个radialGradient对象,并赋值给变量gnt。

(2)调用radialGradient对象(即gnt)的addColorStop()方法N次,第1次表示渐变开始的颜色,第2次表示渐变结束时的颜色。然后第3次则以第2次渐变颜色作为开始颜色,进行渐变,以此类推。

(3)把radialGradient对象(即gnt)赋值给fillStyle属性,并且调用fill()方法来绘制有渐变色的图形。(x1 , y1)表示渐变开始圆心的坐标,r1表示渐变开始圆心的半径。(x2 , y2)表示渐变结束圆心的坐标,r2表示渐变结束圆的半径。

image.png

9.3 阴影

              //定义绘制图形的函数
              window.onload = function () {
                  var cnv = $$("canvas");
                  var cxt = cnv.getContext("2d");                                      
                  //设置右下方向的阴影
                  cxt.shadowOffsetX = 5;
                  cxt.shadowOffsetY = 5;
                  cxt.shadowColor = "LightSkyBlue ";
                  cxt.shadowBlur = 10;
                  cxt.fillStyle = "HotPink";
                  cxt.fillRect(100, 30, 50, 50);
              }

10 Canvas路径

image.png

10.1 beginPath()

Canvas是基于“状态”来绘制图形的。每一次绘制(stroke()或fill()), Canvas会检测整个程序定义的所有状态,这些状态包括strokeStyle、fillStyle、lineWidth等。当一个状态值没有被改变时,Canvas就一直使用最初的值。当一个状态值被改变时,需要分两种情况考虑。

(1)如果使用beginPath()开始一个新的路径,则不同路径使用不同的值。

(2)如果没有使用beginPath()开始一个新的路径,则后面的值会覆盖前面的值(后来者居上原则)。

//第1条直线
cxt.beginPath();
cxt.moveTo(50, 40);
cxt.lineTo(150, 40);
cxt.strokeStyle = "red";
cxt.stroke();

//第2条直线
cxt.beginPath();// 注意:这里开启一个新的路径
cxt.moveTo(50, 80);
cxt.lineTo(150, 80);
cxt.strokeStyle = "green";
cxt.stroke();

10.2 closePath()

“关闭路径”并不等于“结束路径”,所谓的“关闭路径”一般是指将同一个路径的起点与终点这两点连接起来,使其成为一个封闭的图形。

10.3 isPointInPath()方法

cxt.isPointInPath(x , y);

注意,isPointInPath()不支持Canvas自带的两个方法:strokeRect()、fillRect()。如果想要使用strokeRect()和fillRect(),请使用rect()方法来代替。

11 Canvas 状态

Canvas为我们提供了两个操作状态的方法:save()和restore()。在Canvas中,我们可以使用save()方法来保存“当前状态”,然后可以使用restore()方法来恢复“之前保存的状态”。save()和restore()一般情况下都是成对配合使用的。

11.1 clip()方法

cxt.clip();

我们在使用clip()方法之前,必须要在Canvas中绘制一个基本图形。然后调用clip()方法后,这个基本图形就会变为一个剪切区域。

注意,与之前接触的isPointInPath()方法一样,clip()方法也不支持Canvas自带的两个方法:strokeRect()、fillRect()。如果要使用strokeRect()和fillRect(),请使用rect()方法来代替。

11.1 使用场景

Canvas状态的保存和恢复,主要用于以下三种场合。

(1)图形或图片裁切。

(2)图形或图片变形。

(3)以下属性改变的时候:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。

 cxt.save();
 cxt.fillStyle = "HotPink";
 cxt.translate(30, 30);
 cxt.fillRect(0, 0, 100, 50);
 cxt.restore();//恢复 后面的平移坐标系会基于原点位置

cxt.fillStyle = "LightSkyBlue ";
cxt.translate(60, 60);
cxt.fillRect(0, 0, 100, 50);

在变形操作(平移、缩放、旋转)中,我们一般都是在变形操作之前使用save()方法保存当前状态,其中当前状态包括参考坐标、图形大小等。然后再使用restore()方法来恢复之前保存的状态。save( )和restore( ),在变形操作中会大量用到。

12 其他应用

12.1 Canvas对象属性

使用cnv.width和cnv.height来获取Canvas的宽度和高度是经常被用到的,像文本居中时需要计算Canvas的宽度和高度、clearRect()清空Canvas时也需要计算Canvas的宽度和高度。

12.1 Canvas对象方法

image.png
cnv.toDataURL(type);

参数type是可选参数,表示输出的MIME类型。如果参数type省略,将使用image/png类型

12.2 globalAlpha属性

context.globalAlpha = 数值;

globalAlpha默认值为1.0(完全不透明),取值范围为:0.0~1.0。其中0.0表示完全透明,1.0表示完全不透明。globalAlpha属性必须在图形绘制之前定义才有效。注意,定义globalAlpha之后,会对整个画布都起作用,因此我们在定义时要多加小心。

13 Canvas事件

在Canvas中,鼠标事件分为以下三种。(1)鼠标按下:mousedown(2)鼠标松开:mouseup(3)鼠标移动:mousemove

13.1 获取鼠标的位置封装工具

//将tools定义为window对象的属性,该属性的值是一个对象
window.tools = {};
//获取鼠标位置
window.tools.getMouse = function (element) {
  //定义一个mouse的对象
  var mouse = { x: 0, y: 0 };
  //为传入的元素添加mousemove事件
  addEvent(element, "mousemove", function (e) {
    var x, y;
    //在IE中,event对象是作为window对象的一个属性存在
    var e = e || window.event;
    //获取鼠标当前位置,并作兼容处理
    //兼容Firefox、chrome、IE9及以上
    if (e.pageX || e.pageY) {
      x = e.pageX;
      y = e.pageY;
    }
    //兼容IE8及以下,以及混杂模式下的Chrome和Safari
    else {
      x  =  e.clientX  +  document.body.scrollLeft  ||  document.
      documentElement.scrollLeft;
      y  =  e.clientY  +  document.body.scrollTop  ||  document.
      documentElement.scrollTop;
    }
    //将当前的坐标值减去canvas元素的偏移位置,则x、y为鼠标在canvas中的相对坐标
    x -= element.offsetLeft;
    y -= element.offsetTop;

    mouse.x = x;
    mouse.y = y;
  })
  //返回值为mouse对象
  return mouse;
}

13.2 获取键盘方向

//获取键盘控制方向
window.tools.getKey = function () {
  var key = {};
  window.addEventListener("keydown", function (e) {
    if (e.keyCode == 38 || e.keyCode == 87) {
      key.direction = "up";
    } else if (e.keyCode == 39 || e.keyCode == 68) {
      key.direction = "right";
    } else if (e.keyCode == 40 || e.keyCode == 83) {
      key.direction = "down";
    } else if (e.keyCode == 37 || e.keyCode == 65) {
      key.direction = "left";
    } else {
      key.direction = "";
    }
  }, false);
  return key;
}

13.3 循环事件

在Canvas中,我们都是使用requestAnimationFrame()方法来实现循环,从而达到动画效果。

    (function frame(){
        window.requestAnimationFrame(frame);
        cxt.clearRect(0, 0, cnv.width, cnv.height);
        ……
    })();

小球向右移动动画

var cnv = $$("canvas");
var cxt = cnv.getContext("2d");

//初始化变量,也就是初始化圆的x轴坐标为0
var x = 0;
//动画循环
(function frame() {
  window.requestAnimationFrame(frame);
  //每次动画循环都先清空画布,再重绘新的图形
  cxt.clearRect(0, 0, cnv.width, cnv.height);

  //绘制圆
  cxt.beginPath();
  cxt.arc(x, 70, 20, 0, 360 * Math.PI / 180, true);
  cxt.closePath();
  cxt.fillStyle = "#6699FF";
  cxt.fill();

  //变量递增
  x += 2;
  })();

14 物理动画

在Canvas中我们可以使用反正切函数Math.atan2()来求出两条边之间夹角的度数,并且能够准确判断该度数对应的是哪一个夹角。

14.1 跟随鼠标旋转

    function Arrow(x, y, color, angle)
    {
        //箭头中心x坐标,默认值为0
        this.x = x || 0;
        //箭头中心y坐标,默认值为0
        this.y = y || 0;
        //颜色,默认值为"#FF0099"
        this.color = color || "#FF0099";
        //旋转角度,默认值为0
        this.angle = angle || 0;
    }
    Arrow.prototype = {
        stroke: function (cxt) {
              cxt.save();
              cxt.translate(this.x, this.y);
              cxt.rotate(this.angle);
              cxt.strokeStyle = this.color;
              cxt.beginPath();
              cxt.moveTo(-20, -10);
              cxt.lineTo(0, -10);
              cxt.lineTo(0, -20);
              cxt.lineTo(20, 0);
              cxt.lineTo(0, 20);
              cxt.lineTo(0, 10);
              cxt.lineTo(-20, 10);
              cxt.closePath();
              cxt.stroke();
              cxt.restore();
          },
          fill: function (cxt) {
              cxt.save();
              cxt.translate(this.x, this.y);
              cxt.rotate(this.angle);
              cxt.fillStyle = this.color;
              cxt.beginPath();
              cxt.moveTo(-20, -10);
              cxt.lineTo(0, -10);
              cxt.lineTo(0, -20);
              cxt.lineTo(20, 0);
              cxt.lineTo(0, 20);
              cxt.lineTo(0, 10);
              cxt.lineTo(-20, 10);
              cxt.closePath();
              cxt.fill();
              cxt.restore();
          }
      };
    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/arrow.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    //实例化一个箭头,中点坐标为画布中心坐标
                    var arrow = new Arrow(cnv.width / 2, cnv.height / 2);
                    //获取鼠标坐标
                    var mouse = tools.getMouse(cnv);
                    
                    (function drawFrame() {
                          window.requestAnimationFrame(drawFrame, cnv);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          
                          var dx = mouse.x - cnv.width / 2;
                          var dy = mouse.y - cnv.height / 2;
                          //使用Math.atan2()方法计算出鼠标与箭头中心的夹角
                          arrow.angle = Math.atan2(dy, dx);
                          
                      arrow.fill(cxt);
                    })();
              }
          </script>
    </head>
    <body>
          <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
    </body>
    </html>

14.2 圆周运动

image.png

“圆的标准方程”经过数学推理可以得到圆上任意一点的坐标。

    x = centerX + Math.cos(angle)*radius;
    y = centerY + Math.sin(angle)*radius;
image.png
//再建立一个ball.js文件,用于存放一个小球类
function Ball(x, y, radius, color)
    {
        //小球中心的x坐标,默认值为0
        this.x = x || 0;
        //小球中心的y坐标,默认值为0
        this.y = y || 0;
        //小球半径,默认值为12
        this.radius = radius || 12;
        //小球颜色,默认值为"#6699FF"
        this.color = color || "#6699FF";
        
        this.scaleX = 1;
        this.scaleY = 1;
    }
    Ball.prototype = {
        //绘制"描边"小球
        stroke: function (cxt) {
              cxt.save();
              cxt.scale(this.scaleX, this.scaleY);
              cxt.strokeStyle = this.color;
              cxt.beginPath();
              cxt.arc(this.x, this.y, this.radius, 0, 360 * Math.PI / 180, false);
              cxt.closePath();
              cxt.stroke();
              cxt.restore();
        },
        //绘制"填充"小球
        fill: function (cxt) {
              cxt.save();
              cxt.translate(this.x, this.y);
              cxt.rotate(this.rotation);
              cxt.scale(this.scaleX, this.scaleY);
              cxt.fillStyle = this.color;
              cxt.beginPath();
              cxt.arc(0, 0, this.radius, 0, 360 * Math.PI / 180, false);
              cxt.closePath();
              cxt.fill();
              cxt.restore();
        }
    }
    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    //实例化一个小球,中心坐标为(100,25),半径、颜色都取默认值
                    var ball = new Ball(100, 25);
                    var centerX = cnv.width / 2;
                    var centerY = cnv.height / 2;
                    var radius = 50;
                    var angle = 0;
                    
                    (function frame() {
                          window.requestAnimationFrame(frame);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          //绘制圆形
                          cxt.beginPath();
                          cxt.arc(centerX, centerY, 50, 0, 360 * Math.PI / 180, false);
                          cxt.closePath();
                          cxt.stroke();
                          
                          //计算小球坐标
                          ball.x = centerX + Math.cos(angle) * radius;
                          ball.y = centerY + Math.sin(angle) * radius;
                          ball.fill(cxt);
                          
                          //角度递增
                          angle += 0.05;
                    })();
                }
          </script>
    </head>
    <body>
          <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
    </body>
    </html>

14.2 波形运动

image.png

常见的波形运动可以分为三种

(1)作用于x轴坐标。

(2)作用于y轴坐标。

(3)作用于缩放属性(scaleX或scaleY)。

(1) 作用于x轴坐标。

x = centerX + Math.sin(angle) * range;   

angle += speed;

(2)作用于y轴坐标。

y = centerY + Math.sin(angle) * range;   

 angle += speed;

(3) 作用于缩放属性(scaleX或scaleY)。

当正弦函数sin作用于物体的缩放属性(scaleX或scaleY)时,物体会不断地放大然后缩小,从而产生一种脉冲动画的效果。

var ball = new Ball(cnv.width / 2, cnv.height / 2, 25);
var range = 0.5;
var angle = 0;

(function frame() {
window.requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);

ball.scaleX = 1 + Math.sin(angle) * range;
ball.scaleY = 1 + Math.sin(angle) * range;
ball.fill(cxt);

//角度递增
angle += 0.05;
})();

14.3 匀速运动

小球沿着任意方向进行匀速运动

    vx = speed * Math.cos(angle * Math.PI/180);
    vy = speed * Math.sin(angle * Math.PI/180);
    object.x += vx;
    object.y += vy;

14.4 加速运动

小球慢慢减速反向

 //实例化一个小球
 var ball = new Ball(0, cnv.height / 2);
 //初始化x轴速度以及加速度
 var vx = 8;
 var ax = -0.2;

(function frame() {
window.requestAnimationFrame(frame);
cxt.clearRect(0, 0, cnv.width, cnv.height);

ball.x += vx;
ball.fill(cxt);

vx += ax;
})();
    ax = a * Math.cos(angle * Math.PI/180);
    ay = a * Math.sin(angle * Math.PI/180);
    vx += ax;
    vy += ay;
    object.x += vx;
    object.y += vy;

14.5 重力

vy += gravity;   

object.y += vy;

小球从空中自由降落到地面,然后反弹,循环往复,直到它最终速度为0而停止在地面上。

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    //初始化数据
                    var ball = new Ball(cnv.width / 2, 0);
                    //y轴初始速度为0,重力加速度为0.2,反弹系数为-0.8
                    var vy = 0;
                    var gravity = 0.2;
                    var bounce = -0.8;
                    
                    (function drawFrame() {
                          window.requestAnimationFrame(drawFrame);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          
                          ball.y += vy;
                          //边界检测
                          if (ball.y > cnv.height - ball.radius) {
                                ball.y = cnv.height - ball.radius;
                                //速度反向并且减小
                                vy = vy * bounce;
                          }
                          ball.fill(cxt);
                          
                          vy += gravity;
                    })();
              }
            </script>
        </head>
        <body>
            <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
        </body>
        </html>

14.6 摩擦力

摩擦力只会改变速度的大小而不会改变它的方向。换句话说,摩擦力只能将物体的速度降至0,但它无法让物体掉头往相反的方向移动。

    vx *= friction;
    vy *= friction;
    object.x += vx;
    object.y += vy;

15 边界检测

对于边界检测,将从以下四个方面来介绍。(1)边界限制(2)边界生成(3)边界环绕(4)边界反弹

15.1 边界限制

    if (ball.x < ball.radius) {
        //小球"碰到"左边界时
    } else if (ball.x > cnv.width - ball.radius) {
        //小球"碰到"右边界时
    }
    if (ball.y < ball.radius) {
        //小球"碰到"上边界时
    } else if (ball.y > cnv.height - ball.radius) {
        //小球"碰到"下边界时
    }

15.2 边界环绕

指的是当物体从一个边界消失后,它就会从对立的边界重新出现,从而形成一种环绕效果

    if(ball.x < -ball.radius){
        //小球"完全超出"左边界时
    } else if(ball.x>cnv.width + ball.radius){
        //小球"完全超出"右边界时
    }
    if(ball.y<-ball.radius){
        //小球"完全超出"上边界时
    } else if(ball.y>cnv.height + ball.radius){
        //小球"完全超出"下边界时
    }

15.3 边界生成

边界生成,指的是物体完全超出边界之后,会在最开始的位置重新生成。

    if (ball.x < -ball.radius ||
        ball.x > cnv.width + ball.radius ||
        ball.y < -ball.radius ||
        ball.y > cnv.height + ball.radius) {
              ……
    }

15.4 边界反弹

指的是物体触碰到边界之后就会反弹回来,就像现实世界中小球碰到墙壁反弹一样。

    //碰到左边界
    if (ball.x < ball.radius) {
        ball.x = ball.radius;
        vx = -vx;
    //碰到右边界
    } else if (ball.x > canvas.width - ball.radius) {
        ball.x = canvas.width - ball.radius;
        vx = -vx;
    }
    //碰到上边界
    if (ball.y < ball.radius) {
        ball.y = ball.radius;
        vy = -vy;
    //碰到下边界
    } else if (ball.y > canvas.height - ball.radius) {
        ball.y = canvas.height - ball.radius;
        vy = -vy;
    }

16 碰撞检测

碰撞检测比较常用的是以下两种方法。(1)外接矩形判定法(2)外接圆判定法

16.1 外接矩形判定法

两个矩形左上角的坐标所处的范围。如果两个矩形左上角的坐标满足一定条件,则两个矩形就发生了碰撞。

    window.tools.checkRect = function (rectA, rectB) {
        return ! (rectA.x + rectA.width < rectB.x  ||
                  rectB.x + rectB.width < rectA.x  ||
                  rectA.y + rectA.height < rectB.y ||
                  rectB.y + rectB.height < rectA.y);
    }

16.2 外接圆判定法

两个圆心之间的距离。如果两个圆心之间的距离大于或等于两个圆的半径之和,则两个圆没有发生碰撞;如果两个圆心之间的距离小于两个圆的半径之和,则两个圆发生了碰撞。

    window.tools.checkCircle = function (circleB, circleA) {
        var dx = circleB.x - circleA.x;
        var dy = circleB.y - circleA.y;
        var distance = Math.sqrt(dx * dx + dy * dy);
        if (distance < (circleA.radius + circleB.radius)) {
              return true;
        }
        else {
              return false;
        }
    }

17 用户交互

用户交互的效果(1)捕获物体(2)拖曳物体(3)抛掷物体

17.1 捕获物体

(1)矩形的捕获。(2)圆的捕获。

  1. 矩形的捕获

        if (mouse.x > rect.x &&
            mouse.x < rect.x + rect.width &&
            mouse.y > rect.y &&
            mouse.y < rect.y + rect.height) {
            ……
        }
    
  2. 元的捕获

        dx = mouse.x - ball.x;
        dy = mouse.y - ball.y;
        distance = Math.sqrt(dx*dx + dy*dy);
        if(distance < ball.radius){
            ……
        }
    

17.2 拖拽物体

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    //初始化数据
                    var ball = new Ball(cnv.width / 2, cnv.height / 2, 20);
                    ball.fill(cxt);
                    var mouse = tools.getMouse(cnv);
                    //初始化2个变量:dx和dy
                    var dx = 0, dy = 0;
                    
                    cnv.addEventListener("mousedown", function () {
                          if (ball.checkMouse(mouse)) {
                              //dx为鼠标与球心的水平偏移量
                              dx = mouse.x - ball.x;
                              //dy为鼠标与球心的垂直偏移量
                              dy = mouse.y - ball.y;
                              document.addEventListener("mousemove", onMouseMove, false);
                              document.addEventListener("mouseup", onMouseUp, false);
                          }
                    }, false);
                    function onMouseMove() {
                          //更新小球坐标
                          ball.x = mouse.x - dx;
                          ball.y = mouse.y - dy;
                    }
                      function onMouseUp() {
                          //鼠标松开时,移除鼠标松开事件:mouseup(自身事件)
                          document.removeEventListener("mouseup", onMouseUp, false);
                          //鼠标松开时,移除鼠标移动事件:mousemove
                          document.removeEventListener("mousemove", onMouseMove, false);
                      }
                      
                      (function drawFrame() {
                          window.requestAnimationFrame(drawFrame, cnv);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          
                          ball.fill(cxt);
                      })();
              }
          </script>
    </head>
    <body>
          <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
    </body>
    </html>

重力反弹

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    var ball = new Ball(cnv.width / 2, cnv.height, 20);
                    ball.fill(cxt);
                    var mouse = tools.getMouse(cnv);
                    
                    var isMouseDown = false;
                    var dx = 0, dy = 0;
                    //oldX和oldY用于存储小球旧的坐标
                    var oldX, oldY;
                    //初始速度vx和vy都为0
                    var vx = 0, vy = 0;
                    //加入重力和反弹消耗
                    var gravity = 1.5;
                    var bounce = -0.8;
                    
                    cnv.addEventListener("mousedown", function () {
                        //判断鼠标点击是否落在小球上
                        if (ball.checkMouse(mouse)) {
                                //鼠标按下小球时,isMouseDown设置为true
                                isMouseDown = true;
                                //鼠标按下小球时,将当前鼠标位置赋值给oldX和oldY
                                oldX = ball.x;
                                oldY = ball.y;
                                dx = mouse.x - ball.x;
                                dy = mouse.y - ball.y;
                                document.addEventListener("mousemove", onMouseMove, false);
                                document.addEventListener("mouseup", onMouseUp, false);
                        }
                    }, false);
                    function onMouseMove() {
                        //鼠标移动时,更新小球坐标
                        ball.x = mouse.x - dx;
                        ball.y = mouse.y - dy;
                        
                        //加入边界限制
                        //当小球碰到左边界时
                        if (ball.x < ball.radius) {
                                ball.x = ball.radius;
                                //当小球碰到右边界时
                        } else if (ball.x > cnv.width - ball.radius) {
                                ball.x = cnv.width - ball.radius;
                        }
                        //当小球碰到上边界时
                        if (ball.y < ball.radius) {
                              ball.y = ball.radius;
                              //当小球碰到下边界时
                        } else if (ball.y > cnv.height - ball.radius) {
                            ball.y = cnv.height - ball.radius;
                        }
                  }
                  function onMouseUp() {
                        //鼠标松开时,isMouseDown设置为false
                        isMouseDown = false;
                        document.removeEventListener("mouseup", onMouseUp, false);
                        document.removeEventListener("mousemove", onMouseMove, false);
                  }
                  
                  (function drawFrame() {
                        window.requestAnimationFrame(drawFrame, cnv);
                        cxt.clearRect(0, 0, cnv.width, cnv.height);
                        
                        if (isMouseDown) {
                              //如果isMouseDown为true,用当前小球的位置减去上一帧的坐标
                              vx = ball.x - oldX;
                              vy = ball.y - oldY;
                              
                              //如果isMouseDown为true,更新oldX和oldY为当前小球中心坐标
                              oldX = ball.x;
                              oldY = ball.y;
                        } else {
                              //如果isMouseDown为false,小球沿着抛掷方向运动
                              vy += gravity;
                              ball.x += vx;
                              ball.y += vy;
                              //边界检测
                              //碰到右边界
                              if (ball.x > canvas.width - ball.radius) {
                                    ball.x = canvas.width - ball.radius;
                                    vx = vx * bounce;
                                    //碰到左边界
                              } else if (ball.x < ball.radius) {
                                    ball.x = ball.radius;
                                    vx = vx * bounce;
                              }
                              //碰到下边界
                              if (ball.y > canvas.height - ball.radius) {
                                    ball.y = canvas.height - ball.radius;
                                    vy = vy * bounce;
                                    //碰到上边界
                              } else if (ball.y < ball.radius) {
                                      ball.y = ball.radius;
                                      vy = vy * bounce;
                              }
                      }
                      
                      ball.fill(cxt);
                    })();
              }
          </script>
    </head>
    <body>
          <canvas id="canvas" width="300" height="200" style="border:1px solid
silver; "></canvas>
    </body>
    </html>

18 高级动画

18.1 缓动动画

想要实现缓动动画,一般需要以下五个步骤。

(1)定义一个0~1之间的缓动系数easing。

(2)计算出物体与终点之间的距离。

(3)计算出当前速度,其中当前速度=距离×缓动系数。

(4)计算新的位置,其中新的位置=当前位置+当前速度。

(5)重复执行第2~4步,直到物体达到目标。

    var targetX = 任意位置;
    var targetY = 任意位置;
    //动画循环
    var vx = (targetX - object.x) * easing;
    var vy = (targetY- object.y) * easing;

任意方向的缓动动画

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    var ball = new Ball(0, 0);
                    //定义终点的x轴坐标和y轴坐标
                    var targetX = cnv.width * (3 / 4);
                    var targetY = cnv.height * (1 / 2);
                    //定义缓动系数
                    var easing = 0.05;
                    
                    (function frame() {
                          window.requestAnimationFrame(frame);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          var vx = (targetX - ball.x) * easing;
                          var vy = (targetY - ball.y) * easing;
                          ball.x += vx;
                          ball.y += vy;
                          
                          ball.fill(cxt);
                    })();
                    
                }
          </script>
    </head>
    <body>
          <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
    </body>
    </html>

18.2 缓动动画应用

缓动动画不仅可以用于物体的运动,还可以应用于物体的其他各种属性,包括大小、颜色、透明度以及旋转等

不管缓动动画应用于什么方面,其实现思路是一样的,也就是以下两个步骤。

(1)当前速度 =(最终值 - 当前值)×缓动系数。

(2)新的值 = 当前值 + 当前速度。

18.3 弹性动画简介

在缓动动画中,物体滑动到终点就停下来了;但是在弹性动画中,物体滑动到终点后还会来回反弹一会儿,直至停止。

从技术上来说,缓动动画和弹性动画有以下几个共同点。

(1)需要设置一个终点。

(2)需要确定物体到终点的距离。

(3)运动和距离是成正比的。

两者的不同在于“运动和距离成正比的”这一点的实现方式是不一样的。

(1)在缓动动画中,跟距离成正比的是“速度”。物体离终点越远,速度就越快。当物体接近终点时,它就几乎停下来了。

(2)在弹性动画中,跟距离成正比的是“加速度”。物体离终点越远,加速度越大。刚刚开始,由于加速度的影响,速度会快速增大。当物体接近终点时,加速度变得很小,但是它还在加速。由于加速度的影响,物体会越过终点。然后随着距离的变大,反向加速度也随之变大,就会把物体拉回来。物体在终点附近来回反弹一会儿,最终在摩擦力的作用下停止。

    ax = (targetX - object.x) * spring;
    ay = (targetY - object.y) * spring;
    vx += ax;
    vy += ay;
    vx *= friction;
    vy *= friction;
    object.x += vx;
    object.y += vy;

举例:加入摩擦力的弹性动画

    <! DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
        <meta charset="utf-8" />
        <script src="js/tools.js"></script>
        <script src="js/ball.js"></script>
        <script type="text/javascript">
              function $$(id) {
                    return document.getElementById(id);
              }
              window.onload = function () {
                    var cnv = $$("canvas");
                    var cxt = cnv.getContext("2d");
                    
                    var ball = new Ball(0, cnv.height / 2);
                    var targetX = cnv.width / 2;
                    var spring = 0.02;
                    var vx = 0;
                    var friction = 0.95;
                    
                    (function frame() {
                          window.requestAnimationFrame(frame);
                          cxt.clearRect(0, 0, cnv.width, cnv.height);
                          var ax = (targetX - ball.x) * spring;
                          
                          vx += ax;
                          vx *= friction;
                          ball.x += vx;
                          
                          ball.fill(cxt);
                    })();
                    
              }
        </script>
    </head>
    <body>
         <canvas id="canvas" width="200" height="150" style="border:1px solid
silver; "></canvas>
    </body>
    </html>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容