嘿~最近迷上了用 Canvas 绘制手绘风格图形,超级简单又好玩!不需要高级技巧,用几行代码就能DIY出满满的手绘feel,无论你是设计小白还是爱美的创意达人,都能轻松上手~ 快来和我一起体验这个超酷的网页小技巧,让你的页面瞬间活力满满吧!
核心逻辑
线段
- 多次绘制,制造“手绘”感觉
通过重复绘制同一条线,叠加多次轻微的随机偏移,线条不会那么“直”,而是会有轻微波动,显得更自然、更有温度。
- 每次开始新路径
每次都使用 ctx.beginPath() 开始一条新的路径,确保每次绘制互不干扰。
- 分段绘制
把起点到终点的直线分成若干小段,每个小的线段都添加一个随机的偏移量,模拟真实手绘时那种微妙的抖动和不规则性,让每一笔都显得独一无二。
总结来说,就是通过不断细分和叠加随机偏移,来模拟真实手绘线条的那种随性与自然感💫。每一次的小波动,都会让你的画布多一份生动的艺术气息,简直是程序中的“小确幸”✨!
圆
- 定义随机扰动函数
const rand = () => (Math.random() - 0.5) * radius * bezierCurveConfig.randomFactor;
这里定义了一个 rand 函数,用来生成一个基于圆半径和配置参数 randomFactor 的随机偏移量,为每个关键点添加随机扰动,让绘制的曲线带有一种“手绘”的随性与不完美美感。
- 用四段贝塞尔曲线绘制圆形(核心部分)
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) 根据结束角度计算,并加入扰动。
这段代码巧妙地将规则的圆形绘制和手绘风的随机扰动结合在一起,通过贝塞尔曲线加随机偏移,既保留了圆形的整体结构,又注入了艺术家手绘的温度与个性。
双缓冲绘制
- 双画布
同时设置两个画布,主画布和离线画布,离线画布不会展示在页面上,负责记录之前的图形,主画布负责绘制和展示所有的效果。
// 主画布
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;
}
✨ 好啦,今天的手绘风代码解析就到这里啦~希望这段充满创意与灵魂的代码能激发你无限的灵感,快拿起画笔和键盘,打造出属于你自己的艺术之作吧!💖
如果你也觉得这种独特的“手绘风”超级有趣,别忘了点赞、收藏和分享给更多的朋友哦~🔥