Canvas绘图指南:从零基础到实战应用(2)

206 阅读11分钟

前言

Canvas 作为 HTML5 的核心绘图技术,不仅能够绘制基础图形,还提供了丰富的视觉效果和交互能力。本文将探讨 Canvas 的进阶特性,从渐变效果到视频绘制,带你了解 Canvas 技术栈。

1. 渐变效果:让图形更生动

1.1 线性渐变

线性渐变是最常用的渐变类型,通过定义起始点和结束点来创建渐变效果:

// 创建线性渐变对象
// createLinearGradient(x0, y0, x1, y1) - 定义渐变的起始点和结束点
// x0, y0: 渐变起始点坐标
// x1, y1: 渐变结束点坐标
const gradient = ctx.createLinearGradient(100, 200, 400, 500);

// 添加颜色停止点
// addColorStop(offset, color) - 在渐变线上添加颜色
// offset: 0-1之间的值,表示在渐变线上的位置
// color: 该位置的颜色
gradient.addColorStop(0, "red"); // 起始位置为红色
gradient.addColorStop(0.5, "#ffcccc"); // 中间位置为浅红色
gradient.addColorStop(1, "blue"); // 结束位置为蓝色

// 将渐变设置为填充样式
ctx.fillStyle = gradient;

// 绘制矩形,应用渐变填充
ctx.fillRect(100, 200, 300, 300);

技术要点:

  • createLinearGradient(x0, y0, x1, y1) 定义渐变方向
  • addColorStop(offset, color) 添加颜色停止点
  • 支持动画效果,通过改变 addColorStop 的位置实现

1.2 径向渐变

径向渐变从中心点向外扩散,常用于创建球体效果:

// 创建径向渐变对象
// createRadialGradient(x0, y0, r0, x1, y1, r1)
// x0, y0, r0: 内圆的圆心和半径
// x1, y1, r1: 外圆的圆心和半径
const radialGradient = ctx.createRadialGradient(300, 200, 0, 300, 200, 100);

// 添加颜色停止点
radialGradient.addColorStop(0, "white"); // 中心为白色
radialGradient.addColorStop(0.7, "yellow"); // 70%位置为黄色
radialGradient.addColorStop(1, "orange"); // 边缘为橙色

// 应用渐变
ctx.fillStyle = radialGradient;

// 绘制圆形
ctx.beginPath();
ctx.arc(300, 200, 100, 0, Math.PI * 2); // 圆心(300,200),半径100
ctx.fill();

image.png

1.3 圆锥渐变

圆锥渐变按角度分布颜色,适合创建色轮效果:

// 创建圆锥渐变对象
// createConicGradient(startAngle, x, y)
// startAngle: 起始角度(弧度制)
// x, y: 圆锥渐变的中心点坐标
const conicGradient = ctx.createConicGradient(Math.PI / 4, 300, 200);

// 添加颜色停止点
conicGradient.addColorStop(0, "red"); // 起始角度(45度)为红色
conicGradient.addColorStop(0.5, "yellow"); // 中间位置(225度)为黄色
conicGradient.addColorStop(1, "blue"); // 结束位置(405度)为蓝色

// 应用渐变
ctx.fillStyle = conicGradient;
ctx.fillRect(0, 0, 600, 400);

image.png

2. 图案填充:重复的艺术

2.1 基础图案创建

// 创建图片对象用于图案
const img = new Image();
img.src = "./imgs/money.png"; // 设置图片源路径

// 图片加载完成后的回调函数
// 注意:必须在图片加载完成后才能创建pattern
img.onload = function () {
  // 创建图案对象
  // createPattern(image, repetition)
  // image: 图片对象(可以是Image对象,也可以是Canvas对象)
  // repetition: 重复方式
  //   - "repeat": 默认值,在水平和垂直方向都重复
  //   - "repeat-x": 只在水平方向重复
  //   - "repeat-y": 只在垂直方向重复
  //   - "no-repeat": 不重复,只显示一次
  const pattern = ctx.createPattern(img, "repeat");

  // 将图案设置为填充样式
  ctx.fillStyle = pattern;

  // 绘制矩形,应用图案填充
  ctx.fillRect(0, 0, 600, 400);
};

重复模式选项:

  • "repeat":默认值,水平和垂直都重复
  • "repeat-x":只在水平方向重复
  • "repeat-y":只在垂直方向重复
  • "no-repeat":不重复,只显示一次

2.2 动态图案效果

// 创建离屏Canvas用于生成图案
const canvas = document.createElement("canvas");
const patternCtx = canvas.getContext("2d");

