type
status
date
slug
summary
tags
category
icon
password
Edited
Dec 15, 2024 03:11 PM
Created
Dec 13, 2024 02:19 PM
最近,我遇到了一个的需求:设计并实现一个具备拖拽和缩放功能的画布,这在许多设计软件中是一个常见的功能,比如蓝湖等。在这篇文章中,我将分享一种简单而高效的方法来构建这样的画布功能。
画布将具备以下核心功能:
- 拖拽:允许自由移动画布内的元素。
- 缩放:使用鼠标滚轮来放大或缩小画布视图。
- 定位:快速将视图定位到画布上的特定元素。
在线演示
为了让您更直观地体验这个拖拽缩放画布的功能,我已经将其完整实现并部署在StackBlitz平台上。您可以通过以下链接直接访问并尝试这个交互式的画布示例:
点击上面的链接,您将能够看到画布的实际效果,并可以编辑代码来探索其工作原理。
此外,如果您想要嵌入这个示例到您的项目中,以下链接提供了一个嵌入式的版本:
实现
概览
在深入实现细节之前,让我们先来审视一下整体的布局代码,这是构建我们可拖拽缩放画布的基础。
transformOrigin: '0 0'
:我们给画布设置了transformOrigin
属性为'0 0'
,这确保了所有的变换(如拖拽和缩放)都是基于画布的左上角进行的,这与事件的x,y坐标系相匹配,使得逻辑更加直观。
PageItem
:我们定义了一个PageItem
接口来描述每个页面元素的属性,包括标题和位置尺寸信息。这些属性将在后续的拖拽和缩放功能中被用到。
下面是我们的画布布局的截图,展示了页面元素的初始布局:
接下来,我们将逐步实现拖拽和缩放功能。从简单的拖拽开始,逐步深入到缩放功能的实现。
拖拽
拖拽是用户与画布交互的基础功能之一,它允许用户自由移动画布上的内容。
这个功能非常的常见和通用,直接这一功能的详细代码和解释:
通过
transformRef
来存储画布的偏移量信息,避免在每次事件触发时都查询DOM元素,提高性能。缩放
缩放功能允许用户通过
Ctrl + 滚轮
操作来放大或缩小画布。这一功能对于查看细节或调整整体布局非常有用。阻止浏览器默认行为
首先,我们需要阻止浏览器对
Ctrl + 滚轮
的默认缩放行为,以确保我们的画布可以正确响应缩放操作。接下来,我们实现缩放功能。我们定义了一些常量来控制缩放的行为:
WHEEL_RATIO
:每次缩放的基数。
WHEEL_MAX_SCALE
:最大缩放倍数。
WHEEL_MIN_SCALE
:最小缩放倍数。
去除画布偏移值
首先,我们需要计算鼠标相对于画布左上角的位置,这需要去除画布本身的偏移值:
这两行代码的目的是为了去除画布本身的偏移值,因为画布可能并不总是从浏览器视窗的左上角开始。
接着,我们需要思考一个问题,如何在缩放的时候,实现鼠标跟手的状态。
因为缩放后,画布整体进行了放大,但是位置还是原本的位置,这时候就会出现鼠标不跟手的清空,或者说画面出现了抖动。
所以要时候鼠标跟手,这里就需要对原本的偏移值进行额外的处理了。
位移补偿计算
位移补偿的目的是确保在缩放时,鼠标位置与视图保持一致。
让我们先来了解了解概念:
- 为什么需要位移补偿?
如果我们只是简单地改变
scale
而不进行位移补偿:如果我们只是简单地改变
scale
而不进行位移补偿,画布会以左上角(0,0)为中心点进行缩放,导致鼠标下的内容会向外"逃离"鼠标位置。位移补偿确保了缩放中心点在鼠标位置。- 补偿公式分解
这是最关键的部分:
(mouseX - transformRef.current.x)
表示鼠标位置到当前变换原点的距离
(1 - newScale/oldScale)
是一个补偿因子
- 示例
假设有一个场景:
缩放前
缩放后(2倍)
如果不补偿,50px会变成100px,鼠标下的点会偏离,补偿-50px后,确保鼠标位置下的点保持不动。
- 放大时(
newScale > oldScale
):补偿因子为负,产生向原点的位移,抵消了放大造成的距离增加。
- 缩小时(
newScale < oldScale
):补偿因子为正,产生远离原点的位移,抵消了缩小造成的距离减少。
缩放功能的实际效果展示:
优化缩放和定位
到目前为止,我们实现了缩放和定位,但是有个很严重的问题,就是如果我们的画布被缩放的太小,或者拖拽的太远,导致我们触发不了事件了,就会出现问题。
为了解决这个问题,我们在画布上层添加了一层蒙版(
viewportRef
),专门用来接收事件,并将处理结果回显到画布上。通过将事件监听器绑定到
viewportRef
而不是canvasRef
,我们可以确保即使画布移动或缩放,事件也能被正确触发。以下是优化后的代码实现:通过这种方式,我们确保了即使画布被缩放或移动,用户的操作也能被正确响应。以下是优化后的效果展示:
定位
为了实现快速定位到画布内特定
Page
的功能,我们需要利用之前定义的PageItem.rect
属性,这些属性包含了页面元素的位置信息。我们定义了一个
locate
方法来实现定位功能,它接收两个参数:需要定位的Page
数组和定位动画的时间长度。在实现定位功能时,我们首先需要确定需要定位的页面元素(
pages
)的整体位置和尺寸,以便计算出合适的缩放比例和偏移量,确保这些元素能够在视口中完全展示并居中定位。
首先,我们根据提供的pages
数组,计算出所有页面元素的边界坐标minX
、maxX
、minY
、maxY
。再利用上面计算出的边界坐标,我们可以得到整体可视区域的宽度(
viewWidth
)和高度(viewHeight
)。接下来,我们需要计算水平和垂直方向的缩放比例,以确保内容能够完全显示在视口中。同时,我们乘以
LOCATE_PADDING
来在边缘添加内边距,避免内容紧贴边缘。然后,我们取两者的最小值作为最终的缩放比例,并确保这个比例不会超过
LOCATE_MAX_SCALE
,以防止过度放大。最后,我们应用计算出的缩放比例和偏移量,使页面元素居中显示,并加入一个动画过渡效果。动画完成后,我们移除过渡效果,避免影响后续的拖拽操作。
居中定位计算
为了使定位后的页面元素居中显示,我们需要重新计算视口位置,使可视区域居中显示:
我们将其拆开来进行看:
举个例子:
假设视口宽度是 1000px,那么视口中心点是 500px,内容最左边(
minX
)是 100px,内容总宽度(viewWidth
)是 400px,那么内容的中心点就是 100 + 400 / 2 = 300px
。假设缩放比例是 1.5,那么缩放后的内容中心点是
300 * 1.5 = 450px
。所以需要的偏移量是
500 - 450 = 50px
。(正值表示向右移动,负值表示向左移动)初始化视口定位
因为所有的
pages
所处的位置使用默认偏移视口的话可能并不能展示完整。现在有了定位函数,我们就可以将所有的
pages
塞入进去,重新计算一个可见视口位置。Page 定位
接着再来给
Page
添加定位,这里使用双击的方式去定位 Page
。这里我们为
Page
添加了定位,同时在双击画布的时候去重置视口。因为两边都添加了双击事件,所以需要对
Page
的双击做额外的处理,来防止上层画布的事件触发:完整代码
完整代码
最终效果展示
总结
通过本文,我们学习如何实现一个具有拖拽、缩放和定位功能的简易画布。对于画布上的基础功能其实都知道该怎么去实现。
所以本文主要的难点或者说优化点在于:
- 缩放的时候利用位移补偿计算,来确保在缩放时鼠标位置与视图保持一致
- 定位功能和其居中定位计算,来提升体验,同时可以初始化自适应视口。
- 作者:JinSo
- 链接:https://jinso365.top/15bcee950faf80af9c6ac98506abdeb7
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。