使用canvas实现一个简单的绘图板:
实际实现效果

使用vite+vue3,组件使用部分antdv的
第一步:简单布局
主要布局内容:
1.颜色选择器
2.形状选择器(这里只做2种形状的)
3.canvas图层
js
<template>
<div ref="wrapRef" class="canvas_wrap">
<div>
<span>颜色选择: </span>
<a-input
v-model:value="colorVal"
style="width: 200px"
type="color"
></a-input>
<span style="margin-left: 20px">形状选择: </span>
<a-select v-model:value="drawType">
<a-select-option value="rect">方形</a-select-option>
<a-select-option value="circle">圆形</a-select-option>
</a-select>
</div>
<canvas
ref="canvasRef"
:style="{
width: canvasStyleWidth + 'px',
height: canvasStyleHeight + 'px',
}"
></canvas>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const canvasStyleWidth = 1200;
const canvasStyleHeight = 480;
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
</script>
<style scoped lang="scss">
.canvas_wrap {
padding: 30px;
}
canvas {
margin-top: 30px;
border-radius: 10px;
background: #f0f0f0;
}
</style>
布局完

第二步:画矩形
在canvas中画一个矩形只需要使用canvas中strokeRect方法以及提供4个参数分别是起始点x、y坐标以及矩形的宽高。
这里我们新建一个矩形的类RectPolygon,由上面绘制所需肯定就需要定义4个变量startX、startY、endX、endY以及绘制颜色的变量
typescript
export class RectPolygon {
public startX: number;
public startY: number;
public endX: number;
public endY: number;
public drawColor: string;
public ctx: CanvasRenderingContext2D;
public id: number;
constructor(
startX: number,
startY: number,
drawColor: string,
ctx: CanvasRenderingContext2D
) {
this.startX = startX;
this.startY = startY;
this.drawColor = drawColor;
this.endX = startX;
this.endY = startY;
this.ctx = ctx;
this.id = Date.now();
}
}
如何确定起始点的坐标呢?
当鼠标按下的时候肯定可以确认1个点(但不一定就是起始点坐标),当鼠标抬起来的时候可以确认另外1个点;
这样画时,鼠标按下的点就是起始点的坐标,鼠标移动后抬起的点就是结束点坐标
但是这样画时,鼠标按下的点在canvas中绘制矩形时相当于结束坐标,鼠标移动后抬起的点是起始坐标

