资讯专栏INFORMATION COLUMN

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

paulquei / 3203人阅读

摘要:而关键帧事件,则会在每一次界面变动时触发该事件内建了,但仍然会有比较多的数目。关键帧事件的定义如下当前事件触发者的譬如当某个发生移动时候,其会触发如下的事件仅在与级别提供了事件的响应,而在与级别提供了事件的触发。

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 也可以多带带使用作为画板,整体可以被导出为图片:

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 开发基础与工程实践 大前端混合开发与数据可视化 服务端开发实践与工程架构 分布式基础架构 数据科学,人工智能与深度学习 产品设计与用户体验

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105664.html

相关文章

  • Radio Dream流媒体直播平台基于Docker应用

    摘要:支持等主流流媒体格式。控制中心会给直播服务器这些信息,直播服务器调用自身的直播流,分发到各个切片服务器。自动化运维故障恢复这部分主要是监控推流,和切片,以及直播源是否正常。 本文整理自【时速云微信群线上分享】第十一期 首先介绍一下背景,Radio Dream项目是一个开源项目,前身为五雷轰顶网络电台,这个项目是我个人逐渐打磨了将近两年,最开始是因为猫扑网络电台停播,我个人是猫扑电台的老...

    aboutU 评论0 收藏0
  • 全方面了解超宽带信号高速采集记录回放系统

    摘要:超宽带信号高速采集记录回放系统特点超宽带信号采集记录存储与回放,用于实验数据事后分析及外场环境重建。超宽带信号高速采集记录存储回放系统基于高性能及协议,实现标准化模块化可扩展可重构的超宽带信号高速连续采集记录回放产生平台。 超宽带高速记录回放系统 超宽带信号高速采集记录存储回放系统主要用于对...

    nanchen2251 评论0 收藏0
  • 全方位了解超宽带信号高速采集记录回放系统

    摘要:超宽带信号高速采集记录回放系统特点超宽带信号采集记录存储与回放,用于实验数据事后分析及外场环境重建。超宽带信号高速采集记录存储回放系统基于高性能及协议,实现标准化模块化可扩展可重构的超宽带信号高速连续采集记录回放产生平台。 超宽带高速记录回放系统 超宽带信号高速采集记录存储回放系统主要用于对...

    Jaden 评论0 收藏0
  • 新一代智能视频云发展现状分析:五大要素成关键

    摘要:远程医疗这一概念被提出后,已经被广泛应用。但是,如何提高视频传输性能,如何确保家庭基层医疗机构和户外应急的远程医疗快速接入,是当前的远程医疗业务系统面临的主要挑战。 编者按:近日,Gartner最新发布了一份《Five Key Essentials for the New Generation of Intelligent Video Cloud》白皮书报告,报告中针对各行业在视频应用...

    levy9527 评论0 收藏0
  • 在线教育开发实践(一):实时视频与白板教学

    摘要:本系列的第一篇文章,笔者分享了在浏览器端,结合声网的实时音视频互动能力与的在线白板能力,来实现一个简单但实用的在线教室。一引入音视频音视频方案选择声网作为本次的技术方案,先从下载声网最新的备用。 作者:maverick、buhe,本文首发于 RTC 开发者社区 随着技术和基础设施的进一步演进,线下的教育、会议已有很大比重演进为线上的教育和会议,突破了空间的桎梏。需求的多样性爆发增长和...

    Kahn 评论0 收藏0

发表评论

0条评论

paulquei

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<