2019-005-fc-whiteboard,支持镜像、录播、回放的Web 电子白板
          

fc-whiteboard,支持镜像、录播、回放的Web 电子白板
在很多培训、协作、在线演讲的场景下,我们需要有电子白板的功能,能够方便地在演讲者与听众之间共享屏幕、绘制等信息。fc-whiteboard https://parg.co/NiK 是

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 集成
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 | 回放模式
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 
<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();
内部设计

Draw System | 绘制系统
绘制能力最初改造自 markerjs,在
const marker = markerType.createMarker(this.page);
this.markers.push(marker);
this.selectMarker(marker);
this.boardCanvas.appendChild(marker.visual);
// 定位
marker.moveTo(x, y);
目前
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) => {
    // ...
  };
}
这里关于
Event System | 事件系统
事件系统,最基础的理解就是用户的任何操作都会触发事件,也可以通过外部传入某个事件的方式来触发白板的界面变化。事件类型分为
首先是
{
  id: this.id,
  sources: this.sources,
  pageIds: this.pages.map(page => page.id),
  visiblePageIndex: this.visiblePageIndex,
  pages: this.pages.map(p => p.captureSnap())
}
如果是
{
  id: this.id,
  type: this.type,
  isActive: this.isActive,
  x: this.x,
  y: this.y
}
一般来说,
关键帧事件的定义如下:
export interface SyncEvent {
  target: TargetType;
  // 当前事件触发者的 ID
  id?: string;
  parentId?: string;
  event: EventType;
  marker?: MarkerData;
  border?: WhiteboardSnap;
  timestamp?: number;
}
譬如当某个
this.onChange({
  target: "marker",
  id: this.id,
  event: "moveMarker",
  marker: { dx, dy },
});
仅在
延伸阅读
您可以通过以下任一方式阅读笔者的系列文章,涵盖了技术资料归纳、编程语言与理论、
- 在Gitbook 中在线浏览,每个系列对应各自的Gitbook 仓库。
| Awesome Lists | Awesome CheatSheets | Awesome Interviews | Awesome RoadMaps | Awesome MindMaps | Awesome-CS-Books | 
|---|
| 编程语言理论 | 
|---|
| 软件工程、数据结构与算法、设计模式、软件架构 | 现代 | 大前端混合开发与数据可视化 | 服务端开发实践与工程架构 | 分布式基础架构 | 数据科学,人工智能与深度学习 | 产品设计与用户体验 | 
|---|
