Canvas手绘图形,轻松上手!

258 阅读4分钟

嘿~最近迷上了用 Canvas 绘制手绘风格图形,超级简单又好玩!不需要高级技巧,用几行代码就能DIY出满满的手绘feel,无论你是设计小白还是爱美的创意达人,都能轻松上手~ 快来和我一起体验这个超酷的网页小技巧,让你的页面瞬间活力满满吧!

屏幕录制2025-03-10-18.58.31.gif

仓库地址:github.com/nanfriend-1…

核心逻辑

线段

  1. 多次绘制,制造“手绘”感觉

通过重复绘制同一条线,叠加多次轻微的随机偏移,线条不会那么“直”,而是会有轻微波动,显得更自然、更有温度。

  1. 每次开始新路径

每次都使用 ctx.beginPath() 开始一条新的路径,确保每次绘制互不干扰。

  1. 分段绘制

把起点到终点的直线分成若干小段,每个小的线段都添加一个随机的偏移量,模拟真实手绘时那种微妙的抖动和不规则性,让每一笔都显得独一无二。

总结来说,就是通过不断细分和叠加随机偏移,来模拟真实手绘线条的那种随性与自然感💫。每一次的小波动,都会让你的画布多一份生动的艺术气息,简直是程序中的“小确幸”✨!

image.png

  1. 定义随机扰动函数
const rand = () => (Math.random() - 0.5) * radius * bezierCurveConfig.randomFactor;

这里定义了一个 rand 函数,用来生成一个基于圆半径和配置参数 randomFactor 的随机偏移量,为每个关键点添加随机扰动,让绘制的曲线带有一种“手绘”的随性与不完美美感。

  1. 用四段贝塞尔曲线绘制圆形(核心部分)
for (let i = 0; i < 4; i++) {
  const angleStart = (i * Math.PI) / 2;
  const angleEnd = ((i + 1) * Math.PI) / 2;
  // 计算起始点、两个控制点和终点(均加入随机扰动)
  const x0 = cx + Math.cos(angleStart) * radius + rand();
  const y0 = cy + Math.sin(angleStart) * radius + rand();
  const x1 =
    cx +
    (Math.cos(angleStart) - Math.sin(angleStart) * bezierCurveConfig.kappa) * radius +
    rand();
  const y1 =
    cy +
    (Math.sin(angleStart) + Math.cos(angleStart) * bezierCurveConfig.kappa) * radius +
    rand();
  const x2 =
    cx +
    (Math.cos(angleEnd) + Math.sin(angleEnd) * bezierCurveConfig.kappa) * radius +
    rand();
  const y2 =
    cy +
    (Math.sin(angleEnd) - Math.cos(angleEnd) * bezierCurveConfig.kappa) * radius +
    rand();
  const x3 = cx + Math.cos(angleEnd) * radius + rand();
  const y3 = cy + Math.sin(angleEnd) * radius + rand();

  if (i === 0) {
    ctx.moveTo(x0, y0);
  }
  ctx.bezierCurveTo(x1, y1, x2, y2, x3, y3);
}
  • 分段处理:将圆分为四个 90° 的部分,每一段用一条贝塞尔曲线绘制
  • 点的计算:
    • 起始点 (x0, y0) 根据当前起始角度和半径计算,并加入随机扰动。
    • 控制点1 (x1, y1) 根据起始角度和配置的 kappa(常用于近似圆形的贝塞尔曲线参数)计算,并加上扰动。
    • 控制点2 (x2, y2) 根据结束角度和 kappa 计算,同样加入扰动。
    • 终点 (x3, y3) 根据结束角度计算,并加入扰动。

这段代码巧妙地将规则的圆形绘制和手绘风的随机扰动结合在一起,通过贝塞尔曲线加随机偏移,既保留了圆形的整体结构,又注入了艺术家手绘的温度与个性。

双缓冲绘制

  1. 双画布

同时设置两个画布,主画布和离线画布,离线画布不会展示在页面上,负责记录之前的图形,主画布负责绘制和展示所有的效果。

// 主画布
const canvasRef = useRef<HTMLCanvasElement>(null);
// 离线画布
const offscreenCanvasRef = useRef<HTMLCanvasElement>(document.createElement("canvas"));

// 应用主画布
<canvas ref={canvasRef} />

2. 监听事件

useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const options = { passive: true };
    canvas.addEventListener('mousedown', handleMouseDown, options);
    canvas.addEventListener('mousemove', handleMouseMove, options);
    canvas.addEventListener('mouseup', handleMouseUp);
    canvas.addEventListener('mouseleave', handleMouseleave);

    return () => {
        canvas.removeEventListener('mousedown', handleMouseDown);
        canvas.removeEventListener('mousemove', handleMouseMove);
        canvas.removeEventListener('mouseup', handleMouseUp);
        canvas.removeEventListener('mouseleave', handleMouseleave);
    };
}, [handleMouseDown, handleMouseMove, handleMouseUp, handleMouseleave]);

3. mouseDown事件

  • 记录startPoint
  • 在主画布上绘制离线画布数据
mainCtx.drawImage(offscreenCanvasRef.current, 0, 0);

4. mouseMove事件

  • 在主画布绘制图形
  • 利用requestAnimationFrame进行性能优化
requestIdRef.current = requestAnimationFrame(() => {
    const endPoint = getCanvasCoordinates(e);
    drawTempShapes(endPoint);
    requestIdRef.current = null;
});

5. mouseUp事件

  • 在离线画布绘制图形
  • 清除requestAnimationFrame
const offscreenCtx = offscreenCanvasRef.current.getContext("2d");
if (offscreenCtx) {
    drawBasicShapes.drawShapes(
        lastPointRef.current,
        point,
        'off'
    );
}

if (requestIdRef.current) {
    cancelAnimationFrame(requestIdRef.current);
    requestIdRef.current = null;
}

✨ 好啦,今天的手绘风代码解析就到这里啦~希望这段充满创意与灵魂的代码能激发你无限的灵感,快拿起画笔和键盘,打造出属于你自己的艺术之作吧!💖

如果你也觉得这种独特的“手绘风”超级有趣,别忘了点赞、收藏和分享给更多的朋友哦~🔥