🏰实现一个简易的可拖拽缩放画布
00 分钟
2024-12-16
2024-12-15
type
status
date
slug
summary
tags
category
icon
password
Edited
Dec 15, 2024 03:11 PM
Created
Dec 13, 2024 02:19 PM
最近,我遇到了一个的需求:设计并实现一个具备拖拽和缩放功能的画布,这在许多设计软件中是一个常见的功能,比如蓝湖等。在这篇文章中,我将分享一种简单而高效的方法来构建这样的画布功能。
画布将具备以下核心功能:
  • 拖拽:允许自由移动画布内的元素。
  • 缩放:使用鼠标滚轮来放大或缩小画布视图。
  • 定位:快速将视图定位到画布上的特定元素。
notion image
 

在线演示

为了让您更直观地体验这个拖拽缩放画布的功能,我已经将其完整实现并部署在StackBlitz平台上。您可以通过以下链接直接访问并尝试这个交互式的画布示例:
 
点击上面的链接,您将能够看到画布的实际效果,并可以编辑代码来探索其工作原理。
此外,如果您想要嵌入这个示例到您的项目中,以下链接提供了一个嵌入式的版本:
 

实现

概览

在深入实现细节之前,让我们先来审视一下整体的布局代码,这是构建我们可拖拽缩放画布的基础。
  • transformOrigin: '0 0':我们给画布设置了transformOrigin属性为'0 0',这确保了所有的变换(如拖拽和缩放)都是基于画布的左上角进行的,这与事件的x,y坐标系相匹配,使得逻辑更加直观。
  • PageItem:我们定义了一个PageItem接口来描述每个页面元素的属性,包括标题和位置尺寸信息。这些属性将在后续的拖拽和缩放功能中被用到。
 
下面是我们的画布布局的截图,展示了页面元素的初始布局:
notion image
 
接下来,我们将逐步实现拖拽和缩放功能。从简单的拖拽开始,逐步深入到缩放功能的实现。
 

拖拽

拖拽是用户与画布交互的基础功能之一,它允许用户自由移动画布上的内容。
这个功能非常的常见和通用,直接这一功能的详细代码和解释:
通过 transformRef 来存储画布的偏移量信息,避免在每次事件触发时都查询DOM元素,提高性能。
 

缩放

缩放功能允许用户通过Ctrl + 滚轮操作来放大或缩小画布。这一功能对于查看细节或调整整体布局非常有用。
 

阻止浏览器默认行为

首先,我们需要阻止浏览器对Ctrl + 滚轮的默认缩放行为,以确保我们的画布可以正确响应缩放操作。
 
接下来,我们实现缩放功能。我们定义了一些常量来控制缩放的行为:
  • WHEEL_RATIO:每次缩放的基数。
  • WHEEL_MAX_SCALE:最大缩放倍数。
  • WHEEL_MIN_SCALE:最小缩放倍数。
 

去除画布偏移值

首先,我们需要计算鼠标相对于画布左上角的位置,这需要去除画布本身的偏移值:
这两行代码的目的是为了去除画布本身的偏移值,因为画布可能并不总是从浏览器视窗的左上角开始。
 
接着,我们需要思考一个问题,如何在缩放的时候,实现鼠标跟手的状态。
因为缩放后,画布整体进行了放大,但是位置还是原本的位置,这时候就会出现鼠标不跟手的清空,或者说画面出现了抖动。
所以要时候鼠标跟手,这里就需要对原本的偏移值进行额外的处理了。
 

位移补偿计算

位移补偿的目的是确保在缩放时,鼠标位置与视图保持一致。
 
让我们先来了解了解概念:
  1. 为什么需要位移补偿?
如果我们只是简单地改变 scale 而不进行位移补偿:
如果我们只是简单地改变scale而不进行位移补偿,画布会以左上角(0,0)为中心点进行缩放,导致鼠标下的内容会向外"逃离"鼠标位置。位移补偿确保了缩放中心点在鼠标位置。
 
  1. 补偿公式分解
这是最关键的部分:
  • (mouseX - transformRef.current.x) 表示鼠标位置到当前变换原点的距离
  • (1 - newScale/oldScale) 是一个补偿因子
 
  1. 示例
假设有一个场景:
 
缩放前
缩放后(2倍)
如果不补偿,50px会变成100px,鼠标下的点会偏离,补偿-50px后,确保鼠标位置下的点保持不动。
 
  • 放大时(newScale > oldScale):补偿因子为负,产生向原点的位移,抵消了放大造成的距离增加。
  • 缩小时(newScale < oldScale):补偿因子为正,产生远离原点的位移,抵消了缩小造成的距离减少。
 
缩放功能的实际效果展示:
notion image
 

优化缩放和定位

到目前为止,我们实现了缩放和定位,但是有个很严重的问题,就是如果我们的画布被缩放的太小,或者拖拽的太远,导致我们触发不了事件了,就会出现问题。
为了解决这个问题,我们在画布上层添加了一层蒙版(viewportRef),专门用来接收事件,并将处理结果回显到画布上。
通过将事件监听器绑定到viewportRef而不是canvasRef,我们可以确保即使画布移动或缩放,事件也能被正确触发。以下是优化后的代码实现:
 
通过这种方式,我们确保了即使画布被缩放或移动,用户的操作也能被正确响应。以下是优化后的效果展示:
notion image
 

定位

为了实现快速定位到画布内特定Page的功能,我们需要利用之前定义的PageItem.rect属性,这些属性包含了页面元素的位置信息。
我们定义了一个locate方法来实现定位功能,它接收两个参数:需要定位的Page数组和定位动画的时间长度。
 
在实现定位功能时,我们首先需要确定需要定位的页面元素(pages)的整体位置和尺寸,以便计算出合适的缩放比例和偏移量,确保这些元素能够在视口中完全展示并居中定位。 首先,我们根据提供的pages数组,计算出所有页面元素的边界坐标minXmaxXminYmaxY
再利用上面计算出的边界坐标,我们可以得到整体可视区域的宽度(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 塞入进去,重新计算一个可见视口位置。
 
notion image
 

Page 定位

接着再来给 Page 添加定位,这里使用双击的方式去定位 Page
这里我们为 Page 添加了定位,同时在双击画布的时候去重置视口。
因为两边都添加了双击事件,所以需要对 Page 的双击做额外的处理,来防止上层画布的事件触发:
 
notion image
 

完整代码

完整代码
 

最终效果展示

notion image
 

总结

通过本文,我们学习如何实现一个具有拖拽、缩放和定位功能的简易画布。对于画布上的基础功能其实都知道该怎么去实现。
所以本文主要的难点或者说优化点在于:
  • 缩放的时候利用位移补偿计算,来确保在缩放时鼠标位置与视图保持一致
  • 定位功能和其居中定位计算,来提升体验,同时可以初始化自适应视口。
上一篇
我的2024年度总结:工作的第一年
下一篇
理解 Signal 是如何工作的

评论
Loading...