Canvas 08

我们要实现动画的效果,就需要把之前所有的东西进行重绘。

动画实现的基本步骤

  • 清空canvas画布内容。(clearRect)
  • 保存canvas状态。确保每一帧的渲染不受上一帧的影响。
  • 绘制动画图形。
  • 恢复canvas状态。

操控动画

因为我们的画面是在js执行完之后生成,所以我们需要的是在一个周期内执行相关绘制函数。 可以通过三种方法控制在设定的时间点上执行重绘函数。

  • setInterval() 设置好时间间隔后,函数会定期执行。
  • setTimeout() 在设置好时间之后执行函数。
  • requestAnimationFrame() 在浏览器的重绘之前执行相关函数更新动画。

window.requestAnimationFrame() 实现动画效果,这个方法提供了更加平缓和高效的方式来执行动画,并且是在准备好了重绘条件时候,才会执行动画帧的绘制。一秒钟回调函数执行60次。

太阳星系运动的动画

ctx.globalCompositeOperation = “destination-over”;设置动画组合操作的类型 destination-over 它使得新的绘制内容被放置在现有内容的

用于绘制当前坐标系原点的方法

// 绘制坐标原点
  ctx.beginPath()
  ctx.moveTo(3, 0);
  ctx.arc(0,0,3,0,2*Math.PI,true)
  ctx.fill()

我们这里使用beginPath方法开启一个新的路径,避免绘制的形状和之前的路径相互影响。通过beginPath可以开启一个新的路径,会自动结束上一个路径。

const sun = new Image();// 都替换成填充圆形
const moon = new Image();
const earth = new Image();
const ctx = document.getElementById("canvas").getContext("2d");

function init() {
  // sun.src = "canvas_sun.png";
  // moon.src = "canvas_moon.png";
  // earth.src = "canvas_earth.png";
  window.requestAnimationFrame(draw);
}

function draw() {
  ctx.globalCompositeOperation = "destination-over";
  ctx.clearRect(0, 0, 300, 300); // clear canvas

  ctx.fillStyle = "rgb(0 0 0 / 40%)";
  ctx.strokeStyle = "rgb(0 153 255 / 40%)";
  ctx.save();
  ctx.translate(150, 150);

  // 绘制坐标原点
  ctx.beginPath()
  ctx.moveTo(3, 0);
  ctx.arc(0,0,3,0,2*Math.PI,true)
  ctx.fill()

  // Earth  获取当前秒 获取地球自转的角度 360/60  6
  const time = new Date();
    ctx.rotate(Math.PI/30*time.getSeconds()+Math.PI/30000*time.getMilliseconds())
  ctx.translate(105, 0);
  
// 绘制坐标原点
  ctx.beginPath()
  ctx.moveTo(3, 0);
  ctx.arc(0,0,3,0,2*Math.PI,true)
  ctx.fill()

  ctx.beginPath()
  ctx.fillRect(0, -12, 40, 24); // Shadow
  ctx.save();
  ctx.fillStyle="red"
  ctx.arc(0,0,12,0,2*Math.PI,true)
  ctx.fill()
  ctx.restore();
  
  // Moon
  ctx.save();
  ctx.rotate(
    ((2 * Math.PI) / 6) * time.getSeconds() +
      ((2 * Math.PI) / 6000) * time.getMilliseconds(),
  );
  ctx.translate(0, 32);
  // 绘制坐标原点
  ctx.beginPath()
  ctx.moveTo(3, 0);
  ctx.arc(0,0,3,0,2*Math.PI,true)
  ctx.fill()
  
 ctx.save();
  ctx.fillStyle="blue"
  ctx.arc(0,0,6,0,2*Math.PI,true)
  ctx.fill()
  ctx.restore();
  ctx.restore();

  ctx.restore();

  ctx.beginPath();
  ctx.arc(150, 150, 105, 0, Math.PI * 2, false); // Earth orbit
  ctx.stroke();

  ctx.beginPath();
  ctx.fillStyle="orange"
  ctx.arc(150,150,40,0,2*Math.PI,true)
  ctx.fill()
  
  ctx.fillStyle = "rgb(0 0 0)";
  ctx.fillRect(0, 0, 300, 300); // clear canvas

  window.requestAnimationFrame(draw);
}

init();

关于钟表的动画

