共计 4193 个字符,预计需要花费 11 分钟才能阅读完成。
写在前面
本文首发于公众号:符合预期的 CoyPan
demo 体验地址及代码在这里:请用手机或浏览器模拟手机访问
上一篇文章介绍了 canvas 中的拖拽、缩放、旋转中涉及到的数学知识。可以点击下面的链接查看。
canvas 中的拖拽、缩放、旋转 (上) —— 数学知识准备。
代码准备 – 如何在 canvas 中画出一个带旋转角度的元素
在 canvas 中,如果一个元素带有一个旋转角度,可以直接变化 canvas 的坐标轴来画出此元素。举个例子,
代码整体思路
整个 demo 的实现思路如下:
用户开始触摸 (touchstart) 时,获取用户的触摸对象,是 Sprite 的本体?删除按钮?缩放按钮?旋转按钮?并且根据各种情况,对变化参数进行初始化。
用户移动手指 (touchmove) 时,根据手指的坐标,更新 stage 中的所有元素的位置、大小,记录变化参数。修改对应 sprite 的属性值。同时对 canvas 进行重绘。
用户一旦停止触摸 (touchend) 时,根据变化参数,更新 sprite 的坐标,同时对变化参数进行重置。
需要注意的是,在 touchmove 的过程中,并不需要更新 sprite 的坐标,只需要记录变化的参数即可。在 touchend 过程中,再进行坐标的更新。坐标的唯一用处,就是判断用户点击时,落点是否在指定区域内。
代码细节
首先,声明两个类:Stage 和 Sprite。Stage 表示整个 canvas 区域,Sprite 表示 canvas 中的元素。我们可以在 Stage 中添加多个 Sprite,删除 Sprite。这两个类的属性如下。
class Stage {
constructor(props) {
this.canvas = props.canvas;
this.ctx = this.canvas.getContext(‘2d’);
// 用一个数组来保存 canvas 中的元素。每一个元素都是一个 Sprite 类的实例。
this.spriteList = [];
// 获取 canvas 在视窗中的位置,以便计算用户 touch 时,相对与 canvas 内部的坐标。
const pos = this.canvas.getBoundingClientRect();
this.canvasOffsetLeft = pos.left;
this.canvasOffsetTop = pos.top;
this.dragSpriteTarget = null; // 拖拽的对象
this.scaleSpriteTarget = null; // 缩放的对象
this.rotateSpriteTarget = null; // 旋转的对象
this.dragStartX = undefined;
this.dragStartY = undefined;
this.scaleStartX = undefined;
this.scaleStartY = undefined;
this.rotateStartX = undefined;
this.rotateStartY = undefined;
}
}
class Sprite {
constructor(props) {
// 每一个 sprite 都有一个唯一的 id
this.id = Date.now() + Math.floor(Math.random() * 10);
this.pos = props.pos; // 在 canvas 中的位置
this.size = props.size; // sprite 的当前大小
this.baseSize = props.size; // sprite 的初始化大小
this.minSize = props.minSize; // sprite 缩放时允许的最小 size
this.maxSize = props.maxSize; // sprite 缩放时允许的最大 size
// 中心点坐标
this.center = [
props.pos[0] + props.size[0] / 2,
props.pos[1] + props.size[1] / 2
];
this.delIcon = null;
this.scaleIcon = null;
this.rotateIcon = null;
// 四个顶点的坐标, 顺序为:左上,右上,左下,右下
this.coordinate = this.setCoordinate(this.pos, this.size);
this.rotateAngle = 0; // 累计旋转的角度
this.rotateAngleDir = 0; // 每次旋转角度
this.scalePercent = 1; // 缩放比例
}
}
demo 中,点击 canvas 下方的红色方块时,会实例化一个 sprite,调用 stage.append 时,会将实例化的 sprite 直接 push 到 Stage 的 spriteList 属性内。
window.onload = function () {
const stage = new Stage({
canvas: document.querySelector(‘canvas’)
});
document.querySelector(‘.red-box’).addEventListener(‘click’, function () {
const randomX = Math.floor(Math.random() * 200);
const randomY = Math.floor(Math.random() * 200);
const sprite = new Sprite({
pos: [randomX, randomY],
size: [120, 60],
minSize: [40, 20],
maxSize: [240, 120]
});
stage.append(sprite);
});
}
下面是 Stage 的方法:
class Stage {
constructor(props) {}
// 将 sprite 添加到 stage 内
append(sprite) {}
// 监听事件
initEvent() {}
// 处理 touchstart
handleTouchStart(e) {}
// 处理 touchmove
handleTouchMove(e) {}
// 处理 touchend
handleTouchEnd() {}
// 初始化 sprite 的拖拽事件
initDragEvent(sprite, { touchX, touchY}) {}
// 初始化 sprite 的缩放事件
initScaleEvent(sprite, { touchX, touchY}) {}
// 初始化 sprite 的旋转事件
initRotateEvent(sprite, { touchX, touchY}) {}
// 通过触摸的坐标重新计算 sprite 的坐标
reCalSpritePos(sprite, touchX, touchY) {}
// 通过触摸的【横】坐标重新计算 sprite 的大小
reCalSpriteSize(sprite, touchX, touchY) {}
// 重新计算 sprite 的角度
reCalSpriteRotate(sprite, touchX, touchY) {}
// 返回当前 touch 的 sprite
getTouchSpriteTarget({touchX, touchY}) {}
// 判断是否 touch 在了 sprite 中的某一部分上,返回这个 sprite
getTouchTargetOfSprite({touchX, touchY}, part) {}
// 返回触摸点相对于 canvas 的坐标
normalizeTouchEvent(e) {}
// 判断是否在在某个 sprite 中移动。当前默认所有的 sprite 都是长方形的。
checkIfTouchIn({touchX, touchY}, sprite) {}
// 从场景中删除
remove(sprite) {}
// 画出 stage 中的所有 sprite
drawSprite() {}
// 清空画布
clearStage() {}
}
Sprite 的方法:
class Sprite {
constructor(props) {}
// 设置四个顶点的初始化坐标
setCoordinate(pos, size) {}
// 根据旋转角度更新 sprite 的所有部分的顶点坐标
updateCoordinateByRotate() {}
// 根据旋转角度更新顶点坐标
updateItemCoordinateByRotate(target, center, angle){}
// 根据缩放比例更新顶点坐标
updateItemCoordinateByScale(sprite, center, scale) {}
// 根据按钮 icon 的顶点坐标获取 icon 中心点坐标
getIconCenter(iconCoordinate) {}
// 根据按钮 icon 的中心点坐标获取 icon 的顶点坐标
getIconCoordinateByIconCenter(center) {}
// 根据缩放比更新顶点坐标
updateCoordinateByScale() {}
// 画出该 sprite
draw(ctx) {}
// 画出该 sprite 对应的按钮 icon
drawIcon(ctx, icon) {}
// 对 sprite 进行初始化
init() {}
// 初始化删除按钮,左下角
initDelIcon() {}
// 初始化缩放按钮,右上角
initScaleIcon() {}
// 初始化旋转按钮,左上角
initRotateIcon() {}
// 重置 icon 的位置与大小
resetIconPos() {}
// 根据移动的距离重置 sprite 所有部分的位置
resetPos(dirX, dirY) {}
// 根据触摸点移动的距离计算缩放比,并重置 sprite 的尺寸
resetSize(dir) {}
// 设置 sprite 的旋转角度
setRotateAngle(angleDir) {}
}
Stage 的方法主要是处理和用户交互的逻辑,得到用户操作的交互参数,然后根据交互参数调用 Sprite 的方法来进行变化。
代码在这里:https://coypan.info/demo/canvas-drag-scale-rotate.html
写在后面
本文介绍了文章开头给出的 demo 的详细实现过程。代码还有很大的优化空间。事实上,工作上的需求并没有要求【旋转】,只需要实现【拖拽】、【缩放】即可。在只实现【拖拽】和【缩放】的情况下,会容易很多,不需要用到四个顶点的坐标以及之前的那些复杂的数学知识。而在自己实现【旋转】的过程中,也学到了很多。符合预期。