在不少培训、协做、在线演讲的场景下,咱们须要有电子白板的功能,可以方便地在演讲者与听众之间共享屏幕、绘制等信息。fc-whiteboard https://parg.co/NiK 是 Web 在线白板组件库,支持实时直播(一对多)与回放两种模式,其绘制版也可以独立使用。fc-whiteboard 内置了 EventHub,只须要像 Mushi-Chat 这样提供简单的 WebSocket 服务端,便可快速构建实时在线共享电子白板。javascript
直播模式的效果以下图所示:html
示例代码请参考 Code Sandbox,或者直接查看 Demo;前端
import { EventHub, Whiteboard, MirrorWhiteboard } from 'fc-whiteboard'; // 构建消息中间件 const eventHub = new EventHub(); eventHub.on('sync', (changeEv: SyncEvent) => { console.log(changeEv); }); const images = [ 'https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240', 'http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240', 'http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240' ]; // 初始化演讲者端 const whiteboard = new Whiteboard( document.getElementById('root') as HTMLDivElement, { sources: images, eventHub, // Enable this option to disable incremental sync, just use full sync onlyEmitSnap: false } ); whiteboard.open(); // 初始化镜像端,即观众端 const mirrorWhiteboard = new MirrorWhiteboard( document.getElementById('root-mirror') as HTMLDivElement, { sources: images, eventHub } ); mirrorWhiteboard.open();
WebSocket 自然就是以事件驱动的消息通讯,fc-whiteboard 内部对于消息有比较好的封装,咱们建议使用者直接将消息透传便可:java
const wsEventHub = new EventEmitter(); if (isPresenter) { wsEventHub.on('sync', data => { if (data.event === 'finish') { // 单独处理结束事件 if (typeof callback === 'function') { callback(); } } const msg = { from: `${currentUser.id}`, type: 'room', to: `${chatroom.room_id}`, msg: { type: 'cmd', action: 'whiteboard/sync', message: JSON.stringify(data) } }; socket.sendMessage(msg); }); } else { socket.onMessage(([data]) => { const { msg: { type, message } } = data; if (type === 'whiteboard/sync') { wsEventHub.emit('sync', JSON.parse(message)); } }); }
fc-whiteboard 还支持回访模式,即咱们能够将某次白板操做录制下来,能够一次性或者分批将事件传递给 ReplayWhiteboard,它就会按序播放:python
import { ReplayWhiteboard } from 'fc-whiteboard'; import * as events from './events.json'; let hasSend = false; const whiteboard = new ReplayWhiteboard(document.getElementById( 'root' ) as HTMLDivElement); whiteboard.setContext(events[0].timestamp, async (t1, t2) => { if (!hasSend) { hasSend = true; return events as any; } return []; }); whiteboard.open();
The persistent events are listed as follow:react
事件的基本结构以下所示,具体的事件类别咱们会在下文介绍:git
[ { "event": "borderSnap", "id": "08e65660-6064-11e9-be21-fb33250b411f", "target": "whiteboard", "border": { "id": "08e65660-6064-11e9-be21-fb33250b411f", "sources": [ "https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240", "http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240", "http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" ], "pageIds": [ "08e65661-6064-11e9-be21-fb33250b411f", "08e6a480-6064-11e9-be21-fb33250b411f", "08e6cb91-6064-11e9-be21-fb33250b411f" ], "visiblePageIndex": 0, "pages": [ { "id": "08e65661-6064-11e9-be21-fb33250b411f", "markers": [] }, { "id": "08e6a480-6064-11e9-be21-fb33250b411f", "markers": [] }, { "id": "08e6cb91-6064-11e9-be21-fb33250b411f", "markers": [] } ] }, "timestamp": 1555431837 } ... ]
Drawboard 也能够单独使用做为画板,总体能够被导出为图片:github
<img id="root" src="https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></img>
import { Drawboard } from 'fc-whiteboard/src'; const d = new Drawboard({ imgEle: document.getElementById('root') as HTMLImageElement }); d.open();
fc-whiteboard 的内部组件级别,依次是 WhiteBoard, WhitePage, Drawboard 与 Marker,本节即介绍内部设计与实现。web
绘制能力最初改造自 markerjs,在 Drawboard 中提供了基础的画板,即 boardCanvas 与 boardHolder,后续的全部 Marker 即挂载于 boardCanvas 中,并相对于其进行绝对定位。当咱们添加某个 Marker,即执行如下步骤:算法
const marker = markerType.createMarker(this.page); this.markers.push(marker); this.selectMarker(marker); this.boardCanvas.appendChild(marker.visual); // 定位 marker.moveTo(x, y);
目前 fc-whiteboard 中内置了 ArrowMarker, CoverMarker, HighlightMarker, LineMarker, TextMarker 等多种 Marker:
export class BaseMarker extends DomEventAware { id: string = uuid(); type: MarkerType = 'base'; // 归属的 WhitePage page?: WhitePage; // 归属的 Drawboard drawboard?: Drawboard; // Marker 的属性发生变化后的回调 onChange: onSyncFunc = () => {}; // 其余属性 // ... public static createMarker = (page?: WhitePage): BaseMarker => { const marker = new BaseMarker(); marker.page = page; marker.init(); return marker; }; // 响应事件变化 public reactToManipulation( type: EventType, { dx, dy, pos }: { dx?: number; dy?: number; pos?: PositionType } = {} ) { // ... } /** 响应元素视图状态变化 */ public manipulate = (ev: MouseEvent) => { // ... }; public endManipulation() { // ... } public select() { // ... } public deselect() { // ... } /** 生成某个快照 */ public captureSnap(): MarkerSnap { // ... } /** 应用某个快照 */ public applySnap(snap: MarkerSnap): void { // ... } /** 移除该 Marker */ public destroy() { this.visual.style.display = 'none'; } protected resize(x: number, y: number, cb?: Function) { return; } protected resizeByEvent(x: number, y: number, pos?: PositionType) { return; } public move = (dx: number, dy: number) => { // ... }; /** Move to relative position */ public moveTo = (x: number, y: number) => { // ... }; /** Init base marker */ protected init() { // ... } protected addToVisual = (el: SVGElement) => { this.visual.appendChild(el); }; protected addToRenderVisual = (el: SVGElement) => { this.renderVisual.appendChild(el); }; protected onMouseDown = (ev: MouseEvent) => { // ... }; protected onMouseUp = (ev: MouseEvent) => { // ... }; protected onMouseMove = (ev: MouseEvent) => { // ... }; }
这里关于 Marker 的内部实现能够参考具体的 Marker,另外值得一提的是,想 LinearMarker, 或者 RectangleMarker 中,其须要响应对关键点拖拽引起的伸缩事件,这里的拖拽点是自定义的 Grip 组件。
事件系统,最基础的理解就是用户的任何操做都会触发事件,也能够经过外部传入某个事件的方式来触发白板的界面变化。事件类型分为 Snapshot(snap)与 Key Actions(ka)两种。
首先是 Snapshot 事件,即快照事件;快照会记录完整的状态,整个白板能够从快照中快速恢复。白板级别的快照以下:
{ id: this.id, sources: this.sources, pageIds: this.pages.map(page => page.id), visiblePageIndex: this.visiblePageIndex, pages: this.pages.map(p => p.captureSnap()) }
若是是 Shallow 模式,则不会下钻到具体的页面的快照。页面的快照便是 Marker 快照构成,每一个 Marker 的快照则是朴素对象:
{ id: this.id, type: this.type, isActive: this.isActive, x: this.x, y: this.y }
通常来讲,Whiteboard 会按期分发快照,能够经过 snapInterval 来控制间隔。而关键帧事件,则会在每一次界面变更时触发;该事件内建了 Debounce,但仍然会有比较多的数目。所以能够经过 onlyEmitSnap 来控制是否仅使用快照事件来同步。
关键帧事件的定义以下:
export interface SyncEvent { target: TargetType; // 当前事件触发者的 ID id?: string; parentId?: string; event: EventType; marker?: MarkerData; border?: WhiteboardSnap; timestamp?: number; }
譬如当某个 Marker 发生移动时候,其会触发以下的事件:
this.onChange({ target: 'marker', id: this.id, event: 'moveMarker', marker: { dx, dy } });
仅在 WhiteBoard 与 WhitePage 级别提供了事件的响应,而在 Drawboard 与 Marker 级别提供了事件的触发。
您能够经过如下任一方式阅读笔者的系列文章,涵盖了技术资料概括、编程语言与理论、Web 与大前端、服务端开发与基础架构、云计算与大数据、数据科学与人工智能、产品设计等多个领域:
Awesome Lists | Awesome CheatSheets | Awesome Interviews | Awesome RoadMaps | Awesome-CS-Books-Warehouse |
---|
编程语言理论 | Java 实战 | JavaScript 实战 | Go 实战 | Python 实战 | Rust 实战 |
---|
软件工程、数据结构与算法、设计模式、软件架构 | 现代 Web 开发基础与工程实践 | 大前端混合开发与数据可视化 | 服务端开发实践与工程架构 | 分布式基础架构 | 数据科学,人工智能与深度学习 | 产品设计与用户体验 |
---|