// 设置Canvas尺寸
canvas.width = 50;
canvas.height = 50;

// 在离屏Canvas上绘制图案内容
// 绘制蓝色方块
patternCtx.fillStyle = "#3498db";
patternCtx.fillRect(0, 0, 25, 25);

// 绘制红色方块
patternCtx.fillStyle = "#e74c3c";
patternCtx.fillRect(25, 25, 25, 25);

// 使用Canvas作为图案源
// 这样可以创建动态的、可编程的图案
const pattern = ctx.createPattern(canvas, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, 600, 400);

3. 线条样式:细节决定品质

3.1 线条宽度和端点

// 设置线条样式属性

// lineWidth: 设置线条宽度,默认值为1px
ctx.lineWidth = 20;

// lineCap: 设置线条端点样式
// - "butt": 平齐端点(默认值)
// - "round": 圆形端点
// - "square": 方形端点
ctx.lineCap = "round";

// lineJoin: 设置线段连接处的样式
// - "miter": 尖角连接(默认值)
// - "round": 圆角连接
// - "bevel": 斜角连接
ctx.lineJoin = "miter";

// miterLimit: 设置斜接面限制值
// 当lineJoin为"miter"时,如果斜接面长度超过此值,会变成"bevel"
ctx.miterLimit = 10;

// 绘制路径
ctx.beginPath(); // 开始新路径
ctx.moveTo(50, 50); // 移动到起始点
ctx.lineTo(150, 100); // 绘制第一条线段
ctx.lineTo(250, 50); // 绘制第二条线段
ctx.stroke(); // 描边绘制

image.png

3.2 虚线效果

// 设置虚线样式
// setLineDash(pattern): 设置虚线模式
// 参数是一个数组,表示虚线的模式:[实线长度, 空白长度, 实线长度, 空白长度, ...]
ctx.setLineDash([20, 10]); // 20像素实线,10像素空白

// lineDashOffset: 设置虚线偏移量
// 正值向右偏移,负值向左偏移,用于创建动画效果
ctx.lineDashOffset = 0;

// 动画虚线效果
let offset = 0;
function animateDash() {
  offset++; // 每次增加偏移量
  ctx.lineDashOffset = offset;

  // 请求下一帧动画
  // requestAnimationFrame(): 在浏览器下一次重绘之前调用指定函数
  requestAnimationFrame(animateDash);
}

// 开始动画
animateDash();

4. 阴影效果:增加层次感

4.1 基础阴影设置

// 设置阴影效果属性

// shadowOffsetX: 阴影水平偏移量(正值向右,负值向左)
ctx.shadowOffsetX = 10;

// shadowOffsetY: 阴影垂直偏移量(正值向下,负值向上)
ctx.shadowOffsetY = 10;

// shadowBlur: 阴影模糊半径(0表示无模糊,值越大越模糊)
ctx.shadowBlur = 5;

// shadowColor: 阴影颜色(支持所有CSS颜色格式)
ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; // 半透明黑色

// 绘制带阴影的图形
ctx.fillStyle = "#3498db";
ctx.fillRect(100, 100, 200, 100);

4.2 复杂阴影效果

// 创建心形路径对象
// Path2D: 用于封装路径,可以重复使用
const heartPath = new Path2D();

// 绘制心形路径
// moveTo(x, y): 移动到指定坐标点
heartPath.moveTo(300, 200); // 移动到心形起始点

// bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): 绘制贝塞尔三次曲线
// 第一条曲线:从(300,200)到(300,250)
// 第一个控制点:(350,150) - 控制曲线的开始方向
// 第二个控制点:(400,200) - 控制曲线的结束方向
heartPath.bezierCurveTo(350, 150, 400, 200, 300, 250);

// 第二条曲线:从(300,250)到(300,200)
// 第一个控制点:(200,200) - 控制曲线的开始方向
// 第二个控制点:(250,150) - 控制曲线的结束方向
heartPath.bezierCurveTo(200, 200, 250, 150, 300, 200);

// 应用阴影效果
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 10;
ctx.shadowColor = "rgba(255, 100, 100, 0.8)"; // 半透明红色阴影

// 填充绘制心形路径(继承阴影设置)
ctx.fill(heartPath);

5. 图片绘制:多媒体支持

5.1 基础图片绘制

// 创建图片对象
const img = new Image();
img.src = "./imgs/image.png"; // 设置图片源路径

