Hello Canvas

网格

Canvas 的默认尺寸为 300 像素 × 150 像素宽 × 高 ,像素的单位是 px

arduino 复制代码
<canvas width="200" height="420"></canvas>
  • Canvas 元素的尺寸由其 HTML 属性 widthheight 定义,而不是 CSS !!!
  • 绘制时图像会伸缩以适应它的框架尺寸(也就是 HTML 属性 width、height
  • 如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲变形
  • 如果你绘制出来的图像是扭曲的,尝试用 width 和 height 属性为明确规定宽高 ,而不是使用 CSS
  • HTML 属性定义了画布的像素网格大小,避免拉伸
  • CSS 的 width height 仅用于视觉调整
网格 Grid

canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。

canvas 2D 栅格的起点为左上角坐标为 (0, 0),所有元素的位置都相对于原点定位

图形

不同于 SVG 只支持两种形式的 图形绘制 :*矩形、路径, *所有其他图形都是基于这种图形绘制而来

矩形 Rect

上面提供的方法之中每一个都包含了相同的参数:

  1. xy 指定了在 canvas 画布上所绘制的矩形的左上角(相对于原点)的坐标
  2. widthheight 设置矩形的尺寸
xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.fillRect(25, 25, 100, 100); // 绘制了一个边长为 100px 的黑色正方形
  ctx.clearRect(45, 45, 60, 60); // 从正方形的中心开始擦除了一个 60*60px 的正方形
  ctx.strokeRect(50, 50, 50, 50); // 在清除区域内生成一个 50*50 的正方形边框
}

draw()
</script>

路径 Path

图形 的基本元素是 路径

路径 是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。

使用 路径 Path 绘制图形 需要一些额外的步骤:

  1. 首先,你需要创建路径起始点。
  2. 然后你使用 画图命令 ****去画出路径。
  3. 之后你把路径封闭。
  4. 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");
  
  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.beginPath(); // 开始绘制路径
  ctx.moveTo(75, 50); // 【画图命令】指定起始位置点
  ctx.lineTo(100, 75); // 【画图命令】连线
  ctx.lineTo(100, 25); // 【画图命令】连线
  ctx.fill(); // 填充
}

draw()
</script>
xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.beginPath(); // 开始绘制路径
  ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 绘制
  ctx.moveTo(110, 75);
  ctx.arc(75, 75, 35, 0, Math.PI, false); // 口 (顺时针)
  ctx.moveTo(65, 65);
  ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
  ctx.moveTo(95, 65);
  ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
  ctx.stroke();

  // 如果你想看到连续的线,你可以移除调用的 moveTo()
}

draw()
</script>

线 lineTo

lineTo(x, y) :绘制一条从当前位置到指定 x 以及 y 位置的直线。

该方法有两个参数:x 以及 y,代表坐标系中直线结束的点。

开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,以此类推。

开始点也可以通过 moveTo() 函数改变。

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // 填充三角形
  ctx.beginPath();
  ctx.moveTo(25, 25);
  ctx.lineTo(105, 25);
  ctx.lineTo(25, 105);
  ctx.fill();

  // 描边三角形
  ctx.beginPath();
  ctx.moveTo(125, 125);
  ctx.lineTo(125, 45);
  ctx.lineTo(45, 125);
  ctx.closePath();
  ctx.stroke();
}

draw()
</script>

圆弧 arc

arc(x, y, radius, startAngle, endAngle, anticlockwise) 绘制 圆弧

画一个以 (x, y) 为圆心,以 radius 为半径的圆弧(圆)

startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针)来生成

注:arc 函数中表示角的单位是 弧度,不是角度

注:角度 与 弧度 的计算公式: 弧度 = (Math.PI/180) * 角度

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas")

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 3; j++) {
      ctx.beginPath();

      const x = 25 + j * 50; // x 坐标值
      const y = 25 + i * 50; // y 坐标值
      const radius = 20; // 圆弧半径
      const startAngle = 0; // 开始点
      const endAngle = Math.PI + (Math.PI * j) / 2; // 结束点
      const anticlockwise = i % 2 == 0 ? false : true; // 顺时针或逆时针

      ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

      if (i > 1) {
        ctx.fill();
      } else {
        ctx.stroke();
      }
    }
  }
}

