Canvas 基础 & 在 React 项目中使用

January 09, 2024

开始之前

Canvas 显示模糊问题 

  • https://www.cnblogs.com/thyshare/p/13228473.html
  • canvas 标签上的 width height 决定画布的像素点,style.width 和 style.height 只会拉伸变形画布不会改变像素点。
  • 默认情况下,设备像素比大于 1 时,画布会出现模糊。需要设置 dpr * width 和 dpr * height 增加像素数量,并且 style.width = 1 倍 width,style.height = 1 倍 height,保证画布大小不变,但像素数量增加。

canvas in react

https://medium.com/@pdx.lucasm/canvas-with-react-js-32e133c05258

import { CanvasHTMLAttributes, useState } from "react"
import React, { useEffect, useRef, useMemo } from "react"
import cls from "classnames"

// 设备像素比
const getPixelRatio = () => {
  return window.devicePixelRatio || 1
}

interface IProps<T> extends CanvasHTMLAttributes<HTMLCanvasElement> {
  className?: string
  dpr?: number
  width: number
  height: number
  draw: (
    ctx: CanvasRenderingContext2D,
    userConfig: T,
    scale: (num: number) => number
  ) => void
  userConfig: T
}

const CanvasComponents = <T,>({
  draw,
  className,
  style,
  dpr,
  width = 300,
  height = 150,
  userConfig,
  ...canvasAttributes
}: IProps<T>) => {
  const canvasRef = useRef<HTMLCanvasElement>(null)
  const [ratio, setRatio] = useState(1)

  useEffect(() => {
    // 为了支持 SSR,在当前生命周期才能调用 window 对象获取设备像素比例
    setRatio(dpr || getPixelRatio())
  }, [dpr])

  const [hdWidth, hdHeight] = useMemo(() => {
    return [+width * ratio, +height * ratio]
  }, [width, height, ratio])

  useEffect(() => {
    const canvasDom = canvasRef.current
    const ctx: CanvasRenderingContext2D | null = canvasDom
      ? canvasDom.getContext("2d")
      : null
    if (!canvasDom || !ctx) {
      // canvas unsupported or not find
      return
    }
    if (draw) {
      // 使其在水平和垂直方向上都按照 ratio 缩放。这个操作会影响后续在 Canvas 上绘制的内容,将其缩放为原始尺寸的 ratio 倍。
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
      const scale = (num: number) => num * ratio
      draw(ctx, userConfig, scale)
    }
    return () => {
      // 重置 setTransform,以保证矩形的位置大小是正确的
      ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
      ctx.clearRect(0, 0, canvasDom.width, canvasDom.height)
    }
  }, [draw, width, height, ratio, userConfig])

  return (
    <canvas
      ref={canvasRef}
      className={cls("bg-black dark:bg-white", className)}
      style={{ width: `${width}px`, height: `${height}px`, ...style }}
      width={hdWidth}
      height={hdHeight}
      {...canvasAttributes}
    />
  )
}

export default CanvasComponents

使用组件:

<CanvasComponents
  className="mx-1 my-1"
  width={150}
  height={150}
  draw={(ctx, userConfig) => {
    // userConfig.useColor 根据当前页面是 light 还是 dark 模式,自动选择其中一个颜色。
    // 绘制一个矩形的边框
    ctx.strokeStyle = userConfig.useColor(colors.blue[400], colors.blue[700])
    ctx.strokeRect(10, 10, 50, 50)
    // 绘制一个填充的矩形
    ctx.fillStyle = userConfig.useColor(colors.red[600], colors.red[700])
    ctx.fillRect(30, 30, 50, 50)
  }}
  userConfig={userConfig}
/>

相关知识

MDN Cnavas 基础教程,是 MDN 上关于 Canvas 的一个系列教程文档,非常值得 Canvas 入门学习。

示例:

Canvas Demo