Skip to content

netless-io/fastboard

Repository files navigation

@netless/fastboard

Docs | Sandbox | 中文 | Demo online

A starter library for making whiteboard web app, based on white-web-sdk, @netless/window-manager and netless-app.

Starting with version 0.3.22, fastboard integrates the @netless/appliance-plugin plug-in to provide better performance and richer teaching AIDS features

Fastboard now exposes separate normal / lite / full package names. Use @netless/fastboard, @netless/fastboard-lite, @netless/fastboard-full and their React counterparts instead of the old runtime subpath forms such as @netless/fastboard/full.

Starting with version 1.0.6 fastboard integrates the @netless/app-in-mainview-plugin plug-in, used for paging management courseware.

Starting with version 1.0.10, @netless/window-manager added the useBoxesStatus configuration to enable independent management of netless-app window states.

Table of Contents

Install

Package modes

Fastboard now uses standalone package names for the three runtime modes:

Mode Vanilla entry React entry Core package UI package Runtime characteristics
normal @netless/fastboard @netless/fastboard-react @netless/fastboard-core @netless/fastboard-ui white-web-sdk, @netless/window-manager, and optional plugins stay external
lite @netless/fastboard-lite @netless/fastboard-react-lite @netless/fastboard-core-lite @netless/fastboard-ui standalone replacement for the old lite runtime entry; runtime deps stay external like normal
full @netless/fastboard-full @netless/fastboard-react-full @netless/fastboard-core-full @netless/fastboard-ui bundles white-web-sdk, @netless/window-manager, and jspdf; @netless/appliance-plugin and @netless/app-in-mainview-plugin still stay external

Notes:

  • @netless/fastboard, @netless/fastboard-react, and @netless/fastboard-core are normal-only runtime entries.
  • @netless/fastboard-ui is shared by all three modes. @netless/fastboard-ui/lite and @netless/fastboard-ui/full remain type-only subpaths for mode-specific typings.

Vanilla JavaScript

Normal package mode

npm add @netless/fastboard @netless/window-manager white-web-sdk

Note: Add @netless/appliance-plugin only when Use performance is enabled. Add @netless/app-in-mainview-plugin only when Pagination management courseware is enabled.

Lite package mode

npm add @netless/fastboard-lite @netless/window-manager white-web-sdk

Note: Lite mode keeps white-web-sdk and @netless/window-manager external, just like normal mode. Add @netless/appliance-plugin only when Use performance is enabled. Add @netless/app-in-mainview-plugin only when Pagination management courseware is enabled.

Full package mode

npm add @netless/fastboard-full

Note: When using @netless/fastboard-full, do not install @netless/window-manager, white-web-sdk, or jspdf together with it. Add @netless/appliance-plugin only when Use performance is enabled; install a version that exposes ./bridge, and in practice use @netless/appliance-plugin >= 1.1.35 (./bridge first appeared in >= 1.1.34-beta.2).

Keep importing @netless/appliance-plugin in app code. You do not need, and generally should not, replace it with @netless/appliance-plugin/bridge. The /bridge entry is loaded internally by fastboard full to reuse the bundled white-web-sdk runtime.

If you need @netless/app-in-mainview-plugin, note that it still stays external in full mode, but fastboard now loads @netless/app-in-mainview-plugin/bridge internally and reuses the bundled full runtime. Keep using enableAppInMainViewPlugin in app code and do not import bridge manually; in practice, use @netless/app-in-mainview-plugin >= 0.0.10.

Usage

Vanilla JavaScript

// Full package
// import { createFastboard, createUI } from "@netless/fastboard-full";
// Lite package
// import { createFastboard, createUI } from "@netless/fastboard-lite";
// Normal package
import { createFastboard, createUI } from "@netless/fastboard";