// 图片加载完成后的回调函数
// 注意:必须在图片加载完成后才能绘制
img.onload = function () {
  console.log("图片加载成功,尺寸:", img.width, "x", img.height);

  // 绘制图片的三种方式:

  // 第一种:直接绘制(保持原尺寸)
  // drawImage(image, x, y)
  // 参数说明:
  // image: 图片对象
  // x, y: 图片在画布上的位置
  ctx.drawImage(img, 0, 0);

  // 第二种:缩放绘制(指定目标尺寸)
  // drawImage(image, x, y, width, height)
  // 参数说明:
  // image: 图片对象
  // x, y: 图片在画布上的位置
  // width, height: 图片在画布上的尺寸
  ctx.drawImage(img, 0, 0, 600, 400);

  // 第三种:裁剪绘制(从源图片裁剪部分区域)
  // drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
  // 参数说明:
  // image: 图片对象
  // sx, sy: 源图片裁剪的起始位置
  // sWidth, sHeight: 源图片裁剪的宽度和高度
  // dx, dy: 目标画布的位置
  // dWidth, dHeight: 目标画布的宽度和高度
  ctx.drawImage(img, 100, 100, 200, 200, 0, 0, 300, 300);
};

// 图片加载失败的处理
img.onerror = function () {
  console.log("图片加载失败,请检查路径是否正确");
  // 绘制一个占位符
  ctx.fillStyle = "#f0f0f0";
  ctx.fillRect(0, 0, 600, 400);
  ctx.fillStyle = "#666";
  ctx.font = "20px Arial";
  ctx.textAlign = "center";
  ctx.fillText("图片加载失败", 300, 200);
};

5.2 图片处理技巧

// 图片滤镜效果
function applyFilter(imageData) {
  // 获取图片数据
  const data = imageData.data;

  // 遍历每个像素
  for (let i = 0; i < data.length; i += 4) {
    // data[i]: 红色分量 (0-255)
    // data[i+1]: 绿色分量 (0-255)
    // data[i+2]: 蓝色分量 (0-255)
    // data[i+3]: 透明度 (0-255)

    // 计算灰度值
    // 使用标准灰度转换公式:R*0.299 + G*0.587 + B*0.114
    const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;

    // 应用灰度效果
    data[i] = gray; // 红色分量设为灰度值
    data[i + 1] = gray; // 绿色分量设为灰度值
    data[i + 2] = gray; // 蓝色分量设为灰度值
    // data[i + 3] 保持不变(透明度)
  }

  return imageData;
}

// 使用滤镜效果
function applyImageFilter() {
  // 获取Canvas上的图片数据
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // 应用滤镜
  const filteredData = applyFilter(imageData);

  // 将处理后的数据绘制回Canvas
  ctx.putImageData(filteredData, 0, 0);
}

6. 视频绘制:动态内容

6.1 视频播放器

