用Canvas画一个圆、画一条线,很多教程都讲过,但真正面对一个稍微复杂的图形,比如一把水壶,很多人就不知道从哪里下手了。问题的核心在于:复杂图形不是一笔画出来的,而是由若干基础几何形状拼装而成的。只要把结构拆解清楚,再配合一套可配置的参数体系,你就能用很少的代码画出形态各异的水壶,还能让用户实时调整它的样子。这篇文章就从零开始,完整演示这个思路的落地过程。

一、拆解水壶的几何结构,明确绘图单元
动手写代码之前,先花几分钟把水壶“拆开”看。一把典型的水壶可以分解为五个部分:壶身、壶嘴、壶盖(含壶钮)、提手和底座。壶身本质上是一个上窄下宽的圆角梯形,壶嘴是连接在壶身一侧的弯曲管道,壶盖是一个扁椭圆加一个小圆柱,提手可以用一条粗弧线表示,底座则是一个扁矩形。
拆解的意义在于两点。第一,每个部分都可以独立用基础路径描述,比如arc画弧、quadraticCurveTo画二次贝塞尔曲线、ellipse画椭圆,这些API组合起来足以覆盖绝大部分日常图形。第二,拆解之后天然形成了配置维度——壶身有多高、壶嘴朝哪个方向弯、提手是大是小,这些都可以变成参数,图形就从“写死的一幅画”变成了“可以调的一套描述”。
实际操作时建议先在纸上画个草图,标出关键锚点的大致坐标。比如壶身左上角、右上角、底部圆角的半径,壶嘴起点在壶身右侧的位置。有了锚点清单,后面写代码就是纯粹的翻译工作,不会写到一半发现结构对不上。
二、搭建基础画布与坐标工具
先准备一个干净的HTML页面,放一个canvas元素,并指定实际像素尺寸。这里有个新手常踩的坑:canvas的宽高必须通过属性或JavaScript设置,而不是只靠CSS。CSS只改变显示尺寸,会导致画面被拉伸模糊。正确做法是让canvas属性尺寸等于CSS尺寸乘以设备像素比,这样在高分屏上也能保持清晰。
<canvas id="board"></canvas>
<script>
const canvas = document.getElementById('board');
const dpr = window.devicePixelRatio || 1;
const W = 600, H = 500;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr); // 统一坐标系,后续按逻辑像素绘制
</script>坐标系统理顺之后,建议再写一个小工具函数,把“以画布中心为原点”的坐标系建立起来。水壶这类对称图形,用中心点作为参照比用左上角方便得多,后面所有部件的坐标都围绕中心点偏移,调整位置时只需要改一个数。
三、逐个部件绘制,用路径组合出形态
壶身是整把水壶的主体,其他部件都以它为参照。绘制思路是:从左上角开始,用两条二次贝塞尔曲线分别勾勒左右两侧的收腰轮廓,底部用圆弧封口。贝塞尔曲线的控制点决定了腰身的弯曲程度,把它做成参数,壶身就能在“直筒型”和“曲线型”之间切换。
function drawBody(ctx, cfg) {
const { cx, bodyTop, bodyBottom, topHalfW, bottomHalfW, waist } = cfg;
ctx.beginPath();
ctx.moveTo(cx - topHalfW, bodyTop);
// 左侧轮廓:向下收腰再展开
ctx.bezierCurveTo(
cx - topHalfW - waist, bodyTop + (bodyBottom - bodyTop) * 0.3,
cx - bottomHalfW - waist, bodyBottom - (bodyBottom - bodyTop) * 0.3,
cx - bottomHalfW, bodyBottom
);
// 底部圆角
ctx.quadraticCurveTo(cx, bodyBottom + 12, cx + bottomHalfW, bodyBottom);
// 右侧轮廓对称绘制
ctx.bezierCurveTo(
cx + bottomHalfW + waist, bodyBottom - (bodyBottom - bodyTop) * 0.3,
cx + topHalfW + waist, bodyTop + (bodyBottom - bodyTop) * 0.3,
cx + topHalfW, bodyTop
);
ctx.closePath();
ctx.fillStyle = cfg.bodyColor;
ctx.fill();
}壶嘴的画法稍微特殊。它不适合用一条闭合路径,更好的办法是画一条有宽度的曲线:先用moveTo定位到壶身右侧的连接点,用quadraticCurveTo画一条向上扬起的外沿曲线,再反向画一条内沿曲线回来,两条曲线之间的区域就是壶嘴的形状。注意壶嘴根部要稍微宽、出口要窄,这样才有真实感。壶嘴的弯曲角度、长度也全部参数化。
壶盖和提手相对简单。壶盖用ellipse画一个扁椭圆盖沿,再叠一个小的圆角矩形作为盖钮。提手用arc画一条弧线,设置较大的lineWidth并配上lineCap = 'round',一条粗弧线看起来就是一个结实的提手。所有部件绘制完毕后,调用顺序要注意:先画提手(被壶盖部分遮挡),再画壶身,最后画壶嘴和壶盖,遮挡关系才自然。
四、把参数集中管理,实现可配置
前面反复强调参数化,现在把它落实成一个配置对象。把所有几何尺寸、颜色、位置信息集中到一个对象里,渲染函数只读取配置而不写死任何数值,这就是“可配置”的本质。
const kettleConfig = {
cx: 300, cy: 250, // 水壶中心点
bodyTop: 160, bodyBottom: 380,
topHalfW: 70, bottomHalfW: 110,
waist: 18, // 收腰程度,0为直筒
spoutAngle: 0.6, // 壶嘴上扬角度
spoutLen: 90,
handleRadius: 70,
bodyColor: '#4a90d9',
lidColor: '#f5f5f5',
strokeColor: '#2c3e50'
};
function renderKettle(ctx, cfg) {
ctx.clearRect(0, 0, 600, 500);
drawHandle(ctx, cfg);
drawBody(ctx, cfg);
drawSpout(ctx, cfg);
drawLid(ctx, cfg);
}有了这个结构,配合几个<input>滑块监听input事件,实时修改配置并调用renderKettle重绘,用户拖动滑块就能看到壶身变胖变瘦、壶嘴变长变短。这就是参数化带来的直接收益:改一个数字胜过重写一段路径代码。
五、性能与扩展:分层渲染和局部重绘
如果图形更复杂,或者页面上有动画,每次全量重绘就浪费了。常见的优化是把静态部分和动态部分分层:底层canvas只画一次水壶本体,顶层canvas画水壶周围飘动的水蒸气之类的高频变动元素。两个canvas叠放,重绘时只清空顶层,成本大大降低。
另一个值得做的扩展是命中检测。给每个部件记录它的路径数据,重绘后调用ctx.isPointInPath判断鼠标是否点在壶身上,就可以实现点击壶盖换颜色、拖动壶身换位置这类交互。再进一步,把配置对象序列化成JSON存进localStorage,用户自定义的水壶下次打开页面还能还原,一个小小的绘图demo就有了产品的雏形。
总结一下,Canvas绘制复杂图形的方法论其实就三步:拆解结构、逐个实现、参数收敛。掌握了这套流程,水壶换成台灯、换成机器人,思路完全一样。建议你动手把上面的代码跑起来,然后试着加一个“壶身材质”参数,让水壶在陶瓷和金属质感之间切换,练习阴影和高光的处理,对Canvas的理解会更上一层楼。
Canvas绘图JavaScript几何图形HTML5 Canvas修改时间:2026-09-13 21:15:00