记录元素平移、旋转、缩放和镜像翻转(2)
接着前面的继续写,前面简单实现了元素的移动,接下来实现元素的旋转变换,加入旋转变换后,先前实现的部分逻辑也要跟着改变。
首先提取之前用到的部分常量,提取出来后方便后期维护修改
// 初始元素宽度,在外界没有传入宽度时使用
const INIT_WIDTH = 50;
// 同初始元素宽度
const INIT_HEIGHT = 50;
// 初始元素 x 坐标,在外界没有传入时使用
const INIT_X = 10;
// 同初始元素 x 坐标
const INIT_Y = 10;
// 初始元素色值,在外界没有传入时使用
const INIT_COLOR = '#CCCCCC';
因为要实现元素的旋转变换,鼠标必须要在元素旋转变换区域才响应这个操作,所以为元素添加一个响应状态,又因为要实现元素的旋转,所以还得添加一个旋转角度。
对于元素的状态,如果元素未响应,则为 null,其他则是对应状态的字符串,因为元素有多个状态,为了方便维护,单独抽离状态字符串常量和状态数组,之后对于元素的操作,如果状态为状态数组里面的状态才响应操作,其他不再响应操作。
因为元素状态提取,所以对于设置元素状态需要再进行判断,只有在我们定义的状态数组里面的值才允许设置
// 单独提取状态常量
// 元素移动状态
const STATUS_MOVE = 'move';
// 元素旋转变换状态
const STATUS_TRANSFORM = 'transform';
// 状态数组
const STATUS_ARRAY = [STATUS_MOVE, STATUS_TRANSFORM, null];
handleOptions(options = {}) {
// 省略之前的代码...
// 新增元素状态,用于判断鼠标点击的是元素的什么位置,
// 好做出相应的操作
_options.status = null;
// 新增旋转角度
_options.rotate = 0;
return new Proxy(_options, {
set(target, property, value, receiver) {
// 省略之前的代码...
if (
property === 'status' &&
STATUS_ARRAY.indexOf(value) === -1
) {
throw new Error('元素状态设置错误:' + value);
}
// 省略之前的代码...
}
});
}
因为加入了旋转,所以计算顶点坐标得重新考虑旋转角度了,计算规则大致相同,所以抽离计算函数
/**
* 新增计算变换后的元素顶点坐标
* 因为要加入旋转,所以单独提出来
*
* @param {Number} x 元素 x 坐标
* @param {Number} y 元素 y 坐标
* @param {Number} centerX 元素中心点 x 坐标
* @param {Number} centerY 元素中心点 y 坐标
* @param {Number} degrees 元素旋转角度
* @return 返回计算好的顶点坐标数组
* @description
*/
_rotatePoint(x, y, centerX, centerY, degrees) {
let _x = (x - centerX) *
Math.cos(degrees * Math.PI / 180) -
(y - centerY) *
Math.sin(degrees * Math.PI / 180) +
centerX;
let _y = (x - centerX) *
Math.sin(degrees * Math.PI / 180) +
(y - centerY) *
Math.cos(degrees * Math.PI / 180) +
centerY;
return [_x, _y];
}
改造计算元素顶点函数
/**
* 重新计算当前元素的顶点坐标
*/
rotateSquare() {
const currItem = this.currItem;
if (!currItem) return;
// 因为加入旋转,所以更改顶点计算方式
currItem.square = [
this._rotatePoint(
currItem.x,
currItem.y,
currItem.centerX,
currItem.centerY,
currItem.rotate
),
this._rotatePoint(
currItem.x + currItem.width,
currItem.y,
currItem.centerX,
currItem.centerY,
currItem.rotate
),
this._rotatePoint(
currItem.x + currItem.width,
currItem.y + currItem.height,
currItem.centerX,
currItem.centerY,
currItem.rotate
),
this._rotatePoint(
currItem.x,
currItem.y + currItem.height,
currItem.centerX,
currItem.centerY,
currItem.rotate
)
];
}
因为加入了元素的状态,所以鼠标点击的时候,我们必须得将元素状态设置,只要是在对应区域,那就设置对应的状态,之前只是判断点击元素是否在元素内部,现在元素内部需要划分部分区域,鼠标在这些区域响应正确的操作,所以我们得新增点击点位置判断函数,这里要实现的是元素的旋转变换,这个响应区域为元素的右下角,只要点击的坐标在此处,就响应变换操作,这样 render 函数也要进行改造
// 变换区域宽高常量
const TRAN_W = 20;
const TRAN_H = 20;
/**
* 渲染
*/
render() {
let str = '';
this.elementArray.forEach(item => {
str += `
<div style="position:absolute;top:0;left:0;width:${item.width}px;height:${item.height}px;background-color:${item.backgroundColor};transform:translateX(${item.x}px) translateY(${item.y}px) translateZ(0px) rotate(${item.rotate}deg);">
${
// 新增旋转区域,这里只是简单的实现,后期更改
this.currItem && this.currItem.id === item.id ?
`<div style="position:absolute;bottom:-${TRAN_H / 2}px;right:-${TRAN_W / 2}px;width:${TRAN_W}px;height:${TRAN_H}px;border:1px solid #666;font-size:12px;text-align:center;line-height:${TRAN_H}px;border-radius:50%;">旋</div>` :
''
}
</div>
`;
});
this.elementContainer.innerHTML = str;
}
/**
* 新增元素点击区域判断,对于不同的点击区响应不同的操作方法
* 为什么要新增这个函数?
* 因为常规的实现这些元素的操作,元素内部响应移动操作,
* 对于旋转等其他操作,一般就是元素的另外特定的区域,
* 当鼠标在这些特定的区域,为了响应对应的操作,
* 所以抽离成单独的函数
* @param {Number} x 点击的 x 坐标
* @param {Number} y 点击的 y 坐标
* @param {*} item 要判断的元素
* @return 不在元素任何区域内返回 false,在区域内返回相应的操作字符串
*/
isEleClickZone(x, y, item) {
// 判断是否在旋转区域
// 默认定义的区域在右下角
let tranPosition = this._rotatePoint(
item.x + item.width,
item.y + item.height,
item.centerX,
item.centerY,
item.rotate
);
let tranX = tranPosition[0] - TRAN_W / 2;
let tranY = tranPosition[1] - TRAN_H / 2;
if (
x - tranX >= 0 &&
y - tranY >= 0 &&
tranX + TRAN_W - x >= 0 &&
tranY + TRAN_H - y >= 0
) {
return STATUS_TRANSFORM;
} else if (this.insideEle(x, y, item)) {
return STATUS_MOVE;
}
// 不在元素区域里面并且不在元素操作按钮区域内
return false;
}
新增完这个函数我们接着改造鼠标点击函数
handleDown(e) {
// 省略之前的代码...
// 临时元素状态
let tempStatus = null;
// 省略之前的代码...
this.elementArray.forEach(item => {
let status = this.isEleClickZone(x, y, item);
// 选中点击坐标下顶层元素
if (
(status && !currItem) ||
(status && currItem.zIndex < item.zIndex)
) {
currItem = item;
// 更改临时状态
tempStatus = status;
}
});
if (this.currItem) {
// 新增元素状态
this.currItem.status = tempStatus;
// 记录选中元素的部分参数,用于后面计算
this.currItemStartOpt = {
// 省略之前的代码...
// 新增存储点击时当前元素的旋转角度
rotate: this.currItem.rotate
};
}
}
因为加入了元素的状态,在不同的状态进行不同的操作,之前移动的时候都是写在鼠标移动函数中的,后面的状态多了,响应不同状态的操作,这样会导致鼠标移动函数难以维护,所以提取处理元素状态函数单独维护,状态处理函数抽离,使其功能职责单一,因为在变换过程中,元素会进行缩放,不能无限的缩小,所以定义一个最小宽度常量
const MIN_WIDTH = 40;
/**
* 抽离函数处理元素移动
*
* @param {Number} x
* @param {Number} y
*/
handleEleMove(x, y) {
const temp = this.currItemStartOpt;
// 处理元素移动方法
// 通过鼠标点击位置来计算中心点,
// 再通过中心点去计算元素的 xy
// 不考虑边界情况的中心点坐标
let _centerX = temp.centerX + x - this.startX;
let _centerY = temp.centerY + y - this.startY;
// 最小中心点坐标,用于处理边界情况
let minCenterX = temp.width / 2;
let minCenterY = temp.height / 2;
// 最大中心点坐标,用于处理边界情况
let maxCenter = this.width - minCenterX;
let maxHeight = this.height - minCenterY;
// 最终计算出的中心点坐标
let centerX = _centerX >= maxCenter ?
maxCenter :
_centerX <= minCenterX ?
minCenterX : _centerX;
let centerY = _centerY >= maxHeight ?
maxHeight :
_centerY <= minCenterY ?
minCenterY : _centerY;
this.currItem.centerX = centerX;
this.currItem.centerY = centerY;
this.currItem.x = this.currItem.centerX - minCenterX;
this.currItem.y = this.currItem.centerY - minCenterY;
}
/**
* 新增处理元素变换
*
* @param {Number} x
* @param {Number} y
*/
handleEleTran(x, y) {
const currItem = this.currItem;
const {
x: oX,
y: oY,
rotate: oRotate,
width: oWidth,
height: oHeight
} = this.currItemStartOpt;
// 重新计算元素的中心点坐标
currItem.centerX = currItem.x + currItem.width / 2;
currItem.centerY = currItem.y + currItem.height / 2;
// 计算旋转角度
let diffStartX = this.startX - currItem.centerX;
let diffStartY = this.startY - currItem.centerY;
let diffEndX = x - currItem.centerX;
let diffEndY = y - currItem.centerY;
let angleStart = Math.atan2(diffStartY, diffStartX) / Math.PI * 180;
let angleEnd = Math.atan2(diffEndY, diffEndX) / Math.PI * 180;
currItem.rotate = oRotate + angleEnd - angleStart;
// 利用中心点计算鼠标移动前后点距离,
// 用于计算缩放比例,
// 再基于这个比例重新计算元素宽高
let lineStart = Math.sqrt(
Math.pow(currItem.centerX - this.startX, 2) +
Math.pow(currItem.centerY - this.startY, 2)
);
let lineEnd = Math.sqrt(
Math.pow(currItem.centerX - x, 2) +
Math.pow(currItem.centerY - y, 2)
);
let resizeRaito = lineEnd / lineStart;
let newW = oWidth * resizeRaito;
let newH = oHeight * resizeRaito;
// 以短边为基准来计算最小宽高
if (oWidth <= oHeight && newW < MIN_WIDTH) {
newW = MIN_WIDTH;
newH = MIN_WIDTH * oHeight / oWidth;
} else if (oHeight < oWidth && newH < MIN_WIDTH) {
newH = MIN_WIDTH;
newW = MIN_WIDTH * oWidth / oHeight;
}
// 以长边为基准来计算最大宽高
if (oWidth >= oHeight && newW >= this.width) {
newW = this.width;
newH = this.width * oHeight / oWidth;
} else if (oHeight > oWidth && newH >= this.height) {
newH = this.height;
newW = this.height * oWidth / oHeight;
}
currItem.width = Math.round(newW);
currItem.height = Math.round(newH);
currItem.x = Math.round(oX - (newW - oWidth) / 2);
currItem.y = Math.round(oY - (newH - oHeight) / 2);
}
改造鼠标移动函数
handleMove(e) {
requestAnimationFrame(() => {
if (!this.currItem || !this.currItemStartOpt) return;
const { clientX, clientY } = e;
let x = clientX - this.offsetX + this.scrollLeft;
let y = clientY - this.offsetY + this.scrollTop;
let status = this.currItem.status;
if (status === STATUS_MOVE) {
this.handleEleMove(x, y);
} else if (status === STATUS_TRANSFORM) {
this.handleEleTran(x, y);
}
});
}
观察上面改造后的函数,如果我们后面还要加很多状态,就要写很多的 if-else,所以使用策略模式改造,抽离处理函数,所以继续改造
/**
* 抽离函数处理元素 status,对应的 status 做对应的事情
*
* @param {Number} x 移动的 x 坐标
* @param {Number} y 移动的 y 坐标
*/
handleEleStatus(x, y) {
// 之后需要响应其他的状态,接着定义状态常量,
// 写状态函数接着往下添加即可
return {
[STATUS_MOVE]: () => this.handleEleMove(x, y),
[STATUS_TRANSFORM]: () => this.handleEleTran(x, y)
}
}
handleMove(e) {
requestAnimationFrame(() => {
if (!this.currItem || !this.currItemStartOpt) return;
const { clientX, clientY } = e;
let x = clientX - this.offsetX + this.scrollLeft;
let y = clientY - this.offsetY + this.scrollTop;
let status = this.currItem.status;
// if (status === STATUS_MOVE) {
// this.handleEleMove(x, y);
// } else if (status === STATUS_TRANSFORM) {
// this.handleEleTran(x, y);
// }
this.handleEleStatus(x, y)[status]();
});
}
最后,当鼠标按键抬起的时候,将当前选中元素状态置空
handleUp(e) {
if (this.currItem) {
this.currItem.status = null;
}
this.currItem = null;
this.currItemStartOpt = null;
}
关于旋转变换操作涉及到修改的代码已经完成,接下来看看效果
至此我们实现了元素的旋转变换,其中涉及到的变换多用到了数学的知识,这样是不是也知道数学的重要性了,哈哈哈哈,后面完全实现了之后再贴上完整版的代码。