基于Html5 Canvas的T恤、箱包、礼品定制等设计组件

由于产品需要, 自己写了一套画布,主要功能是为产品提供定制体验, 用户上传自己喜欢的素材,根据自己的喜好设计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 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s011803

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值