draw()
</script>

贝塞尔曲线

本节涉及的路径类型就是 贝塞尔曲线

二次及三次贝塞尔曲线都十分有用,一般用来绘制复杂有规律的图形。

二次、三次贝塞尔曲线
二次贝塞尔

quadraticCurveTo(cp1x, cp1y, x, y) 绘制二次贝塞尔曲线


cp1x, cp1y 为 1 个控制点,x, y 为结束点。

二次贝塞尔曲线 有 **1 个开始点(蓝色) **、**1 个结束点(蓝色) **以及 1 个控制点(红色)

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // 二次贝塞尔曲线
  ctx.beginPath();
  ctx.moveTo(75, 25);
  ctx.quadraticCurveTo(25, 25, 25, 62.5);
  ctx.quadraticCurveTo(25, 100, 50, 100);
  ctx.quadraticCurveTo(50, 120, 30, 125);
  ctx.quadraticCurveTo(60, 120, 65, 100);
  ctx.quadraticCurveTo(125, 100, 125, 62.5);
  ctx.quadraticCurveTo(125, 25, 75, 25);
  ctx.stroke();
}

draw()
</script>
三次贝塞尔

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 绘制三次贝塞尔曲线

cp1x, cp1y为控制点一,cp2x, cp2y为控制点二,x, y为结束点。

三次贝塞尔曲线 有 **1 个开始点(蓝色) **、**1 个结束点(蓝色) **以及 2 个控制点(红色)

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // 三次贝塞尔曲线
  ctx.beginPath();
  ctx.moveTo(75, 40);
  ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
  ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
  ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
  ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
  ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
  ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
  ctx.fill();
}

draw()
</script>

矩形

rect(x, y, width, height) 绘制矩形


绘制一个左上角坐标为 **(x, y) **,宽高为 width 以及 height 的矩形

xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // 使用 rect 方法绘制矩形路径
  ctx.rect(0.5, 0.5, 200, 100);
  // 为啥是 .5 呢?
  // 参考:https://njtf.yuque.com/tqgxlr/oussfl/eztfg3dmluzelqqq

  // 填充矩形
  ctx.strokeStyle = 'red';
  ctx.stroke();
}

draw()
</script>

文本

canvas 提供了 2 种方法来渲染文本:fillText strokeText

fillText

fillText(text, x, y [, maxWidth])

在指定的 (x, y) 位置填充指定的文本,绘制的最大宽度是可选的。

xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.font = "48px serif"; // 指定字体
  ctx.fillText("Hello world", 10, 50);
}

draw()
</script>

strokeText

strokeText(text, x, y [, maxWidth])

在指定的 (x, y) 位置绘制文本边框,绘制的最大宽度是可选的。

xml 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.font = "48px serif"; // 指定字体

  ctx.strokeText("Hello world", 10, 50);
}

draw()
</script>

图片

canvas 更有意思的一项能力,就是图片操作能力

drawImage