async function main() {
  const fastboard = await createFastboard({
    // [1]
    sdkConfig: {
      appIdentifier: "whiteboard-appid",
      region: "us-sv", // "cn-hz" | "us-sv" | "sg" | "in-mum" | "eu"
    },
    // [2]
    joinRoom: {
      uid: "unique_id_for_each_client",
      uuid: "room-uuid",
      roomToken: "NETLESSROOM_...",
      // (optional)
      userPayload: {
        nickName: "foo",
      },
    },
    // [3] (optional)
    managerConfig: {
      cursor: true,
    },
    // [4] (optional)
    netlessApps: [],
    // [5] (Optional), enable the appliance-plugin plugin. To enable the configuration, you need to install @netless/appliance-plugin. For specific details, please refer to the introduction of the performance optimization version
    enableAppliancePlugin: {
      ...
    },
    // [6] (Optional), enable appin-Mainview-Plugin. Starting from 1.0.6, to enable the configuration, you need to install @netless/ appin-MainView-Plugin. For specific details, please refer to the introduction in the pagination Management courseware
    enableAppInMainViewPlugin: true
  });

  const container = createContainer();

  const ui = createUI(fastboard, container);

  // .....

  // destroy Fastboard UI
  ui.destroy();

  // .....

  // destroy Fastboard (disconnect from the whiteboard room)
  fastboard.destroy();
}

function createContainer() {
  const container = document.createElement("div");
  // Must give it a visible size
  Object.assign(container.style, {
    height: "400px",
    border: "1px solid",
    background: "#f1f2f3",
  });
  document.body.appendChild(container);
  return container;
}

main().catch(console.error);

[1] Read more about the SDK config at Construct WhiteWebSDK object
[2] Read more about join room config at Construct Room and Player objects
[3] Read more about WindowManager config at WindowManager.mount()

React

Install the React entry for the mode you choose, then use the <Fastboard /> component.

Normal package mode

npm add @netless/fastboard-react react react-dom @netless/window-manager white-web-sdk

Note: Add @netless/appliance-plugin only when Use performance is enabled. Add @netless/app-in-mainview-plugin only when Pagination management courseware is enabled.

Lite package mode

npm add @netless/fastboard-react-lite react react-dom @netless/window-manager white-web-sdk

Note: Lite React mode keeps white-web-sdk and @netless/window-manager external, just like normal mode. Add @netless/appliance-plugin only when Use performance is enabled. Add @netless/app-in-mainview-plugin only when Pagination management courseware is enabled.

Full package

npm add @netless/fastboard-react-full react react-dom

Note: When using @netless/fastboard-react-full, do not install @netless/window-manager, white-web-sdk, or jspdf together with it. Add @netless/appliance-plugin only when Use performance is enabled; install a version that exposes ./bridge, and in practice use @netless/appliance-plugin >= 1.1.35.

Keep importing @netless/appliance-plugin in app code. You do not need, and generally should not, replace it with @netless/appliance-plugin/bridge. The /bridge entry is loaded internally by fastboard full to reuse the bundled white-web-sdk runtime.

If you need @netless/app-in-mainview-plugin, note that it still stays external in full mode, but fastboard now loads @netless/app-in-mainview-plugin/bridge internally and reuses the bundled full runtime. Keep using enableAppInMainViewPlugin in app code and do not import bridge manually; in practice, use @netless/app-in-mainview-plugin >= 0.0.10.

// Full package
// import { useFastboard, Fastboard } from "@netless/fastboard-react-full";
// Lite package
// import { useFastboard, Fastboard } from "@netless/fastboard-react-lite";
// Normal package
import { useFastboard, Fastboard } from "@netless/fastboard-react";

import React from "react";
import { createRoot } from "react-dom/client";

function App() {
  const fastboard = useFastboard(() => ({
    sdkConfig: {
      appIdentifier: "whiteboard-appid",
      region: "us-sv", // "cn-hz" | "us-sv" | "sg" | "in-mum" | "eu"
    },
    joinRoom: {
      uid: "unique_id_for_each_client",
      uuid: "room-uuid",
      roomToken: "NETLESSROOM_...",
    },
    managerConfig: {
      cursor: true,
    },
    // (Optional), enable the appliance-plugin plugin. To enable the configuration, you need to install @netless/appliance-plugin. For specific details, please refer to the introduction of the performance optimization version
    enableAppliancePlugin: {
      ...
    },
    // [6] (Optional), enable appin-Mainview-Plugin. Starting from 1.0.6, to enable the configuration, you need to install @netless/ appin-MainView-Plugin. For specific details, please refer to the introduction in the pagination Management courseware
    enableAppInMainViewPlugin: true
  }));

  // Container must have a visible size
  return (
    <div
      style={{
        height: "400px",
        border: "1px solid",
        background: "#f1f2f3",
      }}
    >
      <Fastboard app={fastboard} />
    </div>
  );
}

