前言
本文介绍fabricjs的部分功能(拖拽,图层,选择,缩放,旋转等)的实现思路,水平有限仅供参考。
项目git仓库 : https://github.com/pengzhijian/easy-fabricjs, 在最底部也有所有效果的完整代码,有需要的自取。
完整效果展示:
1. 拖拽
1. 单个元素拖拽
单元素拖拽只需要当位置在方块区域时,检测鼠标按下位置,按下后的偏移量,然后改变位置重绘即可。
<!DOCTYPE html> <html lang="en"> <head> <style> #canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="500" height="500"></canvas> <script> const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const square = { x: 50, y: 50, size: 50, color: "blue", isDragging: false, }; let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = square.x, lastSquareY = square.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 // 绘制方块 function drawSquare() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = square.color; ctx.fillRect(square.x, square.y, square.size, square.size); } // 判断是否鼠标在方块内 function isMouseInSquare(mouseX, mouseY) { return ( mouseX > square.x && mouseX < square.x + square.size && mouseY > square.y && mouseY < square.y + square.size ); } canvas.addEventListener("mousedown", (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (isMouseInSquare(mouseX, mouseY)) { lastSquareX = square.x; lastSquareY = square.y; canvas.style.cursor = "grabbing"; square.isDragging = true; } }); canvas.addEventListener("mousemove", (e) => { if (square.isDragging) { canvas.style.cursor = "grabbing"; const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = e.clientX - startX; offsetY = e.clientY - startY; square.x = lastSquareX + offsetX; square.y = lastSquareY + offsetY; drawSquare(); } }); canvas.addEventListener("mouseup", () => { square.isDragging = false; lastSquareX = square.x; lastSquareY = square.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; }); canvas.addEventListener("mouseleave", () => { square.isDragging = false; lastSquareX = square.x; lastSquareY = square.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; }); drawSquare(); </script> </body> </html>
复制
实现效果:
2. 多元素拖拽
多元素拖拽需要将上面的拖拽元素抽象出来成为一个可复用的方法,此处采用Class实现。
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 判断是否鼠标在方块内 function isMouseInSquare(mouseX, mouseY, squareSettings) { return ( mouseX > squareSettings.x && mouseX < squareSettings.x + squareSettings.width && mouseY > squareSettings.y && mouseY < squareSettings.y + squareSettings.height ); } class mySquare { isDragging = false; // 方块是否被拖拽 constructor(canvas, squareSettings) { this.x = squareSettings.x; // 方块的x坐标 this.y = squareSettings.y; // 方块的y坐标 this.width = squareSettings.width; // 方块的宽度 this.height = squareSettings.height; // 方块的高度 this.color = squareSettings.color; // 方块的颜色 this.canvas = canvas; // 画布 this.squareHandler(canvas); // 注册方块的鼠标点击移动等事件 this.drawSquare(canvas); // 绘制方块 } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); } // 方块的鼠标点击移动等事件 squareHandler(canvas) { let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 // 方法单列出来,方便后续注销事件 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (isMouseInSquare(mouseX, mouseY, { x: this.x, y: this.y, width: this.width, height: this.height })) { lastSquareX = this.x; lastSquareY = this.y; canvas.style.cursor = "grabbing"; this.isDragging = true; } } const mousemoveHandler = (e) => { if (this.isDragging) { canvas.style.cursor = "grabbing"; const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = e.clientX - startX; offsetY = e.clientY - startY; this.x = lastSquareX + offsetX; this.y = lastSquareY + offsetY; this.drawSquare(canvas); } } const mouseupHandler = (e) => { this.isDragging = false; lastSquareX = this.x; lastSquareY = this.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; } // 注册鼠标事件 canvas.addEventListener("mousedown", mousedownHandler); canvas.addEventListener("mousemove", mousemoveHandler); canvas.addEventListener("mouseup", mouseupHandler); canvas.addEventListener("mouseleave", mouseupHandler); } } const square1 = new mySquare(canvas, { x: 100, y: 100, width: 100, height: 100, color: "red" }); const square2 = new mySquare(canvas, { x: 0, y: 0, width: 50, height: 50, color: "yellow" });
复制
封装后运行代码,发现只画了最后一次注册的方块:
由于这两个 mySquare 对象共用了一个 ctx 对象,所以每次清除重绘都会将别的 mySquare 对象绘制的内容清空,为了让每个 mySquare 对象互不干扰,要使用图层将他们区分开来后再由 ctx 统一处理。
3. 图层
1. 添加图层
所有mySquare对象都是同一个 canvas 以及 ctx 对象,此处只需要在 ctx 上新增属性即可管理所有的mySquare对象。
此处新增了4个属性:
- level: 代表图层的最大值为多少。
- nowLevel: 当前绘制的图层是第几个。
- drawItemList: 存储每一个新增的mySquare对象。
- draw(): ctx 的统一绘画方法。
类中新增 ctxDraw 方法:
// 将图层挂载在ctx上再统一绘制 ctxDraw(canvas) { const ctx = canvas.getContext("2d"); // 将图层挂载在ctx上 if (!ctx.level) { ctx.level = 0; } ctx.level++; // 图层数加1 this.level = ctx.level; // 记录此对象的图层数 // 存储每一个新增的mySquare对象 if (!ctx.drawItemList) { ctx.drawItemList = []; } ctx.drawItemList.push(this); // 添加ctx的统一绘画方法 if (!ctx.draw) { ctx.draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i <= ctx.level; i++) { ctx.nowLevel = i; ctx.drawItemList.forEach((item) => { item.drawSquare(canvas); }); } }; } }
复制
改造drawSquare方法:
// 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.save(); ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.restore(); } }
复制
完整代码:
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 判断是否鼠标在方块内 function isMouseInSquare(mouseX, mouseY, squareSettings) { return ( mouseX > squareSettings.x && mouseX < squareSettings.x + squareSettings.width && mouseY > squareSettings.y && mouseY < squareSettings.y + squareSettings.height ); } class mySquare { isDragging = false; // 方块是否被拖拽 constructor(canvas, squareSettings) { this.x = squareSettings.x; // 方块的x坐标 this.y = squareSettings.y; // 方块的y坐标 this.width = squareSettings.width; // 方块的宽度 this.height = squareSettings.height; // 方块的高度 this.color = squareSettings.color; // 方块的颜色 this.canvas = canvas; // 画布 this.squareHandler(canvas); // 注册方块的鼠标点击移动等事件 this.ctxDraw(canvas); // 注册ctx的统一绘画方法 ctx.draw(); // 绘制所有图层 } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.save(); ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.restore(); } } // 将图层挂载在ctx上再统一绘制 ctxDraw(canvas) { const ctx = canvas.getContext("2d"); // 将图层挂载在ctx上 if (!ctx.level) { ctx.level = 0; } ctx.level++; // 图层数加1 this.level = ctx.level; // 记录此对象的图层数 // 存储每一个新增的mySquare对象 if (!ctx.drawItemList) { ctx.drawItemList = []; } ctx.drawItemList.push(this); // 添加ctx的统一绘画方法 if (!ctx.draw) { ctx.draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i <= ctx.level; i++) { ctx.nowLevel = i; ctx.drawItemList.forEach((item) => { item.drawSquare(canvas); }); } }; } } // 方块的鼠标点击移动等事件 squareHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 // 方法单列出来,方便后续注销事件 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (isMouseInSquare(mouseX, mouseY, { x: this.x, y: this.y, width: this.width, height: this.height })) { lastSquareX = this.x; lastSquareY = this.y; canvas.style.cursor = "grabbing"; this.isDragging = true; } } const mousemoveHandler = (e) => { if (this.isDragging) { canvas.style.cursor = "grabbing"; const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = e.clientX - startX; offsetY = e.clientY - startY; this.x = lastSquareX + offsetX; this.y = lastSquareY + offsetY; ctx.draw(); } } const mouseupHandler = (e) => { this.isDragging = false; lastSquareX = this.x; lastSquareY = this.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; } // 注册鼠标事件 canvas.addEventListener("mousedown", mousedownHandler); canvas.addEventListener("mousemove", mousemoveHandler); canvas.addEventListener("mouseup", mouseupHandler); canvas.addEventListener("mouseleave", mouseupHandler); } } const square1 = new mySquare(canvas, { x: 100, y: 100, width: 100, height: 100, color: "red" }); const square2 = new mySquare(canvas, { x: 0, y: 0, width: 50, height: 50, color: "yellow" });
复制
实现效果:
可以在控制台查看移动后的对象属性:
2. 图层优化
此时我们所有的对象图层是固定的,从上图可以发现,黄色方块永远在红色方块上面,显然这是不合理的,此处添加图层优化,让每次点击的对象处于图层的最上方,且每次点击只会选择最上方图形。
在mousedownHandler中新增代码:
// 当点击的对象不为最高层级时,将其挂载在最高层级 if (ctx.level !== this.level) { ctx.drawItemList.forEach((item) => { item.isDragging = false; item.isSelected = false; // 将之前比当前对象图层数高的对象减1 if (item.level > this.level) { item.level--; } }); this.isDragging = true this.isSelected = true; this.level = ctx.level; // 提到最高层级 // 重新注册鼠标事件,这个代表着执行顺序,最新注册的最后执行 canvas.removeEventListener('mousedown', this.mousedownHandler) canvas.removeEventListener('mousemove', this.mousemoveHandler) canvas.addEventListener('mousedown', this.mousedownHandler) canvas.addEventListener('mousemove', this.mousemoveHandler) }
复制
3. 选中功能
此时方块可以被选择拖中,但是并没有显示具体被选择方块的特征,故加一个边框,代表方块被选中。
类中新增边框的方法:
// 画选中后的框框 drawBorderSquare(canvas, setting) { const ctx = canvas.getContext("2d"); ctx.save(); ctx.strokeStyle = '#51B9F9'; ctx.lineWidth = setting.borderLineWidth; // 边框宽度 ctx.strokeRect(this.x - Math.floor(setting.borderLineWidth / 2), this.y - Math.floor(setting.borderLineWidth / 2), this.width + setting.borderLineWidth, this.height + setting.borderLineWidth) // 恢复到之前的状态 ctx.restore(); // 画边框的5个圆 this.drawCircle(ctx, this.x, this.y); this.drawCircle(ctx, this.x + this.width, this.y); this.drawCircle(ctx, this.x, this.y + this.height); this.drawCircle(ctx, this.x + this.width, this.y + this.height); this.drawCircle(ctx, this.x + this.width / 2, this.y + this.height + 15); } // 画选中的5个圆 drawCircle(ctx, x, y) { ctx.save(); ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; // 阴影颜色 ctx.shadowBlur = 5; // 阴影模糊级别 ctx.shadowOffsetX = 0; // 阴影的水平偏移 ctx.shadowOffsetY = 0; // 阴影的垂直偏移 ctx.beginPath(); ctx.fillStyle = 'white'; ctx.arc(x, y, 6, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); }
复制
drawSquare方法中新增边框绘画代码:
// 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.save(); ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.restore(); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: 2 }); } } }
复制
鼠标事件修改新增的被选择判断属性isSelected:
展示效果:
4. 拉伸功能
1. 边框拉伸
边框拉伸需先检测鼠标点击是否在边框范围中,然后再进行缩放即可,缩放方法详细可见我上篇文章[canvas实现中心旋转,各个顶点缩放](https://juejin.cn/post/7382592324079403027)
新增缩放工具方法:
/** * 缩放功能 * callback 绘制旋转矩形的函数 * mode 从什么地方缩放 * setting.rectX 矩形 x 坐标 * setting.rectY 矩形 y 坐标 * setting.width 矩形宽度 * setting.height 矩形高度 * setting.scaleX 缩放比例 x * setting.scaleY 缩放比例 y */ function scaleRect(ctx, setting, callback) { const { rectX, rectY, width, height, scaleX, scaleY, scaleMode } = setting; ctx.save(); ctx.translate(rectX, rectY); // 平移到 (0, 0) ctx.scale(scaleX, scaleY); let translateX = 0; let translateY = 0; if (scaleMode === 'left-top') { // 左上角点固定的缩放 } else if (scaleMode == 'right-top') { // 右上角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 } else if (scaleMode == 'left-bottom') { // 左下角点固定的缩放 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } else if (scaleMode == 'right-bottom') { // 右下角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } ctx.translate(translateX, translateY); ctx.translate(-rectX, -rectY); // 平移回到原点 if (callback) { callback(); } ctx.restore(); // 恢复原始状态 return { rectX: rectX + translateX * scaleX, rectY: rectY + translateY * scaleY, width: width * scaleX, height: height * scaleY, } }
复制
边框拉伸:
实现思路:用上面的缩放工具方法得到缩放后新的x,y,width,height值,然后赋值给方块。主要代码如下:
类中新增 isMouseInBorder 方法,判断鼠标是否在各个边框中:
// 判断鼠标是否在边框内 isMouseInBorder(mouseX, mouseY, position) { if (position === 'left') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10 }) } else if (position === 'right') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10 }) } else if (position === 'top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y - 5, width: this.width - 10, height: this.borderLineWidth + 5 }) } else if (position === 'bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y + this.height - this.borderLineWidth, width: this.width - 10, height: this.borderLineWidth + 5 }) } }
复制
类中新增 scaleMouseHandler 方法处理拉伸缩放效果:
注意:此处我判断了最小缩放大小为20,如不设置可以为负数翻转,但是需要重新修改鼠标判定逻辑。
// 边框和四角拉伸功能 scaleMouseHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 let startWidth, startHeight; // 记录鼠标按下时方块的宽度和高度 let startRectX, startRectY; // 记录鼠标按下时方块的坐标 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = 0; offsetY = 0; startWidth = this.width; startHeight = this.height; startRectX = this.x; startRectY = this.y; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left')) { // 检测是否在左边框内 this.isScaled = 'left'; canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right')) { // 检测是否在右边框内 this.isScaled = 'right'; canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'top')) { // 检测是否在上边框内 this.isScaled = 'top'; canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom')) { // 检测是否在上边框内 this.isScaled = 'bottom'; canvas.style.cursor = "ns-resize"; } } } const mousemoveHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left')) { // 检测是否在左边框内 canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right')) { // 检测是否在右边框内 canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'top')) { // 检测是否在上边框内 canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom')) { // 检测是否在上边框内 canvas.style.cursor = "ns-resize"; } else if (!this.isDragging && !this.isScaled ) { // 不在边框内时,且不在拉伸和拖拽状态时,回复鼠标指针 canvas.style.cursor = "default"; } if (this.isScaled) { // 边框拉伸功能 offsetX = e.clientX - startX; offsetY = e.clientY - startY; if (this.isScaled === 'left') { // 左边框拉伸 const tmpInfo = scaleRect(ctx, { rectX: startRectX, rectY: startRectY, width: startWidth, height: startHeight, scaleX: (startWidth - offsetX) / startWidth, scaleY: 1, scaleMode: 'right-top' }) if (tmpInfo.width >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; ctx.draw(); } } else if (this.isScaled === 'right') { // 右边框拉伸 const tmpInfo = scaleRect(ctx, { rectX: startRectX, rectY: startRectY, width: startWidth, height: startHeight, scaleX: (startWidth + offsetX) / startWidth, scaleY: 1, scaleMode: 'left-top' }) if (tmpInfo.width >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; ctx.draw(); } } else if (this.isScaled === 'top') { // 上边框拉伸 const tmpInfo = scaleRect(ctx, { rectX: startRectX, rectY: startRectY, width: startWidth, height: startHeight, scaleX: 1, scaleY: (startHeight - offsetY) / startHeight, scaleMode: 'left-bottom' }) if (tmpInfo.height >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; ctx.draw(); } } else if (this.isScaled === 'bottom') { // 右边框拉伸 const tmpInfo = scaleRect(ctx, { rectX: startRectX, rectY: startRectY, width: startWidth, height: startHeight, scaleX: 1, scaleY: (startHeight + offsetY) / startHeight, scaleMode: 'left-top' }) if (tmpInfo.height >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; ctx.draw(); } } } } } const mouseupHandler = (e) => { this.isScaled = false; } return { scaleMouseDown: mousedownHandler, scaleMouseMove: mousemoveHandler, scaleMouseUp: mouseupHandler } }
复制
此时效果:
四个角拖拽的逻辑是一样的,处理完毕优化后的程序完整代码如下:
<!DOCTYPE html> <html lang="en"> <head> <style> #canvas { border: 1px solid black; } </style> </head> <body> <canvas id="canvas" width="500" height="500"></canvas> <script> const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 判断是否鼠标在方块内 const isMouseInSquare = (mouseX, mouseY, squareSettings, Rect) => { return ( mouseX > squareSettings.x && mouseX < squareSettings.x + squareSettings.width && mouseY > squareSettings.y && mouseY < squareSettings.y + squareSettings.height ); } /** * 缩放功能 * callback 绘制旋转矩形的函数 * mode 从什么地方缩放 * setting.rectX 矩形 x 坐标 * setting.rectY 矩形 y 坐标 * setting.width 矩形宽度 * setting.height 矩形高度 * setting.scaleX 缩放比例 x * setting.scaleY 缩放比例 y */ function scaleRect(ctx, setting, callback) { const { rectX, rectY, width, height, scaleX, scaleY, scaleMode } = setting; ctx.save(); ctx.translate(rectX, rectY); // 平移到 (0, 0) ctx.scale(scaleX, scaleY); let translateX = 0; let translateY = 0; if (scaleMode === 'left-top') { // 左上角点固定的缩放 } else if (scaleMode == 'right-top') { // 右上角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 } else if (scaleMode == 'left-bottom') { // 左下角点固定的缩放 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } else if (scaleMode == 'right-bottom') { // 右下角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } ctx.translate(translateX, translateY); ctx.translate(-rectX, -rectY); // 平移回到原点 if (callback) { callback(); } ctx.restore(); // 恢复原始状态 return { rectX: rectX + translateX * scaleX, rectY: rectY + translateY * scaleY, width: width * scaleX, height: height * scaleY, } } /** * 判断是否鼠标是拉伸模式 */ function isScaleing(canvas) { if (canvas.style.cursor === 'n-resize' || canvas.style.cursor === 'e-resize' || canvas.style.cursor === 's-resize' || canvas.style.cursor === 'w-resize' || canvas.style.cursor === 'ew-resize' || canvas.style.cursor === 'ns-resize' || canvas.style.cursor === 'nesw-resize' || canvas.style.cursor === 'nwse-resize' || canvas.style.cursor === 'col-resize' || canvas.style.cursor === 'all-scroll') { return true; } else { return false; } } class mySquare { isDragging = false; // 方块是否被拖拽 isSelected = false; // 方块是否被选中 isScaled = false; // 方块是否正在被拉伸 borderLineWidth = 2; angle = 0; constructor(canvas, squareSettings) { this.x = squareSettings.x; // 方块的x坐标 this.y = squareSettings.y; // 方块的y坐标 this.width = squareSettings.width; // 方块的宽度 this.height = squareSettings.height; // 方块的高度 this.color = squareSettings.color; // 方块的颜色 this.canvas = canvas; // 画布 this.squareHandler(canvas); // 注册方块的鼠标点击移动等事件 this.ctxDraw(canvas); // 注册ctx的统一绘画方法 ctx.draw(); // 绘制所有图层 } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.save(); ctx.fillStyle = this.color; ctx.fillRect(this.x, this.y, this.width, this.height); ctx.restore(); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } // 将图层挂载在ctx上再统一绘制 ctxDraw(canvas) { const ctx = canvas.getContext("2d"); // 将图层挂载在ctx上 if (!ctx.level) { ctx.level = 0; } ctx.level++; // 图层数加1 this.level = ctx.level; // 记录此对象的图层数 // 存储每一个新增的mySquare对象 if (!ctx.drawItemList) { ctx.drawItemList = []; } ctx.drawItemList.push(this); // 添加ctx的统一绘画方法 if (!ctx.draw) { ctx.draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i <= ctx.level; i++) { ctx.nowLevel = i; ctx.drawItemList.forEach((item) => { item.drawSquare(canvas); }); } }; } } // 方块的鼠标点击移动等事件 squareHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 const { dragMove, dragDown, dragUp } = this.dragMouseHandler(canvas); const { scaleMouseDown, scaleMouseMove, scaleMouseUp } = this.scaleMouseHandler(canvas); // 方法单列出来,方便后续注销事件 this.mousedownHandler = (e) => { dragDown(e); scaleMouseDown(e); } this.mousemoveHandler = (e) => { dragMove(e); scaleMouseMove(e); } this.mouseupHandler = (e) => { dragUp(e); scaleMouseUp(e); } // 注册鼠标事件 canvas.addEventListener("mousedown", this.mousedownHandler); canvas.addEventListener("mousemove", this.mousemoveHandler); canvas.addEventListener("mouseup", this.mouseupHandler); canvas.addEventListener("mouseleave", this.mouseupHandler); } // 边框和四角拉伸功能 旋转功能 scaleMouseHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 let startWidth, startHeight; // 记录鼠标按下时方块的宽度和高度 let startRectX, startRectY; // 记录鼠标按下时方块的坐标 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = 0; offsetY = 0; startWidth = this.width; startHeight = this.height; startRectX = this.x; startRectY = this.y; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left')) { // 检测是否在左边框内 this.isScaled = 'left'; canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right')) { // 检测是否在右边框内 this.isScaled = 'right'; canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'top')) { // 检测是否在上边框内 this.isScaled = 'top'; canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom')) { // 检测是否在上边框内 this.isScaled = 'bottom'; canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'left-top')) { // 检测是否在左上角 this.isScaled = 'left-top'; canvas.style.cursor = "nwse-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'left-bottom')) { // 检测是否在左下角 this.isScaled = 'left-bottom'; canvas.style.cursor = "nesw-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right-top')) { // 检测是否在右上角 this.isScaled = 'right-top'; canvas.style.cursor = "nesw-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right-bottom')) { // 检测是否在右下角 this.isScaled = 'right-bottom'; canvas.style.cursor = "nwse-resize"; } } } const mousemoveHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left') && !this.isScaled) { // 检测是否在左边框内 canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right') && !this.isScaled) { // 检测是否在右边框内 canvas.style.cursor = "ew-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'top') && !this.isScaled) { // 检测是否在上边框内 canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom') && !this.isScaled) { // 检测是否在下边框内 canvas.style.cursor = "ns-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'left-top') && !this.isScaled) { // 检测是否在左上角 canvas.style.cursor = "nwse-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'left-bottom') && !this.isScaled) { // 检测是否在左下角 canvas.style.cursor = "nesw-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right-top') && !this.isScaled) { // 检测是否在右上角 canvas.style.cursor = "nesw-resize"; } else if (this.isMouseInBorder(mouseX, mouseY, 'right-bottom') && !this.isScaled) { // 检测是否在右下角 canvas.style.cursor = "nwse-resize"; } else if (!this.isDragging && !this.isScaled ) { // 不在边框内时,且不在拉伸和拖拽状态时,回复鼠标指针 canvas.style.cursor = "default"; } if (this.isScaled) { // 边框拉伸功能 offsetX = e.clientX - startX; offsetY = e.clientY - startY; let tempOffset = Math.abs(offsetX) > Math.abs(offsetY) ? offsetX : offsetY; const helpObj = { startRectX: startRectX, startRectY: startRectY, startWidth: startWidth, startHeight: startHeight, startX: startX, startY: startY, scaleX: 1, scaleY: 1, scaleMode: 'left-top', limitName: 'width' } if (this.isScaled === 'left') { // 左边框拉伸 helpObj.scaleMode = 'right-top' helpObj.scaleX = (startWidth - offsetX) / startWidth this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'right') { // 右边框拉伸 helpObj.scaleMode = 'left-top' helpObj.scaleX = (startWidth + offsetX) / startWidth this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'top') { // 上边框拉伸 helpObj.limitName = 'height' helpObj.scaleMode = 'left-bottom' helpObj.scaleY = (startHeight - offsetY) / startHeight this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'bottom') { // 下边框拉伸 helpObj.limitName = 'height' helpObj.scaleMode = 'left-top' helpObj.scaleY = (startHeight + offsetY) / startHeight this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'left-top') { const tempScale = Math.abs(offsetX) > Math.abs(offsetY) ? (this.height - offsetY) / this.height : (this.width - offsetX) / this.width; // 左上角拉伸 helpObj.scaleMode = 'right-bottom' helpObj.scaleX = (startWidth - offsetX) / startWidth helpObj.scaleY = (startHeight - offsetY) / startHeight this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'left-bottom') { const tempScale = Math.abs(offsetX) > Math.abs(offsetY) ? (this.height - offsetY) / this.height : (this.width - offsetX) / this.width; // 左下角拉伸 helpObj.limitName = 'height' helpObj.scaleMode = 'right-top' helpObj.scaleX = (startWidth - offsetX) / startWidth helpObj.scaleY = (startHeight + offsetY) / startHeight this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'right-top') { const tempScale = Math.abs(offsetX) > Math.abs(offsetY) ? (this.height - offsetY) / this.height : (this.width - offsetX) / this.width; // 右上角拉伸 helpObj.limitName = 'height' helpObj.scaleMode = 'left-bottom' helpObj.scaleX = (startWidth + offsetX) / startWidth helpObj.scaleY = (startHeight - offsetY) / startHeight this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'right-bottom') { // 右下角拉伸 helpObj.limitName = 'height' helpObj.scaleMode = 'left-top' helpObj.scaleX = (startWidth + offsetX) / startWidth helpObj.scaleY = (startHeight + offsetY) / startHeight this.scaleHelpFunc(helpObj) } } } } const mouseupHandler = (e) => { this.isScaled = false; } return { scaleMouseDown: mousedownHandler, scaleMouseMove: mousemoveHandler, scaleMouseUp: mouseupHandler } } // 缩放的重复代码太多,抽离出来 scaleHelpFunc(obj) { const tmpInfo = scaleRect(ctx, { rectX: obj.startRectX, rectY: obj.startRectY, width: obj.startWidth, height: obj.startHeight, scaleX: obj.scaleX, scaleY: obj.scaleY, scaleMode: obj.scaleMode }) if (tmpInfo[obj.limitName] >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; ctx.draw(); } } // 将拖拽方法抽离出来 dragMouseHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 // 方法单列出来,方便后续注销事件 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (isMouseInSquare(mouseX, mouseY, { x: this.x, y: this.y, width: this.width, height: this.height, }, this) && !isScaleing(canvas)) { lastSquareX = this.x; lastSquareY = this.y; canvas.style.cursor = "grabbing"; this.isDragging = true; this.isSelected = true; // 当点击的对象不为最高层级时,将其挂载在最高层级 if (ctx.level !== this.level) { ctx.drawItemList.forEach((item) => { item.isDragging = false; item.isSelected = false; // 将之前比当前对象图层数高的对象减1 if (item.level > this.level) { item.level--; } }); this.isDragging = true this.isSelected = true; this.level = ctx.level; // 提到最高层级 // 重新注册鼠标事件,这个代表着执行顺序,最新注册的最后执行 canvas.removeEventListener('mousedown', this.mousedownHandler) canvas.removeEventListener('mousemove', this.mousemoveHandler) canvas.addEventListener('mousedown', this.mousedownHandler) canvas.addEventListener('mousemove', this.mousemoveHandler) } } else { // 添加一些宽限方便边界拉伸 if (!isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y - this.borderLineWidth - 5, width: this.width + this.borderLineWidth * 2 + 10, height: this.height + this.borderLineWidth * 2 + 10, }, this) && !isScaleing(canvas)) { // console.log('不在方块内') this.isSelected = false; ctx.draw(); } } } const mousemoveHandler = (e) => { if (this.isDragging) { canvas.style.cursor = "grabbing"; const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = e.clientX - startX; offsetY = e.clientY - startY; this.x = lastSquareX + offsetX; this.y = lastSquareY + offsetY; ctx.draw(); } } const mouseupHandler = (e) => { this.isDragging = false; lastSquareX = this.x; lastSquareY = this.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; } return { dragMove: mousemoveHandler, dragDown: mousedownHandler, dragUp: mouseupHandler } } // 画选中后的框框 drawBorderSquare(canvas, setting) { const ctx = canvas.getContext("2d"); ctx.save(); ctx.strokeStyle = '#51B9F9'; ctx.lineWidth = setting.borderLineWidth; // 边框宽度 ctx.strokeRect(this.x - Math.floor(setting.borderLineWidth / 2), this.y - Math.floor(setting.borderLineWidth / 2), this.width + setting.borderLineWidth, this.height + setting.borderLineWidth) // 恢复到之前的状态 ctx.restore(); // 画边框的5个圆 this.drawCircle(ctx, this.x, this.y); this.drawCircle(ctx, this.x + this.width, this.y); this.drawCircle(ctx, this.x, this.y + this.height); this.drawCircle(ctx, this.x + this.width, this.y + this.height); this.drawCircle(ctx, this.x + this.width / 2, this.y + this.height + 15); } // 画选中的5个圆 drawCircle(ctx, x, y) { ctx.save(); ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; // 阴影颜色 ctx.shadowBlur = 5; // 阴影模糊级别 ctx.shadowOffsetX = 0; // 阴影的水平偏移 ctx.shadowOffsetY = 0; // 阴影的垂直偏移 ctx.beginPath(); ctx.fillStyle = 'white'; ctx.arc(x, y, 6, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); } // 判断鼠标是否在边框内 isMouseInBorder(mouseX, mouseY, position) { if (position === 'left') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10 }, this) } else if (position === 'right') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10, }, this) } else if (position === 'top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y - 5, width: this.width - 10, height: this.borderLineWidth + 5 }, this) } else if (position === 'bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y + this.height - this.borderLineWidth, width: this.width - 10, height: this.borderLineWidth + 5, }, this) } else if (position === 'left-top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y - this.borderLineWidth - 5, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'right-top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y - 5, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'left-bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y + this.height - this.borderLineWidth, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'right-bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y + this.height - this.borderLineWidth, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } } } const square1 = new mySquare(canvas, { x: 0, y: 0, width: 100, height: 100, color: "red" }); const square2 = new mySquare(canvas, { x: 0, y: 0, width: 50, height: 50, color: "yellow" }); const square3 = new mySquare(canvas, { x: 200, y: 0, width: 30, height: 50, color: "blue" }); </script> </body> </html>
复制
5. 旋转功能
思路很简单,检测按下后鼠标位置相对于图形中心点的角度,然后旋转即可,旋转方法同样用上篇文章中的旋转方法:
新增旋转方法:
/** * 中心点旋转 * @param {CanvasRenderingContext2D} ctx canvas 2D 上下文 * @param {Function} callback 绘制旋转矩形的回调函数 * @param {Object} setting 旋转设置 * @param {Number} setting.angle 旋转角度,弧度制 */ function rotateCenterPoint(ctx, setting, callback) { const { rectX, rectY, width, height, angle } = setting; ctx.save(); ctx.translate(rectX + width / 2, rectY + height / 2); // 平移到 (100, 100) ctx.rotate(setting.angle); // 旋转 90 度 ctx.translate(-(rectX + width / 2), -(rectY + height / 2)); // 平移回到原点 if (callback) { callback(); // 绘制旋转矩形 } ctx.restore(); // 恢复原始状态 }
复制
新增angle属性记录旋转角度:
else if (this.isScaled === 'center-bottom') { // 中心点的坐标离鼠标的距离 const tempX = mouseX - (this.x + this.width / 2); const tempY = mouseY - (this.y + this.height / 2); // 旋转的角度 this.angle = Math.atan2(tempY, tempX) * 180 / Math.PI - 90; if (this.angle < 0) { this.angle = 360 + this.angle; } console.log('angleeeeeeee', this.angle) ctx.draw(); }
复制
改造 ctx 的 draw 方法:
// 添加ctx的统一绘画方法 if (!ctx.draw) { ctx.draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i <= ctx.level; i++) { ctx.nowLevel = i; ctx.drawItemList.forEach((item) => { // item.drawSquare(canvas); // 新增旋转功能 rotateCenterPoint(ctx, { rectX: item.x, rectY: item.y, width: item.width, height: item.height, angle: item.angle * Math.PI / 180, }, item.drawSquare.bind(item, canvas)); }); } }; }
复制
效果:
6. 重构鼠标点检测方法
此时虽然方块可以旋转,但是在旋转后会发现很多问题,比如点击无法选中方块,边框点击错乱,这些都是因为旋转后方块的鼠标点检测方法没变导致的:
此处我采用将鼠标坐标沿着中心点反向旋转想通角度再判断
新增鼠标坐标转换方法
/** * 将鼠标坐标点转换为旋转后的坐标点 * @param {Number} mouseX 鼠标x坐标 * @param {Number} mouseY 鼠标y坐标 * @param {Object} squareSettings 方块设置 * @param {Number} squareSettings.x 方块x坐标 * @param {Number} squareSettings.y 方块y坐标 * @param {Number} squareSettings.width 方块宽度 * @param {Number} squareSettings.height 方块高度 * @param {Number} squareSettings.angle 方块旋转角度(单位:度) */ const changeMouseCoordinate = (mouseX, mouseY, squareSettings) => { const { x, y, width, height, angle } = squareSettings; // 方块中心点的坐标 const centerX = x + width / 2; const centerY = y + height / 2; // 将角度转换为弧度 const radian = (360 - angle) * (Math.PI / 180); // 将鼠标坐标转换为以中心点为原点的坐标 const relativeX = mouseX - centerX; const relativeY = mouseY - centerY; // 计算旋转后的坐标 const newRelativeX = relativeX * Math.cos(radian) - relativeY * Math.sin(radian); const newRelativeY = relativeX * Math.sin(radian) + relativeY * Math.cos(radian); // 将旋转后的坐标转换回原始坐标系 const newMouseX = newRelativeX + centerX; const newMouseY = newRelativeY + centerY; return { newMouseX, newMouseY }; }
复制
在鼠标判断方法 isMouseInSquare 中使用新坐标:
// 判断是否鼠标在方块内 const isMouseInSquare = (mouseX, mouseY, squareSettings, Rect) => { const { newMouseX, newMouseY } = changeMouseCoordinate(mouseX, mouseY, Rect); return ( newMouseX > squareSettings.x && newMouseX < squareSettings.x + squareSettings.width && newMouseY > squareSettings.y && newMouseY < squareSettings.y + squareSettings.height ); }
复制
修改所有判断代码,新增参数:
此时已经可以正确的检测鼠标位置,鼠标按在方块外时不再能够拖动方块。
但是显而易见的是,拉伸在旋转后出问题了,原因在于拉伸过后再旋转,因为拉伸的缘故旋转中心变了,从而导致了方块的位置产生了偏移,接下来将解决这个问题。
7. 处理旋转后拉伸偏移问题
处理思路:记录拉伸前旋转后左上角的位置,再记录拉伸后旋转后的(同一点,不一定在左上角了)左上角位置,然后计算两者的差值,再用平移补偿偏移量。
注意:不同方向的拉伸记录的点是不一样的,就和拉伸方法一样,我们需要记录不动的那个点,再去计算偏移值。
- 左上角点不动:右边框、下边框、右下角拉伸
- 右上角点不动:左边框、下边框、左下角拉伸
- 左下角点不动:右边框、上边框、右上角拉伸
- 右下角点不动:左边框、上边框、左上角拉伸
此处列举未封装的下边框修改代码方便理解:
else if (this.isScaled === 'bottom') { // 下边框拉伸 const tmpInfo = scaleRect(ctx, { rectX: this.x, rectY: this.y, width: startWidth, height: startHeight, scaleX: 1, scaleY: (startHeight + offsetY) / startHeight, scaleMode: 'left-top' }) if (tmpInfo.height >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; // 新增:记录拉伸前旋转后的x坐标和y坐标 let beforeObj = changeMouseCoordinate(startRectX, startRectY, { x: startRectX, y: startRectY, width: startWidth, height: startHeight, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录拉伸后旋转后的x坐标和y坐标 let afterObj = changeMouseCoordinate(this.x, this.y, { x: this.x, y: this.y, width: this.width, height: this.height, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录偏移量 const translateX = afterObj.newMouseX - beforeObj.newMouseX; const translateY = afterObj.newMouseY - beforeObj.newMouseY; // 将偏移量加给坐标 this.x = this.x - translateX; this.y = this.y - translateY; ctx.draw(); } }
复制
封装后的 scaleHelpFunc 方法:
// 缩放的重复代码太多,抽离出来 scaleHelpFunc = (obj) => { const tmpInfo = scaleRect(ctx, { rectX: obj.startRectX, rectY: obj.startRectY, width: obj.startWidth, height: obj.startHeight, scaleX: obj.scaleX, scaleY: obj.scaleY, scaleMode: obj.scaleMode }) if (tmpInfo[obj.limitName] >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; let beforeX = obj.startRectX; let beforeY = obj.startRectY; let afterX = this.x; let afterY = this.y; if (obj.scaleMode === 'left-top') { // 左上角点不动 beforeX = obj.startRectX; beforeY = obj.startRectY; afterX = this.x; afterY = this.y; } else if (obj.scaleMode === 'right-top') { // 右上角点不动 beforeX = obj.startRectX + obj.startWidth; beforeY = obj.startRectY; afterX = this.x + this.width; afterY = this.y; } else if (obj.scaleMode === 'left-bottom') { // 左下角点不动 beforeX = obj.startRectX; beforeY = obj.startRectY + obj.startHeight; afterX = this.x; afterY = this.y + this.height; } else if (obj.scaleMode === 'right-bottom') { // 右下角点不动 beforeX = obj.startRectX + obj.startWidth; beforeY = obj.startRectY + obj.startHeight; afterX = this.x + this.width; afterY = this.y + this.height; } // 新增:记录拉伸前旋转后的x坐标和y坐标 let beforeObj = changeMouseCoordinate(beforeX, beforeY, { x: obj.startRectX, y: obj.startRectY, width: obj.startWidth, height: obj.startHeight, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录拉伸后旋转后的x坐标和y坐标 let afterObj = changeMouseCoordinate(afterX, afterY, { x: this.x, y: this.y, width: this.width, height: this.height, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录偏移量 const translateX = afterObj.newMouseX - beforeObj.newMouseX; const translateY = afterObj.newMouseY - beforeObj.newMouseY; // 将偏移量加给坐标 this.x = this.x - translateX; this.y = this.y - translateY; ctx.draw(); } }
复制
旋转后的鼠标指针全乱了,此处添加修改(偷了个懒,有兴趣的自己改):
// 旋转过后的鼠标指针全乱了,这里重新计算 changeMouseCursor(setting) { const canvas = this.canvas; const { mouseX, mouseY } = setting; const centerX = this.x + this.width / 2; const centerY = this.y + this.height / 2; if (mouseX < centerX && mouseY < centerY) { canvas.style.cursor = "nw-resize"; } else if (mouseX > centerX && mouseY < centerY) { canvas.style.cursor = "ne-resize"; } else if (mouseX < centerX && mouseY > centerY) { canvas.style.cursor = "sw-resize"; } else if (mouseX > centerX && mouseY > centerY) { canvas.style.cursor = "se-resize"; } else if (mouseX === centerX && mouseY < centerY) { canvas.style.cursor = "n-resize"; } else if (mouseX === centerX && mouseY > centerY) { canvas.style.cursor = "s-resize"; } else if (mouseX < centerX && mouseY === centerY) { canvas.style.cursor = "w-resize"; } else if (mouseX > centerX && mouseY === centerY) { canvas.style.cursor = "e-resize"; } else if (mouseX === centerX && mouseY === centerY) { canvas.style.cursor = "move"; } }
复制
效果图:
此时还有很多能优化的点,比如旋转后拖拽判断不应该是单独的xy偏移,而是旋转后的方向偏移,但是整体功能大差不差所以我就不改了,有兴趣可以自行尝试修改。
封装复用
1. 画圆
canvas 自带的 arc 方法只能画正圆,要想实现椭圆功能需要自己封装下。而且由于封装好的 mySquare 类的主要属性为 x, y , width , height , color 所以封装的椭圆方法也需要用这些参数画圆。
新增画圆方法 drawCircle:
function drawCircle(ctx, setting) { const { x, y, width, height, color = 'blue', rotation = 0, startAngle = 0, endAngle = 2 * Math.PI, anticlockwise = false } = setting; // 保存当前的绘图状态 ctx.save(); // 移动到椭圆的中心 ctx.translate(x + width / 2, y + height / 2); // 缩放绘图 ctx.scale(width / 2, height / 2); // 绘制椭圆 ctx.beginPath(); ctx.arc(0, 0, 1, startAngle, endAngle, anticlockwise); // 设置线条样式并绘制 ctx.fillStyle = color; ctx.fill(); ctx.restore(); }
复制
复用 mySquare 类,直接继承重写 drawSquare 方法, 将画方块的代码改成画圆的即可:
class myCircle extends mySquare { // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { drawCircle(ctx, { x: this.x, y: this.y, width: this.width, height: this.height, color: this.color }); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } } const circle1 = new myCircle(canvas, { x: 0, y: 0, width: 100, height: 100, color: "yellow", })
复制
效果:
2. 画自定义图片
原理和画圆一样,canvas提供了 drawImage 方法绘制图片,该方法还可以裁剪图片。
需要注意的是,由于画图片必须要用到图片,也就是说多了一个参数,所以我们还需要重写下 constructor 方法。
先修改 mySquare 父类的 constructor 方法,新增isDraw参数
constructor(canvas, squareSettings, isDraw = true) { const ctx = canvas.getContext("2d"); this.x = squareSettings.x; // 方块的x坐标 this.y = squareSettings.y; // 方块的y坐标 this.width = squareSettings.width; // 方块的宽度 this.height = squareSettings.height; // 方块的高度 this.color = squareSettings.color; // 方块的颜色 this.canvas = canvas; // 画布 this.squareHandler(canvas); // 注册方块的鼠标点击移动等事件 this.ctxDraw(canvas); // 注册ctx的统一绘画方法 // 新增参数,方便子类继承添加属性 if (isDraw) { ctx.draw() } }
复制
新增 myImage 类:
class myImage extends mySquare { // 重写constructor方法 constructor(canvas, squareSettings) { super(canvas, squareSettings, false); const ctx = canvas.getContext("2d"); this.img = squareSettings.img; ctx.draw(); // 绘制所有图层 } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } } const img = document.querySelector('#img'); const image1 = new myImage(canvas, { x: 300, y: 300, width: 100, height: 100, img: img })
复制
效果:
其他图形的原理都是一样的,此处不再一一列举,有兴趣的可以自己尝试。
项目完整html代码:
<!DOCTYPE html> <html lang="en"> <head> <style> #canvas { border: 1px solid black; } img { display: none; } </style> </head> <body> <canvas id="canvas" width="500" height="500"></canvas> <img id="img" src="https://p3-passport.byteacctimg.com/img/user-avatar/cf1360aaf487985ef6416ae977d3ccca~90x90.awebp" alt=""> <script> const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); // 判断是否鼠标在方块内 const isMouseInSquare = (mouseX, mouseY, squareSettings, Rect) => { const { newMouseX, newMouseY } = changeMouseCoordinate(mouseX, mouseY, Rect); // const newMouseX = mouseX; // const newMouseY = mouseY; return ( newMouseX > squareSettings.x && newMouseX < squareSettings.x + squareSettings.width && newMouseY > squareSettings.y && newMouseY < squareSettings.y + squareSettings.height ); } /** * 将鼠标坐标点转换为旋转后的坐标点 * @param {Number} mouseX 鼠标x坐标 * @param {Number} mouseY 鼠标y坐标 * @param {Object} squareSettings 方块设置 * @param {Number} squareSettings.x 方块x坐标 * @param {Number} squareSettings.y 方块y坐标 * @param {Number} squareSettings.width 方块宽度 * @param {Number} squareSettings.height 方块高度 * @param {Number} squareSettings.angle 方块旋转角度(单位:度) */ const changeMouseCoordinate = (mouseX, mouseY, squareSettings) => { const { x, y, width, height, angle } = squareSettings; // 方块中心点的坐标 const centerX = x + width / 2; const centerY = y + height / 2; // 将角度转换为弧度 const radian = (360 - angle) * (Math.PI / 180); // 将鼠标坐标转换为以中心点为原点的坐标 const relativeX = mouseX - centerX; const relativeY = mouseY - centerY; // 计算旋转后的坐标 const newRelativeX = relativeX * Math.cos(radian) - relativeY * Math.sin(radian); const newRelativeY = relativeX * Math.sin(radian) + relativeY * Math.cos(radian); // 将旋转后的坐标转换回原始坐标系 const newMouseX = newRelativeX + centerX; const newMouseY = newRelativeY + centerY; return { newMouseX, newMouseY }; } /** * 判断是否鼠标是拉伸模式 */ function isScaleing(canvas) { if (canvas.style.cursor === 'n-resize' || canvas.style.cursor === 'e-resize' || canvas.style.cursor === 's-resize' || canvas.style.cursor === 'w-resize' || canvas.style.cursor === 'ew-resize' || canvas.style.cursor === 'ns-resize' || canvas.style.cursor === 'nesw-resize' || canvas.style.cursor === 'nwse-resize' || canvas.style.cursor === 'col-resize' || canvas.style.cursor === 'all-scroll' || canvas.style.cursor === 'move' || canvas.style.cursor === 'nw-resize' || canvas.style.cursor === 'ne-resize' || canvas.style.cursor === 'ne-resize' || canvas.style.cursor === 'nesw-resize' || canvas.style.cursor === 'ns-resize' || canvas.style.cursor === 'nwse-resize') { return true; } else { return false; } } /** * 中心点旋转 * @param {CanvasRenderingContext2D} ctx canvas 2D 上下文 * @param {Function} callback 绘制旋转矩形的回调函数 * @param {Object} setting 旋转设置 * @param {Number} setting.angle 旋转角度,弧度制 */ function rotateCenterPoint(ctx, setting, callback) { const { rectX, rectY, width, height, angle = 0, translateX = 0, translateY = 0} = setting; ctx.save(); // ctx.translate(-translateX, -translateY); // 补偿偏移量 ctx.translate(rectX + width / 2, rectY + height / 2); // 平移到 (100, 100) ctx.rotate(setting.angle); // 旋转 90 度 ctx.translate(-(rectX + width / 2), -(rectY + height / 2)); // 平移回到原点 if (callback) { callback(); // 绘制旋转矩形 } ctx.restore(); // 恢复原始状态 } /** * 缩放功能 * callback 绘制旋转矩形的函数 * mode 从什么地方缩放 * setting.rectX 矩形 x 坐标 * setting.rectY 矩形 y 坐标 * setting.width 矩形宽度 * setting.height 矩形高度 * setting.scaleX 缩放比例 x * setting.scaleY 缩放比例 y */ function scaleRect(ctx, setting, callback) { const { rectX, rectY, width, height, scaleX, scaleY, scaleMode } = setting; ctx.save(); ctx.translate(rectX, rectY); // 平移到 (0, 0) ctx.scale(scaleX, scaleY); let translateX = 0; let translateY = 0; if (scaleMode === 'left-top') { // 左上角点固定的缩放 } else if (scaleMode == 'right-top') { // 右上角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 } else if (scaleMode == 'left-bottom') { // 左下角点固定的缩放 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } else if (scaleMode == 'right-bottom') { // 右下角点固定的缩放 translateX = (width - width * scaleX) / scaleX; // 补偿偏移量 translateY = (height - height * scaleY) / scaleY; // 补偿偏移量 } ctx.translate(translateX, translateY); ctx.translate(-rectX, -rectY); // 平移回到原点 if (callback) { callback(); } ctx.restore(); // 恢复原始状态 return { rectX: rectX + translateX * scaleX, rectY: rectY + translateY * scaleY, width: width * scaleX, height: height * scaleY, } } function drawCircle(ctx, setting) { const { x, y, width, height, color = 'blue', rotation = 0, startAngle = 0, endAngle = 2 * Math.PI, anticlockwise = false } = setting; // 保存当前的绘图状态 ctx.save(); // 移动到椭圆的中心 ctx.translate(x + width / 2, y + height / 2); // 缩放绘图 ctx.scale(width / 2, height / 2); // 绘制椭圆 ctx.beginPath(); ctx.arc(0, 0, 1, startAngle, endAngle, anticlockwise); // 设置线条样式并绘制 ctx.fillStyle = color; ctx.fill(); ctx.restore(); } class mySquare { isDragging = false; // 方块是否被拖拽 isSelected = false; // 方块是否被选中 isScaled = false; // 方块是否正在被拉伸 borderLineWidth = 2; angle = 0; // 旋转角度 translateX = 0; // 旋转后的补偿偏移量 translateY = 0; // 旋转后的补偿偏移量 constructor(canvas, squareSettings, isDraw = true) { const ctx = canvas.getContext("2d"); this.x = squareSettings.x; // 方块的x坐标 this.y = squareSettings.y; // 方块的y坐标 this.width = squareSettings.width; // 方块的宽度 this.height = squareSettings.height; // 方块的高度 this.color = squareSettings.color; // 方块的颜色 this.canvas = canvas; // 画布 this.squareHandler(canvas); // 注册方块的鼠标点击移动等事件 this.ctxDraw(canvas); // 注册ctx的统一绘画方法 // 新增参数,方便子类继承添加属性 if (isDraw) { ctx.draw() } } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.save(); ctx.fillStyle = this.color; // ctx.translate(-this.translateX, -this.translateY) ctx.fillRect(this.x, this.y, this.width, this.height); ctx.restore(); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } // 将图层挂载在ctx上再统一绘制 ctxDraw(canvas) { const ctx = canvas.getContext("2d"); // 将图层挂载在ctx上 if (!ctx.level) { ctx.level = 0; } ctx.level++; // 图层数加1 this.level = ctx.level; // 记录此对象的图层数 // 存储每一个新增的mySquare对象 if (!ctx.drawItemList) { ctx.drawItemList = []; } ctx.drawItemList.push(this); // 添加ctx的统一绘画方法 if (!ctx.draw) { ctx.draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i <= ctx.level; i++) { ctx.nowLevel = i; ctx.drawItemList.forEach((item) => { // item.drawSquare(canvas); // 新增旋转功能 rotateCenterPoint(ctx, { rectX: item.x, rectY: item.y, width: item.width, height: item.height, translateX: item.translateX, translateY: item.translateY, angle: item.angle * Math.PI / 180, }, item.drawSquare.bind(item, canvas)); }); } }; } } // 方块的鼠标点击移动等事件 squareHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 const { dragMove, dragDown, dragUp } = this.dragMouseHandler(canvas); const { scaleMouseDown, scaleMouseMove, scaleMouseUp } = this.scaleMouseHandler(canvas); // 方法单列出来,方便后续注销事件 this.mousedownHandler = (e) => { dragDown(e); scaleMouseDown(e); } this.mousemoveHandler = (e) => { dragMove(e); scaleMouseMove(e); } this.mouseupHandler = (e) => { dragUp(e); scaleMouseUp(e); } // 注册鼠标事件 canvas.addEventListener("mousedown", this.mousedownHandler); canvas.addEventListener("mousemove", this.mousemoveHandler); canvas.addEventListener("mouseup", this.mouseupHandler); canvas.addEventListener("mouseleave", this.mouseupHandler); } // 边框和四角拉伸功能 旋转功能 scaleMouseHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 let startWidth, startHeight; // 记录鼠标按下时方块的宽度和高度 let startRectX, startRectY; // 记录鼠标按下时方块的坐标 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = 0; offsetY = 0; startWidth = this.width; startHeight = this.height; startRectX = this.x; startRectY = this.y; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left')) { // 检测是否在左边框内 this.isScaled = 'left'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right')) { // 检测是否在右边框内 this.isScaled = 'right'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'top')) { // 检测是否在上边框内 this.isScaled = 'top'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom')) { // 检测是否在上边框内 this.isScaled = 'bottom'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'left-top')) { // 检测是否在左上角 this.isScaled = 'left-top'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'left-bottom')) { // 检测是否在左下角 this.isScaled = 'left-bottom'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right-top')) { // 检测是否在右上角 this.isScaled = 'right-top'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right-bottom')) { // 检测是否在右下角 this.isScaled = 'right-bottom'; this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'center-bottom')) { // 检测是否在下方 this.isScaled = 'center-bottom'; canvas.style.cursor = "all-scroll"; } } } const mousemoveHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; if (this.isSelected) { if (this.isMouseInBorder(mouseX, mouseY, 'left') && !this.isScaled) { // 检测是否在左边框内 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right') && !this.isScaled) { // 检测是否在右边框内 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'top') && !this.isScaled) { // 检测是否在上边框内 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'bottom') && !this.isScaled) { // 检测是否在下边框内 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'left-top') && !this.isScaled) { // 检测是否在左上角 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'left-bottom') && !this.isScaled) { // 检测是否在左下角 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right-top') && !this.isScaled) { // 检测是否在右上角 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'right-bottom') && !this.isScaled) { // 检测是否在右下角 this.changeMouseCursor({ mouseX, mouseY }); } else if (this.isMouseInBorder(mouseX, mouseY, 'center-bottom') && !this.isScaled) { // 检测是否在右下角 canvas.style.cursor = "all-scroll"; } else if (!this.isDragging && !this.isScaled ) { // 不在边框内时,且不在拉伸和拖拽状态时,回复鼠标指针 canvas.style.cursor = "default"; } if (this.isScaled) { // 边框拉伸功能 if (this.angle >= 90 && this.angle <= 270) { offsetY = -e.clientY + startY; offsetX = -e.clientX + startX; } else { offsetY = e.clientY - startY; offsetX = e.clientX - startX; } const helpObj = { startRectX: startRectX, startRectY: startRectY, startWidth: startWidth, startHeight: startHeight, scaleX: 1, scaleY: 1, scaleMode: 'left-top', limitName: 'width' } if (this.isScaled === 'left') { // 左边框拉伸 helpObj.scaleMode = 'right-top'; helpObj.scaleX = (startWidth - offsetX) / startWidth; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'right') { // 右边框拉伸 helpObj.scaleMode = 'left-top'; helpObj.scaleX = (startWidth + offsetX) / startWidth; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'top') { // 上边框拉伸 helpObj.limitName = 'height'; helpObj.scaleMode = 'left-bottom'; helpObj.scaleY = (startHeight - offsetY) / startHeight; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'bottom') { // 下边框拉伸 helpObj.limitName = 'height'; helpObj.scaleMode = 'left-top'; helpObj.scaleY = (startHeight + offsetY) / startHeight; this.scaleHelpFunc(helpObj) } else if (this.isScaled === 'left-top') { const tempScale = (startWidth - offsetX) / startWidth > (startHeight - offsetY) / startHeight ? (startWidth - offsetX) / startWidth : (startHeight - offsetY) / startHeight; // 左上角拉伸 helpObj.scaleMode = 'right-bottom'; helpObj.scaleX = tempScale; helpObj.scaleY = tempScale; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'left-bottom') { const tempScale = (startWidth - offsetX) / startWidth > (startHeight + offsetY) / startHeight ? (startWidth - offsetX) / startWidth : (startHeight + offsetY) / startHeight; // 左下角拉伸 helpObj.limitName = 'height'; helpObj.scaleMode = 'right-top'; helpObj.scaleX = tempScale; helpObj.scaleY = tempScale; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'right-top') { const tempScale = (startWidth + offsetX) / startWidth > (startHeight - offsetY) / startHeight ? (startWidth + offsetX) / startWidth : (startHeight - offsetY) / startHeight; // 右上角拉伸 helpObj.limitName = 'height'; helpObj.scaleMode = 'left-bottom'; helpObj.scaleX = tempScale; helpObj.scaleY = tempScale; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'right-bottom') { const tempScale = (startWidth + offsetX) / startWidth > (startHeight + offsetY) / startHeight ? (startWidth + offsetX) / startWidth : (startHeight + offsetY) / startHeight; // 右下角拉伸 helpObj.limitName = 'height'; helpObj.scaleMode = 'left-top'; helpObj.scaleX = tempScale; helpObj.scaleY = tempScale; this.scaleHelpFunc(helpObj); } else if (this.isScaled === 'center-bottom') { // 中心点的坐标离鼠标的距离 const tempX = mouseX - (this.x + this.width / 2); const tempY = mouseY - (this.y + this.height / 2); // 旋转的角度 this.angle = Math.atan2(tempY, tempX) * 180 / Math.PI - 90; if (this.angle < 0) { this.angle = 360 + this.angle; } ctx.draw(); } } } } const mouseupHandler = (e) => { this.isScaled = false; } return { scaleMouseDown: mousedownHandler, scaleMouseMove: mousemoveHandler, scaleMouseUp: mouseupHandler } } // 旋转过后的鼠标指针全乱了,这里重新计算 changeMouseCursor(setting) { const canvas = this.canvas; const { mouseX, mouseY } = setting; const centerX = this.x + this.width / 2; const centerY = this.y + this.height / 2; if (mouseX < centerX && mouseY < centerY) { canvas.style.cursor = "nw-resize"; } else if (mouseX > centerX && mouseY < centerY) { canvas.style.cursor = "ne-resize"; } else if (mouseX < centerX && mouseY > centerY) { canvas.style.cursor = "sw-resize"; } else if (mouseX > centerX && mouseY > centerY) { canvas.style.cursor = "se-resize"; } else if (mouseX === centerX && mouseY < centerY) { canvas.style.cursor = "n-resize"; } else if (mouseX === centerX && mouseY > centerY) { canvas.style.cursor = "s-resize"; } else if (mouseX < centerX && mouseY === centerY) { canvas.style.cursor = "w-resize"; } else if (mouseX > centerX && mouseY === centerY) { canvas.style.cursor = "e-resize"; } else if (mouseX === centerX && mouseY === centerY) { canvas.style.cursor = "move"; } } // 缩放的重复代码太多,抽离出来 scaleHelpFunc = (obj) => { const tmpInfo = scaleRect(ctx, { rectX: obj.startRectX, rectY: obj.startRectY, width: obj.startWidth, height: obj.startHeight, scaleX: obj.scaleX, scaleY: obj.scaleY, scaleMode: obj.scaleMode }) if (tmpInfo[obj.limitName] >= 20) { this.x = tmpInfo.rectX; this.width = tmpInfo.width; this.y = tmpInfo.rectY; this.height = tmpInfo.height; let beforeX = obj.startRectX; let beforeY = obj.startRectY; let afterX = this.x; let afterY = this.y; if (obj.scaleMode === 'left-top') { // 左上角点不动 beforeX = obj.startRectX; beforeY = obj.startRectY; afterX = this.x; afterY = this.y; } else if (obj.scaleMode === 'right-top') { // 右上角点不动 beforeX = obj.startRectX + obj.startWidth; beforeY = obj.startRectY; afterX = this.x + this.width; afterY = this.y; } else if (obj.scaleMode === 'left-bottom') { // 左下角点不动 beforeX = obj.startRectX; beforeY = obj.startRectY + obj.startHeight; afterX = this.x; afterY = this.y + this.height; } else if (obj.scaleMode === 'right-bottom') { // 右下角点不动 beforeX = obj.startRectX + obj.startWidth; beforeY = obj.startRectY + obj.startHeight; afterX = this.x + this.width; afterY = this.y + this.height; } // 新增:记录拉伸前旋转后的x坐标和y坐标 let beforeObj = changeMouseCoordinate(beforeX, beforeY, { x: obj.startRectX, y: obj.startRectY, width: obj.startWidth, height: obj.startHeight, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录拉伸后旋转后的x坐标和y坐标 let afterObj = changeMouseCoordinate(afterX, afterY, { x: this.x, y: this.y, width: this.width, height: this.height, angle: 360 - this.angle, // 注意鼠标转化方法这个是反的,所以得再反一下 }) // 新增:记录偏移量 const translateX = afterObj.newMouseX - beforeObj.newMouseX; const translateY = afterObj.newMouseY - beforeObj.newMouseY; // 将偏移量加给坐标 this.x = this.x - translateX; this.y = this.y - translateY; ctx.draw(); } } // 将拖拽方法抽离出来 dragMouseHandler(canvas) { const ctx = canvas.getContext("2d"); let startX, startY; // 记录鼠标按下时鼠标的坐标 let lastSquareX = this.x, lastSquareY = this.y; // 记录每次按下时方块的起始坐标 let offsetX = 0, offsetY = 0; // 记录每次按下后的偏移量 // 方法单列出来,方便后续注销事件 const mousedownHandler = (e) => { const mouseX = e.offsetX; const mouseY = e.offsetY; startX = e.clientX - offsetX; startY = e.clientY - offsetY; if (isMouseInSquare(mouseX, mouseY, { x: this.x, y: this.y, width: this.width, height: this.height, }, this) && !isScaleing(canvas)) { lastSquareX = this.x; lastSquareY = this.y; canvas.style.cursor = "grabbing"; this.isDragging = true; this.isSelected = true; // 当点击的对象不为最高层级时,将其挂载在最高层级 if (ctx.level !== this.level) { ctx.drawItemList.forEach((item) => { item.isDragging = false; item.isSelected = false; // 将之前比当前对象图层数高的对象减1 if (item.level > this.level) { item.level--; } }); this.isDragging = true this.isSelected = true; this.level = ctx.level; // 提到最高层级 // 重新注册鼠标事件,这个代表着执行顺序,最新注册的最后执行 canvas.removeEventListener('mousedown', this.mousedownHandler) canvas.removeEventListener('mousemove', this.mousemoveHandler) canvas.addEventListener('mousedown', this.mousedownHandler) canvas.addEventListener('mousemove', this.mousemoveHandler) } } else { // 添加一些宽限方便边界拉伸 if (!isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y - this.borderLineWidth - 5, width: this.width + this.borderLineWidth * 2 + 10, height: this.height + this.borderLineWidth * 2 + 10, }, this) && !isScaleing(canvas)) { // console.log('不在方块内') this.isSelected = false; ctx.draw(); } } } const mousemoveHandler = (e) => { if (this.isDragging) { canvas.style.cursor = "grabbing"; const mouseX = e.offsetX; const mouseY = e.offsetY; offsetX = e.clientX - startX; offsetY = e.clientY - startY; this.x = lastSquareX + offsetX; this.y = lastSquareY + offsetY; ctx.draw(); } } const mouseupHandler = (e) => { this.isDragging = false; lastSquareX = this.x; lastSquareY = this.y; offsetX = 0; offsetY = 0; canvas.style.cursor = "default"; } return { dragMove: mousemoveHandler, dragDown: mousedownHandler, dragUp: mouseupHandler } } // 画选中后的框框 drawBorderSquare(canvas, setting) { const ctx = canvas.getContext("2d"); ctx.save(); ctx.strokeStyle = '#51B9F9'; ctx.lineWidth = setting.borderLineWidth; // 边框宽度 ctx.strokeRect(this.x - Math.floor(setting.borderLineWidth / 2), this.y - Math.floor(setting.borderLineWidth / 2), this.width + setting.borderLineWidth, this.height + setting.borderLineWidth) // 恢复到之前的状态 ctx.restore(); // 画边框的5个圆 this.drawCircle(ctx, this.x, this.y); this.drawCircle(ctx, this.x + this.width, this.y); this.drawCircle(ctx, this.x, this.y + this.height); this.drawCircle(ctx, this.x + this.width, this.y + this.height); this.drawCircle(ctx, this.x + this.width / 2, this.y + this.height + 15); } // 画选中的5个圆 drawCircle(ctx, x, y, color) { ctx.save(); ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'; // 阴影颜色 ctx.shadowBlur = 5; // 阴影模糊级别 ctx.shadowOffsetX = 0; // 阴影的水平偏移 ctx.shadowOffsetY = 0; // 阴影的垂直偏移 ctx.beginPath(); ctx.fillStyle = 'white'; if (color) { ctx.fillStyle = color; } ctx.arc(x, y, 6, 0, 2 * Math.PI); ctx.fill(); ctx.restore(); } // 判断鼠标是否在边框内 isMouseInBorder(mouseX, mouseY, position) { if (position === 'left') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10 }, this) } else if (position === 'right') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y + 5, width: this.borderLineWidth + 5, height: this.height - 10, }, this) } else if (position === 'top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y - 5, width: this.width - 10, height: this.borderLineWidth + 5 }, this) } else if (position === 'bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + 5, y: this.y + this.height - this.borderLineWidth, width: this.width - 10, height: this.borderLineWidth + 5, }, this) } else if (position === 'left-top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y - this.borderLineWidth - 5, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'right-top') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y - 5, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'left-bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth - 5, y: this.y + this.height - this.borderLineWidth, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'right-bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x - this.borderLineWidth + this.width, y: this.y + this.height - this.borderLineWidth, width: this.borderLineWidth + 10, height: this.borderLineWidth + 10, }, this) } else if (position === 'center-bottom') { return isMouseInSquare(mouseX, mouseY, { x: this.x + this.width / 2 - 10, y: this.y + this.height + 2, width: 20, height: 20, }, this) } } } class myCircle extends mySquare { // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { drawCircle(ctx, { x: this.x, y: this.y, width: this.width, height: this.height, color: this.color }); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } } class myImage extends mySquare { // 重写constructor方法 constructor(canvas, squareSettings) { super(canvas, squareSettings, false); const ctx = canvas.getContext("2d"); this.img = squareSettings.img; ctx.draw(); // 绘制所有图层 } // 绘制方块 drawSquare(canvas) { const ctx = canvas.getContext("2d"); // 当在本图层绘制时才绘制 if (this.level === ctx.nowLevel) { ctx.drawImage(this.img, this.x, this.y, this.width, this.height); // 当被选中后画边框 if (this.isSelected) { this.drawBorderSquare(canvas, { borderLineWidth: this.borderLineWidth }); } } } } const img = document.querySelector('#img'); const image1 = new myImage(canvas, { x: 300, y: 300, width: 100, height: 100, img: img }) const circle1 = new myCircle(canvas, { x: 0, y: 0, width: 100, height: 100, color: "yellow", }) const square1 = new mySquare(canvas, { x: 100, y: 0, width: 100, height: 100, color: "red" }); const square2 = new mySquare(canvas, { x: 100, y: 100, width: 100, height: 100, color: "blue" }); </script> </body> </html>
复制
结尾
看似简单的功能,修改起来有很多复杂的坑,虽然是一次无意义的重复造轮子工程,但是在编程中学习了很多。个人水平有限,欢迎提出问题和建议,点赞多的话会考虑出一些其他功能的开发思路。