基于Dom的无限画布实现方案
针对无限画布这个场景,在业界figma的画布交互体验是公认的比较好的。不过figma
采用的是canvas
方案。本文介绍基于Dom的无限画布实现方案。
主体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <InfiniteCanvas> <Renders> <Layouter> <ItemRender/> </Layouter> </Renders> <Designer> <SelectedBox/> <LayoutHintLine/> <ContentMenu/> <SelectBox/> <DragOverMask/> <Designer/> <Controller/> </InfiniteCanvas>
|
核心属性
1 2 3 4 5 6 7 8 9 10 11
| canvasTransform: { scale: 1, translateX: 0, translateY: 0, canvasOffsetX: 0, canvasOffsetY: 0, }
|
布局方式
根据画布的移动属性计算画布内对应元素的位置和缩放比例,根据计算结果绘制各元素。
Q: 为什么计算画布内对应元素的位置而不是计算画布的位置对画布进行整体平移?
A: 画布的宽高需要固定为100%。一是为了方便挂载具体事件处理方法。二是为了方便计算新元素进来的时候新元素的布局信息。
布局算法
平移
1 2 3
| translateX += dx translateY += dy
|
缩放
1 2 3 4 5 6 7 8 9 10 11 12
|
originDx = (transformOriginX - canvasOffsetX) / scale; originDy = (transformOriginY - canvasOffsetY) / scale;
dx = originDx * deltaScale; dy = originDy * deltaScale;
canvasOffsetX += dx; canvasOffsetY += dy;
|
布局
1 2
| x = itemLeft * scale + canvasOffsetX + translateX; y = itemTop * scale + canvasOffsetY + translateY;
|
自适应画布
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
if (itemCollectionWidth < width && itemCollectionHeight < height) { scale = 1; canvasOffsetX = -itemCollectionLeft canvasOffsetY = -itemCollectionTop } else { const scaleX = width / (itemCollectionWidth); const scaleY = height / (itemCollectionHeight); if (scaleX < scaleY) { scale = scaleX; canvasOffsetX = -itemCollectionLeft * scaleX; canvasOffsetY = (height - itemCollectionHeight * scaleX) / 2 - itemCollectionTop * scaleX; } else { scale = scaleY; canvasOffsetX = (width - itemCollectionWidth * scaleY) / 2 - itemCollectionLeft * scaleY; canvasOffsetY = -itemCollectionTop * scaleY } } }
|
各模块功能
InfiniteCanvas
提供画布容器。
Renders
渲染器 为画布中的各元素提供容器
1 2 3 4 5 6 7 8 9 10 11
| <template> <Layouter v-for="(item, index) in items" :canvasTransform="canvasTransform" :key="`${item.uuid}`" :layout="item.layout" :index="index" > <slot name="ItemRender"></slot> </Layouter> </template>
|
Layouter
布局器 根据布局信息 渲染容器
1 2 3 4 5 6 7
| <template> <div :style="layout(layout, canvasTransform)" > <slot></slot> </div> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
export const changeLayoutByCanvasTransform = function ( layout, canvasTransform ) { const { x, y } = layout; const { translateX, translateY, scale, canvasOffsetX, canvasOffsetY } = canvasTransform; const left = x * scale + canvasOffsetX + translateX; const top = y * scale + canvasOffsetY + translateY; return { left, top, }; };
layout(layout, canvasTransform) { if (!layout) return {}; const { left, top } = changeLayoutByCanvasTransform( layout, canvasTransform ); const style = { left: left + "px", top: top + "px", transform: `scale(${canvasTransform.scale})`, position: "absolute", "z-index": layout.z, }; return style; },
|
ItemRender
1 2 3
| <template> <item /> </template>
|
Designer
设计器为画布的交互和事件提供容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <SelectedBox :canvasTransform="canvasTransform" :xgChartsPackage="xgChartsPackage" /> <DragOverMask :width="config.initChartWidth" :height="config.initChartHeight" :showDragOverMask="showDragOverMask" :dragOnPosition="dragOnPosition" :canvasTransform="canvasTransform" /> <LayoutHintLine :vLine="vLine" :hLine="hLine" :canvasTransform="canvasTransform" /> <ContentMenu v-show="contextMenuVisible" :actionList="contentMenuActionList" :contextMenuPosition="contextMenuPosition" /> <SelectBox :show="showSelectBox" :position="selectBoxPosition" /> </template>
|
SelectedBox
选中元素的提示框
1 2 3 4 5 6 7 8
| <template> <div :style="selectBoxStyle" > <div :class="SELECT_BORDER"></div> <div :class="RESIZE_ICON"></div> </div> </template>
|
LayoutHintLine
拖拽时 元素的对齐指示线和吸附
SelectBox
刷选元素时的范围提示框
DragOverMask
新增元素时的提示框
Controller
控制器
右键菜单