引入图片到 canvas 中,需要 两个基本操作

  1. 获取一个指向 HTMLImageElement (new Image())的对象,或者另一个 canvas 元素的引用作为源,也可以通过提供一个 URL 的方式来使用图片(参见 例子
  2. 使用 drawImage() 函数,将图片绘制到画布上
ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");
  const img = new Image();

  img.onload = function () {
    ctx.drawImage(img, 0, 0); // 绘制图片
    ctx.beginPath();
    ctx.moveTo(30, 96);
    ctx.lineTo(70, 66);
    ctx.lineTo(103, 76);
    ctx.lineTo(170, 15);
    ctx.stroke();
  };

  img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAACCCAMAAADYMBKwAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAELUExURUdwTPb29v39/fv7+8rKyszMzM3NzczMzMzMzMzMzPn5+fr6+vj4+P7+/vz8/P////f3983NzUJCQurq6uvr6+np6efn5+bm5js7Ozw8PHx8fL+/v39/f4uLiyoqKru7u46Ojujo6FxcXH5+fl1dXd/f3319fcrKykxMTD09Pc7Ozqurq42Njby8vK2trbq6uikpKa+vrysrK/Pz8/Hx8ePj44yMjJ6enmxsbNra2s/Pz97e3l5eXpubm76+vszMzJ2dnaysrJ+fn19fXxkZGb29vezs7E1NTdnZ2cnJyd3d3dzc3G1tbcvLy0tLSxgYGHBwcG9vb+7u7u3t7U5OTpycnK6urmtra9vb2zkVlaEAAAAKdFJOUwD///8dcDnqt7CBu8DRAAAC40lEQVR42u3c2VLiQBSA4UCIgSwYwjLdgWwIhB2RzR3cd5195v2fZDoKlOOQGy+m+uD5KdKaq69OHaG8iSAICSkpZ4AkJ6WEwNpIVbODGJAG2Wpqg805VYiBqpBKCFI1BqyqJCSz0NDZpCAPoKEHspCJgSuDaES/F23yj755IiQW+0XIDf9oY5HdCi+24W8v7pgGl71GbxPLN8hCDwTdM+zyM7pnwUGziBGuRmuJ/sRTq9B+uCHG47VhlAMwk962eo9HRkB6bEnAoP0jq8eOa+vIMEDt9JsQjWhEIxrRiH4fWovO1LhsHdA+QHRga9ouIQEotGNptq35DiT07q6lEXbaLThot+y+oNkBBe07PtOGq9Faogs8tQpdr1tl4tb3NK0cQJm077quXd4LCLuC+vTYc9nb+oHfiIiGjVaiMxUuQzSiEY1oRINB5wlxHhSF/Y/4AAd9cqL8rivjsZJ3QK1Hvq4QdoxPuEery2ZPU5Wys1Ob3zBVLvsLrXZrqvNyzNF5nlqFnrE3Vet3qlqZgpl0rdKpdNUp7YwrKpz16NQ67HpXm6kqpJ1+G6IRjWhErxtajM4UuQzRiF5jdI5Sh/1yT2kGDrpywdxiuy3mHDjo8AfKXqLY7kLa6cnkGX2xBQg9uRfFcDW6S3SWp1aiMxV2ubpl253hftLpec3zn8fH6SY9bh8ubplpLnuFbm+x0unbrUkaDnpFiEY0ohGNaER/IHQ8OjPOZeDR3nmRXb9Q2oQ06RJDe1782w40NA1H3geILpaAocPV6C/ROZ6KRl8dxOOHTWCTbtKidwjpD7HIphw/KJ3ilwuiEf3f0Xp0ps5liEY0ohH9odGfG419aGjP07/uQEM3QngfILpYAoYOV6M/R5t8FY3+XtX1s31gk95vFL0zne/+/cirlk51/tGb4EI0otcPLYvQzKIsJIfQ0MOkII2goUeSkEhdwjJfphLho5xHQzB7LQ5H4aOcIT40+w8Cj+zDQXkYpQAAAABJRU5ErkJggg==";
}

draw()
</script>

变换 Transform

状态 State

当我们开始绘制复杂图形时,对状态的保存和恢复,是必不可少的操作!

Canvas 的状态 就是当前画面应用的所有样式和变形的一个快照!

Canvas 状态 存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存!

save() 保存画布(canvas)的所有状态

restore() 恢复画布(canvas)上一个状态

一个绘画的状态包括:

  • 当前应用的变换(平移、旋转、缩放)
  • 以下属性设置::strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
  • 当前的裁切路径(clipping path)

你可以调用任意多次 save方法。

每一次调用 restore 方法,上一个保存的状态就从栈中弹出,所有设定都恢复。

save 和 restore 往往成对儿出现

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  ctx.fillRect(0, 0, 150, 150); // 使用默认设置绘制一个矩形
  ctx.save(); // 保存默认状态

  ctx.fillStyle = "#09F"; // 在原有配置基础上对颜色做改变
  ctx.fillRect(15, 15, 120, 120); // 使用新的设置绘制一个矩形

  ctx.save(); // 保存当前状态
  ctx.fillStyle = "#FFF"; // 再次改变颜色配置
  ctx.globalAlpha = 0.5;
  ctx.fillRect(30, 30, 90, 90); // 使用新的配置绘制一个矩形

  ctx.restore(); // 重新加载之前的颜色状态
  ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形

  ctx.restore(); // 加载默认颜色配置
  ctx.fillRect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形
}

