说在前面
哆啦A梦应该是不少90后的童年回忆吧,今天让我们一起来看看怎么用canvas来画一个哆啦A梦。
效果展示
codepen在线查看:codepen.io/yongtaozhen…
码上掘金在线查看:code.juejin.cn/pen/7492379…
代码实现
基础图形绘制
椭圆
function drawEllipse(ctx, x, y, radiusX, radiusY) {
const kappa = 0.5522848;
const ox = radiusX * kappa; // 水平控制点偏移量
const oy = radiusY * kappa; // 垂直控制点偏移量
ctx.beginPath();
ctx.moveTo(x, y - radiusY);
ctx.bezierCurveTo(
x + ox,
y - radiusY,
x + radiusX,
y - oy,
x + radiusX,
y
);
ctx.bezierCurveTo(
x + radiusX,
y + oy,
x + ox,
y + radiusY,
x,
y + radiusY
);
ctx.bezierCurveTo(
x - ox,
y + radiusY,
x - radiusX,
y + oy,
x - radiusX,
y
);
ctx.bezierCurveTo(
x - radiusX,
y - oy,
x - ox,
y - radiusY,
x,
y - radiusY
);
ctx.closePath();
}
kappa常数
0.5522848是绘制标准圆弧所需的贝塞尔曲线修正系数,由公式 (4/3)*(sqrt(2)-1) 计算得出,保证四段贝塞尔曲线拼接后的图形接近真实圆弧。
控制点偏移量
const ox = radiusX * kappa; // 水平控制点偏移
const oy = radiusY * kappa; // 垂直控制点偏移
通过椭圆长短轴半径与kappa值的乘积,动态计算贝塞尔控制点的坐标偏移量。
起点定位
ctx.moveTo(x, y - radiusY); // 定位到椭圆顶点
四次贝塞尔曲线拼接
- 右上半圆
ctx.bezierCurveTo(x + ox, y - radiusY, x + radiusX, y - oy, x + radiusX, y)
控制点分布在顶点右侧和右端点上方,形成顺时针弧线。
- 右下半圆
ctx.bezierCurveTo(x + radiusX, y + oy, x + ox, y + radiusY, x, y + radiusY)
- 左下半圆
ctx.bezierCurveTo(x - ox, y + radiusY, x - radiusX, y + oy, x - radiusX, y)
- 左上半圆
ctx.bezierCurveTo(x - radiusX, y - oy, x - ox, y - radiusY, x, y - radiusY)
示例
ctx.beginPath();
drawEllipse(ctx, 170, 100, 30, 35);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.beginPath();
圆角矩形
function drawRoundedRect(x, y, w, h, radius, rotationAngle = 0) {
ctx.save(); // 保存当前绘图状态
ctx.translate(x, y); // 移动到指定位置
ctx.rotate(rotationAngle); // 旋转指定角度
ctx.beginPath();
ctx.moveTo(radius, 0);
ctx.arcTo(w, 0, w, h, radius); // 右上角
ctx.arcTo(w, h, 0, h, radius); // 右下角
ctx.arcTo(0, h, 0, 0, radius); // 左下角
ctx.arcTo(0, 0, w, 0, radius); // 左上角
ctx.closePath();
ctx.fill();
ctx.restore(); // 恢复之前保存的绘图状态
}
每个arcTo依次处理相邻两条边的连接点,根据当前点、控制点1、控制点2自动计算切线圆弧,相比直接使用arc,避免手动计算圆心坐标的复杂度。
示例
ctx.beginPath();
drawRoundedRect(85, 310, 235, 100, 50);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.beginPath();
桶状矩形
function drawBarrelShape(
startX,
startY,
width,
height,
curveRadius,
fillColor,
strokeColor,
lineWidth
) {
// 移动到起始点
ctx.beginPath();
ctx.moveTo(startX + curveRadius, startY);
// 绘制上侧边直线
ctx.lineTo(startX + width - curveRadius, startY);
// 绘制右侧曲线
const controlX1 = startX + width;
const controlY1 = startY + height / 2;
ctx.quadraticCurveTo(
controlX1,
controlY1,
startX + width - curveRadius,
startY + height
);
// 绘制下侧边直线
ctx.lineTo(startX + curveRadius, startY + height);
// 绘制左侧曲线
const controlX2 = startX;
const controlY2 = startY + height / 2;
ctx.quadraticCurveTo(
controlX2,
controlY2,
startX + curveRadius,
startY
);
// 闭合路径
ctx.closePath();
// 设置填充颜色和边框颜色
ctx.fillStyle = fillColor;
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
// 填充和绘制边框
ctx.fill();
ctx.stroke();
}
上侧直线
ctx.moveTo(startX + curveRadius, startY);
moveTo从左侧起点(含曲线偏移)向右侧绘制水平线,预留右侧曲线空间。
右侧贝塞尔曲线
const controlX1 = startX + width;
const controlY1 = startY + height / 2;
ctx.quadraticCurveTo(
controlX1,
controlY1,
startX + width - curveRadius,
startY + height
);
控制点(controlX1, controlY1)位于右侧边中点,使曲线向外凸起形成弧形。终点位于右下侧,与下边直线起点衔接。
下侧直线与左侧曲线
// 绘制下侧边直线
ctx.lineTo(startX + curveRadius, startY + height);
// 绘制左侧曲线
const controlX2 = startX;
const controlY2 = startY + height / 2;
ctx.quadraticCurveTo(
controlX2,
controlY2,
startX + curveRadius,
startY
);
// 闭合路径
ctx.closePath();
下边反向绘制至左侧后,左侧控制点(controlX2, controlY2)同样位于中点,使左右对称弯曲。
示例
ctx.beginPath();
drawBarrelShape(40, 310, 325, 230, 50, "#0096FF", "black", 1);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.beginPath();
哆啦A梦绘制
头部绘制
头部最外圈
一个3/4圆。
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(200, 200, 160, (3 / 4) * Math.PI, (9 / 4) * Math.PI);
ctx.fillStyle = "#0096FF";
ctx.fill();
ctx.stroke();
脸部白色区域
一个3/4圆。
ctx.beginPath();
ctx.arc(200, 220, 130, (3 / 4) * Math.PI, (9 / 4) * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
眼睛
最外圈是一个椭圆,眼黑部分为一个小椭圆,内部再点缀一个白色的圆形高光。
// 左眼
ctx.beginPath();
drawEllipse(ctx, 170, 100, 30, 35);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.beginPath();
drawEllipse(ctx, 175, 110, 8, 10);
ctx.fillStyle = "black";
ctx.fill();
ctx.beginPath();
ctx.arc(175, 110, 3, 0, 2 * Math.PI);
ctx.fillStyle = "#FFF7FF";
ctx.fill();
ctx.stroke();
// 右眼
ctx.beginPath();
drawEllipse(ctx, 230, 100, 30, 35);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.restore();
ctx.beginPath();
drawEllipse(ctx, 225, 110, 8, 10);
ctx.fillStyle = "black";
ctx.fill();
ctx.beginPath();
ctx.arc(225, 110, 3, 0, 2 * Math.PI);
ctx.fillStyle = "#FFF7FF";
ctx.fill();
ctx.stroke();
鼻子
红色的圆形鼻子,内有一个白色圆形高光。
ctx.beginPath();
ctx.arc(200, 140, 18, 0, 2 * Math.PI);
ctx.fillStyle = "red";
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.arc(200, 135, 5, 0, 2 * Math.PI);
ctx.fillStyle = "#FFF7FF";
ctx.strokeStyle = "rgba(0, 0, 0, 0)";
ctx.fill();
ctx.stroke();
嘴巴
一段1/3圆弧。
ctx.beginPath();
ctx.arc(200, 180, 100, (1 / 6) * Math.PI, (5 / 6) * Math.PI);
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.stroke();
鼻子到嘴巴的连线
连接鼻子和嘴巴的直线。
ctx.beginPath();
ctx.moveTo(200, 155);
ctx.lineTo(200, 280);
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.stroke();
胡须
嘴巴和鼻子之间有三组对称的直线。
// 左边胡须
ctx.lineWidth = 2;
ctx.moveTo(100, 150);
ctx.lineTo(180, 180);
ctx.stroke();
ctx.moveTo(100, 200);
ctx.lineTo(180, 200);
ctx.stroke();
ctx.moveTo(100, 240);
ctx.lineTo(180, 220);
ctx.stroke();
// 右边胡须
ctx.moveTo(220, 180);
ctx.lineTo(300, 150);
ctx.stroke();
ctx.moveTo(220, 200);
ctx.lineTo(300, 200);
ctx.stroke();
ctx.moveTo(220, 220);
ctx.lineTo(300, 240);
ctx.stroke();
铃铛绘制
脖子?
红色圆边矩阵
ctx.beginPath();
ctx.fillStyle = "#CD0833"; // 设置填充颜色
drawRoundedRect(85, 310, 235, 10, 5);
ctx.stroke();
铃铛
主体是一个圆形,中间有一个小圆洞,小洞到底部有一条小缝,上方用一个圆边矩形模拟铃铛的凸起部分。
ctx.beginPath();
ctx.arc(200, 340, 30, 0, 2 * Math.PI);
ctx.fillStyle = "#F6E03D";
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "#F6E03D"; // 设置填充颜色
drawRoundedRect(170, 325, 60, 5, 2.5);
ctx.stroke();
ctx.beginPath();
ctx.arc(200, 345, 8, 0, 2 * Math.PI);
ctx.fillStyle = "#816D4C";
ctx.fill();
ctx.beginPath();
ctx.moveTo(200, 353);
ctx.lineTo(200, 370);
ctx.stroke();
身体绘制
身体主体
一个蓝底桶状,直接调用前面准备好的基础图形来绘制。
drawBarrelShape(40, 310, 325, 230, 50, "#0096FF", "black", 1);
肚子白色区域
一个白色圆形,中间有一个半圆四次元口袋。
ctx.beginPath();
ctx.arc(200, 410, 100, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(200, 410, 80, 0, Math.PI);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(120, 410);
ctx.lineTo(280, 410);
ctx.stroke();
手臂绘制
一个倾斜的矩阵,可以直接调用前面准备好的基础图形来绘制。
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.fillStyle = "#0096FF"; // 设置填充颜色
drawRoundedRect(90, 310, 40, 80, 0, Math.PI / 4);
ctx.stroke();
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
ctx.fillStyle = "#0096FF"; // 设置填充颜色
drawRoundedRect(315, 310, 80, 40, 0, Math.PI / 4);
ctx.stroke();
手掌绘制
一个圆形,覆盖到手臂的下部末端。
ctx.beginPath();
// 左手
ctx.lineWidth = 1;
ctx.arc(40, 405, 40, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.stroke();
// 右手
ctx.beginPath();
ctx.lineWidth = 1;
ctx.arc(360, 405, 40, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.stroke();
脚部绘制
腿部
一个T字形划分出两条腿
ctx.moveTo(190, 515);
ctx.lineTo(210, 515);
ctx.stroke();
ctx.moveTo(200, 515);
ctx.lineTo(200, 550);
ctx.stroke();
脚掌
一个椭圆形,可以直接调用前面准备好的基础图形来绘制。
ctx.beginPath();
ctx.ellipse(131, 550, 70, 30, 0, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.ellipse(269, 550, 70, 30, 0, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
ctx.strokeStyle = "black";
ctx.lineWidth = 2;
ctx.stroke();
绘制顺序
绘制的时候我们需要根据各个身体部位的覆盖关系来调整绘制顺序,底层优先绘制。
drawHands();//手臂
drawBody();//身体
drawPalms();//手掌
drawFeet();//脚
drawHead();//头
drawBell();//铃铛
源码
gitee
- 🌟觉得有点意思的可以点个star~
- 🖊有什么问题或错误可以指出,欢迎pr~
- 📬有什么想要实现的功能或想法可以联系我~
codePen
码上掘金
代码地址:code.juejin.cn/pen/7492379…
公众号
关注公众号『 前端也能这么有趣 』,获取更多有趣内容。
发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~
说在后面
🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『
前端也能这么有趣
』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。