react 无限画布难点和实现

实现本质

超大固定画布 + 可移动窗口 = 模拟无限画布

方法

首先先了解渲染过程

复制代码
JavaScript -> style -> layout -> paint -> composition

(一般来说做这个都是为了业务开发)

  1. ❌ 动态更新position ------ 更新 x y(也就是容器的left top)
    • 修改拖拽触发reflow
      • 如使用 cahrts.forEach(c => {c.style.top = `${chart.originTop + deltaX}px`})会导致所有图表重新渲染
      • 同上,需要维护每个图标的原始位置,更新的话同时也要更新所有图的位置(即使其他图标并没有被拖拽)
    • 需要管理状态 Drag(图) Resize(图) Pan(幕布) Resize(幕布)
    • 从style开始重新排布,渲染效率低下
  2. ✅(建议)CSStransformation ------ 移动整个容器
    • 仅仅移动画布本身,chart作为子元素跟随,仅仅触发composite层渲染
    • 此时仅针对Canvas管理 offset,zoom两个变量
    • React可以状态控制UI <div style-{````{ translate : `translate(${x}px, ${y}px) scale(${zoom})`}}>
    • 包含了chart本身的容器可以继续塞入新的dom元素(如toolbar)
  3. ❌ Canvas 像素级别重新绘制
    • 像素绘制没有dom结构,所有操作都需要手动计算 x y
    • 且需要手动判断(重写所有组件)不支持第三方库,开发成本高且难以维护
    • 合适游戏、百万级数据可视化,不合适业务交互

架构设计

基本属性

js 复制代码
const canvsSize = {
	width:2000,
	height:1500
	// rechart位置百分比 * 固定尺寸 = 不变的像素位置
}

const [zoom, setZoom] = useState(1)
const [offset, setOffset] = useState({x:0, y:0}) // pi
const [isPanning, setIsPanning] = useState(false) // drag

不要搞响应式画布width:window.innerWidth * 2,不然画布大小改变会导致chart自动放缩

dom层次

js 复制代码
<div class='canvas'>
	// 头部工具栏
	<div class='canvas-header'></div>
	// main body
	<div class='canvas-body'
		ref={containerRef}
		onMouseDown={handleCanvasPanStart} //capture dragging events
	>
		<div class = 'canvans-container'
			ref={canvasRef}
			style={{
				transform:`translate(${canvasOffset.x}px, ${canvasOffset.y}px, scale(${canvasZoom})`
			}}
			// internal canvas for applying transform
		>
			<ChartCard chart={chart1}/>
			<ChartCard chart={chart2}/>
			
		</div>
	</div>
	
<div>

body 负责滚轮缩放拖拽

container接受transform的变换

chartcard 内部的chart absolute定位在画布内部

步骤实现

复制代码
mousedown(空白位置记录起始位置) -> mousemove 拖拽 -> moseup 结束监听器 

Pan代码

js 复制代码
const handleCanvasPanStart = (e) = {
	//1. only invoke pan when clicking empty room
	if(e.target == containerRef.current || e.target == canvasRef.current) {
	// canvas initial states
	const startX = e.clientX // mouse X
	const startY = e.clentY // mouse Y
	const startOffsetX = canvasOffset.x 
	const startOffsetY = canvasOffset.y
	
	//2. handle move event
	const handlePanMove=(moveE) => {
		const deltaX = moveE.clientX - startX;
		const deltaY = moveE.clientY - startY;
		setCanvasOffset({
			x: startOffsetX + deltaX,
			y: startOffsetY + deltaY
		})
	}
	//3. handle dragging end
	const handlePanEnd = ()=>{
		setIsPanning(false)
		document.removeEventListener('mousemove', handlePanMove)
		document.removeEventListener('mouseup' handlePanEnd)
	}
	// bind up listeners to documents instead of elements
	document.addEventListener('mousemove', handlePanMove)
	document.addEventListener('mouseup' handlePanEnd)



	}
}

流程

  1. DOM 绑定 start 函数;
  2. 点击时触发 start,start 给 document 绑定 move 和 end 的监听;
  3. 移动时,浏览器自动传事件参数(含坐标)给 move;
  4. 松开鼠标时,end 解绑监听。

滚动放缩

js 复制代码
const handleWheel = (e)=>{
	if(e.shiftKey){
		e.preventDefault();//阻止默认的wheel 滚动
		const delta = -e.deltaY * 0.001 // 滚轮增量变为缩放增量
		setCanvasZoom(prev => {
			return Math.max(0.1, Math.min(3, prev + delta) //限制0.1 - 3
		})
	}
}

transform应用

复制代码
{
transform:`translate(${canvasOffset.x}px, ${canvasOffset.y}px, scale(${canvasZoom}),
transformOrigin:'0 0',
}

基于左上角进行x y的点位放缩+平移

逻辑顺序 1. 先移动 2. 后缩放

双坐标

存储层:百分比坐标

渲染层: 转换成像素坐标

  • 屏幕坐标
    • 鼠标事件 client x y
    • 相对窗口左上角
  • 画布坐标
    • transform放到像素上
  • 保存成百分比坐标到数据库

转换公式

js 复制代码
// render percentage -> pixels
const left = chart.position_x / 100 * containerWidth
const top = chart.position_y/100 * containerHeight
const width = chart.position_x/100 * containerWidth
const height = chart.position_y/100 * containerHeight

//storage delta pixel -> delta percentage
const deltaX = moveE.clentX - startX
const deltaPercentageX = deltaX / canvasWidth

//update
onUpdate({
   ...chart,
   position_x:startPosX + deltaPercentX,
   position_y:startPosY + deltaPercentY
})

resize handle (四个角四个边)

八方向

复制代码
nw -- n -- ne
|			|
w			e
|			|
sw -- s -- se

边缘手柄css

复制代码
resize-n{top: 0; left 50%; width:8px; height 4px; cursor ns-resize;}
resize-ne{top: 0; left 0; width:8px; height 4px; cursor nesw-resize;}

函数实现

js 复制代码
const handleResizeMove = (e) => {
	//..calculate delta
	...
	// init new position
	// new的初始化就是start各个top left width height

	if(direction.includes('e'){
		newWidth = Math.max(100, startWidth + deltaX)
	}
	...

	if(direction.includes('w)){
		newWidth = Math.max(100, startWidth - deltaX)
		newLeft = startLeft + (startWidth - newWidth)
	}
}

// storage逻辑
newX = (newLeft/containerWidht)*100
newW = (newWidth/containerWidth)*100
//同时调用api更新到后端
//此时update完毕 rechart重新渲染
相关推荐
To_OC3 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic4 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy4 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志4 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble5 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯6 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜7 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司7 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g8 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森8 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http