draw()
</script>

平移 Translate

translate(x, y) 用来移动 canvas 和它的原点到一个不同的位置

接受两个参数:x 是左右偏移量,y ****是上下偏移量

平移
ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      ctx.save(); // 状态保存
      ctx.fillStyle = "rgb(" + 51 * i + ", " + (255 - 51 * i) + ", 255)";
      ctx.translate(10 + j * 50, 10 + i * 50); // 平移
      ctx.fillRect(0, 0, 25, 25); // 画图形
      ctx.restore(); // 恢复状态
    }
  }
}

draw()
</script>

旋转 Rotate

rotate(angle) 用于以原点为中心旋转 canvas


只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值。

旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate方法。

旋转
ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // left rectangles, rotate from canvas origin
  ctx.save();

  // 蓝色矩形
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(30, 30, 100, 100);
  ctx.rotate((Math.PI / 180) * 25);

  // 灰色矩形
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(30, 30, 100, 100);
  ctx.restore();

  // right rectangles, rotate from rectangle center
  // draw blue rect
  ctx.fillStyle = "#0095DD";
  ctx.fillRect(150, 30, 100, 100);

  ctx.translate(200, 80); // translate to rectangle center
  ctx.rotate((Math.PI / 180) * 25); // rotate
  ctx.translate(-200, -80); // translate back

  // draw grey rect
  ctx.fillStyle = "#4D4E53";
  ctx.fillRect(150, 30, 100, 100);
}

draw()
</script>

缩放 Scale

scale(x, y) 用 来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大

两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子

ini 复制代码
<canvas id="canvas"></canvas>

<script>
function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // draw a simple rectangle, but scale it.
  ctx.save();
  ctx.scale(10, 3);
  ctx.fillRect(1, 10, 10, 10);
  ctx.restore();

  // mirror horizontally 水平镜像
  ctx.scale(-1, 1);
  ctx.font = "48px serif";
  ctx.fillText("MDN", -135, 120);
}

draw()
</script>

Composite and Clip

在此之前,我们总是将一个图形画在另一个之上,然而这远远是不够的。比如对于一些合成的图形来讲,绘制顺序就会有限制!

本节为了解决这种限制,采用下面的办法:

  1. 利用 globalCompositeOperation 属性来改变绘制顺序限制
  2. 利用 clip属性允许我们隐藏不想看到的部分图形

遮盖 Composite

globalCompositeOperation = type

这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识 ******12 ******种遮盖方式的字符串。

裁切 clip

clip() 将当前正在构建的路径转换为当前的裁剪路径

裁切路径也属于 canvas 状态的一部分

裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分

裁切

globalCompositeOperation 相比,两者最重要的区别是裁切路径不会在 canvas 上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。

ini 复制代码
<canvas id="canvas"></canvas>

