跳至主要內容

Canvas 基本动画

Mr.LRHCanvasCanvas动画大约 6 分钟

Canvas 基本动画

动画的基本步骤

  • 清空 canvas : 如果存在绘制的内容会完全清空 canvas (例如:背景图),否则需要清空所有。最简单的做法是使用 cleanRect() 方法。
  • 保存 canvas 状态 : 如果一些设置(样式、变形之类的)会改变 canvas 状态,而在画每一帧的时候是原始状态,则需要使用 save() 方法先保存一下。
  • 绘制动画图形(animated shapes) : 重绘动画帧。
  • 恢复 canvas 状态 : 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。

操控动画

  • 如果不涉及与用户互动,可使用 setInterval() 方法,定期执行指定代码。
  • 如果涉及与用户互动,可以使用键盘或者鼠标事件配合 setTimeout() 方法来实现。通过设置事件监听,可以捕捉用户的交互,并执行相应的动作。

相关API:

  • setInterval(function, delay) : 当设定好间隔时间后,function 会定期执行。
  • setTimeout(function, delay) : 在设定好的时间之后,执行函数。
  • requestAnimationFrame(callback) : 告诉浏览器希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行60次,也有可能会被降低。

动画案例

太阳系动画

<canvas id="drawSunAnimation" width="300" height="300"></canvas>
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init() {
  sun.src = 'https://mdn.mozillademos.org/files/1456/Canvas_sun.png';
  moon.src = 'https://mdn.mozillademos.org/files/1443/Canvas_moon.png';
  earth.src = 'https://mdn.mozillademos.org/files/1429/Canvas_earth.png';
  window.requestAnimationFrame(draw);
}

function draw() {
  var ctx = document.getElementById('drawSunAnimation').getContext('2d');

  // 设置图像混合模式为 destination-over - 在现有的画布内容后面绘制新的图形。
  ctx.globalCompositeOperation = 'destination-over';
  ctx.clearRect(0, 0, 300, 300); // 清空 canvas

  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  ctx.strokeStyle = 'rgba(0,153,255,0.4)';
  ctx.save(); // 保存 canvas 状态 : canvas_state_01
  ctx.translate(150, 150);

  // 绘制地球
  var time = new Date();
  // getSeconds() : 根据本地时间,返回一个指定的日期对象的秒数
  // getMilliseconds() : 根据本地时间,返回一个指定的日期对象的毫秒数
  ctx.rotate(
    ((2 * Math.PI) / 60) * time.getSeconds() +
      ((2 * Math.PI) / 60000) * time.getMilliseconds()
  );
  ctx.translate(105, 0);
  ctx.fillRect(0, -12, 50, 24); // 阴影部分
  ctx.drawImage(earth, -12, -12); // 地球图像:宽 24px 高 24px

  // 绘制月球
  ctx.save(); // 保存 canvas 状态 : canvas_state_02
  ctx.rotate(
    ((2 * Math.PI) / 6) * time.getSeconds() +
      ((2 * Math.PI) / 6000) * time.getMilliseconds()
  );
  ctx.translate(0, 28.5);
  ctx.drawImage(moon, -3.5, -3.5); // 月球图像:宽 7px 高 7px
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_02

  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_01

  ctx.beginPath();
  // 以 (150, 150) 为圆形, 105 为半径,圆弧开始角度为 0 ,圆弧结束角度为 Math.PI * 2 ,顺时针绘制圆弧
  ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // 地球轨道
  ctx.stroke(); // 对路径进行描边

  ctx.drawImage(sun, 0, 0, 300, 300); // 绘制背景图

  window.requestAnimationFrame(draw);
}

init();

时钟动画

