Skip to main content

Intro to Canvas

· 11 min read
Kimi Gao
Fullstack & AI

画布尺寸

Canvas 有两种 widthheight

  1. 一种是 width、height 属性,一般称其为 画布尺寸,即图形绘制的地方。默认值分别为 300px、150px。

例如:

<canvas id="canvas" width="300" height="150"></canvas>
  1. 另一种是 CSS 样式里的 width、height 属性,可通过内联样式、内部样式表或外部样式表设置。一般称其为 画板尺寸,用于渲染绘制完成的图形。默认值为空。

例如:

<canvas id="canvas" style="width: 300px; height: 150px;"></canvas>

<style>
#canvas {
width: 300px;
height: 150px;
}
</style>
画布尺寸画板尺寸说明
已设置未设置画板尺寸随画布尺寸改变
未设置已设置画板尺寸将不再随画布尺寸而改变
已设置已设置画板尺寸将不再随画布尺寸而改变

如果两者设置的尺寸不一样时,就会产生一个问题,渲染时画布要通过缩放来使其与画板尺寸一样,那么画布上已经绘制好的图形也会随之缩放,随之导致变形失真。

下面为绘制从原点到 200*200 的直线:

Live Editor
class Demo extends React.Component {
  componentDidMount() {
    const canvasList = document.querySelectorAll('.canvas');

    canvasList.forEach(item => {
      const context = item.getContext('2d');

      this.renderPath(context);
    });
  }

  renderPath(ctx) {
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(200, 200);
    ctx.stroke();
  }

  render() {
    return (
      <>
        <p>1、已设置画布宽高属性(200px * 200px),未设置画板样式宽高,画板尺寸随画布尺寸改变</p>
        <canvas id="canvas1" className="canvas" width="200" height="200"></canvas>
        <p>2、未设置画布宽高属性,但已设置画板样式宽高(200px * 200px)</p>
        <canvas id="canvas2" className="canvas" style={{ width: 200, height: 200 }}></canvas>
        <p>3、设置画布宽高属性(300 * 300),设置画板样式宽高(200 * 200)</p>
        <canvas
          id="canvas3"
          className="canvas"
          width="300"
          height="300"
          style={{ width: 200, height: 200, border: '1px solid #000' }}></canvas>
        <p>4、设置画布宽高属性(200 * 200),设置画板样式宽高(300 * 300)</p>
        <canvas
          id="canvas4"
          className="canvas"
          width="200"
          height="200"
          style={{ width: 300, height: 300, border: '1px solid #000' }}></canvas>
      </>
    );
  }
}
Result
Loading...

高清分辨率

上面说过,避免图形变形失真,要保持画布尺寸和画板尺寸一致。

这只是针对分辨率不高的设备而言,其 window.devicePixelRatio 为 1。而高分辨率屏幕,它的 window.devicePixelRatio 大于 1。

Canvas 绘制的图形是位图,即 栅格图像点阵图像,当将它渲染到高清屏时,会被放大,每个像素点会用 window.devicePixelRatio 平方个物理像素点来渲染,因此图片会变得模糊。

解决方法:

  1. 通过 window.devicePixelRatio 获取当前设备屏幕的 DPR
  2. 获取或设置 Canvas 容器的画板尺寸
  3. 根据 DPR,设置 Canvas 元素的宽高属性(在 DPR 为 2 时,相当于扩大画布的两倍)
  4. 通过 context.scale(dpr, dpr) 缩放 Canvas 画布的坐标系,在 DPR 为 2 时相当于把 Canvas 坐标系也扩大了两倍,这样绘制比例放大了两倍,之后 Canvas 的实际绘制像素就可以按原先的像素值处理
Live Editor
function Demo() {
  const hdCanvasRef = useRef(null);
  const ldCanvasRef = useRef(null);

  const renderPath = context => {
    context.beginPath();
    context.moveTo(0, 0);
    context.lineTo(200, 200);
    context.lineWidth = context.lineWidth;
    context.stroke();
  };

  // 绘制高清晰度画布
  const initHDCanvas = () => {
    const canvas = hdCanvasRef.current;
    // 获取 DPR
    const dpr = window.devicePixelRatio;
    const ctx = canvas.getContext('2d');

    // 获取容器高度
    const { width, height } = hdCanvasRef.current.getBoundingClientRect();
    // 根据 DPR 设置 Canvas 的宽高,使 1 个 Canvas 元素和 1 个物理像素相等
    canvas.width = dpr * width;
    canvas.height = dpr * height;
    // 根据 DPR 设置 Canvas 的宽高属性
    ctx.scale(dpr, dpr);

    renderPath(ctx);
  };

  // 绘制低清晰度画布
  const initLDCanvas = () => {
    const ctx = ldCanvasRef.current.getContext('2d');

    renderPath(ctx);
  };

  useEffect(() => {
    initHDCanvas();
    initLDCanvas();
  }, []);

  return (
    <>
      <p>已适配 画布像素:画板像素(物理像素)= 1:1</p>
      {/* 高清画布 */}
      <canvas ref={hdCanvasRef} style={{ border: '1px solid #000', width: 200, height: 200 }}></canvas>
      <br />
      <p>未适配 画布像素:画板像素(物理像素)= {window.devicePixelRatio}:1</p>
      {/* 普通画布 */}
      <canvas ref={ldCanvasRef} width="200" height="200" style={{ border: '1px solid #000' }}></canvas>
    </>
  );
}
Result
Loading...
tip

样式设置的 width 是的元素内容宽度,不包括内边距、边框、外边距的,而 clientWidth 包括内边距,不包括边框、外边距、滚动条的(如果有)。

画布状态

