Giter Site home page Giter Site logo

wx-chevalier / web-whiteboard Goto Github PK

View Code? Open in Web Editor NEW
60.0 6.0 17.0 7.38 MB

Online web annotation tools & whiteboard(both live, playback, and collaborative mode) for real-time visual collaboration, in business and education. | 在线电子白板,你画我猜,图片编辑,网页注解

Home Page: https://codesandbox.io/s/3q1z35q53p?fontsize=14

License: MIT License

TypeScript 86.47% JavaScript 2.43% HTML 4.13% Shell 3.94% CSS 3.03%
wx-code wx-fe fractal-components micro-components

web-whiteboard's Introduction

web-whiteboard

Web whiteboard screencasting(both live and playback mode) with background slides, can be used as a graphics tablet for online tutoring or remote collaboration.

在很多培训、协作、在线演讲的场景下,我们需要有电子白板的功能,能够方便地在演讲者与听众之间共享屏幕、绘制等信息。fc-whiteboard https://parg.co/NiK 是 Web 在线白板组件库,支持实时直播(一对多)与回放两种模式,其绘制版也能够独立使用。fc-whiteboard 内置了 EventHub,只需要像 Mushi-Chat 这样提供简单的 WebSocket 服务端,即可快速构建实时在线共享电子白板。

web-whiteboard 是 fractal-components 项目的一部分,其使用的项目模板源自 m-fe-rtw

Usage | 使用

Whiteboard live mode | 直播模式

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

Source code can be found in Code Sandbox or Demo;

源代码请参考 Code Sandbox or Demo;

import { EventHub, Whiteboard, MirrorWhiteboard } from '@m-fe/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 '@m-fe/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 '@m-fe/whiteboard';

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 级别提供了事件的触发。

Todos

  • 结构化事件信息,添加绝对时间戳以适应重放的需求;将 WhitePage 中的事件响应统一提取到 Whiteboard 中。
  • 引入全量的状态订正,每 5 秒订正一次,设置线性 Marker 的全量同步规则
  • 设置矩形类 Marker 的全量同步规则,设置仅全量同步模式
  • 将白板划分为 Whiteboard, MirrorWhiteboard, ReplayWhiteboard 三种模式,开始编写录播模式,修复增量同步与全量同步冲突的问题。
  • 根据获得到的事件的时间进行重播,完善录播模式功能。
  • 优化 Toolbar 样式,增加 Toolbar 拖拽功能
  • 增加拖拽绘制功能
  • 添加全屏的绘制功能,全屏绘制会基于新的全局 div 元素,而非直接将当前元素扩大化
  • 添加 Whiteboard 的 Loading 界面
  • 支持编辑中途的缩放能力,将全屏的画板与局部画板的事件达到同步

About

Motivation & Credits

  • markerjs

  • WeOutline: WeOutline is a shared whiteboard, designed to work among distributed teams

  • screenshots: electron 截图插件和 react 截图界面插件

Copyright & More | 延伸阅读

License: CC BY-NC-SA 4.0

笔者所有文章遵循知识共享 署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,欢迎转载,尊重版权。您还可以前往 NGTE Books 主页浏览包含知识体系、编程语言、软件工程、模式与架构、Web 与大前端、服务端开发实践与工程架构、分布式基础架构、人工智能与深度学习、产品运营与创业等多类目的书籍列表:

NGTE Books

web-whiteboard's People

Contributors

wx-chevalier avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

web-whiteboard's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.