function clockTita(){
  const ctx = document.getElementById("canvas").getContext("2d");
ctx.globalCompositeOperation = "destination-over";
    ctx.save()
ctx.clearRect(0, 0, 150, 150);
ctx.translate(75,75)
ctx.rotate(-Math.PI / 2);
ctx.moveTo(60,0)
ctx.strokeStyle="rgb(50, 95, 162)"
ctx.lineWidth=6
ctx.arc(0,0,60,0,2*Math.PI)
ctx.stroke()

// 绘制秒针刻度
for(let i=0;i<60;i++){
  ctx.save()
  ctx.beginPath()
  ctx.rotate((Math.PI*i*6)/180)
  ctx.lineCap="round"
  ctx.strokeStyle="rgb(0,0,0)"
  ctx.lineWidth=1
  ctx.moveTo(48,0)
  ctx.lineTo(52,0)
  ctx.stroke()
  ctx.closePath()
  ctx.restore()
}

// 绘制小时刻度
for(let i=0;i<12;i++){
  ctx.save()
  ctx.beginPath()
  ctx.rotate((Math.PI*i*30)/180)
  ctx.lineCap="round"
  ctx.strokeStyle="rgb(0,0,0)"
  ctx.lineWidth=2
  ctx.moveTo(44,0)
  ctx.lineTo(52,0)
  ctx.stroke()
  ctx.closePath()
  ctx.restore()
}
// 绘制时针
const date=new Date()
let hours = date.getHours(); // 获取24小时制的小时
let minutes = date.getMinutes() 
let seconds = date.getSeconds()
hours = hours % 12; // 24小时制转12小时制
hours = hours ? hours : 12; // 0小时变成12
ctx.save()
  ctx.rotate((Math.PI*30*hours)/180+(Math.PI*30*minutes)/(180*60)+(Math.PI*30*seconds)/(180*3600))
ctx.beginPath()
ctx.moveTo(-6,0)
ctx.lineTo(38,0)
ctx.lineWidth=6
ctx.lineCap="round"
ctx.strokeStyle="rgb(0,0,0)"
ctx.stroke()
ctx.restore()

// 绘制分针
ctx.save()
ctx.beginPath()
ctx.rotate((Math.PI*6*minutes)/180+(Math.PI*6*seconds)/(180*60))
ctx.moveTo(-10,0)
ctx.lineTo(42,0)
ctx.lineWidth=4
ctx.lineCap="round"
ctx.strokeStyle="rgb(0,0,0)"
ctx.stroke()
ctx.restore()


// 绘制秒针
ctx.save()
ctx.beginPath()
ctx.rotate((Math.PI*6*seconds)/(180))
ctx.moveTo(-12,0)
ctx.lineTo(46,0)
ctx.lineWidth=2
ctx.lineCap="round"
ctx.strokeStyle="red"
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle="red"
ctx.arc(46,0,4,0,2*Math.PI)
ctx.stroke()
ctx.restore()
  
  ctx.restore()
window.requestAnimationFrame(clockTita)
}

window.requestAnimationFrame(clockTita)

全景图片

const img = new Image();

// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.
img.src = "capitan_meadows_yosemite_national_park.jpg";
const canvasXSize = 800;
const canvasYSize = 200;
const speed = 30; // lower is faster
const scale = 1.05;
const y = -4.5; // vertical offset

// Main program
const dx = 0.75;
let imgW;
let imgH;
let x = 0;
let clearX;
let clearY;
let ctx;

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

  if (imgW > canvasXSize) {
    // Image larger than canvas
    x = canvasXSize - imgW;
  }

  // Check if image dimension is larger than canvas
  clearX = Math.max(imgW, canvasXSize);
  clearY = Math.max(imgH, canvasYSize);

  // Get canvas context
  ctx = document.getElementById("canvas").getContext("2d");

  // Set refresh rate
  return setInterval(draw, speed);
};

function draw() {
  ctx.clearRect(0, 0, clearX, clearY); // clear the canvas

  // If image is <= canvas size
  if (imgW <= canvasXSize) {
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = -imgW + x;
    }

    // Draw additional image1
    if (x > 0) {
      ctx.drawImage(img, -imgW + x, y, imgW, imgH);
    }

    // Draw additional image2
    if (x - imgW > 0) {
      ctx.drawImage(img, -imgW * 2 + x, y, imgW, imgH);
    }
  } else {
    // Image is > canvas size
    // Reset, start from beginning
    if (x > canvasXSize) {
      x = canvasXSize - imgW;
    }

    // Draw additional image
    if (x > canvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }

  // Draw image
  ctx.drawImage(img, x, y, imgW, imgH);

  // Amount to move
  x += dx;
}

