<template>
|
|
<canvas canvas-id="canvas-drag" disable-scroll="true" @touchstart="start" @touchmove="move" @touchend="end"
|
|
:style="'width: ' + width + 'rpx; height: ' + height + 'rpx;'"></canvas>
|
|
</template>
|
|
|
|
<script>
|
|
// components/canvas-drag/index.js
|
|
let DELETE_ICON = '../../static/components/canvas-drag/close.png'; // 删除按钮
|
|
// 删除按钮
|
|
let DRAG_ICON = '../../static/components/canvas-drag/scale.png'; // 缩放按钮
|
|
// 缩放按钮
|
|
const STROKE_COLOR = '#fff';
|
|
const ROTATE_ENABLED = false;
|
|
let isMove = false; // 标识触摸后是否有移动,用来判断是否需要增加操作历史
|
|
// 标识触摸后是否有移动,用来判断是否需要增加操作历史
|
|
const DEBUG_MODE = false; // 打开调试后会渲染操作区域边框(无背景时有效)
|
|
// 打开调试后会渲染操作区域边框(无背景时有效)
|
|
const dragGraph = function({
|
|
id,
|
|
x = 30,
|
|
y = 30,
|
|
w,
|
|
h,
|
|
type,
|
|
text,
|
|
fontSize = 20,
|
|
color = 'red',
|
|
url = null,
|
|
rotate = 0,
|
|
sourceId = null,
|
|
selected = true,
|
|
permitSelected = false,
|
|
}, canvas, factor) {
|
|
if (type === 'text') {
|
|
canvas.setFontSize(fontSize);
|
|
const textWidth = canvas.measureText(text).width;
|
|
const textHeight = fontSize + 10;
|
|
this.centerX = x + textWidth / 2;
|
|
this.centerY = y + textHeight / 2;
|
|
this.w = textWidth;
|
|
this.h = textHeight;
|
|
} else {
|
|
this.centerX = x + w / 2;
|
|
this.centerY = y + h / 2;
|
|
this.w = w;
|
|
this.h = h;
|
|
}
|
|
|
|
this.id = id
|
|
this.x = x;
|
|
this.y = y; // 4个顶点坐标
|
|
this.permitSelected = permitSelected; // 4个顶点坐标
|
|
|
|
this.square = [
|
|
[this.x, this.y],
|
|
[this.x + this.w, this.y],
|
|
[this.x + this.w, this.y + this.h],
|
|
[this.x, this.y + this.h]
|
|
];
|
|
this.fileUrl = url;
|
|
this.text = text;
|
|
this.fontSize = fontSize;
|
|
this.color = color;
|
|
this.ctx = canvas;
|
|
this.rotate = rotate;
|
|
this.type = type;
|
|
this.selected = selected;
|
|
this.factor = factor;
|
|
this.sourceId = sourceId;
|
|
this.MIN_WIDTH = 20;
|
|
this.MIN_FONTSIZE = 10;
|
|
};
|
|
dragGraph.prototype = {
|
|
/**
|
|
* 绘制元素
|
|
*/
|
|
paint() {
|
|
this.ctx.save(); // 由于measureText获取文字宽度依赖于样式,所以如果是文字元素需要先设置样式
|
|
|
|
let textWidth = 0;
|
|
let textHeight = 0;
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.setFontSize(this.fontSize);
|
|
this.ctx.setTextBaseline('middle');
|
|
this.ctx.setTextAlign('center');
|
|
this.ctx.setFillStyle(this.color);
|
|
textWidth = this.ctx.measureText(this.text).width;
|
|
textHeight = this.fontSize + 10; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
} // 旋转元素
|
|
|
|
|
|
this.ctx.translate(this.centerX, this.centerY);
|
|
this.ctx.rotate(this.rotate * Math.PI / 180);
|
|
this.ctx.translate(-this.centerX, -this.centerY); // 渲染元素
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.fillText(this.text, this.centerX, this.centerY);
|
|
} else if (this.type === 'image') {
|
|
this.ctx.drawImage(this.fileUrl, this.x, this.y, this.w, this.h);
|
|
} // 如果是选中状态,绘制选择虚线框,和缩放图标、删除图标
|
|
|
|
|
|
if (this.selected && !this.permitSelected) {
|
|
this.ctx.setLineDash([2, 5]);
|
|
this.ctx.setLineWidth(2);
|
|
this.ctx.setStrokeStyle(STROKE_COLOR);
|
|
this.ctx.lineDashOffset = 6;
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.strokeRect(this.x, this.y, textWidth, textHeight);
|
|
this.ctx.drawImage(DELETE_ICON, this.x - 15, this.y - 15, 30, 30);
|
|
this.ctx.drawImage(DRAG_ICON, this.x + textWidth - 15, this.y + textHeight - 15, 30, 30);
|
|
} else {
|
|
this.ctx.strokeRect(this.x, this.y, this.w, this.h);
|
|
// this.ctx.drawImage(DELETE_ICON, this.x - 15, this.y - 15, 30, 30);
|
|
this.ctx.drawImage(DRAG_ICON, this.x + this.w - 15, this.y + this.h - 15, 30, 30);
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* 给矩形描边
|
|
* @private
|
|
*/
|
|
_drawBorder() {
|
|
let p = this.square;
|
|
let ctx = this.ctx;
|
|
this.ctx.save();
|
|
this.ctx.beginPath();
|
|
ctx.setStrokeStyle('orange');
|
|
|
|
this._draw_line(this.ctx, p[0], p[1]);
|
|
|
|
this._draw_line(this.ctx, p[1], p[2]);
|
|
|
|
this._draw_line(this.ctx, p[2], p[3]);
|
|
|
|
this._draw_line(this.ctx, p[3], p[0]);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* 画一条线
|
|
* @param ctx
|
|
* @param a
|
|
* @param b
|
|
* @private
|
|
*/
|
|
_draw_line(ctx, a, b) {
|
|
ctx.moveTo(a[0], a[1]);
|
|
ctx.lineTo(b[0], b[1]);
|
|
ctx.stroke();
|
|
},
|
|
|
|
/**
|
|
* 判断点击的坐标落在哪个区域
|
|
* @param {*} x 点击的坐标
|
|
* @param {*} y 点击的坐标
|
|
*/
|
|
isInGraph(x, y) {
|
|
// 删除区域左上角的坐标和区域的高度宽度
|
|
const delW = 30;
|
|
const delH = 30; // 旋转后的删除区域坐标
|
|
|
|
const transformedDelCenter = this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate);
|
|
|
|
const transformDelX = transformedDelCenter[0] - delW / 2;
|
|
const transformDelY = transformedDelCenter[1] - delH / 2; // 变换区域左上角的坐标和区域的高度宽度
|
|
|
|
const scaleW = 30;
|
|
const scaleH = 30;
|
|
|
|
const transformedScaleCenter = this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this
|
|
.centerY, this.rotate); // 旋转后的变换区域坐标
|
|
|
|
|
|
const transformScaleX = transformedScaleCenter[0] - scaleW / 2;
|
|
const transformScaleY = transformedScaleCenter[1] - scaleH / 2; // 调试使用,标识可操作区域
|
|
|
|
if (DEBUG_MODE) {
|
|
// 标识删除按钮区域
|
|
this.ctx.setLineWidth(1);
|
|
this.ctx.setStrokeStyle('red');
|
|
this.ctx.strokeRect(transformDelX, transformDelY, delW, delH); // 标识旋转/缩放按钮区域
|
|
|
|
this.ctx.setLineWidth(1);
|
|
this.ctx.setStrokeStyle('black');
|
|
this.ctx.strokeRect(transformScaleX, transformScaleY, scaleW, scaleH); // 标识移动区域
|
|
|
|
this._drawBorder();
|
|
}
|
|
|
|
if (x - transformScaleX >= 0 && y - transformScaleY >= 0 && transformScaleX + scaleW - x >= 0 &&
|
|
transformScaleY + scaleH - y >= 0) {
|
|
// 缩放区域
|
|
return 'transform';
|
|
} else if (x - transformDelX >= 0 && y - transformDelY >= 0 && transformDelX + delW - x >= 0 &&
|
|
transformDelY + delH - y >= 0) {
|
|
// 删除区域
|
|
return 'del';
|
|
} else if (this.insidePolygon(this.square, [x, y])) {
|
|
return 'move';
|
|
} // 不在选择区域里面
|
|
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 判断一个点是否在多边形内部
|
|
* @param points 多边形坐标集合
|
|
* @param testPoint 测试点坐标
|
|
* 返回true为真,false为假
|
|
* */
|
|
insidePolygon(points, testPoint) {
|
|
let x = testPoint[0],
|
|
y = testPoint[1];
|
|
let inside = false;
|
|
|
|
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
let xi = points[i][0],
|
|
yi = points[i][1];
|
|
let xj = points[j][0],
|
|
yj = points[j][1];
|
|
let intersect = yi > y != yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
if (intersect) inside = !inside;
|
|
}
|
|
|
|
return inside;
|
|
},
|
|
|
|
/**
|
|
* 计算旋转后矩形四个顶点的坐标(相对于画布)
|
|
* @private
|
|
*/
|
|
_rotateSquare() {
|
|
this.square = [this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate), this
|
|
._rotatePoint(this.x + this.w, this.y, this.centerX, this.centerY, this.rotate), this._rotatePoint(
|
|
this.x + this.w, this.y + this.h, this.centerX, this.centerY, this.rotate), this._rotatePoint(
|
|
this.x, this.y + this.h, this.centerX, this.centerY, this.rotate)
|
|
];
|
|
},
|
|
|
|
/**
|
|
* 计算旋转后的新坐标(相对于画布)
|
|
* @param x
|
|
* @param y
|
|
* @param centerX
|
|
* @param centerY
|
|
* @param degrees
|
|
* @returns {*[]}
|
|
* @private
|
|
*/
|
|
_rotatePoint(x, y, centerX, centerY, degrees) {
|
|
let newX = (x - centerX) * Math.cos(degrees * Math.PI / 180) - (y - centerY) * Math.sin(degrees * Math.PI /
|
|
180) + centerX;
|
|
let newY = (x - centerX) * Math.sin(degrees * Math.PI / 180) + (y - centerY) * Math.cos(degrees * Math.PI /
|
|
180) + centerY;
|
|
return [newX, newY];
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {*} px 手指按下去的坐标
|
|
* @param {*} py 手指按下去的坐标
|
|
* @param {*} x 手指移动到的坐标
|
|
* @param {*} y 手指移动到的坐标
|
|
* @param {*} currentGraph 当前图层的信息
|
|
*/
|
|
transform(px, py, x, y, currentGraph) {
|
|
// 获取选择区域的宽度高度
|
|
if (this.type === 'text') {
|
|
this.ctx.setFontSize(this.fontSize);
|
|
const textWidth = this.ctx.measureText(this.text).width;
|
|
const textHeight = this.fontSize + 10;
|
|
this.w = textWidth;
|
|
this.h = textHeight; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
} else {
|
|
this.centerX = this.x + this.w / 2;
|
|
this.centerY = this.y + this.h / 2;
|
|
}
|
|
|
|
const diffXBefore = px - this.centerX;
|
|
const diffYBefore = py - this.centerY;
|
|
const diffXAfter = x - this.centerX;
|
|
const diffYAfter = y - this.centerY;
|
|
const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;
|
|
const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180; // 旋转的角度
|
|
|
|
if (ROTATE_ENABLED) {
|
|
this.rotate = currentGraph.rotate + angleAfter - angleBefore;
|
|
}
|
|
|
|
const lineA = Math.sqrt(Math.pow(this.centerX - px, 2) + Math.pow(this.centerY - py, 2));
|
|
const lineB = Math.sqrt(Math.pow(this.centerX - x, 2) + Math.pow(this.centerY - y, 2));
|
|
|
|
if (this.type === 'image') {
|
|
let resize_rito = lineB / lineA;
|
|
let new_w = currentGraph.w * resize_rito;
|
|
let new_h = currentGraph.h * resize_rito;
|
|
|
|
if (currentGraph.w < currentGraph.h && new_w < this.MIN_WIDTH) {
|
|
new_w = this.MIN_WIDTH;
|
|
new_h = this.MIN_WIDTH * currentGraph.h / currentGraph.w;
|
|
} else if (currentGraph.h < currentGraph.w && new_h < this.MIN_WIDTH) {
|
|
new_h = this.MIN_WIDTH;
|
|
new_w = this.MIN_WIDTH * currentGraph.w / currentGraph.h;
|
|
}
|
|
|
|
this.w = new_w;
|
|
this.h = new_h;
|
|
this.x = currentGraph.x - (new_w - currentGraph.w) / 2;
|
|
this.y = currentGraph.y - (new_h - currentGraph.h) / 2;
|
|
} else if (this.type === 'text') {
|
|
const fontSize = currentGraph.fontSize * ((lineB - lineA) / lineA + 1);
|
|
this.fontSize = fontSize <= this.MIN_FONTSIZE ? this.MIN_FONTSIZE : fontSize; // 旋转位移后重新计算坐标
|
|
|
|
this.ctx.setFontSize(this.fontSize);
|
|
const textWidth = this.ctx.measureText(this.text).width;
|
|
const textHeight = this.fontSize + 10;
|
|
this.w = textWidth;
|
|
this.h = textHeight; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
}
|
|
},
|
|
|
|
toPx(rpx) {
|
|
return rpx * this.factor;
|
|
}
|
|
|
|
};
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
bgImage: '',
|
|
history: []
|
|
};
|
|
},
|
|
|
|
components: {},
|
|
props: {
|
|
graph: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
bgColor: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
bgSourceId: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
width: {
|
|
type: Number,
|
|
default: 750
|
|
},
|
|
height: {
|
|
type: Number,
|
|
default: 750
|
|
},
|
|
enableUndo: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
watch: {
|
|
graph: {
|
|
handler: 'onGraphChange',
|
|
deep: true
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 绘制元素
|
|
*/
|
|
paint() {
|
|
this.ctx.save(); // 由于measureText获取文字宽度依赖于样式,所以如果是文字元素需要先设置样式
|
|
|
|
let textWidth = 0;
|
|
let textHeight = 0;
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.setFontSize(this.fontSize);
|
|
this.ctx.setTextBaseline('middle');
|
|
this.ctx.setTextAlign('center');
|
|
this.ctx.setFillStyle(this.color);
|
|
textWidth = this.ctx.measureText(this.text).width;
|
|
textHeight = this.fontSize + 10; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
} // 旋转元素
|
|
|
|
|
|
this.ctx.translate(this.centerX, this.centerY);
|
|
this.ctx.rotate(this.rotate * Math.PI / 180);
|
|
this.ctx.translate(-this.centerX, -this.centerY); // 渲染元素
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.fillText(this.text, this.centerX, this.centerY);
|
|
} else if (this.type === 'image') {
|
|
this.ctx.drawImage(this.fileUrl, this.x, this.y, this.w, this.h);
|
|
} // 如果是选中状态,绘制选择虚线框,和缩放图标、删除图标
|
|
|
|
|
|
if (this.selected && !this.permitSelected) {
|
|
this.ctx.setLineDash([2, 5]);
|
|
this.ctx.setLineWidth(2);
|
|
this.ctx.setStrokeStyle(STROKE_COLOR);
|
|
this.ctx.lineDashOffset = 6;
|
|
|
|
if (this.type === 'text') {
|
|
this.ctx.strokeRect(this.x, this.y, textWidth, textHeight);
|
|
this.ctx.drawImage(DELETE_ICON, this.x - 15, this.y - 15, 30, 30);
|
|
this.ctx.drawImage(DRAG_ICON, this.x + textWidth - 15, this.y + textHeight - 15, 30, 30);
|
|
} else {
|
|
this.ctx.strokeRect(this.x, this.y, this.w, this.h);
|
|
this.ctx.drawImage(DELETE_ICON, this.x - 15, this.y - 15, 30, 30);
|
|
this.ctx.drawImage(DRAG_ICON, this.x + this.w - 15, this.y + this.h - 15, 30, 30);
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* 给矩形描边
|
|
* @private
|
|
*/
|
|
_drawBorder() {
|
|
let p = this.square;
|
|
let ctx = this.ctx;
|
|
this.ctx.save();
|
|
this.ctx.beginPath();
|
|
ctx.setStrokeStyle('orange');
|
|
|
|
this._draw_line(this.ctx, p[0], p[1]);
|
|
|
|
this._draw_line(this.ctx, p[1], p[2]);
|
|
|
|
this._draw_line(this.ctx, p[2], p[3]);
|
|
|
|
this._draw_line(this.ctx, p[3], p[0]);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
/**
|
|
* 画一条线
|
|
* @param ctx
|
|
* @param a
|
|
* @param b
|
|
* @private
|
|
*/
|
|
_draw_line(ctx, a, b) {
|
|
ctx.moveTo(a[0], a[1]);
|
|
ctx.lineTo(b[0], b[1]);
|
|
ctx.stroke();
|
|
},
|
|
|
|
/**
|
|
* 判断点击的坐标落在哪个区域
|
|
* @param {*} x 点击的坐标
|
|
* @param {*} y 点击的坐标
|
|
*/
|
|
isInGraph(x, y) {
|
|
// 删除区域左上角的坐标和区域的高度宽度
|
|
const delW = 30;
|
|
const delH = 30; // 旋转后的删除区域坐标
|
|
|
|
const transformedDelCenter = this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate);
|
|
|
|
const transformDelX = transformedDelCenter[0] - delW / 2;
|
|
const transformDelY = transformedDelCenter[1] - delH / 2; // 变换区域左上角的坐标和区域的高度宽度
|
|
|
|
const scaleW = 30;
|
|
const scaleH = 30;
|
|
|
|
const transformedScaleCenter = this._rotatePoint(this.x + this.w, this.y + this.h, this.centerX, this.centerY,
|
|
this.rotate); // 旋转后的变换区域坐标
|
|
|
|
|
|
const transformScaleX = transformedScaleCenter[0] - scaleW / 2;
|
|
const transformScaleY = transformedScaleCenter[1] - scaleH / 2; // 调试使用,标识可操作区域
|
|
|
|
if (DEBUG_MODE) {
|
|
// 标识删除按钮区域
|
|
this.ctx.setLineWidth(1);
|
|
this.ctx.setStrokeStyle('red');
|
|
this.ctx.strokeRect(transformDelX, transformDelY, delW, delH); // 标识旋转/缩放按钮区域
|
|
|
|
this.ctx.setLineWidth(1);
|
|
this.ctx.setStrokeStyle('black');
|
|
this.ctx.strokeRect(transformScaleX, transformScaleY, scaleW, scaleH); // 标识移动区域
|
|
|
|
this._drawBorder();
|
|
}
|
|
|
|
if (x - transformScaleX >= 0 && y - transformScaleY >= 0 && transformScaleX + scaleW - x >= 0 &&
|
|
transformScaleY + scaleH - y >= 0) {
|
|
// 缩放区域
|
|
return 'transform';
|
|
} else if (x - transformDelX >= 0 && y - transformDelY >= 0 && transformDelX + delW - x >= 0 && transformDelY +
|
|
delH - y >= 0) {
|
|
// 删除区域
|
|
return 'del';
|
|
} else if (this.insidePolygon(this.square, [x, y])) {
|
|
return 'move';
|
|
} // 不在选择区域里面
|
|
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* 判断一个点是否在多边形内部
|
|
* @param points 多边形坐标集合
|
|
* @param testPoint 测试点坐标
|
|
* 返回true为真,false为假
|
|
* */
|
|
insidePolygon(points, testPoint) {
|
|
let x = testPoint[0],
|
|
y = testPoint[1];
|
|
let inside = false;
|
|
|
|
for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
|
|
let xi = points[i][0],
|
|
yi = points[i][1];
|
|
let xj = points[j][0],
|
|
yj = points[j][1];
|
|
let intersect = yi > y != yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi;
|
|
if (intersect) inside = !inside;
|
|
}
|
|
|
|
return inside;
|
|
},
|
|
|
|
/**
|
|
* 计算旋转后矩形四个顶点的坐标(相对于画布)
|
|
* @private
|
|
*/
|
|
_rotateSquare() {
|
|
this.square = [this._rotatePoint(this.x, this.y, this.centerX, this.centerY, this.rotate), this._rotatePoint(
|
|
this.x + this.w, this.y, this.centerX, this.centerY, this.rotate), this._rotatePoint(this.x + this
|
|
.w, this.y + this.h, this.centerX, this.centerY, this.rotate), this._rotatePoint(this.x, this.y +
|
|
this.h, this.centerX, this.centerY, this.rotate)];
|
|
},
|
|
|
|
/**
|
|
* 计算旋转后的新坐标(相对于画布)
|
|
* @param x
|
|
* @param y
|
|
* @param centerX
|
|
* @param centerY
|
|
* @param degrees
|
|
* @returns {*[]}
|
|
* @private
|
|
*/
|
|
_rotatePoint(x, y, centerX, centerY, degrees) {
|
|
let newX = (x - centerX) * Math.cos(degrees * Math.PI / 180) - (y - centerY) * Math.sin(degrees * Math.PI /
|
|
180) + centerX;
|
|
let newY = (x - centerX) * Math.sin(degrees * Math.PI / 180) + (y - centerY) * Math.cos(degrees * Math.PI /
|
|
180) + centerY;
|
|
return [newX, newY];
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {*} px 手指按下去的坐标
|
|
* @param {*} py 手指按下去的坐标
|
|
* @param {*} x 手指移动到的坐标
|
|
* @param {*} y 手指移动到的坐标
|
|
* @param {*} currentGraph 当前图层的信息
|
|
*/
|
|
transform(px, py, x, y, currentGraph) {
|
|
// 获取选择区域的宽度高度
|
|
if (this.type === 'text') {
|
|
this.ctx.setFontSize(this.fontSize);
|
|
const textWidth = this.ctx.measureText(this.text).width;
|
|
const textHeight = this.fontSize + 10;
|
|
this.w = textWidth;
|
|
this.h = textHeight; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
} else {
|
|
this.centerX = this.x + this.w / 2;
|
|
this.centerY = this.y + this.h / 2;
|
|
}
|
|
|
|
const diffXBefore = px - this.centerX;
|
|
const diffYBefore = py - this.centerY;
|
|
const diffXAfter = x - this.centerX;
|
|
const diffYAfter = y - this.centerY;
|
|
const angleBefore = Math.atan2(diffYBefore, diffXBefore) / Math.PI * 180;
|
|
const angleAfter = Math.atan2(diffYAfter, diffXAfter) / Math.PI * 180; // 旋转的角度
|
|
|
|
if (ROTATE_ENABLED) {
|
|
this.rotate = currentGraph.rotate + angleAfter - angleBefore;
|
|
}
|
|
|
|
const lineA = Math.sqrt(Math.pow(this.centerX - px, 2) + Math.pow(this.centerY - py, 2));
|
|
const lineB = Math.sqrt(Math.pow(this.centerX - x, 2) + Math.pow(this.centerY - y, 2));
|
|
|
|
if (this.type === 'image') {
|
|
let resize_rito = lineB / lineA;
|
|
let new_w = currentGraph.w * resize_rito;
|
|
let new_h = currentGraph.h * resize_rito;
|
|
|
|
if (currentGraph.w < currentGraph.h && new_w < this.MIN_WIDTH) {
|
|
new_w = this.MIN_WIDTH;
|
|
new_h = this.MIN_WIDTH * currentGraph.h / currentGraph.w;
|
|
} else if (currentGraph.h < currentGraph.w && new_h < this.MIN_WIDTH) {
|
|
new_h = this.MIN_WIDTH;
|
|
new_w = this.MIN_WIDTH * currentGraph.w / currentGraph.h;
|
|
}
|
|
|
|
this.w = new_w;
|
|
this.h = new_h;
|
|
this.x = currentGraph.x - (new_w - currentGraph.w) / 2;
|
|
this.y = currentGraph.y - (new_h - currentGraph.h) / 2;
|
|
} else if (this.type === 'text') {
|
|
const fontSize = currentGraph.fontSize * ((lineB - lineA) / lineA + 1);
|
|
this.fontSize = fontSize <= this.MIN_FONTSIZE ? this.MIN_FONTSIZE : fontSize; // 旋转位移后重新计算坐标
|
|
|
|
this.ctx.setFontSize(this.fontSize);
|
|
const textWidth = this.ctx.measureText(this.text).width;
|
|
const textHeight = this.fontSize + 10;
|
|
this.w = textWidth;
|
|
this.h = textHeight; // 字体区域中心点不变,左上角位移
|
|
|
|
this.x = this.centerX - textWidth / 2;
|
|
this.y = this.centerY - textHeight / 2;
|
|
}
|
|
},
|
|
|
|
toPx(rpx) {
|
|
return rpx * this.factor;
|
|
},
|
|
|
|
beforeMount() {
|
|
const sysInfo = wx.getSystemInfoSync();
|
|
const screenWidth = sysInfo.screenWidth;
|
|
this.factor = screenWidth / 750;
|
|
|
|
if (typeof this.drawArr === 'undefined') {
|
|
this.drawArr = [];
|
|
}
|
|
|
|
this.ctx = wx.createCanvasContext('canvas-drag', this);
|
|
this.draw();
|
|
},
|
|
|
|
created() {
|
|
uni.downloadFile({
|
|
url: DELETE_ICON, //仅为示例,并非真实的资源
|
|
success: (res) => {
|
|
if (res.statusCode === 200) {
|
|
console.log('下载成功:' + res.tempFilePath);
|
|
DELETE_ICON = res.tempFilePath;
|
|
}
|
|
}
|
|
})
|
|
uni.downloadFile({
|
|
url: DRAG_ICON, //仅为示例,并非真实的资源
|
|
success: (res) => {
|
|
if (res.statusCode === 200) {
|
|
console.log('下载成功:' + res.tempFilePath);
|
|
DRAG_ICON = res.tempFilePath;
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
methods: {
|
|
toPx(rpx) {
|
|
return rpx * this.factor;
|
|
},
|
|
|
|
initBg() {
|
|
this.bgColor = '';
|
|
this.bgSourceId = '';
|
|
this.bgImage = '';
|
|
},
|
|
|
|
initHistory() {
|
|
this.history = [];
|
|
},
|
|
|
|
recordHistory() {
|
|
if (!this.enableUndo) {
|
|
return;
|
|
}
|
|
|
|
this.exportJson().then(imgArr => {
|
|
this.history.push(JSON.stringify(imgArr));
|
|
}).catch(e => {
|
|
console.error(e);
|
|
});
|
|
},
|
|
|
|
undo() {
|
|
if (!this.enableUndo) {
|
|
console.log(`后退功能未启用,请设置enableUndo="{{true}}"`);
|
|
return;
|
|
}
|
|
|
|
if (this.history.length > 1) {
|
|
this.history.pop();
|
|
let newConfigObj = this.history[this.history.length - 1];
|
|
this.initByArr(JSON.parse(newConfigObj));
|
|
} else {
|
|
console.log('已是第一步,不能回退');
|
|
}
|
|
},
|
|
|
|
onGraphChange(n, o) {
|
|
if (JSON.stringify(n) === '{}') return;
|
|
this.drawArr.push(new dragGraph(Object.assign({
|
|
x: 30,
|
|
y: 30
|
|
}, n), this.ctx, this.factor));
|
|
|
|
this.emitDrawArrChange()
|
|
|
|
this.draw(); // 参数有变化时记录历史
|
|
|
|
this.recordHistory();
|
|
},
|
|
|
|
initByArr(newArr) {
|
|
this.drawArr = []; // 重置绘画元素
|
|
|
|
this.initBg(); // 重置绘画背景
|
|
// 循环插入 drawArr
|
|
|
|
newArr.forEach((item, index) => {
|
|
switch (item.type) {
|
|
case 'bgColor':
|
|
this.bgImage = '';
|
|
this.bgSourceId = '';
|
|
this.bgColor = item.color;
|
|
break;
|
|
|
|
case 'bgImage':
|
|
this.bgColor = '';
|
|
this.bgImage = item.url;
|
|
|
|
if (item.sourceId) {
|
|
this.bgSourceId = item.sourceId;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'image':
|
|
case 'text':
|
|
if (index === newArr.length - 1) {
|
|
item.selected = true;
|
|
} else {
|
|
item.selected = false;
|
|
}
|
|
|
|
this.drawArr.push(new dragGraph(item, this.ctx, this.factor));
|
|
break;
|
|
}
|
|
});
|
|
this.draw();
|
|
},
|
|
|
|
draw() {
|
|
if (this.bgImage !== '') {
|
|
this.ctx.drawImage(this.bgImage, 0, 0, this.toPx(this.width), this.toPx(this.height));
|
|
}
|
|
|
|
if (this.bgColor !== '') {
|
|
this.ctx.save();
|
|
this.ctx.setFillStyle(this.bgColor);
|
|
this.ctx.fillRect(0, 0, this.toPx(this.width), this.toPx(this.height));
|
|
this.ctx.restore();
|
|
}
|
|
|
|
this.drawArr.forEach(item => {
|
|
item.paint();
|
|
});
|
|
return new Promise(resolve => {
|
|
this.ctx.draw(false, () => {
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
|
|
start(e) {
|
|
isMove = false; // 重置移动标识
|
|
|
|
const {
|
|
x,
|
|
y
|
|
} = e.touches[0];
|
|
this.tempGraphArr = [];
|
|
let lastDelIndex = null; // 记录最后一个需要删除的索引
|
|
|
|
this.drawArr && this.drawArr.forEach((item, index) => {
|
|
const action = item.isInGraph(x, y);
|
|
|
|
if (action) {
|
|
item.action = action;
|
|
this.tempGraphArr.push(item); // 保存点击时的坐标
|
|
|
|
this.currentTouch = {
|
|
x,
|
|
y
|
|
};
|
|
|
|
if (action === 'del') {
|
|
// lastDelIndex = index; // 标记需要删除的元素
|
|
item.selected = true;
|
|
}
|
|
} else {
|
|
item.action = false;
|
|
item.selected = false;
|
|
}
|
|
}); // 保存点击时元素的信息
|
|
|
|
if (this.tempGraphArr.length > 0) {
|
|
for (let i = 0; i < this.tempGraphArr.length; i++) {
|
|
let lastIndex = this.tempGraphArr.length - 1; // 对最后一个元素做操作
|
|
|
|
if (i === lastIndex) {
|
|
// 未选中的元素,不执行删除和缩放操作
|
|
if (lastDelIndex !== null && this.tempGraphArr[i].selected) {
|
|
if (this.drawArr[lastDelIndex].action == 'del') {
|
|
this.drawArr.splice(lastDelIndex, 1);
|
|
this.ctx.clearRect(0, 0, this.toPx(this.width), this.toPx(this.height));
|
|
}
|
|
} else {
|
|
this.tempGraphArr[lastIndex].selected = true;
|
|
this.currentGraph = Object.assign({}, this.tempGraphArr[lastIndex]);
|
|
}
|
|
} else {
|
|
// 不是最后一个元素,不需要选中,也不记录状态
|
|
this.tempGraphArr[i].action = false;
|
|
this.tempGraphArr[i].selected = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.draw();
|
|
},
|
|
|
|
move(e) {
|
|
const {
|
|
x,
|
|
y
|
|
} = e.touches[0];
|
|
|
|
if (this.tempGraphArr && this.tempGraphArr.length > 0) {
|
|
isMove = true; // 有选中元素,并且有移动时,设置移动标识
|
|
|
|
const currentGraph = this.tempGraphArr[this.tempGraphArr.length - 1];
|
|
|
|
if (currentGraph.action === 'move') {
|
|
currentGraph.centerX = this.currentGraph.centerX + (x - this.currentTouch.x);
|
|
currentGraph.centerY = this.currentGraph.centerY + (y - this.currentTouch
|
|
.y); // 使用中心点坐标计算位移,不使用 x,y 坐标,因为会受旋转影响。
|
|
|
|
if (currentGraph.type !== 'text') {
|
|
currentGraph.x = currentGraph.centerX - this.currentGraph.w / 2;
|
|
currentGraph.y = currentGraph.centerY - this.currentGraph.h / 2;
|
|
}
|
|
} else if (currentGraph.action === 'transform') {
|
|
currentGraph.transform(this.currentTouch.x, this.currentTouch.y, x, y, this.currentGraph);
|
|
} // 更新4个坐标点(相对于画布的坐标系)
|
|
|
|
|
|
currentGraph._rotateSquare();
|
|
|
|
this.draw();
|
|
}
|
|
},
|
|
|
|
end(e) {
|
|
|
|
this.emitDrawArrChange()
|
|
|
|
this.tempGraphArr = [];
|
|
|
|
if (isMove) {
|
|
isMove = false; // 重置移动标识
|
|
// 用户操作结束时记录历史
|
|
|
|
this.recordHistory();
|
|
}
|
|
},
|
|
|
|
emitDrawArrChange(){
|
|
|
|
let map = {}
|
|
|
|
let arr = this.drawArr.map((item, index) => {
|
|
let p = {
|
|
x : item.x,
|
|
y : item.y,
|
|
w : item.w,
|
|
h : item.h,
|
|
id : item.id || index,
|
|
}
|
|
map[p.id] = p
|
|
return p
|
|
})
|
|
|
|
this.$emit('onDrawArrChange', map)
|
|
},
|
|
|
|
exportFun() {
|
|
return new Promise((resolve, reject) => {
|
|
this.drawArr = this.drawArr.map(item => {
|
|
item.selected = false;
|
|
return item;
|
|
});
|
|
this.draw().then(() => {
|
|
wx.canvasToTempFilePath({
|
|
canvasId: 'canvas-drag',
|
|
success: res => {
|
|
resolve(res.tempFilePath);
|
|
},
|
|
fail: e => {
|
|
reject(e);
|
|
}
|
|
}, this);
|
|
});
|
|
});
|
|
},
|
|
|
|
exportJson() {
|
|
return new Promise((resolve, reject) => {
|
|
let exportArr = this.drawArr.map(item => {
|
|
item.selected = false;
|
|
|
|
switch (item.type) {
|
|
case 'image':
|
|
return {
|
|
type: 'image',
|
|
url: item.fileUrl,
|
|
y: item.y,
|
|
x: item.x,
|
|
w: item.w,
|
|
h: item.h,
|
|
rotate: item.rotate,
|
|
sourceId: item.sourceId
|
|
};
|
|
break;
|
|
|
|
case 'text':
|
|
return {
|
|
type: 'text',
|
|
text: item.text,
|
|
color: item.color,
|
|
fontSize: item.fontSize,
|
|
y: item.y,
|
|
x: item.x,
|
|
w: item.w,
|
|
h: item.h,
|
|
rotate: item.rotate
|
|
};
|
|
break;
|
|
}
|
|
});
|
|
|
|
if (this.bgImage) {
|
|
let tmp_img_config = {
|
|
type: 'bgImage',
|
|
url: this.bgImage
|
|
};
|
|
|
|
if (this.bgSourceId) {
|
|
tmp_img_config['sourceId'] = this.bgSourceId;
|
|
}
|
|
|
|
exportArr.unshift(tmp_img_config);
|
|
} else if (this.bgColor) {
|
|
exportArr.unshift({
|
|
type: 'bgColor',
|
|
color: this.bgColor
|
|
});
|
|
}
|
|
|
|
resolve(exportArr);
|
|
});
|
|
},
|
|
|
|
changColor(color) {
|
|
const selected = this.drawArr.filter(item => item.selected);
|
|
|
|
if (selected.length > 0) {
|
|
selected[0].color = color;
|
|
}
|
|
|
|
this.draw(); // 改变文字颜色时记录历史
|
|
|
|
this.recordHistory();
|
|
},
|
|
|
|
changeBgColor(color) {
|
|
this.bgImage = '';
|
|
this.bgColor = color;
|
|
this.draw(); // 改变背景颜色时记录历史
|
|
|
|
this.recordHistory();
|
|
},
|
|
|
|
changeBgImage(newBgImg) {
|
|
this.bgColor = '';
|
|
|
|
if (typeof newBgImg == 'string') {
|
|
this.bgSourceId = '';
|
|
this.bgImage = newBgImg;
|
|
} else {
|
|
this.bgSourceId = newBgImg.sourceId;
|
|
this.bgImage = newBgImg.url;
|
|
}
|
|
|
|
this.draw(); // 改变背景图片时记录历史
|
|
|
|
this.recordHistory();
|
|
},
|
|
|
|
clearCanvas() {
|
|
this.ctx.clearRect(0, 0, this.toPx(this.width), this.toPx(this.height));
|
|
this.ctx.draw();
|
|
this.drawArr = [];
|
|
this.initBg(); // 重置绘画背景
|
|
|
|
this.initHistory(); // 清空历史记录
|
|
}
|
|
|
|
}
|
|
};
|
|
</script>
|
|
<style>
|
|
@import "./index.css";
|
|
</style>
|