体验地址: http://1.14.12.55:8882/(已过期)
目录结构解释
assetsLoader
用来加载需要用到的静态资源,目前主要用于加载图片(输出ImageBitmap)。
ImageBitmap接口表示能够被绘制到<canvas>上的位图图像,具有低延迟的特性。运用Window.createImageBitmap()或WorkerGlobalScope.createImageBitmap()工厂方法模式,它可以从多种源中生成。ImageBitmap提供了一种异步且高资源利用率的方式来为 WebGL 的渲染准备基础结构。
behaviorTasks
维护一个Map结构的behaviorTasks实现指定时间间隔触发某个行为,也能勉强用来实现关键帧。
// 指定时间间隔触发器的例子
behaviorTasksInstance.addBehavior<number | undefined>(
"demo",
() => {
return 1
},
(frequency) => {
if (frequency) {
// console.log(frequency);
// todo 改变某个值
}
},
1000 / 0.1,
false,
); // 0.1 次/秒
canvasTool
画布上的一些工具存放在此。目前实现了zoomTool用作画布的缩放操作控件。
class ZoomTool {
private static instance: ZoomTool;
private toolElement: HTMLDivElement | null = null;
private readonly handleToolClickBind: ((e: MouseEvent) => void);
private readonly onTransformStateChangeHandlerBind: (() => void);
static getInstance(): ZoomTool {
if (!ZoomTool.instance) {
ZoomTool.instance = new ZoomTool();
}
return ZoomTool.instance;
}
constructor() {
this.handleToolClickBind = this.handleToolClick.bind(this);
this.onTransformStateChangeHandlerBind = this.onTransformStateChangeHandler.bind(this);
}
...........
}
container
用于主容器的相关操作,重置画布大小。维护containerTransformState画布映射状态,如缩放,移动。
export interface ContainerTransformState {
lastX: number;
lastY: number;
offsetX: number;
offsetY: number;
scale: number;
}
contextMenu
画布内右键菜单的封装
core
init函数初始化画布、数据中心、右键菜单、画布组件、静态资源、注册全局事件、主渲染方法、注册图形并返回外界与画布交互的一些方法。
export async function init(
target: HTMLElement,
fps = 30,
): Promise<OperateFunc> {
MY_CANVAS.cvs = document.createElement('canvas')
MY_CANVAS.cvs.setAttribute('id', 'zx-drag-canvas');
MY_CANVAS.cvs.style.display = 'block'
MY_CANVAS.cvs.style.backgroundColor = MY_CANVAS_BG
MY_CANVAS.pen = MY_CANVAS.cvs.getContext('2d')
graphicUtilsInit()
ContainerInstance = new Container(target.clientWidth, target.clientHeight, MY_CANVAS)
target.appendChild(MY_CANVAS.cvs)
await AssetsLoader.load().catch(console.error)
const func = initGraphicInstances(MY_CANVAS, instances)
initRender(fps);
registerAllEvents(MY_CANVAS.cvs)
menu.init(instances);
menu.generateContextMenuItem(func);
zoomTool.init()
return func
}
eventCenter
处理和分发画布中的一些交互事件
graphic
画布上不同图形的管理合集
circle
圆形布局的绘制方法和更新方法
matrix
行列布局的绘制方法和更新方法
strip
矩形布局的绘制方法和更新方法
render
用于管理 Canvas 渲染循环的核心模块
class Render {
cvs: HTMLCanvasElement;
$: CanvasRenderingContext2D;
fps: number;
fpsInterval: number;
lastRenderTime: number;
frame: number | undefined;
constructor(fps: number, context: Canvaser) {
if (!context.cvs || !context.pen) {
throw new Error("Canvaser context is incomplete.");
}
this.cvs = context.cvs!;
this.$ = context.pen!;
this.fps = fps;
this.fpsInterval = 1000 / this.fps;
this.lastRenderTime = performance.now();
}
run(instances: RenderTargetInstances) {
this.frame = window.requestAnimationFrame(this.run.bind(this, instances));
let currentTime = performance.now();
let elapsed = currentTime - this.lastRenderTime;
if (elapsed > this.fpsInterval) {
this.lastRenderTime = currentTime - (elapsed % this.fpsInterval);
this.clearScreen();
this.renderGrid();
this.renderInstances(instances);
}
this.processBehavior(currentTime);
}
clear() {
this.frame && window.cancelAnimationFrame(this.frame);
delBehaviorAll()
}
private clearScreen() {
this.$.clearRect(0, 0, this.cvs.width, this.cvs.height);
}
private renderGrid() {
drawGrid(this.$, this.cvs.width, this.cvs.height);
}
private renderInstances(instances: RenderTargetInstances) {
for (const key in instances) {
instances[key]?.draw?.();
}
}
private processBehavior(time: number) {
if (behaviorTasksInstance.getBehaviorTasksSize) {
behaviorTasksInstance.behaviorProcess(time);
}
}
}
this.processBehavior(currentTime);调动behaviorTasks中注册的事件。
runtimeStore
整合画布所有状态的容器
type RuntimeState = {
highlightElements: boolean
currentDragEl: Element | null
cvs: HTMLCanvasElement | null;
containerTransformState: ContainerTransformState
graphicMatrix: Graphic
groupTree: RBush<RBushGroupItem>
canvasState: CanvasState
};
export interface Graphic {
groups: Record<GroupType, Record<string, Group>>; // 组ID与组的MAP
elements: Record<string, Element>; // 元素ID与元素的MAP
groupElements: Record<string, string[]>; // 组id与元素id[]的MAP
// groupElementsMatrix: Record<string, Element[][]>; // 方便矩阵操作更直观
}
transform
画布缩放移动后,屏幕坐标 <--> 逻辑坐标的转换。
一些关键帧动画的实现。
utils
一些工具函数。
使用方式
以VUE为例
<div
class="wrapper"
id="zx-drag-canvas-wrapper"
@contextmenu.prevent
ref="wrapperRef"
:style="{ width: seatingPersonnelOpen ? `calc(100% - 450px)` : '100%' }"
></div>
const cFuncClickMenuHandler = (type: string, ags: string) => {
const data = JSON.parse(ags);
currentOperatingPosition.value = ags;
switch (type) {
case 'insert': // 插入座位
break;
case 'newPersonnelAdded': // 新增人员
break;
case 'selectFromPersonnelPool': // 从人员库选择
break;
case 'areaEditing': // 区域编辑
break;
case 'delPersonnel': // 删除人员
cFunc?.contextMenuOperateFunc.updateElementBusinessState(
data.element.id,
'',
'',
'idle'
);
break;
case 'deleteSeat': // 删除座位
cFunc?.contextMenuOperateFunc.updateElementBusinessState(
data.element.id,
'',
'',
'idle'
);
break;
case 'elementChanged': // 交换座位
break;
case 'deleteGroup': // 删除区域
break;
}
};
const resizeHandler = (entries: ResizeObserverEntry[]) => {
const entry = entries[0];
if (entry && entry.contentRect) {
const { width, height } = entry.contentRect;
resize(width, height);
}
};
const resizeThrottleHandler = throttle(resizeHandler, 300);
const resizeObserver = new ResizeObserver(resizeThrottleHandler);
onMounted(async () => {
if (wrapperRef.value) {
cFunc = await init(wrapperRef.value, 60);
resizeObserver.observe(wrapperRef.value);
cFunc!.clickMenu(cFuncClickMenuHandler);
}
});
onBeforeUnmount(() => {
if (wrapperRef.value) {
resizeObserver.unobserve(wrapperRef.value);
exit();
cFunc = null;
}
});
总结
此示例中,所有事件采用发布订阅管理。菜单、画布组件等采用单例模式实现。画布状态的维护一些需要被其它地方知道的状态,会采用发布订阅。图形的状态则使用JS Object 引用类型的特征实现。
图形在渲染过程中将原坐标转为屏幕坐标const [x, y] = canvasToScreen(pos.x, pos.y)
以上项目结构,后续新增画布组件,新增图形,新增右键菜单的功能实现成本较低。
由于不爱写注释的习惯,代码中缺少注释并且目前代码结构存在部分不合理。