// Draw additional image if (x > canvasXSize - imgW) {ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);} 绘制图片的一部分,为了保证能够无缝衔接,所以将移动的部分添加到负值然后进行补偿缺少的部分。

绘制跟随鼠标运动的动画

 var cn;
      //= document.getElementById('cw');
      var c;
      var u = 10;
      const m = {
        x: innerWidth / 2,
        y: innerHeight / 2,
      };
      window.onmousemove = function (e) {
        m.x = e.clientX;
        m.y = e.clientY;
      };
      function gc() {
        var s = "0123456789ABCDEF";
        var c = "#";
        for (var i = 0; i < 6; i++) {
          c += s[Math.ceil(Math.random() * 15)];
        }
        return c;
      }
      var a = [];
      window.onload = function myfunction() {
        cn = document.getElementById("cw");
        c = cn.getContext("2d");

        for (var i = 0; i < 10; i++) {
          var r = 30;
          var x = Math.random() * (innerWidth - 2 * r) + r;
          var y = Math.random() * (innerHeight - 2 * r) + r;
          var t = new ob(
            innerWidth / 2,
            innerHeight / 2,
            5,
            "red",
            Math.random() * 200 + 20,
            2,
          );
          a.push(t);
        }
        //cn.style.backgroundColor = "#700bc8";

        c.lineWidth = "2";
        c.globalAlpha = 0.5;
        resize();
        anim();
      };
      window.onresize = function () {
        resize();
      };
      function resize() {
        cn.height = innerHeight;
        cn.width = innerWidth;
        for (var i = 0; i < 101; i++) {
          var r = 30;
          var x = Math.random() * (innerWidth - 2 * r) + r;
          var y = Math.random() * (innerHeight - 2 * r) + r;
          a[i] = new ob(
            innerWidth / 2,
            innerHeight / 2,
            4,
            gc(),
            Math.random() * 200 + 20,
            0.02,
          );
        }
        //  a[0] = new ob(innerWidth / 2, innerHeight / 2, 40, "red", 0.05, 0.05);
        //a[0].dr();
      }
      function ob(x, y, r, cc, o, s) {
        this.x = x;
        this.y = y;
        this.r = r;
        this.cc = cc;
        this.theta = Math.random() * Math.PI * 2;
        this.s = s;
        this.o = o;
        this.t = Math.random() * 150;

        this.o = o;
        this.dr = function () {
          const ls = {
            x: this.x,
            y: this.y,
          };
          this.theta += this.s;
          this.x = m.x + Math.cos(this.theta) * this.t;
          this.y = m.y + Math.sin(this.theta) * this.t;
          c.beginPath();
          c.lineWidth = this.r;
          c.strokeStyle = this.cc;
          c.moveTo(ls.x, ls.y);
          c.lineTo(this.x, this.y);
          c.stroke();
          c.closePath();
        };
      }
      function anim() {
        requestAnimationFrame(anim);
        c.fillStyle = "rgba(0,0,0,0.05)";
        c.fillRect(0, 0, cn.width, cn.height);
        a.forEach(function (e, i) {
          e.dr();
        });
      }

每个小圆移动路径通过坐标变化组成多个点,利用三角函数实现围绕圆形的坐标变化,给人以沿着一个旋转轨迹移动的感觉。

this.x = m.x + Math.cos(this.theta) * this.t;
this.y = m.y + Math.sin(this.theta) * this.t;

每次更新小圆的位置,可以根据角度计算出相对鼠标位置的坐标点,如此我们小圆就会围绕鼠标中心按角度变换的方式运动。
这里的lineTo就是绘制当前位置和上一个直线,随着角度变换和小圆位置更新,可以绘制出圆形的轨迹。

  • 每个小圆的运动轨迹基于 theta 角度的变化。随时间不断变化(this.theta += this.s),使得每个小圆按顺时针或逆时针旋转。
  • 类似旋转的运动路径是在在画布上不断绘制线条,因此看起来就像是多个小圆围绕鼠标旋转,并绘制出旋转的轨迹。
  • 最后添加带有动画背景色且带有透明度,故每一帧的运动路径形成一个渐变消失的效果,展现出旋转拖尾的效果。

写在最后

未完待续…
希望大家好好生活,健康快乐。