createRoot(document.getElementById("app")).render(<App />);

Whiteboard Functions

Insert Picture

await fastboard.insertImage(fileUrl);

The fileUrl is the url to load the image file, like "src" in <img src>. Fastboard itself does not contain any logic about upload/save a file.

Redo & Undo

fastboard.undo();
fastboard.redo();

Move Camera

fastboard.moveCamera({ centerX: 0, centerY: 0, scale: 1 });
fastboard.moveCameraToContain({ originX: -300, originY: -200, width: 600, height: 400 });

Set Tool

fastboard.setAppliance("pencil");
fastboard.setAppliance("shape", "triangle");
fastboard.setStrokeWidth(2);
fastboard.setStrokeColor([r, g, b]);

Netless Apps

To develop your own app, see Write you a Netless App.

Register & Insert Apps

Except for built-in apps in Fastboard, you can also insert your own apps. To do that, You have to register app at each client before entering room (createFastboard):

import { register } from "@netless/fastboard";
import MyApp from "my-app";

register({ kind: MyApp.kind, src: MyApp });

Or you can set netlessApps in createFastboard config:

createFastboard({
  ..., // other config
  netlessApps: [MyApp],
});

Then add app into the room via:

fastboard.manager.addApp({ kind: MyApp.kind });

Read more about Netless Apps.

Insert PDF, PPT and PPTX

// insert PDF/PPT/PPTX to the main whiteboard
const appId = await fastboard.insertDocs("filename.pptx", conversionResponse);

The conversionResponse is the result of this api.

Note: If you're using the new projector api, there's another way:

const appId1 = await fastboard.insertDocs({
  fileType: "pdf",
  scenePath: `/pdf/${response.uuid}`,
  scenes: [
    { name: "1", ppt: { width: 714, height: 1010, src: images[1].url } },
    { name: "2", ppt: { width: 714, height: 1010, src: images[2].url } },
  ],
  title: "filename.pdf",
});

const appId2 = await fastboard.insertDocs({
  fileType: "pptx",
  scenePath: `/pptx/${response.uuid}`,
  taskId: response.uuid,
  title: "filename.pptx",
  // "https://convertcdn.netless.link/dynamicConvert" by default
  url: response.prefix,
});

Listen PDF/PPTX Page Change Event

Note: This feature requires the following versions of dependencies:

  • @netless/app-slide ≥ 0.2.50
  • @netless/window-manager ≥ 0.4.66
// For static documents i.e. PDF files
const dispose = fastboard.manager.onAppEvent("DocsViewer", event => {
  if (event.type === "pageStateChange") console.log(event.value);
});
// For dynamic documents i.e. PPTX files
const dispose = fastboard.manager.onAppEvent("Slide", console.log);

onExitRoom(() => dispose());

The event above will be like:

{
  "kind": "Slide",
  "appId": "Slide-aa1840ba",
  "type": "pageStateChange",
  "value": {
    "index": 0,
    "length": 12
  }
}

The dispose above is a function to stop listening.

Control the PDF/PPTX Apps

import { dispatchDocsEvent } from "@netless/fastboard";

dispatchDocsEvent(fastboard, "nextPage"); // prevPage, nextStep, prevStep
dispatchDocsEvent(fastboard, "jumpToPage", { page: 2 });

By default it will dispatch event to the focused PDF/PPTX app, you can also specify the appId:

dispatchDocsEvent(fastboard, "nextPage", { appId });

Set PPTX Render Options

import { register, SlideApp, addSlideHooks } from "@netless/fastboard";

register({
  kind: SlideApp.kind,
  src: SlideApp,
  appOptions: {
    // ... your slide options here
    // Note: import type {SlideOptions} to get type hints
  },
  addHooks: addSlideHooks,
});