<canvas id="drawClockAnimation" width="300"></canvas>
function clock() {
  var now = new Date();
  var ctx = document.getElementById('drawClockAnimation').getContext('2d');
  ctx.save(); // 保存 canvas 状态 : canvas_state_01
  ctx.clearRect(0, 0, 150, 150); // 清空 canvas 区域
  ctx.translate(75, 75); // 对 canvas 坐标系进行整体移动,将中心点从 (0, 0) 变换到 (75, 75)
  ctx.scale(0.4, 0.4); // 缩放Canvas画布的坐标系,只是影响坐标系
  ctx.rotate(-Math.PI / 2);
  ctx.strokeStyle = 'black'; // 设置边框颜色为 black (黑色)
  ctx.fillStyle = 'white'; // 设置填充颜色为 white (白色)
  ctx.lineWidth = 8; // 设置线段厚度为 8
  ctx.lineCap = 'round'; // 设置线条端点样式为 round - 线段末端以圆形结束。

  // 小时标记
  ctx.save(); // 保存 canvas 状态 : canvas_state_02
  for (var i = 0; i < 12; i++) {
    ctx.beginPath();
    ctx.rotate(Math.PI / 6);
    ctx.moveTo(100, 0); // 将一个新的子路径的起始点移动到 (100, 0) 坐标
    ctx.lineTo(120, 0); // 使用直线连接子路径的终点到 (120, 0) 坐标
    ctx.stroke(); // 对路径进行描边
  }
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_02

  // 分钟标记
  ctx.save(); // 保存 canvas 状态 : canvas_state_03
  ctx.lineWidth = 5;
  for (i = 0; i < 60; i++) {
    if (i % 5 != 0) {
      ctx.beginPath();
      ctx.moveTo(117, 0);
      ctx.lineTo(120, 0);
      ctx.stroke();
    }
    ctx.rotate(Math.PI / 30);
  }
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_03

  var sec = now.getSeconds();
  var min = now.getMinutes();
  var hr = now.getHours();
  hr = hr >= 12 ? hr - 12 : hr;

  ctx.fillStyle = 'black';

  // write Hours
  ctx.save(); // 保存 canvas 状态 : canvas_state_04
  ctx.rotate(
    hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec
  );
  ctx.lineWidth = 14;
  ctx.beginPath();
  ctx.moveTo(-20, 0);
  ctx.lineTo(80, 0);
  ctx.stroke();
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_04

  // write Minutes
  ctx.save(); // 保存 canvas 状态 : canvas_state_05
  ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
  ctx.lineWidth = 10;
  ctx.beginPath();
  ctx.moveTo(-28, 0);
  ctx.lineTo(112, 0);
  ctx.stroke();
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_05

  // Write seconds
  ctx.save(); // 保存 canvas 状态 : canvas_state_06
  ctx.rotate((sec * Math.PI) / 30);
  ctx.strokeStyle = '#D40000';
  ctx.fillStyle = '#D40000';
  ctx.lineWidth = 6;
  ctx.beginPath();
  ctx.moveTo(-30, 0);
  ctx.lineTo(83, 0);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(95, 0, 10, 0, Math.PI * 2, true);
  ctx.stroke();
  ctx.fillStyle = 'rgba(0,0,0,0)';
  ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
  ctx.fill();
  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_06

  ctx.beginPath();
  ctx.lineWidth = 14;
  ctx.strokeStyle = '#325FA2';
  ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
  ctx.stroke();

  ctx.restore(); // 恢复为 canvas 状态 : canvas_state_01

  window.requestAnimationFrame(clock);
}

window.requestAnimationFrame(clock);

循环全景照片

<canvas id="drawPanoramicPhotoAnimation" width="400" height="200"></canvas>
var img = new Image();

// 用户设置变量 - 自对应全景图片、方向、速度
img.src =
  'https://mdn.mozillademos.org/files/4553/Capitan_Meadows,_Yosemite_National_Park.jpg';
var CanvasXSize = 400;
var CanvasYSize = 200;
var speed = 30; // 速度:数值越小越快
var scale = 1.05; // 缩放比例
var y = -4.5; // 垂直偏移

// 主程序

var dx = 0.75;
var imgW;
var imgH;
var x = 0;
var clearX; // 清空 canvas 矩形区域宽度
var clearY; // 清空 canvas 矩形区域高度
var ctx; // canvas 对象

img.onload = function () {
  imgW = img.width * scale;
  imgH = img.height * scale;

  if (imgW > CanvasXSize) {
    // 图像宽度大于画布
    x = CanvasXSize - imgW;
  }
  if (imgW > CanvasXSize) {
    // 图像宽度大于画布
    clearX = imgW;
  } else {
    clearX = CanvasXSize;
  }
  if (imgH > CanvasYSize) {
    // 图像高度大于画布
    clearY = imgH;
  } else {
    clearY = CanvasYSize;
  }

  // 获取 canvas 上下文
  ctx = document.getElementById('drawPanoramicPhotoAnimation').getContext('2d');

  // 设置刷新频率
  return setInterval(draw, speed);
};

