由于产品需要, 自己写了一套画布,主要功能是为产品提供定制体验, 用户上传自己喜欢的素材,根据自己的喜好设计T恤。 当然您可以应用在箱包、礼品定制等其他商业场景里。 本组件支持在移动端,小程序,H5下运行。 使用Vue封装,本来是允许在HBuilderX下的代码, 被我摘了下来。
按照惯例,先上代码: 源码下载地址
里面用了许多Html5的绘图方法, 同时运用了大量的三角函数进行计算。不多说,先上图:
接下来,我们聊下几个具体的代码实现。
1. 一些辅助函数,不知道是否勾引起对初中数学的痛苦回忆?
- 根据直线1与直线2的坐标计算旋转角度
calculateAngle: function(p1, p2) {
let xDis
let yDis
xDis = p2.x - p1.x;
yDis = p2.y - p1.y;
let angle = Math.atan2(yDis, xDis)
return angle;
}
- 矩形旋转后的当前点所对应的世界坐标系的点
calculatePoint: function(point, angle) {
const sin = Math.sin(-angle)
const cos = Math.cos(-angle)
const newPos = {
x: point.x * cos - point.y * sin,
y: point.y * cos + point.x * sin
}
return newPos
}
- 矩形旋转后的所对应的世界坐标系的大小
calculateBox: function(size, angle) {
return {
w: Math.abs(size.h * Math.sin(angle)) + Math.abs(size.w * Math.cos(angle)),
h: Math.abs(size.w * Math.sin(angle)) + Math.abs(size.h * Math.cos(angle))
}
}
- 判断点击区域是否存在矩形内
inBox: function(point, rect) {
let x1 = 999,
x2 = -999,
y1 = 999,
y2 = -999
rect.map(item => {
if (x1 > item.x) x1 = item.x
if (x2 < item.x) x2 = item.x
if (y1 > item.y) y1 = item.y
if (y2 < item.y) y2 = item.y
})
if (
point.x >= x1 &&
point.x <= x2 &&
point.y >= y1 &&
point.y <= y2
) return true
return false
}
2. 根据光标点击的坐标判断当前需要执行的操作,如平移、旋转、缩放或删除
// selectError 为选择误差
judgeMode: function(point) {
this.mode = ''
let selected = this.select(point)
if (this.selected !== selected) {
// 选择新的对象
this.selected = selected
} else {
const item = this.layers[this.selected]
if (item) {
//原点
let x = item.pos.x
let y = item.pos.y
const newPoint = this.calculatePoint({
x: point.x - item.pos.x,
y: point.y - item.pos.y
}, item.rot)
const moveRect = [{
x: -item.size.w / 2 - selectError / 2,
y: -item.size.h / 2 - selectError / 2
},
{
x: -item.size.w / 2 + selectError / 2,
y: -item.size.h / 2 - selectError / 2
},
{
x: -item.size.w / 2 + selectError / 2,
y: -item.size.h / 2 + selectError / 2
},
{
x: -item.size.w / 2 - selectError / 2,
y: -item.size.h / 2 + selectError / 2
}
];
// 移动
if (this.inBox(newPoint, moveRect)) {
this.mode = 'move'
}
const rotateRect = [{
x: item.size.w / 2 - selectError / 2,
y: -item.size.h / 2 - selectError / 2
},
{
x: item.size.w / 2 + selectError / 2,
y: -item.size.h / 2 - selectError / 2
},
{
x: item.size.w / 2 + selectError / 2,
y: -item.size.h / 2 + selectError / 2
},
{
x: item.size.w / 2 - selectError / 2,
y: -item.size.h / 2 + selectError / 2
}
];
// 旋转
if (this.inBox(newPoint, rotateRect)) {
this.mode = 'rotate'
}
const scaleRect = [{
x: item.size.w / 2 - selectError / 2,
y: item.size.h / 2 - selectError / 2
},
{
x: item.size.w / 2 + selectError / 2,
y: item.size.h / 2 - selectError / 2
},
{
x: item.size.w / 2 + selectError / 2,
y: item.size.h / 2 + selectError / 2
},
{
x: item.size.w / 2 - selectError / 2,
y: item.size.h / 2 + selectError / 2
}
];
// 缩放
if (this.inBox(newPoint, scaleRect)) {
this.mode = 'scale'
}
const deleteRect = [{
x: -item.size.w / 2 - selectError / 2,
y: item.size.h / 2 - selectError / 2
},
{
x: -item.size.w / 2 + selectError / 2,
y: item.size.h / 2 - selectError / 2
},
{
x: -item.size.w / 2 + selectError / 2,
y: item.size.h / 2 + selectError / 2
},
{
x: -item.size.w / 2 - selectError / 2,
y: item.size.h / 2 + selectError / 2
}
];
// 删除
if (this.inBox(newPoint, deleteRect)) {
this.mode = 'delete'
this.layers.splice(this.selected, 1)
this.selected = -1
}
}
}
// 工具条变更
this.bottomNavChange()
this.draw()
}
3. 绘制操作句柄
draw: function() {
const context = this.context
context.clearRect(0, 0, canvasw, canvash)
//绘制背景图
this.drawBackground()
this.layers.map((item, index) => {
context.translate(item.pos.x, item.pos.y)
context.rotate(item.rot)
switch (item.type) {
case 'img':
context.drawImage(item.url, -item.size.w / 2, -item.size.h / 2, item.size.w, item.size.h)
break
case 'txt':
context.setFillStyle(item.color)
context.font = item.size.h + 'px ' + this.txtStyle[item.style].label
context.fillText(item.content, -item.size.w / 2, -item.size.h / 2 + item.size.h * 9 / 10)
break
}
if (index === this.selected) {
context.setStrokeStyle(selectStrokeColor)
context.setLineWidth(1)
//选择框
context.translate(-item.size.w / 2, -item.size.h / 2)
context.beginPath()
context.moveTo(selectError / 2, 0);
context.lineTo(-selectError / 2 + item.size.w, 0);
context.moveTo(selectError / 2, item.size.h);
context.lineTo(-selectError / 2 + item.size.w, item.size.h);
context.moveTo(0, selectError / 2);
context.lineTo(0, -selectError / 2 + item.size.h);
context.moveTo(0 + item.size.w, selectError / 2);
context.lineTo(0 + item.size.w, -selectError / 2 + item.size.h);
context.stroke()
context.translate(item.size.w / 2, item.size.h / 2)
//移动句柄
context.translate(-item.size.w / 2, -item.size.h / 2)
context.strokeRect(
-selectError / 2,
-selectError / 2,
selectError,
selectError
)
context.beginPath()
context.moveTo(-selectError / 4, 0);
context.lineTo(selectError / 4, 0);
context.moveTo(0, -selectError / 4);
context.lineTo(0, selectError / 4);
//左箭头
context.moveTo(-selectError / 4, -selectError / 8);
context.lineTo(-selectError * 3 / 8, 0);
context.lineTo(-selectError / 4, selectError / 8);
//右箭头
context.moveTo(selectError / 4, -selectError / 8);
context.lineTo(selectError * 3 / 8, 0);
context.lineTo(selectError / 4, selectError / 8);
//上箭头
context.moveTo(-selectError / 8, -selectError / 4);
context.lineTo(0, -selectError * 3 / 8);
context.lineTo(selectError / 8, -selectError / 4);
//下箭头
context.moveTo(-selectError / 8, selectError / 4);
context.lineTo(0, selectError * 3 / 8);
context.lineTo(selectError / 8, selectError / 4);
context.stroke()
context.translate(item.size.w / 2, item.size.h / 2)
//旋转句柄
context.translate(item.size.w / 2, -item.size.h / 2)
context.strokeRect(
-selectError / 2,
-selectError / 2,
selectError,
selectError
)
context.beginPath()
context.arc(0, 0, selectError / 4, 0, Math.PI / 4, true)
context.stroke()
//箭头
context.beginPath()
context.setLineWidth(2)
context.moveTo(selectError / 8, -selectError / 8);
context.lineTo(selectError / 4, 0);
context.lineTo(selectError / 4 + selectError / 16, -selectError / 6);
context.stroke()
context.translate(-item.size.w / 2, item.size.h / 2)
//缩放句柄
context.translate(item.size.w / 2, item.size.h / 2)
context.setLineWidth(1)
context.strokeRect(
-selectError / 2,
-selectError / 2,
selectError,
selectError
)
//斜线
context.beginPath()
context.moveTo(-selectError / 4, -selectError / 4);
context.lineTo(selectError / 4, selectError / 4);
//左上箭头
context.moveTo(-selectError * 3 / 8, -selectError / 8);
context.lineTo(-selectError * 3 / 8, -selectError * 3 / 8);
context.lineTo(-selectError * 1 / 8, -selectError * 3 / 8);
//右下箭头
context.moveTo(selectError * 3 / 8, selectError / 8);
context.lineTo(selectError * 3 / 8, selectError * 3 / 8);
context.lineTo(selectError * 1 / 8, selectError * 3 / 8);
context.stroke()
context.translate(-item.size.w / 2, -item.size.h / 2)
//删除句柄
context.translate(-item.size.w / 2, item.size.h / 2)
context.setLineWidth(1)
context.strokeRect(
-selectError / 2,
-selectError / 2,
selectError,
selectError
)
//斜线一
context.beginPath()
context.moveTo(-selectError / 4, -selectError / 4);
context.lineTo(selectError / 4, +selectError / 4);
//斜线二
context.moveTo(selectError / 4, -selectError / 4);
context.lineTo(-selectError / 4, selectError / 4);
context.stroke()
context.translate(item.size.w / 2, -item.size.h / 2)
}
context.rotate(-item.rot)
context.translate(-item.pos.x, -item.pos.y)
})
context.draw()
}
4. 平移、缩放、旋转的操作算法
onMove: function(e) {
let point = {
x: e.touches[0].x,
y: e.touches[0].y
}
touchs.push(point)
if (touchs.length >= 2 && this.layers[this.selected]) {
const item = this.layers[this.selected]
if (!item) return;
switch (this.mode) {
case 'move':
const thur = Math.PI / 2 - item.rot - Math.atan(item.size.h / item.size.w)
const c = Math.sqrt(Math.pow(item.size.w, 2) + Math.pow(item.size.h, 2)) / 2
item.pos.x = point.x + c * Math.sin(thur)
item.pos.y = point.y + c * Math.cos(thur)
break
case 'rotate':
item.rot = this.calculateAngle({
x: item.pos.x,
y: item.pos.y,
}, point) - this.calculateAngle({
x: item.pos.x,
y: item.pos.y
}, {
x: item.pos.x + item.size.w / 2,
y: item.pos.y - item.size.h / 2
})
break
case 'scale':
const thur1 = Math.atan(item.size.h / item.size.w)
const c1 = Math.sqrt(Math.pow((point.x - item.pos.x), 2) + Math.pow((point.y - item.pos.y), 2))
let w = c1 * Math.sin(thur1)
let h = c1 * Math.cos(thur1)
w = (w > canvasw - 20) ? canvasw - 20 : w
w = (w < 20) ? 20 : w
h = (h > canvash - 20) ? canvash - 20 : h
h = (h < 20) ? 20 : h
if (w > h * item.size.w / item.size.h) h = w * item.size.h / item.size.w
else w = h * item.size.w / item.size.h
item.size.w = Math.round(w)
item.size.h = Math.round(h)
break
case 'delete':
//在 judgeMode 中处理
break
}
this.draw()
}
}
其他的简单代码就不贴啦,读者自行研究吧, 有问题可以发我邮箱共同探讨: davin.bao@foxmail.com