在不少培訓、協做、在線演講的場景下,咱們須要有電子白板的功能,可以方便地在演講者與聽衆之間共享屏幕、繪製等信息。fc-whiteboard https://parg.co/NiK 是 Web 在線白板組件庫,支持實時直播(一對多)與回放兩種模式,其繪製版也可以獨立使用。fc-whiteboard 內置了 EventHub,只須要像 Mushi-Chat 這樣提供簡單的 WebSocket 服務端,便可快速構建實時在線共享電子白板。javascript
直播模式的效果以下圖所示:html
點擊查看前端
示例代碼請參考 Code Sandbox,或者直接查看 Demo;java
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 內部對於消息有比較好的封裝,咱們建議使用者直接將消息透傳便可:python
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,它就會按序播放:react
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:git
事件的基本結構以下所示,具體的事件類別咱們會在下文介紹:github
[
{
"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 也能夠單獨使用做爲畫板,總體能夠被導出爲圖片:web
<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,本節即介紹內部設計與實現。算法
繪製能力最初改造自 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 開發基礎與工程實踐 | 大前端混合開發與數據可視化 | 服務端開發實踐與工程架構 | 分佈式基礎架構 | 數據科學,人工智能與深度學習 | 產品設計與用戶體驗 |
---|