function draw() {
  ctx.clearRect(0, 0, clearX, clearY); // 清空 canvas 区域

  // 如果图像宽度【小于等于】画布宽度
  if (imgW <= CanvasXSize) {
    if (x > CanvasXSize) {
      x = -imgW + x;
    }
    if (x > 0) {
      ctx.drawImage(img, -imgW + x, y, imgW, imgH);
    }
    if (x - imgW > 0) {
      ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
    }
  }

  // 如果图像宽度【大于】画布宽度
  else {
    if (x > CanvasXSize) {
      x = CanvasXSize - imgW;
    }
    if (x > CanvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }
  ctx.drawImage(img, x, y, imgW, imgH);
  x += dx; // 移动量
}

鼠标追踪动画

<div>暂时去除鼠标移动事件,移除鼠标跟随动画,如果需要可在代码中放开</div>
<canvas id="drawMouseAnimationCanvas" width="400" height="300"></canvas>
/* #drawMouseAnimationCanvas {
  position: fixed;
  z-index: -1;
} */
var ctxDOM; // canvas DOM
var ctx; // canvas 对象上下文
var ctxDomInnerWidth; // canvas DOM 宽度
var ctxDomInnerHeight; // canvas DOM 高度
const circleCenter = {}; // 圆心对象
var lineArr = [];
window.onload = function myfunction() {
  ctxDOM = document.getElementById('drawMouseAnimationCanvas');
  ctx = ctxDOM.getContext('2d');
  ctxDomInnerWidth = ctxDOM.offsetWidth;
  ctxDomInnerHeight = ctxDOM.offsetHeight;
  circleCenter.x = ctxDomInnerWidth / 2;
  circleCenter.y = ctxDomInnerHeight / 2;
  for (var i = 0; i < 10; i++) {
    var radius = 30;
    var x = Math.random() * (ctxDomInnerWidth - 2 * radius) + radius;
    var y = Math.random() * (ctxDomInnerHeight - 2 * radius) + radius;
    var lineItem = new lineObserver(
      ctxDomInnerWidth / 2,
      ctxDomInnerHeight / 2,
      5,
      'red',
      2
    );
    lineArr.push(lineItem);
  }
  ctx.lineWidth = '2';
  ctx.globalAlpha = 0.5;
  resize();
  runAnimation();
  // 暂时去除鼠标移动事件,移除鼠标跟随动画,如果需要可放开
  // ctxDOM.onmousemove = function (e) {
  //   circleCenter.x = e.clientX;
  //   circleCenter.y = e.clientY;
  // };
};
window.onresize = function () {
  resize();
};

// 获取随机颜色
function getRandomColor() {
  var colorStr = '0123456789ABCDEF';
  var colorPrefix = '#';
  for (var i = 0; i < 6; i++) {
    colorPrefix += colorStr[Math.ceil(Math.random() * 15)];
  }
  return colorPrefix;
}
function resize() {
  ctxDOM.width = ctxDomInnerWidth;
  ctxDOM.height = ctxDomInnerHeight;
  for (var i = 0; i < 101; i++) {
    var r = 30;
    var x = Math.random() * (ctxDomInnerWidth - 2 * r) + r;
    var y = Math.random() * (ctxDomInnerHeight - 2 * r) + r;
    lineArr[i] = new lineObserver(
      ctxDomInnerWidth / 2,
      ctxDomInnerHeight / 2,
      4,
      getRandomColor(),
      0.02
    );
  }
}
function lineObserver(x, y, lineWidth, lineColor, moveAngle) {
  this.x = x;
  this.y = y;
  this.lineWidth = lineWidth;
  this.lineColor = lineColor;
  this.theta = Math.random() * Math.PI * 2; // 角度 : 180度 = π 弧度
  this.moveAngle = moveAngle;
  this.t = Math.random() * 150;

  this.draw = function () {
    const lineStartPoint = {
      x: this.x,
      y: this.y,
    };
    this.theta += this.moveAngle;
    // 1 弧度 = 180 / Math.PI 度
    // 1 度 = Math.PI / 180 度
    // 180度 = π 弧度
    // 计算圆上的坐标点 x : x = x0 + radius * cos(angle * Math.PI / 180)
    // 计算圆上的坐标点 y : y = y0 + radius * sin(angle * Math.PI / 180)
    this.x = circleCenter.x + Math.cos(this.theta) * this.t;
    this.y = circleCenter.y + Math.sin(this.theta) * this.t;
    ctx.beginPath();
    ctx.lineWidth = this.lineWidth;
    ctx.strokeStyle = this.lineColor;
    // moveTo() : 将一个新的子路径的起始点移动到(x,y)坐标
    ctx.moveTo(lineStartPoint.x, lineStartPoint.y);
    ctx.lineTo(this.x, this.y);
    ctx.stroke();
    ctx.closePath();
  };
}
function runAnimation() {
  requestAnimationFrame(runAnimation);
  ctx.fillStyle = 'rgba(0,0,0,0.05)';
  ctx.fillRect(0, 0, ctxDOM.width, ctxDOM.height);
  lineArr.forEach(function (item, index) {
    item.draw();
  });
}
上次编辑于:
贡献者: lrh21g