<script>
// 绘制星星
function drawStar(ctx, r) {
  ctx.save();
  ctx.beginPath();
  ctx.moveTo(r, 0);
  for (let i = 0; i < 9; i++) {
    ctx.rotate(Math.PI / 5);
    if (i % 2 === 0) {
      ctx.lineTo((r / 0.525731) * 0.200811, 0);
    } else {
      ctx.lineTo(r, 0);
    }
  }
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

function draw() {
  const canvas = document.getElementById("canvas");

  if (!canvas.getContext) return

  const ctx = canvas.getContext("2d");

  // 画了一个与 canvas 一样大小的黑色方形作为背景,并移动原点至中心点
  ctx.fillRect(0, 0, 150, 150);
  ctx.translate(75, 75);

  // 创建一个弧形的裁切路径
  ctx.beginPath();
  ctx.arc(0, 0, 60, 0, Math.PI * 2, true);
  ctx.clip();

  // draw background
  const lingrad = ctx.createLinearGradient(0, -75, 0, 75);
  lingrad.addColorStop(0, "#232256");
  lingrad.addColorStop(1, "#143778");

  ctx.fillStyle = lingrad;
  ctx.fillRect(-75, -75, 150, 150);

  // draw stars
  for (let j = 1; j < 50; j++) {
    ctx.save();
    ctx.fillStyle = "#fff";
    ctx.translate(
      75 - Math.floor(Math.random() * 150),
      75 - Math.floor(Math.random() * 150),
    );
    drawStar(ctx, Math.floor(Math.random() * 4) + 2);
    ctx.restore();
  }
}

draw()
</script>

动画

使用 2D canvas 动画的基本步骤是:

  1. 清空 canvas . *除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。
    *最简单的做法就是用 clearRect方法
  2. 保存 canvas 状态 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。用 save** 方法
  3. 绘制动画图形(animated shapes) 这一步才是重绘动画帧
  4. 恢复 canvas 状态 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。用 restore方法

2D 太阳星系

xml 复制代码
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import sunSrc from '@/assets/imgs/canvas_sun.png'
import moonSrc from '@/assets/imgs/canvas_moon.png'
import earthSrc from '@/assets/imgs/canvas_earth.png'

let rafId
const canvasEl = ref()
const sun = new Image()
const moon = new Image()
const earth = new Image()

function init2DSolar() {
  sun.src = sunSrc
  moon.src = moonSrc
  earth.src = earthSrc

  setSize()

  rafId = requestAnimationFrame(draw)
}

function draw() {
  const ctx = canvasEl.value.getContext('2d')

  ctx.globalCompositeOperation = 'destination-over'
  ctx.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height) // 清除画布

  ctx.fillStyle = 'rgb(0 0 0 / 40%)'
  ctx.strokeStyle = 'rgb(0 153 255 / 40%)'
  ctx.save() // 保存状态
  ctx.translate(canvasEl.value.width / 2, canvasEl.value.height / 2)

  // 地球
  const time = new Date()
  ctx.rotate(
    ((2 * Math.PI) / 60) * time.getSeconds() +
    ((2 * Math.PI) / 60000) * time.getMilliseconds(),
  )
  ctx.translate(105, 0)
  ctx.fillRect(0, -12, 40, 24) // 阴影
  ctx.drawImage(earth, -12, -12)

  // 月亮
  ctx.save()
  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)
  ctx.restore()

  ctx.restore()

  ctx.beginPath()
  ctx.arc(
    canvasEl.value.width / 2,
    canvasEl.value.height / 2,
    105,
    0,
    Math.PI * 2,
    false
  ) // 地球轨道
  ctx.stroke()

  ctx.drawImage(sun, 0, 0, canvasEl.value.width, canvasEl.value.height)

  rafId = requestAnimationFrame(draw)
}

function setSize() {
  canvasEl.value.width = 600
  canvasEl.value.height = 600
}

onMounted(() => {
  init2DSolar()

  window.onresize = () => setSize()
})

onUnmounted(() => {
  cancelAnimationFrame(rafId)
})
</script>

<template>
  <canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>

<style scoped>
.paint-2d-canvas {
  position: fixed;
}
</style>

2D 时钟表盘

ini 复制代码
<script setup>
import { zeroPadding } from '@/utils/index.js'
import { ref, onMounted, onUnmounted } from 'vue'

let rafId
const canvasEl = ref()
const isSweepSecond = ref(false) // 是否为扫秒式

function init2DClock() {
  setSize()

  rafId = requestAnimationFrame(animate)
}

