利用矩阵转换实现盒子的定点拉伸缩放

747 阅读3分钟

利用矩阵转换实现盒子的定点拉伸缩放

背景(需求):

自由调整盒子尺寸,增强用户鼠标交互。

需求要点

鼠标点击盒子的四个点拖拉缩放盒子,并且点击位置的关于盒子中心的对称点保持不动。包括盒子旋转后进行拖拉缩放,不能出现抖动。拖拉缩放时能实现等比缩放和非等比缩放。 选中框只有四个边角上有缩放操作点(拖拽操作点可调整水印大小)

方案:

求出组件缩放后在未旋转状态的位置信息、宽高度。由于组件是以中心点进行旋转,所以无论组件旋转多少度,它的中心点都是不变的,根据组件信息求出组件的中心点centerPosition;然后根据当前控制点和中心点求出组件的对称点symmetricPoint;在移动过程中,利用当前点currentPosition和对称点求出新的对称点newCenterPoint。当前面信息求出后,我们就可以根据已有信息求出组件未旋转状态的信息了;根据currentPosition和newCenterPoint和旋转角度求出组件未旋转状态的点newControlPoint;同理求出newSymmetricPoint;最后组件的坐标就newControlPoint,宽度 = newSymmetricPoint.x - newControlPoint.x,高度 = newSymmetricPoint.y - newControlPoint.y。

方案说明

背景知识

代码实现:

// 求某点以center为中心旋转rotate角度后的新坐标
 calculateRotatedPointCoordinate(point, center, rotate) {
    /**
     * 旋转公式:
     *  点a(x, y)
     *  旋转中心c(x, y)
     *  旋转后点n(x, y)
     *  旋转角度θ
     * nx = cosθ * (ax - cx) - sinθ * (ay - cy) + cx
     * ny = sinθ * (ax - cx) + cosθ * (ay - cy) + cy
     */
    return {
        x: (point.x - center.x) * Math.cos(this.angleToRadian(rotate)) - (point.y - center.y) * Math.sin(this.angleToRadian(rotate)) + center.x,
        y: (point.x - center.x) * Math.sin(this.angleToRadian(rotate)) + (point.y - center.y) * Math.cos(this.angleToRadian(rotate)) + center.y,
    }
}

angleToRadian(angle) {
    return angle * Math.PI / 180
}

实现:

第一步:点击鼠标时获取初始信息

// 点击位置
this.start.clickPoint = {
    x: downEvent.clientX,
    y: downEvent.clientY,
};
// 中心点
this.start.centerPosition = {
    x: editBoxBcr.left + editBoxBcr.width / 2,
    y: editBoxBcr.top + editBoxBcr.height / 2,
}
// 初始水印信息
this.start.mark = {
    // 相对屏幕
    x: editBoxBcr.left,
    y: editBoxBcr.right,
    // 相对画布
    left: currentSelectedMark.left,
    top: currentSelectedMark.top,
    // 角度
    angle: currentSelectedMark.angle,
    // 尺寸
    width: currentSelectedMark.width,
    height: currentSelectedMark.height,
    scale: currentSelectedMark.scale,
    fitScale: currentSelectedMark.fitScale,
}

第二步:鼠标移动时获取实时位置

const currentPosition = {
    x: event.clientX,
    y: event.clientY,
}

第三步:计算初始对称点

// 初始对称点
const symmetricPoint = {
    x: this.start.centerPosition.x - (this.currentControl.x - this.start.centerPosition.x),
    y: this.start.centerPosition.y - (this.currentControl.y - this.start.centerPosition.y),
};

第四步:如果需要等比缩放,计算出宽高比例

const proportion = this.start.mark.width / this.start.mark.height;

第五步:获取矩阵转换后的尺寸和坐标

const res = this.calculateSizeAndPos(this.start.mark.angle, currentPosition, proportion, true, {
    centerPosition: this.start.centerPosition,
    control: this.currentControl,
    symmetricPoint,
})
/**
 * 求矩阵转换后的尺寸和坐标。
 *  angle: 旋转角度,
 *  curPositon: 实时坐标,
 *  proportion: 宽高比,
 *  needLockProportion: 是否需要等比缩放,
 *  pointInfo: 传入中心点坐标centerPosition、控制点坐标control、初始对称点坐标symmetricPoint
 */
calculateSizeAndPos(angle, curPositon, proportion, needLockProportion, pointInfo) {
    const { symmetricPoint } = pointInfo;
    let newCenterPoint = this.getCenterPoint(curPositon, symmetricPoint);
    // 控制点转换后的坐标
    let newControlPoint = this.calculateRotatedPointCoordinate(curPositon, newCenterPoint, -angle);
    // 对称点转换后的坐标
    let newSymmetricPoint = this.calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -angle);

    let newWidth = Math.abs(newControlPoint.x - newSymmetricPoint.x);
    let newHeight = Math.abs(newSymmetricPoint.y - newControlPoint.y);

    // 处理等比缩放的情况
    if (needLockProportion) {
        if (newWidth / newHeight > proportion) {
            newControlPoint.x += ['tl', 'bl'].includes(this.currentControl.name) ? Math.abs(newWidth - newHeight * proportion) : -Math.abs(newWidth - newHeight * proportion);
            newWidth = newHeight * proportion;
        } else {
            newControlPoint.y += ['tl', 'tr'].includes(this.currentControl.name) ? Math.abs(newHeight - newWidth / proportion) : -Math.abs(newHeight - newWidth / proportion);
            newHeight = newWidth / proportion;
        }

        // 由于现在求的未旋转前的坐标是以没按比例缩减宽高前的坐标来计算的
        // 所以缩减宽高后,需要按照原来的中心点旋转回去,获得缩减宽高并旋转后对应的坐标
        // 然后以这个坐标和对称点获得新的中心点,并重新计算未旋转前的坐标
        const rotatedControlPoint = this.calculateRotatedPointCoordinate(newControlPoint, newCenterPoint, angle);
            newCenterPoint = this.getCenterPoint(rotatedControlPoint, symmetricPoint);
            newControlPoint = this.calculateRotatedPointCoordinate(rotatedControlPoint, newCenterPoint, -angle);
        newSymmetricPoint = this.calculateRotatedPointCoordinate(symmetricPoint, newCenterPoint, -angle)

        newWidth = ['tl', 'bl'].includes(this.currentControl.name) ? (newSymmetricPoint.x - newControlPoint.x) : (newControlPoint.x - newSymmetricPoint.x);
        newHeight = ['tl', 'tr'].includes(this.currentControl.name) ? (newSymmetricPoint.y - newControlPoint.y) : (newControlPoint.y - newSymmetricPoint.y);
    }

    if (newWidth > 0 && newHeight > 0) {
        return {
            width: newWidth,
            height: newHeight,
            x: Math.min(newControlPoint.x, newSymmetricPoint.x),
            y: Math.min(newSymmetricPoint.y, newControlPoint.y),
        }
    }
    return null;
}

getCenterPoint(p1, p2) {
    return {
        x: p1.x + ((p2.x - p1.x) / 2),
        y: p1.y + ((p2.y - p1.y) / 2),
    }
}

成果:

盒子旋转后也可以固定某个角进行缩放 缩放水印2.gif

注意:

  1. 注意角度转换
  2. 点击控制点时应取控制点的中心位置而不是鼠标的位置
  3. 等比缩放时需要将缩减尺寸的盒子按照原来的中心点旋转回去,再计算新的中心点和对称点,最后再按照新的中心点计算未旋转状态的盒子尺寸、位置