但是可以看出起始坐标就是x、y相对于较小的坐标,所以在RectPolygon类中新增几个getter分别获取起始/结束的x、y坐标
typescript
export class RectPolygon {
public startX: number;
public startY: number;
public endX: number;
public endY: number;
public drawColor: string;
public ctx: CanvasRenderingContext2D;
public id: number;
constructor(
startX: number,
startY: number,
drawColor: string,
ctx: CanvasRenderingContext2D
) {
this.startX = startX;
this.startY = startY;
this.drawColor = drawColor;
this.endX = startX;
this.endY = startY;
this.ctx = ctx;
this.id = Date.now();
}
get StartX() {
return Math.min(this.startX, this.endX);
}
get StartY() {
return Math.min(this.startY, this.endY);
}
get EndX() {
return Math.max(this.startX, this.endX);
}
get EndY() {
return Math.max(this.startY, this.endY);
}
change(x: number, y: number) {
this.endX = x;
this.endY = y;
}
draw() {
this.ctx.fillStyle = this.drawColor;
this.ctx.fillRect(
this.StartX,
this.StartY,
this.EndX - this.StartX,
this.EndY - this.StartY
);
}
}
当我们鼠标按下时就创建了一个矩形,这个时候起始坐标和结束坐标相当于重叠的,矩形还没有宽高,在鼠标移动的时候实际就是修改矩形的结束坐标的x、y,添加一个change方法用于修改结束点的x、y坐标
开始绘制
注册事件把矩形类应用上,然后调用draw方法绘制,在鼠标移动时调用change方法修改结束点坐标位置
js
<template>
<div ref="wrapRef" class="canvas_wrap">
<div>
<span>颜色选择: </span>
<a-input
v-model:value="colorVal"
style="width: 200px"
type="color"
></a-input>
<span style="margin-left: 20px">形状选择: </span>
<a-select v-model:value="drawType">
<a-select-option value="rect">方形</a-select-option>
<a-select-option value="circle">圆形</a-select-option>
</a-select>
</div>
<canvas
ref="canvasRef"
:style="{
width: canvasStyleWidth + 'px',
height: canvasStyleHeight + 'px',
}"
></canvas>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { RectPolygon } from "./utils";
type GeometryType = RectPolygon;
const canvasStyleWidth = 1200; //canvas样式宽度
const canvasStyleHeight = 480; //canvas样式高度
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
const ctx = ref<CanvasRenderingContext2D>();
const canvasRef = ref<HTMLCanvasElement>();
const currentPolygon = ref<GeometryType>(); //当前绘制的矩形
const drawData = ref<GeometryType[]>([]); //当前画布上所有的矩形数据源
const isDrawing = ref<boolean>(false); //是否正在绘制
let canvasLeft: number, canvasTop: number;
onMounted(() => {
initCanvas();
const { x, y } = canvasRef.value.getBoundingClientRect();
canvasLeft = x;
canvasTop = y;
});
const initCanvas = () => {
canvasRef.value.width = canvasStyleWidth * window.devicePixelRatio;
canvasRef.value.height = canvasStyleHeight * window.devicePixelRatio;
ctx.value = canvasRef.value.getContext("2d");
canvasRef.value.addEventListener("mousedown", canvasMouseDown);
};
const canvasMouseDown = (e: MouseEvent) => {
//计算按下的起始坐标
const startX = e.clientX - canvasLeft;
const startY = e.clientY - canvasTop;
const poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
currentPolygon.value = poylogn;
drawData.value.push(currentPolygon.value);
window.onmousemove = (e: MouseEvent) => {
const endX = e.clientX - canvasLeft;
const endY = e.clientY - canvasTop;
ctx.value.clearRect(
0,
0,
1200,
480
);
if (currentPolygon.value) {
currentPolygon.value.change(endX, endY);
}
for (const item of drawData.value) {
item.draw();
}
};
window.onmouseup = () => {
currentPolygon.value = null;
window.onmousemove = null;
window.onmouseup = null;
};
};
</script>
<style scoped lang="scss">
.canvas_wrap {
padding: 30px;
}
canvas {
margin-top: 30px;
border-radius: 10px;
background: #f0f0f0;
}
</style>
成功绘制

现在我们想如果鼠标在当前已经绘制的矩形内是选中矩形拖动,而不是又重新绘制一个矩形,所以在矩形类新增一个方法isPointIn用于判断一个坐标是否位于当前图形内,并且添加一个move方法用于图形移动
js
isPointIn(x: number, y: number) {
if (
x >= this.StartX &&
x < this.EndX &&
y >= this.StartY &&
y <= this.EndY
) {
return true;
}
return false;
}
move(x: number, y: number) {
this.startX += x;
this.startY += y;
this.endX += x;
this.endY += y;
}
然后为window添加一个mousemove监听器,用于检测当前鼠标移动的点是否在矩形内
js
window.addEventListener("mousemove", checkPolygonAtPixel);
const checkPolygonAtPixel = (e: MouseEvent) => {
if (isDrawing.value) return;
const checkX = e.clientX - canvasLeft;
const chexkY = e.clientY - canvasTop;
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
for (const item of drawData.value) {
if (!item.isPointIn(checkX, chexkY)) {
document.body.style.cursor = "default";
currentPolygon.value = null;
} else {
const index = drawData.value.findIndex((items) => items === item);
drawData.value.splice(index, 1);
drawData.value.push(item);
document.body.style.cursor = "pointer";
currentPolygon.value = item;
}
item.draw();
}
};
并且在我们鼠标按下后要修改对应逻辑,判断当前鼠标位置是否有图形,有的话鼠标移动就是调用图形的move方法,没有则是新建一个图形,在移动时调用其change方法
js
const canvasMouseDown = (e: MouseEvent) => {
//计算按下的起始坐标
isDrawing.value = true;
const startX = e.clientX - canvasLeft;
const startY = e.clientY - canvasTop;
if (currentPolygon.value) {
window.onmousemove = (e: MouseEvent) => {
currentPolygon.value.move(e.movementX, e.movementY);
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
for (const item of drawData.value) {
item.draw();
}
};
} else {
const poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
currentPolygon.value = poylogn;
drawData.value.push(poylogn);
window.onmousemove = (e: MouseEvent) => {
const endX = e.clientX - canvasLeft;
const endY = e.clientY - canvasTop;
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
if (currentPolygon.value) {
currentPolygon.value.change(endX, endY);
}
for (const item of drawData.value) {
item.draw();
}
};
}
window.onmouseup = () => {
currentPolygon.value = null;
window.onmousemove = null;
window.onmouseup = null;
isDrawing.value = false;
};
};

至此就实现了矩形的绘制和移动功能
实现一种图形的绘制和移动后,其他图形就可以依葫芦画瓢了,比如画圆形,新建CirclePolygon类实现其核心方法draw、move、change即可
全部代码
index.vue
js
<template>
<div ref="wrapRef" class="canvas_wrap">
<div>
<span>颜色选择: </span>
<a-input
v-model:value="colorVal"
style="width: 200px"
type="color"
></a-input>
<span style="margin-left: 20px">形状选择: </span>
<a-select v-model:value="drawType">
<a-select-option value="rect">方形</a-select-option>
<a-select-option value="circle">圆形</a-select-option>
</a-select>
</div>
<canvas
ref="canvasRef"
:style="{
width: canvasStyleWidth + 'px',
height: canvasStyleHeight + 'px',
}"
></canvas>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { CirclePolygon, RectPolygon } from "./utils";
type GeometryType = RectPolygon | CirclePolygon;
const canvasStyleWidth = 1200; //canvas样式宽度
const canvasStyleHeight = 480; //canvas样式高度
const colorVal = ref<string>("#000");
const drawType = ref<string>("rect");
const ctx = ref<CanvasRenderingContext2D>();
const canvasRef = ref<HTMLCanvasElement>();
const currentPolygon = ref<GeometryType>(); //当前绘制的矩形
const drawData = ref<GeometryType[]>([]); //当前画布上所有的矩形数据源
const isDrawing = ref<boolean>(false); //是否正在绘制
let canvasLeft: number, canvasTop: number;
onMounted(() => {
initCanvas();
const { x, y } = canvasRef.value.getBoundingClientRect();
canvasLeft = x;
canvasTop = y;
});
const initCanvas = () => {
canvasRef.value.width = canvasStyleWidth * window.devicePixelRatio;
canvasRef.value.height = canvasStyleHeight * window.devicePixelRatio;
ctx.value = canvasRef.value.getContext("2d");
canvasRef.value.addEventListener("mousedown", canvasMouseDown);
window.addEventListener("mousemove", checkPolygonAtPixel);
};
const checkPolygonAtPixel = (e: MouseEvent) => {
if (isDrawing.value) return;
const checkX = e.clientX - canvasLeft;
const chexkY = e.clientY - canvasTop;
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
for (const item of drawData.value) {
if (!item.isPointIn(checkX, chexkY)) {
//如果当前鼠标点不在该矩形范围内
item.changeIsAcative(false);
item.draw();
document.body.style.cursor = "default";
currentPolygon.value = null;
} else {
//如果当前鼠标点在该矩形范围内
const index = drawData.value.findIndex((items) => items === item); //找到图形数据index
const hasActive = drawData.value
.filter((items) => items !== item)
.some((i) => i.isActive);
if (!hasActive) {
//这里主要是处理一下如果2个图形有遮挡,在激活时保证激活的图形在最上面
item.changeIsAcative(true);
item.drawActive();
document.body.style.cursor = "pointer";
drawData.value.splice(index, 1);
drawData.value.push(item);
item.changeIsAcative(true);
item.drawActive();
currentPolygon.value = item;
document.body.style.cursor = "pointer";
} else {
item.changeIsAcative(false);
item.draw();
document.body.style.cursor = "default";
currentPolygon.value = null;
}
}
}
};
const canvasMouseDown = (e: MouseEvent) => {
//计算按下的起始坐标
isDrawing.value = true;
const startX = e.clientX - canvasLeft;
const startY = e.clientY - canvasTop;
if (currentPolygon.value) {
window.onmousemove = (e: MouseEvent) => {
currentPolygon.value.move(e.movementX, e.movementY);
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
for (const item of drawData.value) {
if (item.id === currentPolygon.value.id) {
item.drawActive();
} else {
item.draw();
}
}
};
} else {
let poylogn: GeometryType;
if (drawType.value === "rect") {
poylogn = new RectPolygon(startX, startY, colorVal.value, ctx.value);
} else {
poylogn = new CirclePolygon(startX, startY, colorVal.value, ctx.value);
}
currentPolygon.value = poylogn;
drawData.value.push(poylogn);
window.onmousemove = (e: MouseEvent) => {
const endX = e.clientX - canvasLeft;
const endY = e.clientY - canvasTop;
ctx.value.clearRect(
0,
0,
1200 * window.devicePixelRatio,
480 * window.devicePixelRatio
);
if (currentPolygon.value) {
currentPolygon.value.change(endX, endY);
}
for (const item of drawData.value) {
item.draw();
}
};
}
window.onmouseup = () => {
currentPolygon.value = null;
window.onmousemove = null;
window.onmouseup = null;
isDrawing.value = false;
};
};
</script>
<style scoped lang="scss">
.canvas_wrap {
padding: 30px;
}
canvas {
margin-top: 30px;
border-radius: 10px;
background: #f0f0f0;
}
</style>
utils.ts
js
export class CirclePolygon {
public startX: number;
public startY: number;
public radius: number;
public ctx: CanvasRenderingContext2D;
public drawColor: string;
public id: number;
public isActive: boolean;
constructor(
startX: number,
startY: number,
drawColor: string,
ctx: CanvasRenderingContext2D
) {
this.startX = startX;
this.startY = startY;
this.drawColor = drawColor;
this.isActive = false;
this.ctx = ctx;
this.radius = 0;
this.id = Date.now();
this.draw();
}
draw(color = this.drawColor) {
this.ctx.fillStyle = color;
this.ctx.beginPath();
this.ctx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
this.ctx.stroke();
this.ctx.fill();
}
changeIsAcative(val: boolean) {
this.isActive = val;
}
drawActive() {
this.draw("red");
}
isPointIn(x: number, y: number) {
this.ctx.beginPath();
this.ctx.arc(this.startX, this.startY, this.radius, 0, Math.PI * 2);
if (this.ctx.isPointInPath(x, y)) {
return true;
}
return false;
}
change(x: number, y: number) {
const rmax = Math.min(this.startX, this.startY);
this.radius = Math.min(
rmax,
Math.sqrt((x - this.startX) ** 2 + (y - this.startY) ** 2)
);
}
move(x: number, y: number) {
this.startX += x;
this.startY += y;
}
}
export class RectPolygon {
public startX: number;
public startY: number;
public endX: number;
public endY: number;
public drawColor: string;
public ctx: CanvasRenderingContext2D;
public isActive: boolean;
public id: number;
constructor(
startX: number,
startY: number,
drawColor: string,
ctx: CanvasRenderingContext2D
) {
this.startX = startX;
this.startY = startY;
this.drawColor = drawColor;
this.endX = startX;
this.isActive = false;
this.endY = startY;
this.ctx = ctx;
this.id = Date.now();
}
get StartX() {
return Math.min(this.startX, this.endX);
}
get StartY() {
return Math.min(this.startY, this.endY);
}
get EndX() {
return Math.max(this.startX, this.endX);
}
get EndY() {
return Math.max(this.startY, this.endY);
}
changeIsAcative(val: boolean) {
this.isActive = val;
}
drawActive() {
this.draw("red");
}
isPointIn(x: number, y: number) {
if (
x >= this.StartX &&
x <= this.EndX &&
y >= this.StartY &&
y <= this.EndY
) {
return true;
}
return false;
}
move(x: number, y: number) {
this.startX += x;
this.startY += y;
this.endX += x;
this.endY += y;
}
change(x: number, y: number) {
this.endX = x;
this.endY = y;
}
draw(color = this.drawColor) {
this.ctx.fillStyle = color;
this.ctx.fillRect(
this.StartX,
this.StartY,
this.EndX - this.StartX,
this.EndY - this.StartY
);
}
}