当前位置: 首页 > news >正文

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
    • 且需要手动判断(重写所有组件)不支持第三方库,开发成本高且难以维护
    • 合适游戏、百万级数据可视化,不合适业务交互

架构设计

基本属性

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层次

<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代码

const handleCanvasPanStart = (e) = {//1. only invoke pan when clicking empty roomif(e.target == containerRef.current || e.target == canvasRef.current) {// canvas initial statesconst startX = e.clientX // mouse Xconst startY = e.clentY // mouse Yconst startOffsetX = canvasOffset.x const startOffsetY = canvasOffset.y//2. handle move eventconst handlePanMove=(moveE) => {const deltaX = moveE.clientX - startX;const deltaY = moveE.clientY - startY;setCanvasOffset({x: startOffsetX + deltaX,y: startOffsetY + deltaY})}//3. handle dragging endconst handlePanEnd = ()=>{setIsPanning(false)document.removeEventListener('mousemove', handlePanMove)document.removeEventListener('mouseup' handlePanEnd)}// bind up listeners to documents instead of elementsdocument.addEventListener('mousemove', handlePanMove)document.addEventListener('mouseup' handlePanEnd)}
}

流程

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

滚动放缩

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放到像素上
  • 保存成百分比坐标到数据库

转换公式

// 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;}

函数实现

const handleResizeMove = (e) => {//..calculate delta...// init new position// new的初始化就是start各个top left width heightif(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重新渲染
http://www.dtcms.com/a/490563.html

相关文章:

  • 网站开发浏览器wordpress投票类主题
  • Qt_day2
  • DMXAPI |使用1个Key接入主流大模型
  • 三星企业网站建设ppt网站建设需要使用哪些设备
  • 23种设计模式——中介者模式 (Mediator Pattern)详解
  • iOS八股文之 RunLoop
  • zibbix
  • Macbook数据恢复 Disk Drill
  • 公司招聘一个网站建设来做推广制作表情包的软件
  • WebSocket实时通信:Socket.io
  • xml方式bean的配置---实例化bean的方式
  • 212. Java 函数式编程风格 - Java 编程风格转换:命令式 vs 函数式(以循环为例)
  • Ubuntu 24.04 修改 ssh 监听端口
  • 1千元以下做网站的公司wordpress sso插件开发
  • Pytorch神经网络工具箱
  • PyTorch DataLoader 高级用法
  • 怎么做一个网站app吗金华网站建设价格
  • 芷江建设局网站石家庄网站建设公司黄页
  • Excel表----VLOOKUP函数实现两表的姓名、身份证号码、银行卡号核对
  • XMLHttpRequest.responseType:前端获取后端数据的一把“格式钥匙”
  • office便捷办公06:根据相似度去掉excel中的重复行
  • Vue+mockjs+Axios 案例实践
  • http的发展历程
  • Python中使用HTTP 206状态码实现大文件下载的完整指南
  • AngularJS下 $http 上传文件
  • 如何弄死一个网站锡林郭勒盟建设工程造价管理网站
  • 【Node.js】为什么擅长处理 I/O 密集型应用?
  • 基于SpringBoot的无人机飞行管理系统
  • STM32的HardFault错误处理技巧
  • Tekever-固定翼无人机系统:模块化垂直起降、远程海上无人机、战术 ISR 无人机