Read more about these options.

Insert Video & Audio

const appId = await fastboard.insertMedia("filename.mp3", fileUrl);

The fileUrl is the url to load the media file, like "src" in <video src>. Fastboard itself does not contain any logic about upload/save a file.

const appId = await fastboard.manager.addApp({
  kind: "Monaco",
  options: { title: "Code Editor" },
});
const appId = await fastboard.manager.addApp({
  kind: "Countdown",
  options: { title: "Countdown" },
});

Note

GeoGebra is licensed under GPLv3 and is free only in non-commercial use. If you want to use it in commercially, please refer to their license first: https://www.geogebra.org/license

const appId = await fastboard.manager.addApp({
  kind: "GeoGebra",
  options: { title: "GeoGebra" },
});
const appId = await fastboard.manager.addApp({
  kind: "Plyr",
  options: { title: "YouTube" },
  attributes: {
    src: "https://www.youtube.com/embed/bTqVqk7FSmY",
    provider: "youtube",
  },
});
const appId = await fastboard.manager.addApp({
  kind: "EmbeddedPage",
  options: { title: "Google Docs" },
  attributes: {
    src: "https://docs.google.com/document/d/1bd4SRb5BmTUjPGrFxU2V7KI2g_mQ-HQUBxKTxsEn5e4/edit?usp=sharing",
  },
});

Note: EmbeddedPage uses <iframe> to display external web resources, you'd better not embedding 2 more nested iframes (i.e. webpage>iframe1>iframe2) in the same page.

More apps goto netless-app.

To develop your own app, see Write you a Netless App.

performance

Through enableAppliancePlugin configuration items open appliance-plugins plugin. In order to enhance performance and provide new whiteboard features, or refer to the appliance-plugin document for more information.

Note: To enable the use of the performance optimized version, you need to install @netless/appliance-plugin.

Full package note: When using @netless/fastboard-full or @netless/fastboard-react-full, fastboard loads @netless/appliance-plugin/bridge internally. Keep using enableAppliancePlugin in your fastboard config and keep importing @netless/appliance-plugin in app code; do not manually switch your app import to @netless/appliance-plugin/bridge. In practice, use @netless/appliance-plugin >= 1.1.35.

// The method of importing worker.js is optional. If cdn is used, it does not need to be imported from dist. If dist is imported, it needs to be configured into options.cdn in the form of resource module and bolb inline. Such as '?raw', this requires packer support,vite default support '?raw',webpack needs to configure raw-loader or asset/source.
import fullWorkerString from '@netless/appliance-plugin/dist/fullWorker.js?raw';
import subWorkerString from '@netless/appliance-plugin/dist/subWorker.js?raw';
const fullWorkerBlob = new Blob([fullWorkerString], {type: 'text/javascript'});
const fullWorkerUrl = URL.createObjectURL(fullWorkerBlob);
const subWorkerBlob = new Blob([subWorkerString], {type: 'text/javascript'});
const subWorkerUrl = URL.createObjectURL(subWorkerBlob);

// interconnection with fastboard-react
// Full package mode reference
// import { useFastboard, Fastboard } from "@netless/fastboard-react-full";
// Subcontract reference
import { useFastboard, Fastboard } from "@netless/fastboard-react";

const app = useFastboard(() => ({
    sdkConfig: {
      ...
    },
    joinRoom: {
      ...
    },
    managerConfig: {
      cursor: true,
      enableAppliancePlugin: true,
      ...
    },
    // about enableAppliancePlugin: https://github.com/netless-io/fastboard/blob/main/docs/en/appliance-plugin.md#configure-parameters
    enableAppliancePlugin: {
      cdn: {
          fullWorkerUrl,
          subWorkerUrl,
      }
      ...
    }
  }));
...
// Obtain the instance of 'AppliancePlugin' through 'app.appliancePlugin'
app.appliancePlugin
...

// interconnection with fastboard
// Full package mode reference
// import { createFastboard, createUI } from "@netless/fastboard-full";
// Subcontract reference
import { createFastboard, createUI } from "@netless/fastboard";

