记得以前遇到过一个手机绘图,就是将画笔画出的东西镜像对半分或者8分,这样随手画出来的东西有了对称也很有特色,就像这样:
想了想,觉得canvas应该是可以实现的。因此决定做一个试试。目标是做一个8分镜像的,但是直接上手没有思路,就先从左右镜像开始做起。不过最先开始还是需要实现canvas画笔的功能。
1. canvas画笔与画布功能
这里原本的思路是使用fillRect()
一个点一个点的画出来连成线,但是当鼠标移动稍微快一点就会出现断层(mousemove的触发间隔不够短),想了半天没解决办法。后面偶然去了canvas官网,发现居然有现成的画笔示例,而且用的是stroke()
实现的连线。。。
想想也是,你滑的快,就直接用线连起来了。再快一点也就是一条直线了。
2. 镜像绘制
这个就比较简单,计算出当前点的中心y轴对称点的坐标,在绘制完当前点以后,再绘制对称点即可。
3. 四分镜像绘制
这里有点不同了,这里多了一个计算以中心点放射对称点坐标的方法(其实这里直接用两次镜面对称绘制更方便,但是当时思路是先想到了镜面对称,所以多了个计算放射对称点的方法)。思路也差不多,先绘制镜面对称的点;再绘制当前点和镜面对称点的x中心轴对称的点。
这两个方法有很多类似,因此准备后面优化。
4. 终极8分镜像
这里开始准备用旋转、变形操作实现,但是试了试不太好实现,后来还是用了镜像对称
以及放射对称
实现的。
主要的绘制顺序如下:
这里多了一个求当前点同一象限内对称点的操作,由于canvas画布是一个正方形,所以将当前点的(x,y)轴位置互换就是对称点的位置了。然后就接着按象限对称点继续执行放射对称和镜像对称的操作即可。
5.最终优化后的代码
最后将整个过程封装成了类,让代码更加直观,完整页面代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas id="c"></canvas>
<br />
<button id="clear">清空</button>
<input type="number" value="2" placeholder="画笔宽度" min="0" id="num-inp" />
</body>
<script>
class Artboard {
constructor(c, cwh) {
this.c = c
this.ctx = c.getContext('2d')
this.cwh = cwh // 画布宽高
this.lineWidth = 2 // 画笔宽度
this.lastTimePoint = [] // 上一次mouseMove的(x, y)位置
this.isLeftDown = false // 鼠标是否按下
this.middlePosition = this.cwh / 2 + 0.05
}
init() {
this.c.width = this.c.height = this.cwh
// this.drawMarkLine()
}
// 绘制
drawLine(x1, y1, x2, y2, w) {
this.ctx.beginPath();
this.ctx.lineWidth = w;
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.closePath();
this.ctx.stroke();
}
// 初始化绑定事件
initEventListener() {
// 其他绑定事件
document.getElementById("num-inp").addEventListener('change', (e) => {
this.lineWidth = e.target.value
})
document.getElementById("clear").addEventListener("click", () => {
this.ctx.clearRect(0, 0, this.cwh, this.cwh);
this.drawMarkLine();
});
// 鼠标按下
this.c.addEventListener("mousedown", (e) => {
const x = e.offsetX;
const y = e.offsetY;
this.lastTimePoint = [x, y];
this.isLeftDown = true;
});
// 鼠标抬起
this.c.addEventListener("mouseup", () => {
this.isLeftDown = false;
});
// 鼠标离开画布
this.c.addEventListener("mouseleave", (e) => {
if (!this.isLeftDown) return;
const x = e.offsetX >= 0 ? e.offsetX : 0;
const y = e.offsetY >= 0 ? e.offsetY : 0;
this.isLeftDown = false;
this.drawLine(this.lastTimePoint[0], this.lastTimePoint[1], x, y, this.lineWidth);
});
// 鼠标移动
this.c.addEventListener("mousemove", (e) => {
if (!this.isLeftDown) return;
const x = e.offsetX;
const y = e.offsetY;
this.drawAllLines(x, y)
// 这一步至关重要,需要保存上一个mousemove事件时的鼠标位置
this.lastTimePoint = [x, y];
});
}
// 绘制每个分区的点
drawAllLines(x, y) {
// 1 - 绘制当前鼠标位置的点
this.drawLine(this.lastTimePoint[0], this.lastTimePoint[1], x, y, this.lineWidth);
// 2 - 绘制中心点放射对称位置的点
const xy1 = this.getRadialSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1]); //
const xy2 = this.getRadialSymmetryPoint(x, y);
const revertXY = this.drawLine(...xy1, ...xy2, this.lineWidth);
// 3 - 绘制当前鼠标y轴镜像对称的点
const xy11 = this.getMirrorSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1], "x");
const xy22 = this.getMirrorSymmetryPoint(x, y, "x");
this.drawLine(...xy11, ...xy22, this.lineWidth);
// 4 - 绘制当前鼠标x轴镜像对称的点
const xy111 = this.getMirrorSymmetryPoint(this.lastTimePoint[0], this.lastTimePoint[1], "y");
const xy222 = this.getMirrorSymmetryPoint(x, y, "y");
this.drawLine(...xy111, ...xy222, this.lineWidth);
// 5 - 绘制当前点同一象限内的对称点
const xy1111 = this.getSameQuadrantPoint(this.lastTimePoint[0], this.lastTimePoint[1]);
const xy2222 = this.getSameQuadrantPoint(x, y);
this.drawLine(...xy1111, ...xy2222, this.lineWidth);
// 6 - 绘制象限对称点的放射对成点
const xy11111 = this.getRadialSymmetryPoint(...xy1111);
const xy22222 = this.getRadialSymmetryPoint(...xy2222);
this.drawLine(...xy11111, ...xy22222, this.lineWidth);
// 7 - 绘制象限内对称点的y轴纵向向镜面对称点
const xy111111 = this.getMirrorSymmetryPoint(...xy1111, 'x')
const xy222222 = this.getMirrorSymmetryPoint(...xy2222, 'x')
this.drawLine(...xy111111, ...xy222222, this.lineWidth);
// 8 - 绘制象限内对称点的x轴横向镜面对称点
const xy1111111 = this.getMirrorSymmetryPoint(...xy1111, 'y')
const xy2222222 = this.getMirrorSymmetryPoint(...xy2222, 'y')
this.drawLine(...xy1111111, ...xy2222222, this.lineWidth);
}
// 绘制辅助线
drawMarkLine() {
this.drawLine(this.middlePosition + 0.5, 0, this.middlePosition + 0.5, this.cwh, 0.5);
this.drawLine(0, this.middlePosition + 0.5, this.cwh, this.middlePosition + 0.5, 0.5);
this.drawLine(0, 0, this.cwh, this.cwh, 0.5);
this.drawLine(this.cwh, 0, 0, this.cwh, 0.5);
}
// 计算与中心轴偏移量的方法
calcNewPosit(val, offset) {
let newVal = val
if (val < this.middlePosition) { // 点在中心轴左侧/上方
newVal = offset > 0 ? this.middlePosition + offset : this.middlePosition - offset;
} else { // 点在中心轴右侧/下方
newVal = offset < 0 ? this.middlePosition + offset : this.middlePosition - offset;
}
return newVal
}
// 计算这个点同一象限内的那个对称点。由于是正方形,所以直接将其x,y轴位置对调即可
getSameQuadrantPoint(x, y) {
return [y, x];
}
// 计算中心放射对称的点
getRadialSymmetryPoint(x, y) {
const xl = this.middlePosition - x;
const yl = this.middlePosition - y;
let nx = x, ny = y;
nx = this.calcNewPosit(x, xl)
ny = this.calcNewPosit(y, yl)
return [nx, ny];
}
// 计算镜面对称位置的点
getMirrorSymmetryPoint(x, y, type) {
const xl = this.middlePosition - x; // 与中心轴的偏移量
const yl = this.middlePosition - y;
let nx = x,
ny = y; // nx,ny是与对称轴对称的点x,y轴坐标
// 根据type决定是返回当前点的纵向镜面对称还是横向镜面对称
switch (type) {
// 以y轴镜面对称
case "x":
nx = this.calcNewPosit(x, xl)
break;
// 以x轴镜面对称
case "y":
ny = this.calcNewPosit(y, yl)
break;
default:
break;
}
return [nx, ny];
}
}
window.onload = function () {
const c = document.getElementById('c')
const c1 = new Artboard(c, 400)
c1.initEventListener()
c1.init()
}
</script>
<style>
#c {
border: 1px solid #ddd;
margin-left: 100px;
}
</style>
</html>
“我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!”