JS前端Canvas实现物体拖拽、旋转与缩放交互详解

作者:像素猫时间:2026-10-26 04:33:26浏览:568

本文详细解析如何在JS Canvas中实现物体的拖拽、旋转和缩放交互。通过数据与视图分离的设计,仅更新top、left、angle及scaleX/scaleY等属性值,配合Math.atan2计算角度与变换基点,即可实现流畅的物体变换效果,无需修改渲染逻辑。

正文

此前我们已实现了物体的点选与框选功能,但画布仍显静态,缺乏实际交互感。本章旨在通过引入拖拽、旋转和缩放等常见交互操作,使画布中的物体“活”起来。这是本系列教程的关键章节,希望能为你提供实质性帮助。

拖拽

首先讨论拖拽平移功能的实现,该功能相对简单。由于物体位置由 top 和 left 属性定义,平移操作仅需更新这两个属性的值。每次移动后触发 renderAll 方法进行重新渲染,从而在新位置绘制物体。

这体现了典型的数据与视图分离思想。在本章及后续章节中,我们无需修改物体的render方法。只要物体状态(数据)改变,画布会自动响应并重新绘制,无需关心具体绘制过程,这一点至关重要。

以下是平移操作的代码示例:

/** 平移当前选中物体 */
_translateObject(x: number, y: number) {
    const target = this._currentTransform.target;
    target.set('left', x - this._currentTransform.offsetX); // offsetX 是画布整体偏移
    target.set('top', y - this._currentTransform.offsetY); // offsetY 是画布整体偏移
}

代码简洁易懂,因为绘制逻辑固定,变换操作仅涉及数据修改。需注意代码中的_currentTransform,它记录了鼠标按下时的初始状态信息,具体结构如下所示,了解其大致形态即可:

正文

可见,拖拽平移部分代码简短,因其逻辑确实简单。

旋转

接下来介绍旋转操作,其原理同样简单。物体通过angle属性表示旋转角度。旋转时,计算拖拽产生的角度变化deltaAngle,新角度等于旧角度加上deltaAngle,然后重新赋值angle变量。此过程同样不涉及修改物体的_render方法,难点在于如何计算变换角度。

旋转本质是鼠标点的旋转,即计算当前鼠标点与初始鼠标点之间的角度差,如图所示:

正文

首先看单个点的情况,通常计算该点与原点连线及x轴正方向之间的逆时针夹角,如下图所示:

正文

通常使用radian = Math.atan2(y, x)计算弧度,注意这里是弧度(radian)而非角度(angle)。Canvas中使用弧度,但角度更便于理解,因此需适时进行转换。

需注意使用Math.atan2而非Math.atan。虽然两者相似,但atan无法确定唯一方向,例如点(1, 1)和(-1, -1)的atan值相同但方向相反。atan2取值范围为[-Math.PI, Math.PI],且四个象限取值不同,因此更适合用于计算。

基于上述原理,计算变得简单:原点为物体中心点,鼠标按下点与中心点连线形成起始角度,拖拽点与中心点连线形成最终角度,两者之差即为变换角度。

切记,旋转时原点通常为物体中心点。以下是核心代码示例,代码量少且易于理解:

/** 旋转当前选中物体 */
_rotateObject(x: number, y: number) {
    const t = this._currentTransform;
    const o = this._offset;
    // 鼠标按下的点与物体中心点连线和 x 轴正方向形成的弧度
    const lastRadian = Math.atan2(t.ey - o.top - t.top, t.ex - o.left - t.left);
    // 鼠标拖拽的终点与物体中心点连线和 x 轴正方向形成的弧度
    const curRadian = Math.atan2(y - o.top - t.top, x - o.left - t.left);
    const deltaRadian = curRadian - lastRadian;
    let angle = Util.radiansToDegrees(t.theta + deltaRadian); // 新的角度 = 原来的角度 + 变换的角度
    if (angle < 0) angle = 360 + angle;
    angle = angle % 360;
    t.target.angle = angle;
}

缩放

缩放比旋转稍复杂,以右侧中间控制点为例,其他控制点逻辑类似(复制修改即可),先看效果:

正文

观察图中右侧中间红色控制点,其缩放本质是沿x轴拉伸。直观想法是计算水平拖拽距离dx,然后改变物体宽度,如object.width += dx。若width变为负数,需处理最小值限制,防止右侧控制点拉至左侧。

