fc-whiteboard,支持鏡像、錄播、回放的 Web 電子白板

fc-whiteboard,支持鏡像、錄播、回放的 Web 電子白板

在很多培訓、協作、在線演講的場景下,我們需要有電子白板的功能,能夠方便地在演講者與聽衆之間共享屏幕、繪製等信息。fc-whiteboard https://parg.co/NiK 是 Web 在線白板組件庫,支持實時直播(一對多)與回放兩種模式,其繪製版也能夠獨立使用。fc-whiteboard 內置了 EventHub,只需要像 Mushi-Chat 這樣提供簡單的 WebSocket 服務端,即可快速構建實時在線共享電子白板。

Usage | 使用

Whiteboard live mode | 直播模式

直播模式的效果如下圖所示:

示例代碼請參考 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 集成

WebSocket 天然就是以事件驅動的消息通信,fc-whiteboard 內部對於消息有比較好的封裝,我們建議使用者直接將消息透傳即可:

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

Whiteboard replay mode | 回放模式

fc-whiteboard 還支持回訪模式,即我們可以將某次白板操作錄製下來,可以一次性或者分批將事件傳遞給 ReplayWhiteboard,它就會按序播放:

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:

事件的基本結構如下所示,具體的事件類別我們會在下文介紹:

[
  {
    "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
  }
  ...
]

Use drawboard alone | 單獨使用 Drawboard

Drawboard 也可以單獨使用作爲畫板,整體可以被導出爲圖片:

<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,本節即介紹內部設計與實現。

Draw System | 繪製系統

繪製能力最初改造自 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 組件。

Event System | 事件系統

事件系統,最基礎的理解就是用戶的任何操作都會觸發事件,也可以通過外部傳入某個事件的方式來觸發白板的界面變化。事件類型分爲 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 與大前端、服務端開發與基礎架構、雲計算與大數據、數據科學與人工智能、產品設計等多個領域:

  • 在 Gitbook 中在線瀏覽,每個系列對應各自的 Gitbook 倉庫。
Awesome Lists Awesome CheatSheets Awesome Interviews Awesome RoadMaps Awesome-CS-Books-Warehouse
編程語言理論 Java 實戰 JavaScript 實戰 Go 實戰 Python 實戰 Rust 實戰
軟件工程、數據結構與算法、設計模式、軟件架構 現代 Web 開發基礎與工程實踐 大前端混合開發與數據可視化 服務端開發實踐與工程架構 分佈式基礎架構 數據科學,人工智能與深度學習 產品設計與用戶體驗
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章