const fastboard = await createFastboard({
    sdkConfig: {
      ...
    },
    joinRoom: {
      ...
    },
    managerConfig: {
      cursor: true,
      ...
    },
    // about enableAppliancePlugin: https://github.com/netless-io/fastboard/blob/main/docs/en/appliance-plugin.md#configure-parameters
    enableAppliancePlugin: {
      cdn: {
          fullWorkerUrl,
          subWorkerUrl,
      }
      ...
    }
  });
...
// Obtain the instance of 'AppliancePlugin' through 'fastboard.appliancePlugin'
fastboard.appliancePlugin
...

Note:

  • First, you must ensure that the appliance plugin configuration is enabled on all three ends of Android \ios\web. Notes drawn after appliance-plugin is enabled will not be displayed on the unoccupied whiteboard.
  • After the appliance plugin is turned on, the old contents drawn on the previous whiteboard are displayed, but cannot be manipulated and upgraded into new notes. So in order not to affect the experience, please use on a whiteboard without any historical data. Similarly, when the plugin is closed, the newly drawn content will be lost.
  • only the browser for web apis offscreenCanvas Full support, in order to experience more performance and rich teaching AIDS functional experience.

appInMainView

Through enableAppInMainViewPlugin configuration items open app-in-mainView-plugin plug-in, can improve fastboard management way of courseware, allows the user to switch the main board of the page to show or hide the current page open courseware. Specific reference documents: app-in-mainview-plugins for more content.

Note: To enable the pagination management courseware plugin, you need to install @netless/app-in-mainview-plugin.

Full package note: @netless/app-in-mainview-plugin still stays external in full mode, but @netless/fastboard-full / @netless/fastboard-react-full now load @netless/app-in-mainview-plugin/bridge internally. Keep using enableAppInMainViewPlugin in app code and do not import bridge manually; in practice, use @netless/app-in-mainview-plugin >= 0.0.10.

// interconnection with fastboard-react
// Full package mode reference
// import { useFastboard, Fastboard } from "@netless/fastboard-react-full";
// Subcontract reference
import { useFastboard, Fastboard } from "@netless/fastboard-react";

const app = useFastboard(() => ({
    sdkConfig: {
      ...
    },
    joinRoom: {
      ...
    },
    managerConfig: {
      ...
    },
    // Enable the appInMainViewPlugin plugin,
    // The default UI is enabled by default. If you need to customize the UI, you can pass "enableDefaultUI: false"
    enableAppInMainViewPlugin: true || {
        enableDefaultUI:  true,
        language: "en",
        ...
    }
  }));
...
// Obtain the instance of 'AppliancePlugin' through 'app.appInMainViewPlugin'
app.appInMainViewPlugin
...

// interconnection with fastboard
// Full package mode reference
// import { createFastboard, createUI } from "@netless/fastboard-full";
// Subcontract reference
import { createFastboard, createUI } from "@netless/fastboard";

const fastboard = await createFastboard({
    sdkConfig: {
      ...
    },
    joinRoom: {
      ...
    },
    managerConfig: {
      ...
    },
    // Enable the appInMainViewPlugin plugin,
    // The default UI is enabled by default. If you need to customize the UI, you can pass "enableDefaultUI: false"
    enableAppInMainViewPlugin: true || {
        enableDefaultUI:  true,
        language: "en",
        ...
    }
  });
...
// Obtain the instance of 'AppliancePlugin' through 'fastboard.appInMainViewPlugin'
fastboard.appInMainViewPlugin
...

Note:

  • Currently, only the web end supports the 'appInMainViewPlugin' plugin, while the mobile end does not support it for the time being.

Full compatibility

@netless/fastboard-full and @netless/fastboard-react-full bundle the core SDK runtime, but not every third-party package is a safe fit yet.

The following status is based on the current full baseline: full is aligned with @netless/window-manager 1.0.14 and has been regression-tested with @netless/appliance-plugin 1.1.35.