// 获取视频元素
const video = document.querySelector("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

// 渲染函数 - 将视频帧绘制到Canvas并叠加水印
function render() {
  // 绘制视频帧到Canvas
  // drawImage(video, x, y, width, height)
  // 参数说明:
  // video: 视频对象(可以是video元素或Image对象)
  // x, y: 视频在画布上的位置
  // width, height: 视频在画布上的尺寸
  ctx.drawImage(video, 0, 0, 600, 400);

  // 绘制水印图片
  // drawImage(image, x, y, width, height)
  // 参数说明:
  // image: 图片对象
  // x, y: 水印在画布上的位置(右下角)
  // width, height: 水印在画布上的尺寸
  ctx.drawImage(logo, 400, 350, 200, 50);

  // 请求下一帧动画
  // requestAnimationFrame(): 在浏览器下一次重绘之前调用指定函数
  // 这样可以实现流畅的视频播放效果
  requestAnimationFrame(render);
}

// 播放控制
document.getElementById("playBtn").onclick = function () {
  if (video.paused) {
    // 如果视频暂停,则播放视频并开始渲染
    video.play();
    render(); // 开始渲染循环
  } else {
    // 如果视频播放,则暂停视频
    video.pause();
  }
};

6.2 视频特效

// 视频滤镜效果
function applyVideoFilter() {
  // 获取Canvas上的图片数据
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;

  // 遍历每个像素
  for (let i = 0; i < data.length; i += 4) {
    // 增加对比度
    // 将RGB值乘以1.2,但不超过255
    data[i] = Math.min(255, data[i] * 1.2); // 红色分量
    data[i + 1] = Math.min(255, data[i + 1] * 1.2); // 绿色分量
    data[i + 2] = Math.min(255, data[i + 2] * 1.2); // 蓝色分量
    // data[i + 3] 保持不变(透明度)
  }

  // 将处理后的数据绘制回Canvas
  ctx.putImageData(imageData, 0, 0);
}

7. 性能优化技巧

7.1 离屏 Canvas

// 创建离屏Canvas
// 离屏Canvas可以提高性能,避免在主Canvas上进行复杂计算
const offscreenCanvas = document.createElement("canvas");
const offscreenCtx = offscreenCanvas.getContext("2d");

// 设置离屏Canvas尺寸
offscreenCanvas.width = 600;
offscreenCanvas.height = 400;

// 在离屏Canvas上绘制复杂图形
function drawComplexShape() {
  // 创建渐变
  const gradient = offscreenCtx.createLinearGradient(0, 0, 600, 400);
  gradient.addColorStop(0, "#3498db");
  gradient.addColorStop(1, "#e74c3c");

  // 在离屏Canvas上进行复杂的绘制操作
  offscreenCtx.fillStyle = gradient;
  offscreenCtx.fillRect(0, 0, 600, 400);

  // 将结果复制到主Canvas
  // 这样可以避免在主Canvas上重复进行复杂计算
  ctx.drawImage(offscreenCanvas, 0, 0);
}

7.2 图层管理

// 分层绘制
// 将背景和前景分离,提高渲染效率
const backgroundLayer = document.createElement("canvas");
const foregroundLayer = document.createElement("canvas");

// 设置图层尺寸
backgroundLayer.width = 600;
backgroundLayer.height = 400;
foregroundLayer.width = 600;
foregroundLayer.height = 400;

function renderLayers() {
  // 背景层 - 绘制视频
  const bgCtx = backgroundLayer.getContext("2d");
  bgCtx.drawImage(video, 0, 0, 600, 400);

  // 前景层 - 绘制水印
  const fgCtx = foregroundLayer.getContext("2d");
  fgCtx.drawImage(logo, 400, 350, 200, 50);

  // 合并图层
  // 先绘制背景层
  ctx.drawImage(backgroundLayer, 0, 0);
  // 再绘制前景层
  ctx.drawImage(foregroundLayer, 0, 0);
}

8. 实际应用场景

8.1 数据可视化

// 渐变图表
function drawGradientChart(data) {
  // 创建垂直渐变
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  gradient.addColorStop(0, "#3498db"); // 顶部为蓝色
  gradient.addColorStop(1, "#e74c3c"); // 底部为红色

  // 绘制数据柱状图
  data.forEach((value, index) => {
    // 计算柱状图高度(相对于最大值)
    const height = (value / Math.max(...data)) * 300;

    // 设置填充样式为渐变
    ctx.fillStyle = gradient;

    // 绘制柱状图
    // x位置:index * 60(每个柱子间隔60像素)
    // y位置:400 - height(从底部向上绘制)
    // 宽度:50像素
    // 高度:计算出的height
    ctx.fillRect(index * 60, 400 - height, 50, height);
  });
}

// 使用示例
const chartData = [10, 25, 15, 30, 20];
drawGradientChart(chartData);

8.2 游戏开发

// 游戏背景
function drawGameBackground() {
  // 创建天空渐变
  // 从上到下的渐变效果
  const skyGradient = ctx.createLinearGradient(0, 0, 0, 200);
  skyGradient.addColorStop(0, "#87CEEB"); // 顶部为天蓝色
  skyGradient.addColorStop(1, "#E0F6FF"); // 底部为浅蓝色

  // 绘制天空
  ctx.fillStyle = skyGradient;
  ctx.fillRect(0, 0, 800, 200);

  // 创建地面渐变
  // 从上到下的渐变效果
  const groundGradient = ctx.createLinearGradient(0, 200, 0, 400);
  groundGradient.addColorStop(0, "#90EE90"); // 顶部为浅绿色
  groundGradient.addColorStop(1, "#228B22"); // 底部为深绿色

  // 绘制地面
  ctx.fillStyle = groundGradient;
  ctx.fillRect(0, 200, 800, 200);
}

总结

Canvas 的进阶特性为 Web 开发提供了强大的图形处理能力。从基础的渐变效果到复杂的视频处理,Canvas 能够满足各种视觉需求。掌握这些技术,你将能够创建出更加丰富和生动的 Web 应用。

关键技术点:

  1. 渐变效果:线性、径向、圆锥渐变的灵活运用
  2. 图案填充:重复模式的巧妙设计
  3. 线条样式:细节处理的品质保证
  4. 阴影效果:层次感的视觉增强
  5. 多媒体支持:图片和视频的完整处理
  6. 性能优化:离屏 Canvas 和图层管理