function animate() {
  const now = new Date()
  const canvas = canvasEl.value
  const context = canvas.getContext('2d')

  context.save()
  context.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height)
  context.translate(canvasEl.value.width / 2, canvasEl.value.height / 2 - 60)
  context.rotate(-Math.PI / 2)
  context.strokeStyle = 'black'
  context.fillStyle = 'white'
  context.lineWidth = 4
  context.lineCap = 'round'

  // 小时刻度
  context.save()
  for (let i = 0; i < 12; i++) {
    context.beginPath()
    context.rotate(Math.PI / 6)
    context.moveTo(108, 0)
    context.lineTo(120, 0)
    context.stroke()
  }
  context.restore()

  // 分钟刻度
  context.save()
  context.lineWidth = 1
  for (let i = 0; i < 60; i++) {
    if (i % 5 !== 0) {
      context.beginPath()
      context.moveTo(117, 0)
      context.lineTo(120, 0)
      context.stroke()
    }
    context.rotate(Math.PI / 30)
  }
  context.restore()

  let sec = now.getSeconds() // 钟摆式

  if (isSweepSecond.value) {
    sec = now.getSeconds() + now.getMilliseconds() / 1000
  } // 扫秒式

  const min = now.getMinutes()
  const hr = now.getHours() % 12

  context.fillStyle = 'black'

  // 时针
  const hourRadian = (Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec
  context.save()
  context.rotate(hourRadian)
  context.lineWidth = 14
  context.beginPath()
  context.moveTo(-20, 0)
  context.lineTo(80, 0)
  context.stroke()
  context.restore()

  // 分针
  const minuteRadian = (Math.PI / 30) * min + (Math.PI / 1800) * sec
  context.save()
  context.rotate(minuteRadian)
  context.lineWidth = 10
  context.beginPath()
  context.moveTo(-28, 0)
  context.lineTo(100, 0)
  context.stroke()
  context.restore()

  // 秒针
  const secondRadian = (sec * Math.PI) / 30
  context.save()
  context.rotate(secondRadian)
  context.strokeStyle = '#D40000'
  context.fillStyle = '#D40000'
  context.lineWidth = 6
  context.beginPath()
  context.moveTo(-30, 0)
  context.lineTo(120, 0)
  context.stroke()
  context.beginPath()
  context.arc(0, 0, 10, 0, Math.PI * 2, true)
  context.fill()
  context.restore()

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

  context.restore()

  const hours = zeroPadding(now.getHours())
  const minutes = zeroPadding(now.getMinutes())
  const seconds = zeroPadding(now.getSeconds())
  const time = `${hours}:${minutes}:${seconds}`

  context.font = '48px serif' // 指定字体
  context.fillText(time, 210, canvasEl.value.height / 2 + 150)
  context.strokeText('双击切换钟摆方式', 108, canvasEl.value.height / 2 + 220)

  rafId = requestAnimationFrame(animate)
}

function setSize() {
  canvasEl.value.width = 600
  canvasEl.value.height = 600
}

onMounted(() => {
  init2DClock()

  window.onresize = () => setSize()
  // 双击切换钟摆方式
  canvasEl.value.ondblclick = () => isSweepSecond.value = !isSweepSecond.value
})

onUnmounted(() => {
  cancelAnimationFrame(rafId)
})
</script>

<template>
  <canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>

<style scoped>
.paint-2d-canvas {
  position: fixed;
  margin: 10px;
  border-radius: var(--el-border-radius-base);
  border: solid 1px var(--el-border-color);
}
</style>

2D 自由小球

xml 复制代码
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

let rafId, context
const canvasEl = ref()

const ball = {
  x: 100,
  y: 100,
  vx: 5,
  vy: 2,
  radius: 25,
  color: 'blue',
  drawFrame() {
    context.beginPath()
    context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true)
    context.closePath()
    context.fillStyle = this.color
    context.fill()
  } // 绘制单帧
}

function init2DBall() {
  setSize()

  context = canvasEl.value.getContext('2d')

  canvasEl.value.onmouseover = () => rafId = window.requestAnimationFrame(animate)
  canvasEl.value.onmouseout = () => window.cancelAnimationFrame(rafId)

  ball.drawFrame()
}

function animate() {
  context.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height)
  ball.drawFrame()
  ball.x += ball.vx
  ball.y += ball.vy
  ball.vy *= 0.99
  ball.vy += 0.25

  if (
    ball.y + ball.vy > canvasEl.value.height - ball.radius ||
    ball.y + ball.vy < ball.radius
  ) {
    ball.vy = -ball.vy
  }

  if (
    ball.x + ball.vx > canvasEl.value.width - ball.radius ||
    ball.x + ball.vx < ball.radius
  ) {
    ball.vx = -ball.vx
  }

  rafId = requestAnimationFrame(animate)
}

function setSize() {
  canvasEl.value.width = 600
  canvasEl.value.height = 600
}

onMounted(() => {
  init2DBall()

  window.onresize = () => setSize()
})

onUnmounted(() => {
  cancelAnimationFrame(rafId)
})
</script>

<template>
  <canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>

<style scoped>
.paint-2d-canvas {
  position: fixed;
  margin: 10px;
  border-radius: var(--el-border-radius-base);
  border: solid 1px var(--el-border-color);
}
</style>

2D 鼠标追踪

ini 复制代码
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { generateColor } from '@/utils/index.js'

let context, rafId, particlesArray = []

const canvasEl = ref() // 画布节点
const cursor = { x: 0, y: 0 } // 鼠标位置

function init2DMouse() {
  context = canvasEl.value.getContext('2d')
  context.lineWidth = '2'
  context.globalAlpha = 0.5

  setSize()
  animate()
  generateParticles(101)
}

function generateParticles(amount) {
  for (let i = 0; i < amount; i++) {
    particlesArray[i] = new Particle(
      canvasEl.value.width / 2,
      canvasEl.value.height / 2,
      4,
      generateColor(),
      Math.PI / 360,
    )
  }
}

function setSize() {
  canvasEl.value.width = 600
  canvasEl.value.height = 600
  cursor.x = canvasEl.value.width / 2
  cursor.y = canvasEl.value.height / 2
}

function Particle(x, y, particleTrailWidth, strokeColor, rotateSpeed) {
  this.x = x
  this.y = y
  this.rotateSpeed = rotateSpeed
  this.strokeColor = strokeColor
  this.particleTrailWidth = particleTrailWidth

  this.t = Math.random() * 150
  this.theta = Math.random() * Math.PI * 2

  this.rotate = () => {
    const ls = {
      x: this.x,
      y: this.y,
    }
    this.theta += this.rotateSpeed
    this.x = cursor.x + Math.cos(this.theta) * this.t
    this.y = cursor.y + Math.sin(this.theta) * this.t

    context.beginPath()
    context.lineWidth = this.particleTrailWidth
    context.strokeStyle = this.strokeColor
    context.moveTo(ls.x, ls.y)
    context.lineTo(this.x, this.y)
    context.stroke()
  } // 旋转
}

function animate() {
  rafId = requestAnimationFrame(animate)

  context.fillStyle = "rgb(0 0 0 / 5%)"
  context.fillRect(0, 0, canvasEl.value.width, canvasEl.value.height)

  particlesArray.forEach((particle) => particle.rotate())
}

onMounted(() => {
  init2DMouse()

  window.onresize = () => setSize()

  canvasEl.value.onmousemove = (e) => {
    cursor.x = e.clientX - (e.x - e.layerX)
    cursor.y = e.clientY - (e.y - e.layerY)
  }

  canvasEl.value.ontouchmove = (e) => {
    e.preventDefault()
    cursor.x = e.touches[0].clientX
    cursor.y = e.touches[0].clientY
  }
})

onUnmounted(() => {
  cancelAnimationFrame(rafId)
})
</script>

<template>
  <canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>

<style scoped>
.paint-2d-canvas {
  position: fixed;
  margin: 10px;
  border-radius: var(--el-border-radius-base);
  border: solid 1px var(--el-border-color);
}
</style>
相关推荐
wayhome在哪2 天前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
德育处主任2 天前
p5.js 掌握圆锥体 cone
前端·数据可视化·canvas
德育处主任3 天前
p5.js 3D 形状 "预制工厂"——buildGeometry ()
前端·javascript·canvas
德育处主任4 天前
p5.js 3D盒子的基础用法
前端·数据可视化·canvas
掘金安东尼5 天前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
百万蹄蹄向前冲5 天前
让AI写2D格斗游戏,坏了我成测试了
前端·canvas·trae
用户2519162427118 天前
Canvas之画图板
前端·javascript·canvas
FogLetter11 天前
玩转Canvas:从静态图像到动态动画的奇妙之旅
前端·canvas
用户25191624271111 天前
Canvas之贪吃蛇
前端·javascript·canvas
用户25191624271112 天前
Canvas之粒子烟花
前端·javascript·canvas