Supported in full

  • @netless/appliance-plugin: supported through the internal @netless/appliance-plugin/bridge loading path. Use @netless/appliance-plugin >= 1.1.35 in practice.
  • @netless/app-in-mainview-plugin: supported through the internal @netless/app-in-mainview-plugin/bridge loading path. The package still stays external; use @netless/app-in-mainview-plugin >= 0.0.10 in practice.
  • @netless/app-little-white-board: the current regression baseline uses 0.0.4 and has passed regression together with @netless/appliance-plugin 1.1.35.

Needs dedicated validation

  • @netless/window-manager-maths-kit-extend: the current regression baseline uses 0.0.3.
  • @netless/window-manager-paste-extend: the current regression baseline uses 0.0.6.
  • @netless/app-plyr: the current regression baseline uses 0.2.12.

Not part of the current full compatibility baseline

  • @netless/window-manager-ai-extend.

As a rule of thumb, any package that directly imports white-web-sdk or @netless/window-manager, dynamically loads its own app/plugin bundle, or depends on runtime class identity should be treated as full-mode risky until it ships a bridge or dedicated full entry.

Customization

Fastboard isn't that customizable due to its fast design goal. But we do have some lightweight configuration for easy changes.

// vanilla js
const ui = createUI(fastboard, container);
ui.update({ config: { ...ui_config } });

// react
<Fastboard app={fastboard} config={{ ...ui_config }} />;

The ui_config looks like:

{
  toolbar: {
    enable: true,
    placement: 'left',
    items: ['pencil', 'eraser'],
    apps: { enable: true },
  },
  redo_undo: { enable: true },
  zoom_control: { enable: true },
  page_control: { enable: true },
}

For example, you can hide the zoom control component with:

// vanilla js
ui.update({ config: { zoom_control: { enable: false } } });
// react
<Fastboard app={fastboard} config={{ zoom_control: { enable: false } }} />;

Or change the items on toolbar with:

Available items:
clicker, selector, pencil, text, shapes, eraser, clear, hand, laserPointer.

const toolbar_items = ["pencil", "eraser"];
// vanilla js
ui.update({ config: { toolbar: { items: toolbar_items } } });
// react
<Fastboard app={fastboard} config={{ toolbar: { items: toolbar_items } }} />;

You can also write your own component with the same source of truth as Fastboard UI. Just disable those components and refer to Write Your Own UI (for Fastboard) & How do I customize Your own UI (for Fastboard).

Replay Mode

Fastboard has a similar usage to replay a whiteboard.

const player = await replayFastboard(...)
const ui = createReplayUI(player, container);

const player = useReplayFastboard(() => ({...}))
return <ReplayFastboard player={player} />

The player instance is similar to a native video player that has methods like play() stop() seek() pause() etc. To sync the progress of the whiteboard player with other players (like a video player), see @netless/sync-player.

Error Handling

You will get callbacks when:

  • Cannot connect to server at the very beginning
  • Connecting status changed (to reconnecting or disconnected)
  • Kicked from room (when the room is banned from the backend)

Note that Fastboard will reconnect automatically internally, you only have to do things below to ensure the user experience.

To properly handle these cases, please refer to this piece of code:

try {
  fastboard = await createFastboard({
    sdkConfig: {
      onWhiteSetupFailed(error) {
        console.error("Failed to find the whiteboard server", error);
      },
    },
    joinRoom: {
      callbacks: {
        onPhaseChanged(phase) {
          if (phase === "reconnecting") console.log("Whiteboard connection lost, reconnecting...");
        },
        onDisconnectWithError(error) {
          console.error("Failed to connect to whiteboard server");
        },
        onKickedWithReason(reason) {
          console.log("You're kicked by", reason);
          // Properly close this room
          leaveRoom();
        },
      },
    },
    managerConfig: {
      cursor: true,
      // (Optional), turn on the appliance-plugin starting at 0.3.22
      supportAppliancePlugin: true,
    },
    // (Optional), turn on the appliance-plugin starting at 0.3.22
    enableAppliancePlugin: {
      ...
    },
  });
} catch (error) {
  console.error("Failed to join whiteboard room", error);
}

The React way should be similar.

License

MIT @ netless

About

An open sourced whiteboard starter based on white-web-sdk.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors