在线绘图小工具
- 文章说明
- 程序源码
- 功能展示
- 源码下载
文章说明
本文主要是在看了袁老师的canvas绘图小视频后所写,记录一个简单的canvas绘图功能,并学习一下较为传统的JavaScript事件写法,同时了解一下拖拽事件的更合理写法,等待后续将头像上传功能进行优化。
参考教程
程序源码
主要为核心的绘图类Shape.js,以及页面和事件,在App.vue里面
Shape.js
export class Shape {constructor(color, startX, startY, dpr) {this.endX = this.startX;this.endY = this.startY;this.color = color;this.startX = startX;this.startY = startY;this.dpr = dpr;}get minX() {return Math.min(this.startX, this.endX);}get maxX() {return Math.max(this.startX, this.endX);}get minY() {return Math.min(this.startY, this.endY);}get maxY() {return Math.max(this.startY, this.endY);}draw(ctx) {ctx.fillStyle = this.color;ctx.fill();ctx.strokeStyle = "#fff";ctx.lineWidth = 3 * this.dpr;ctx.lineCap = "square";ctx.stroke();}drawDashed(ctx) {ctx.strokeStyle = "#fff";ctx.lineWidth = 1 * this.dpr;ctx.lineCap = "square";ctx.stroke();}inSide(x, y) {return (this.minX <= x && this.maxX >= x) && (this.minY <= y && this.maxY >= y);}// 上1、下2、左4、右8// 得到上1、下2、左4、左上5、左下6、右8、右上9、右下10inGap(gap, x, y) {let result = 0;if (Math.abs(this.minY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {result += 1;}if (Math.abs(this.maxY - y) <= gap && (this.minX - gap <= x && this.maxX + gap >= x)) {result += 2;}if (Math.abs(this.minX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {result += 4;}if (Math.abs(this.maxX - x) <= gap && (this.minY - gap <= y && this.maxY + gap >= y)) {result += 8;}return result;}mouseMoveCreate(e, rect) {const clickX = e.clientX - rect.left;const clickY = e.clientY - rect.top;this.endX = clickX;this.endY = clickY;this.startX = this.minX;this.endX = this.maxX;this.startY = this.minY;this.endY = this.maxY;}mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, canvas) {const disX = e.clientX - rect.left - clickX;const disY = e.clientY - rect.top - clickY;this.startX = startX + disX;this.startY = startY + disY;this.endX = endX + disX;this.endY = endY + disY;canvas.style.cursor = "move";}mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas) {const disX = e.clientX - rect.left - clickX;const disY = e.clientY - rect.top - clickY;if (endX + disX < startX || endY + disY < startY || startX + disX > endX || startY + disY > endY) {return;}switch (inGap) {case 1:canvas.style.cursor = "n-resize";this.startY = startY + disY;break;case 2:canvas.style.cursor = "s-resize";this.endY = endY + disY;break;case 4:canvas.style.cursor = "w-resize";this.startX = startX + disX;break;case 5:canvas.style.cursor = "nw-resize";this.startX = startX + disX;this.startY = startY + disY;break;case 6:canvas.style.cursor = "sw-resize";this.startX = startX + disX;this.endY = endY + disY;break;case 8:canvas.style.cursor = "e-resize";this.endX = endX + disX;break;case 9:canvas.style.cursor = "ne-resize";this.endX = endX + disX;this.startY = startY + disY;break;case 10:canvas.style.cursor = "se-resize";this.endX = endX + disX;this.endY = endY + disY;break;default:canvas.style.cursor = "default";break;}}
}
简单实现了矩形和椭圆形的绘制相关功能
import {Shape} from "@/Shape";export class Rectangle extends Shape {constructor(color, startX, startY, dpr) {super(color, startX, startY, dpr);}draw(ctx) {ctx.beginPath();ctx.setLineDash([]);ctx.moveTo(this.minX, this.minY);ctx.lineTo(this.maxX, this.minY);ctx.lineTo(this.maxX, this.maxY);ctx.lineTo(this.minX, this.maxY);ctx.lineTo(this.minX, this.minY);super.draw(ctx);}
}
import {Shape} from "@/Shape";export class Circle extends Shape {constructor(color, startX, startY, dpr) {super(color, startX, startY, dpr);}draw(ctx) {ctx.beginPath();ctx.setLineDash([]);ctx.ellipse((this.maxX + this.minX) / 2,(this.maxY + this.minY) / 2,(this.maxX - this.minX) / 2,(this.maxY - this.minY) / 2,0, 0, Math.PI * 2);super.draw(ctx);ctx.beginPath();ctx.moveTo(this.minX, this.minY);ctx.setLineDash([1, 5]);ctx.lineTo(this.maxX, this.minY);ctx.lineTo(this.maxX, this.maxY);ctx.lineTo(this.minX, this.maxY);ctx.lineTo(this.minX, this.minY);super.drawDashed(ctx);}
}
App.vue
<script setup>
import {onMounted, reactive} from "vue";
import {Rectangle} from "@/Rectangle";
import {Circle} from "@/Circle";let canvas;
let rect;
let ctx;
let color;
let dpr;
let shapes = [];
let change = true;
let gap;function setCanvas() {canvas.width = 1000 * dpr;canvas.height = 500 * dpr;rect = canvas.getBoundingClientRect();
}function resizeShape(ratio) {for (let i = 0; i < shapes.length; i++) {shapes[i].startX *= ratio;shapes[i].startY *= ratio;shapes[i].endX *= ratio;shapes[i].endY *= ratio;}
}function draw() {if (change) {ctx.clearRect(0, 0, canvas.width, canvas.height);for (let i = 0; i < shapes.length; i++) {shapes[i].draw(ctx);}change = false;}requestAnimationFrame(draw);
}onMounted(() => {canvas = document.getElementById("canvas");ctx = canvas.getContext("2d");color = document.getElementById("color");dpr = window.devicePixelRatio;gap = 10 * dpr;setCanvas(canvas);draw();window.onresize = () => {const oldRatio = dpr;dpr = window.devicePixelRatio;gap = 10 * dpr;setCanvas();resizeShape(dpr / oldRatio);change = true;draw();};window.onkeydown = (e) => {if (e.ctrlKey && e.code === "KeyZ") {shapes.pop();change = true;}}canvas.onmousedown = mouseDown;canvas.ondblclick = (e) => {const clickX = e.clientX - rect.left;const clickY = e.clientY - rect.top;let shape;for (let i = shapes.length - 1; i >= 0; i--) {if (shapes[i].inSide(clickX, clickY)) {shapes[i].color = color.value;shape = shapes[i];shapes.splice(i, 1);break;}}if (shape) {shapes.push(shape);}change = true;};canvas.onmousemove = (e) => {changeSize(e);};
});function changeSize(e) {const clickX = e.clientX - rect.left;const clickY = e.clientY - rect.top;for (let i = shapes.length - 1; i >= 0; i--) {const inGap = shapes[i].inGap(gap, clickX, clickY);const {startX, startY, endX, endY} = shapes[i];shapes[i].mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas);if (inGap > 0) {break;}}
}function getShape(clickX, clickY) {for (let i = shapes.length - 1; i >= 0; i--) {const inGap = shapes[i].inGap(gap, clickX, clickY);if (inGap > 0) {return shapes[i];}if (shapes[i].inSide(clickX, clickY)) {return shapes[i];}}return null;
}function mouseDown(e) {const clickX = e.clientX - rect.left;const clickY = e.clientY - rect.top;let shape = getShape(clickX, clickY);if (!shape) {if (data.checkType === "矩形") {shape = new Rectangle(color.value, clickX, clickY, dpr);} else if (data.checkType === "椭圆形") {shape = new Circle(color.value, clickX, clickY, dpr);}shapes.push(shape);canvas.onmousemove = (e) => {shape.mouseMoveCreate(e, rect);change = true;};} else {const {startX, startY, endX, endY} = shape;const inGap = shape.inGap(gap, clickX, clickY);if (inGap > 0) {canvas.onmousemove = (e) => {shape.mouseMoveChangeSize(e, rect, startX, startY, endX, endY, clickX, clickY, inGap, canvas);change = true;};} else {canvas.onmousemove = (e) => {shape.mouseMoveChangePos(e, rect, startX, startY, endX, endY, clickX, clickY, canvas);change = true;};}}window.onmouseup = () => {canvas.onmousemove = null;canvas.onmousemove = (e) => {changeSize(e);};};
}const data = reactive({typeList: [{id: 1,name: "矩形",},{id: 2,name: "椭圆形",},],checkType: "矩形",
});function select(item) {data.checkType = item.name;
}
</script><template><div style="margin: 100px auto; display: flex; align-items: center; justify-content: center; flex-direction: column"><ul class="type"><li v-for="item in data.typeList" :key="item.id" :class="data.checkType === item.name ? 'selected' : ''"@click="select(item)">{{ item.name }}</li></ul><input id="color" style="margin-bottom: 20px" type="color"/><canvas id="canvas"></canvas></div>
</template><style>
* {padding: 0;margin: 0;box-sizing: border-box;
}.type {list-style: none;li {padding: 20px;float: left;&:hover {color: #ff0000aa;cursor: pointer;}}.selected {color: #ff0000;}
}#canvas {background-color: coral;
}
</style>
功能展示
绘制矩形和椭圆形
拖动矩形和椭圆形
放大缩小
双击切换颜色并让其在最上层
源码下载
在线绘图小工具