前言
本文介绍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>
结尾
看似简单的功能,修改起来有很多复杂的坑,虽然是一次无意义的重复造轮子工程,但是在编程中学习了很多。个人水平有限,欢迎提出问题和建议,点赞多的话会考虑出一些其他功能的开发思路。