类似 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的originoffset两个值是相互作用的,如果用户的鼠标从左上角移动到右下角, 就明显是一个非常巨大的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,成熟、美观的开源库。