回顾早期知识点:我们通常不改变物体自身大小,而是修改变换值。因此,缩放本质是改变scaleX和scaleY值。以拖拽右侧中间控制点为例,计算scaleX时,可将变换基点临时设为左侧中间控制点(蓝色点),这样计算当前宽度更方便:

  • 当前宽度 = 鼠标位置的x - 左边中间控制点的位置的x
  • scaleX = 当前宽度 / 自身宽度。注意,自身width值未变,仅改变scaleX。对于反向拉伸,可通过临时变换基点处理:一旦反向,立即切换为按左侧中间控制点拖拽的逻辑,即红点变为参考基点,蓝点变为拖拽点,请结合上方动图体会。
  • 此外,拖拽缩放时top和left值也会改变,因此计算scaleX后需更新这两个值,注意观察上方动图中的黑点变化。还需提醒两点:
  • 缩放时中心点并非物体中心,可简单理解为单边缩放;当然也可沿中心点缩放,但本文讲解默认形式。
  • 若为竖直拉伸,只需将x换为y,宽度换为高度;若为右下角控制点,则需同时处理xy代码。

此处简要展示核心代码:

/**
 * 缩放当前选中物体
 * @param x 鼠标点 x
 * @param y 鼠标点 y
 * @param by 是否等比缩放,x | y | equally
 */
_scaleObject(x: number, y: number, by = 'equally') {
    let t = this._currentTransform, // 在鼠标按下的时候会记录物体的状态
        offset = this._offset, // 画布偏移
        target: FabricObject = t.target;
    // 缩放基点:比如拖拽右边中间的控制点,其实我们参考的变换基点是左边中间的控制点
    let constraintPosition = target.translateToOriginPoint(target.getCenterPoint(), t.originX, t.originY);
    // 以物体变换中心为原点的鼠标点坐标值
    let localMouse = target.toLocalPoint(new Point(x - offset.left, y - offset.top), t.originX, t.originY);
    if (t.originX === 'right') {
        localMouse.x *= -1;
    }
    // 计算新的缩放值,以变换中心为原点,根据本地鼠标坐标点/原始宽度进行计算,重新设定物体缩放值
    let newScaleX = target.scaleX;
    if (by === 'x') {
        newScaleX = localMouse.x / (target.width + target.padding);
        target.set('scaleX', newScaleX);
    }
    // 如果是反向拉伸 x
    if (newScaleX < 0) {
        if (t.originX === 'left') t.originX = 'right';
        else if (t.originX === 'right') t.originX = 'left';
    }
    // 缩放会改变物体位置,所以要重新设置
    target.setPositionByOrigin(constraintPosition, t.originX, t.originY);
}

该变换稍显复杂,我单独编写了一个小demo,感兴趣者可点击链接查看。建议大家多动手实践,核心要点在于:

我们不改变物体自身的宽高大小,也不改变物体的渲染方法,而只是改变三种变换的值。

可能有同学疑问:鼠标移动时频繁调用renderAll渲染函数,性能是否较差?尤其是物体增多时。

确实如此。在前端,数据或元素过多时性能易受影响,如数据多需分页或虚拟滚动,元素多则能不绘制就不绘制。

Canvas中也有解决方案,如缓存、分层或WebGL,这些将在后续优化章节详细讲解,敬请期待。不过,普通页面较少遇到性能瓶颈,遇到时再优化即可,否则属于过度优化。但在Canvas中,性能是常见问题,易写出卡顿效果,因此有必要讨论。

小结

本章主要讲解物体变换操作。得益于之前良好的结构划分,即数据与渲染层分离,本章核心仅在于改变数据,其他未变。这种数据驱动视图的感觉令人印象深刻。简单总结三种基本操作:

  • 拖拽,计算新的top、left
  • 旋转,计算新的angle
  • 缩放,计算新的scaleX、scaleY

三种变换操作的本质均依托于鼠标坐标点的计算,至此结束。

此处提供简版fabric.js代码链接,感兴趣者可查阅。本次分享到此结束。

canvas 中如何实现物体的框选(六)🏖

canvas 中如何实现物体的点选(五)🏖

canvas 中物体边框和控制点的实现(四)🏖

实现一个轻量 fabric.js 系列三(物体基类)🏖

实现一个轻量 fabric.js 系列二(画布初始化)🏖

实现一个轻量 fabric.js 系列一(摸透 canvas)💥

更多关于JS前端canvas交互的资料请关注本站其它相关文章!

作者最新文章