类似 Figma 这样强大的无限画布,本身是用 WebAssembly 技术实现的,复刻起来难度太大, 如果只想要一个基于传统 HTML & CSS & Javascript 的版本,可以快速使用和修改,可以做到吗?
Heptabase 也有类似这样的无线画布,现在的需求是做一个通用的无线画布组件,其主要特点就是通常响应 drag 的操作为主, 控制画布缩放、移动,控制内部的节点移动和变换等等。
不考虑节点拖动的前提下,支持触摸板的手势交互,基本操作方面:
- 用滚轮来控制画布移动
- 用
Control
键配合滚动来缩放画布 - 按住空格键配合左键拖动画布
查看这个源码文件,其中引入了 React、Tailwind、react-use 几个工具库,很简单的 demo,你可以在安装好依赖后改改这个文件试试,看看能不能做出你想要的效果。
文件虽然有 200 行,其核心工作只有 onWheel
的部份,不过考虑到鼠标用户只能 Y 轴滚动,需要额外添加一个空格键来配合左键拖动,如此一来就可以全方位拖拽。
看 Pointer 事件代码,先确认用户是想要缩放还是移动?缩放是需要按下 Ctrl
键的,移动是按下 Space
键的,
缩放本身很简单,直接设置 scale 的值即可。
const handleScale = deltaY => {
let ratio = -deltaY
scaleMark.current += ratio * 0.015
setScale(scaleMark.current)
}
// 伪代码,实际代码请参考 Github 源文件
需要注意的是,在 React 里设置 setScale 不是一件“立刻生效” 的事,对于别处可能会用到 scale 变量来参与计算时, 就会因为数据的更新不及时变成一个 Bug,因此,对于这种要参与计算的值,应该先赋给一个 ref 变量,而每次 state 更新总是读取这个 ref 变量来更新, 这样就可以做到保证数据时效性、一致性的同时,正常 render 渲染。
到这虽然实现了缩放功能,但是有个大问题:没有用上缩放原点。 事实上应该在缩放之前,先根据鼠标的位置计算出缩放原点,这样缩放时才能做到缩放中心不变。
这件事是在 updateOrigin
函数中实现的,过程如下:
- 读取
Event
给出的event.clientX
和clientY,这个是鼠标相对于整个网页的位置
- 将上述 xy 转换成画板内部的坐标点位,记作
coord
- 将
coord
再同本来的缩放原点origin
相加,就得到本次Event
正确的缩放原点 - 由于
origin
发生了变化,因此需要重新计算画板的偏移量,记作offset
,计算方法见代码
改完
origin
之后为什么要改offset
呢?
因为
css
中transform
的origin
offset
两个值是相互作用的,如果用户的鼠标从左上角移动到右下角, 就明显是一个非常巨大的origin
改变,此时如果offset
不做任何调整,用户的观感就是画面突然跳飞了特别远, 因此,offset
这个值优先服务给css
(而不是人阅读),是一个紧密配合origin
和scale
的参数,目的是为了配合用户的交互,让用户感觉画面的移动是自然的。
如此一来,onWheel
内部的计算就成了一个有机结合的整体,offset, scale, origin
三方互相影响,每次计算都要考虑到三方的变化,就可以正常实现缩放功能了。
有了 updateOrigin
来控制原点,offset
功能也就简单了,只需要根据用户的移动设置 offset
即可。
const handleMove = e => {
let moveX = e.deltaX * 0.15
let moveY = e.deltaY * 0.15
let next = {
x: offsetMark.current.x - moveX,
y: offsetMark.current.y - moveY,
}
offsetMark.current = next
}
同时,也要调用一次 updateOrigin
来确保 origin
值的正确性,同时确保 setOffset 确实改变了其值,render 也就随之刷新。
这样一来,可以随意放大、缩小、移动的画板就实现了。 自行实现这个功能稍显麻烦,后面如果要添加移动节点、连线、选中等功能也很复杂,想降低开发成本且开箱即用的话,可以考虑使用下面这些库:
- draggable-board,正在开发中,请期待。
- react-flow,成熟、美观的开源库。