CanvasRenderingContext2D 渲染环境包含了多种绘图的样式状态(属性有线的样式、填充样式、阴影样式、文本样式)。

Canvas 的 API 提供了两个名叫 CanvasRenderingContext2D.save()CanvasRenderingContext2D.restore() 的方法,用于保存及恢复当前 Canvas 绘画环境的所有属性。其中 CanvasRenderingContext2D.save() 可以保存当前状态,而 CanvasRenderingContext2D.restore() 可以还原之前保存的状态。

这两个方法在绘图中有着重要的作用,比如我们在绘图的时候需要使用多种颜色,颜色需要不时的切换。那么使用 save()restore() 方法即可比较方便地实现此功能。

save

CanvasRenderingContxt2D.save() 是 Canvas 2D API 通过将当前状态放入栈中,保存 Canvas 全部状态的方法。

Live Editor
function Demo() {
  const canvasRef = useRef(null);

  useEffect(() => {
    const ctx = canvasRef.current.getContext('2d');

    // 保存默认的状态
    ctx.save();

    ctx.fillStyle = 'green';
    ctx.fillRect(10, 10, 100, 100);

    // 还原到上次保存(save)的默认状态
    ctx.restore();
    ctx.fillRect(150, 75, 100, 100);
  }, []);

  return <canvas ref={canvasRef} width="200" height="200"></canvas>;
}
Result
Loading...

restore

CanvasRenderingContxt2D.restore() 是 Canvas 2D API 通过在绘图状态栈中弹出顶端的状态,将 Canvas 恢复到最近的保存状态的方法。 如果没有保存状态,此方法不做任何改变。

当该方法和 save 一起使用时,恢复到 ctx.save 保存时的状态。

状态和非状态

在 Canvas 环境中绘图时,可以利用所谓的绘图堆栈状态。每个状态随时存储 Canvas 上下文数据。

下面是存储在状态堆栈的数据列表。

  • 当前的坐标变换(变换矩阵)信息,比如旋转或平移时使用的 rotate()setTransform() 方法
  • 当前剪贴区域
  • 图形上下文对象(CanvasRenderingContext2D)的当前属性值

CanvasRenderingConext2D 的当前属性值主要包括:

属性默认值描述
canvas-取得画布 <canvas> 元素
fillStyle#000000填充路径的当前的颜色、模式或渐变
strokeStyle#000000指定线段颜色
globalCompositeOperationsource-over指定颜色如何与画布上已有颜色组合(合成)
lineCapbutt指定线段端点的绘制方式
lineJoinmiter指定线段端点的绘制方式
lineWidth1绘制线段的宽度
miterLimit10lineJoinmiter 时,这个属性指定斜连接长度和二分之一线宽的最大比率
shadowColorrgba(0, 0, 0, 0)指定阴影颜色
shadowBlur0指定阴影模糊度
shadowOffsetX0指定阴影水平偏移值
shadowOffsetY0指定阴影垂直偏移值

save()restore() 方法允许你保存和恢复一个 CanvasRenderingContext2D 对象的状态。save() 把当前状态推入到绘图堆栈中,而 restore() 从绘图堆栈中的顶端弹出最近保存的状态,并且根据这些存储的值来设置当前绘图状态。

简单来说,save() 主要用来保存目前 Canvas 的状态,通过 save() 函数它会将目前 Canvas 的状态推到绘图堆栈中;而 restore() 函数就是从绘图堆栈中弹出上一个 Canvas 的状态。

应用实例

制作一个扇形

在实际使用当中,save()restore() 的使用还是非常广泛的,特别是涉及到坐标系统的变换和图形变换方面。

Live Editor
function Demo() {
  const canvasRef = useRef(null);

  /**
   *
   * @param ctx Canvs 绘图上下文
   * @param x 位移目标点横坐标
   * @param y 位移目标点纵坐标
   * @param r 圆弧半径
   * @param sDeg 旋转起始角度
   * @param eDeg 旋转终点角度
   */
  const drawSector = (ctx, x, y, r, sDeg, eDeg) => {
    // 初始保存
    ctx.save();

    // 位移到目标点
    ctx.translate(x, y);
    ctx.beginPath();

    // 画出圆弧
    ctx.arc(0, 0, r, sDeg, eDeg);

    // 再次保存以备旋转
    ctx.save();

    // 旋转至起始角度
    ctx.rotate(eDeg);

    // 移动到终点,准备连接终点与圆心
    ctx.moveTo(r, 0);

    // 连接到圆心
    ctx.lineTo(0, 0);

    // 还原
    ctx.restore();

    //旋转至起点角度
    ctx.rotate(sDeg);

    // 从圆心连接到起点
    ctx.lineTo(r, 0);

    ctx.closePath();

    // 还原到最初保存的状态
    ctx.restore();
  };

  const drawScreen = () => {
    const ctx = canvasRef.current.getContext('2d');

    const deg = Math.PI / 180;
    const payload = {
      x: 300,
      y: 150,
      r: 80,
      sDeg: [30, 111, 190, 233, 280, 345],
      eDeg: [111, 190, 233, 280, 345, 30],
      style: ['#f00', '#0f0', '#00f', '#789', '#abcdef'],
    };
    for (let i = 0; i < payload.sDeg.length; i++) {
      drawSector(ctx, payload.x, payload.y, payload.r, payload.sDeg[i] * deg, payload.eDeg[i] * deg);

      ctx.fill();
      ctx.fillStyle = payload.style[i];
    }
  };

  useEffect(() => {
    drawScreen();
  }, []);

  return <canvas ref={canvasRef}></canvas>;
}
Result
Loading...

References

  1. Visualization Guidebook: canvas basic by tsejx