@netless/fastboard 0.0.7 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +21 -19
  2. package/dist/index.cjs.js +4 -4
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.es.js +678 -410
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/svelte.cjs.js +1 -1
  7. package/dist/svelte.cjs.js.map +1 -1
  8. package/dist/svelte.es.js +1 -0
  9. package/dist/svelte.es.js.map +1 -1
  10. package/dist/vue.cjs.js +1 -1
  11. package/dist/vue.cjs.js.map +1 -1
  12. package/dist/vue.es.js +1 -0
  13. package/dist/vue.es.js.map +1 -1
  14. package/package.json +11 -2
  15. package/src/WhiteboardApp.ts +91 -20
  16. package/src/components/{PageControl.scss → PageControl/PageControl.scss} +0 -0
  17. package/src/components/PageControl/PageControl.tsx +110 -0
  18. package/src/components/PageControl/hooks.ts +70 -0
  19. package/src/components/PageControl/index.ts +2 -0
  20. package/src/components/PlayerControl/PlayerControl.tsx +7 -8
  21. package/src/components/PlayerControl/hooks.ts +3 -10
  22. package/src/components/PlayerControl/index.ts +1 -0
  23. package/src/components/{RedoUndo.scss → RedoUndo/RedoUndo.scss} +0 -0
  24. package/src/components/{RedoUndo.tsx → RedoUndo/RedoUndo.tsx} +13 -29
  25. package/src/components/RedoUndo/hooks.ts +50 -0
  26. package/src/components/RedoUndo/index.ts +2 -0
  27. package/src/components/Root.tsx +10 -6
  28. package/src/components/Toolbar/Content.tsx +4 -3
  29. package/src/components/Toolbar/Toolbar.scss +35 -1
  30. package/src/components/Toolbar/Toolbar.tsx +78 -28
  31. package/src/components/Toolbar/components/Mask.tsx +44 -0
  32. package/src/components/Toolbar/components/assets/collapsed.png +0 -0
  33. package/src/components/Toolbar/components/assets/expanded.png +0 -0
  34. package/src/components/Toolbar/hooks.ts +28 -29
  35. package/src/components/Toolbar/index.ts +1 -0
  36. package/src/components/{ZoomControl.scss → ZoomControl/ZoomControl.scss} +0 -0
  37. package/src/components/ZoomControl/ZoomControl.tsx +109 -0
  38. package/src/components/ZoomControl/hooks.ts +111 -0
  39. package/src/components/ZoomControl/index.ts +2 -0
  40. package/src/components/hooks.ts +80 -0
  41. package/src/index.ts +20 -5
  42. package/src/internal/Instance.tsx +31 -7
  43. package/src/internal/helpers.ts +44 -0
  44. package/src/internal/hooks.ts +9 -0
  45. package/src/react.tsx +52 -0
  46. package/src/style.scss +9 -3
  47. package/src/components/PageControl.tsx +0 -181
  48. package/src/components/ZoomControl.tsx +0 -221
  49. package/src/hooks.ts +0 -53
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export { name, RedoUndo, type RedoUndoProps } from "./RedoUndo";
@@ -5,6 +5,7 @@ import { Toolbar } from "./Toolbar";
5
5
  import { RedoUndo } from "./RedoUndo";
6
6
  import { ZoomControl } from "./ZoomControl";
7
7
  import { PageControl } from "./PageControl";
8
+ import { useHideControls } from "./hooks";
8
9
 
9
10
  export interface RootProps {
10
11
  instance: Instance;
@@ -21,12 +22,8 @@ export function Root({ instance: app }: RootProps) {
21
22
  [app, mux]
22
23
  );
23
24
 
24
- const {
25
- Toolbar: toolbar = true,
26
- RedoUndo: redo_undo = true,
27
- ZoomControl: zoom_control = true,
28
- PageControl: page_control = true,
29
- } = app.config.layout || {};
25
+ const hideControls = useHideControls(app.manager);
26
+ const showControls = !hideControls;
30
27
 
31
28
  const props = {
32
29
  room: app.room,
@@ -34,6 +31,13 @@ export function Root({ instance: app }: RootProps) {
34
31
  i18n: app.i18n,
35
32
  };
36
33
 
34
+ const {
35
+ Toolbar: toolbar = showControls || hideControls === "toolbar-only",
36
+ RedoUndo: redo_undo = showControls,
37
+ ZoomControl: zoom_control = showControls,
38
+ PageControl: page_control = showControls,
39
+ } = app.config.layout || {};
40
+
37
41
  return (
38
42
  <Instance.Context.Provider value={app}>
39
43
  <div className="fastboard-root">
@@ -14,8 +14,9 @@ import { AppsButton } from "./components/AppsButton";
14
14
  import { PencilButton } from "./components/PencilButton";
15
15
  import { TextButton } from "./components/TextButton";
16
16
  import { ShapesButton } from "./components/ShapesButton";
17
+ import clsx from "clsx";
17
18
 
18
- export function Content() {
19
+ export const Content = React.memo(() => {
19
20
  const app = useInstance();
20
21
  const ref = useRef<HTMLDivElement>(null);
21
22
  const [scrollTop, setScrollTop] = useState(0);
@@ -65,7 +66,7 @@ export function Content() {
65
66
  )}
66
67
  <div
67
68
  ref={ref}
68
- className={`${name}-section`}
69
+ className={clsx(`${name}-section`)}
69
70
  style={{
70
71
  height: `${sectionHeight}px`,
71
72
  overflow: needScroll ? "hidden" : "visible",
@@ -90,4 +91,4 @@ export function Content() {
90
91
  )}
91
92
  </>
92
93
  );
93
- }
94
+ });
@@ -40,10 +40,23 @@ $name: "fastboard-toolbar";
40
40
  border: 1px solid rgba(0, 0, 0, 0.15);
41
41
  }
42
42
 
43
+ &.expanded {
44
+ border: 1px solid rgba(0, 0, 0, 0.15);
45
+ }
46
+
43
47
  &.dark {
44
48
  color: #ddd;
45
49
  background-color: rgba($color: #333, $alpha: 0.85);
46
- border: 1px solid rgba(0, 0, 0, 0.45);
50
+ }
51
+
52
+ &.expanded:hover {
53
+ box-shadow: 0px 0px 5px 0px rgba(0, 0, 0, 0.25);
54
+ transform: translate3d(0, 0, 0);
55
+ }
56
+
57
+ &.collapsed {
58
+ padding: 0;
59
+ background-color: transparent;
47
60
  }
48
61
 
49
62
  &-tooltip {
@@ -132,6 +145,11 @@ $name: "fastboard-toolbar";
132
145
  flex-flow: column nowrap;
133
146
  gap: 4px;
134
147
  scroll-behavior: smooth;
148
+
149
+ &.collapsed {
150
+ transform: translateX(-100%);
151
+ transition: 1s transform;
152
+ }
135
153
  }
136
154
 
137
155
  &-panel {
@@ -244,4 +262,20 @@ $name: "fastboard-toolbar";
244
262
  outline-offset: 2px;
245
263
  }
246
264
  }
265
+
266
+ &-mask-btn {
267
+ width: 17px;
268
+ height: 62px;
269
+ cursor: pointer;
270
+ &.dark {
271
+ filter: invert(0.8);
272
+ }
273
+ }
274
+
275
+ &-expand-btn {
276
+ display: flex;
277
+ align-items: center;
278
+ position: absolute;
279
+ left: 0;
280
+ }
247
281
  }
@@ -1,16 +1,19 @@
1
- import type { CommonProps, GenericIcon, Theme } from "../../types";
2
1
  import type { i18n } from "i18next";
2
+ import type { CommonProps, GenericIcon, Theme } from "../../types";
3
+ import { AnimatePresence, motion } from "framer-motion";
3
4
 
4
- import clsx from "clsx";
5
5
  import React, { createContext, useCallback, useState } from "react";
6
6
 
7
7
  import { Icon } from "../../icons";
8
- import { Icons } from "./icons";
9
- import { Button } from "./components/Button";
10
- import { CutLine } from "./components/CutLine";
11
8
  import { EmptyToolbarHook, useToolbar, type ToolbarHook } from "./hooks";
12
9
  import { Content } from "./Content";
13
10
 
11
+ import collapsePNG from "./components/assets/collapsed.png";
12
+ import expandPNG from "./components/assets/expanded.png";
13
+
14
+ import clsx from "clsx";
15
+ import { Mask } from "./components/Mask";
16
+
14
17
  export type ToolbarProps = CommonProps & {
15
18
  icons?: GenericIcon<
16
19
  | "clicker"
@@ -44,39 +47,86 @@ export const Toolbar = ({
44
47
  theme = "light",
45
48
  icons,
46
49
  room,
50
+ manager,
47
51
  i18n,
48
52
  }: ToolbarProps) => {
49
53
  const [expanded, setExpanded] = useState(true);
50
- const hook = useToolbar(room);
51
- const toggle = useCallback(() => setExpanded(e => !e), []);
52
-
54
+ const hook = useToolbar(room, manager);
55
+ const [toolbar, toolbarRef] = useState<HTMLDivElement | null>(null);
56
+ const [onHover, setOnHover] = useState<boolean>(false);
57
+ const [pointEvents, setPointEvents] = useState(true);
53
58
  const disabled = !hook.writable;
54
59
 
60
+ const toggle = useCallback(() => {
61
+ setExpanded(e => !e);
62
+ }, []);
63
+
55
64
  return (
56
65
  <ToolbarContext.Provider value={{ theme, icons, ...hook, i18n }}>
57
- <div className={clsx(name, theme)}>
66
+ <AnimatePresence>
58
67
  {expanded ? (
59
- <Button content={i18n?.t("collapse")} onClick={toggle}>
60
- <Icon
61
- fallback={<Icons.Collapse theme={theme} />}
62
- src={disabled ? icons?.collapseIconDisable : icons?.collapseIcon}
63
- />
64
- </Button>
65
- ) : (
66
- <Button content={i18n?.t("expand")} onClick={toggle}>
67
- <Icon
68
- fallback={<Icons.Expand theme={theme} />}
69
- src={disabled ? icons?.expandIconDisable : icons?.expandIcon}
70
- />
71
- </Button>
72
- )}
73
- {expanded && (
74
- <>
75
- <CutLine />
68
+ <motion.div
69
+ initial={{ x: -100 }}
70
+ animate={{
71
+ x: 0,
72
+ transition: { duration: 0.5 },
73
+ }}
74
+ key="toolbar"
75
+ ref={toolbarRef}
76
+ className={clsx(name, theme)}
77
+ onPointerEnter={() => {
78
+ if (expanded) {
79
+ setOnHover(true);
80
+ }
81
+ }}
82
+ onMouseLeave={() => setOnHover(false)}
83
+ exit={{
84
+ x: -100,
85
+ transition: { duration: 0.5 },
86
+ }}
87
+ onAnimationStart={() => setPointEvents(false)}
88
+ onAnimationComplete={() => setPointEvents(true)}
89
+ style={{ pointerEvents: pointEvents ? "auto" : "none" }}
90
+ >
76
91
  <Content />
77
- </>
92
+ {expanded && onHover && (
93
+ <Mask toolbar={toolbar}>
94
+ <div onClick={toggle}>
95
+ <img
96
+ draggable={false}
97
+ className={clsx(`${name}-mask-btn`, theme)}
98
+ src={collapsePNG}
99
+ />
100
+ </div>
101
+ </Mask>
102
+ )}
103
+ </motion.div>
104
+ ) : (
105
+ <motion.div
106
+ className={clsx(`${name}-expand-btn`, theme)}
107
+ key="expand"
108
+ onClick={toggle}
109
+ initial={{ x: -100 }}
110
+ animate={{
111
+ x: 0,
112
+ transition: { duration: 0.5 },
113
+ }}
114
+ >
115
+ {!expanded && (
116
+ <Icon
117
+ fallback={
118
+ <img
119
+ draggable={false}
120
+ src={expandPNG}
121
+ className={clsx(`${name}-mask-btn`, theme)}
122
+ />
123
+ }
124
+ src={disabled ? icons?.expandIconDisable : icons?.expandIcon}
125
+ />
126
+ )}
127
+ </motion.div>
78
128
  )}
79
- </div>
129
+ </AnimatePresence>
80
130
  </ToolbarContext.Provider>
81
131
  );
82
132
  };
@@ -0,0 +1,44 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import ReactDOM from "react-dom";
3
+
4
+ type MaskProps = {
5
+ toolbar: HTMLDivElement | null;
6
+ children: React.ReactNode;
7
+ };
8
+
9
+ export const Mask = React.memo(({ toolbar, children }: MaskProps) => {
10
+ const [rootElement] = useState<HTMLDivElement | null>(() => {
11
+ const element = document.createElement("div");
12
+ element.style.position = "absolute";
13
+ return element;
14
+ });
15
+
16
+ useEffect(() => {
17
+ if (toolbar && rootElement) {
18
+ toolbar.appendChild(rootElement);
19
+ }
20
+ }, [rootElement, toolbar]);
21
+
22
+ useEffect(() => {
23
+ if (rootElement && toolbar) {
24
+ toolbar.appendChild(rootElement);
25
+
26
+ const toolbarRect = toolbar.getBoundingClientRect();
27
+ const halfHeight = toolbarRect.height / 2 - 31;
28
+ rootElement.style.top = halfHeight + "px";
29
+ rootElement.style.left = "41px";
30
+ rootElement.style.width = "17px";
31
+ rootElement.style.height = "62px";
32
+
33
+ return () => {
34
+ toolbar.removeChild(rootElement);
35
+ };
36
+ }
37
+ }, [rootElement, toolbar]);
38
+
39
+ if (rootElement) {
40
+ return ReactDOM.createPortal(children, rootElement);
41
+ } else {
42
+ return null;
43
+ }
44
+ });
@@ -6,24 +6,11 @@ import type {
6
6
  RoomState,
7
7
  ShapeType,
8
8
  } from "white-web-sdk";
9
-
9
+ import type { WindowManager } from "@netless/window-manager";
10
10
  import { useCallback, useEffect, useState } from "react";
11
- import { noop } from "../../internal";
12
-
13
- export function useWritable(room?: Room | null) {
14
- const [value, setValue] = useState(false);
15
-
16
- useEffect(() => {
17
- if (room) {
18
- const setWritable = () => setValue(room.isWritable);
19
- setWritable();
20
- room.callbacks.on("onEnableWriteNowChanged", setWritable);
21
- return () => room.callbacks.off("onEnableWriteNowChanged", setWritable);
22
- }
23
- }, [room]);
24
11
 
25
- return value;
26
- }
12
+ import { noop } from "../../internal";
13
+ import { useWritable } from "../hooks";
27
14
 
28
15
  export function useRoomState(room?: Room | null) {
29
16
  const [memberState, setMemberState] = useState<MemberState | undefined>(
@@ -53,44 +40,56 @@ export interface ToolbarHook {
53
40
  setStrokeColor(color: Color): void;
54
41
  }
55
42
 
56
- export function useToolbar(room?: Room | null): ToolbarHook {
43
+ export function useToolbar(
44
+ room?: Room | null,
45
+ manager?: WindowManager | null
46
+ ): ToolbarHook {
57
47
  const writable = useWritable(room);
58
48
  const { memberState } = useRoomState(room);
59
49
 
60
50
  const cleanCurrentScene = useCallback(() => {
61
- if (room?.isWritable) {
51
+ if (manager) {
52
+ manager.mainView.cleanCurrentScene();
53
+ } else if (room) {
62
54
  room.cleanCurrentScene();
63
55
  }
64
- }, [room]);
56
+ }, [manager, room]);
65
57
 
66
58
  const setAppliance = useCallback(
67
59
  (appliance: ApplianceNames, shape?: ShapeType) => {
68
- if (room?.isWritable) {
69
- room.setMemberState({
70
- currentApplianceName: appliance,
71
- shapeType: shape,
72
- });
60
+ const memberState = {
61
+ currentApplianceName: appliance,
62
+ shapeType: shape,
63
+ };
64
+ if (manager) {
65
+ manager.mainView.setMemberState(memberState);
66
+ } else if (room) {
67
+ room.setMemberState(memberState);
73
68
  }
74
69
  },
75
- [room]
70
+ [manager, room]
76
71
  );
77
72
 
78
73
  const setStrokeWidth = useCallback(
79
74
  (strokeWidth: number) => {
80
- if (room?.isWritable) {
75
+ if (manager) {
76
+ manager.mainView.setMemberState({ strokeWidth });
77
+ } else if (room) {
81
78
  room.setMemberState({ strokeWidth });
82
79
  }
83
80
  },
84
- [room]
81
+ [manager, room]
85
82
  );
86
83
 
87
84
  const setStrokeColor = useCallback(
88
85
  (strokeColor: Color) => {
89
- if (room?.isWritable) {
86
+ if (manager) {
87
+ manager.mainView.setMemberState({ strokeColor });
88
+ } else if (room) {
90
89
  room.setMemberState({ strokeColor });
91
90
  }
92
91
  },
93
- [room]
92
+ [manager, room]
94
93
  );
95
94
 
96
95
  return {
@@ -1 +1,2 @@
1
+ export * from "./hooks";
1
2
  export { name, Toolbar, type ToolbarProps } from "./Toolbar";
@@ -0,0 +1,109 @@
1
+ import type { CommonProps, GenericIcon } from "../../types";
2
+
3
+ import clsx from "clsx";
4
+ import React from "react";
5
+ import Tippy from "@tippyjs/react";
6
+
7
+ import { TopOffset } from "../../theme";
8
+ import { Icon } from "../../icons";
9
+ import { Minus } from "../../icons/Minus";
10
+ import { Plus } from "../../icons/Plus";
11
+ import { Reset } from "../../icons/Reset";
12
+ import { useWritable } from "../hooks";
13
+ import { useZoomControl } from "./hooks";
14
+
15
+ export const name = "fastboard-zoom-control";
16
+
17
+ export type ZoomControlProps = CommonProps &
18
+ GenericIcon<"reset" | "minus" | "plus">;
19
+
20
+ export function ZoomControl({
21
+ room,
22
+ manager,
23
+ theme = "light",
24
+ resetIcon,
25
+ resetIconDisable,
26
+ minusIcon,
27
+ minusIconDisable,
28
+ plusIcon,
29
+ plusIconDisable,
30
+ i18n,
31
+ }: ZoomControlProps) {
32
+ const writable = useWritable(room);
33
+ const { scale, resetCamera, zoomIn, zoomOut } = useZoomControl(room, manager);
34
+
35
+ const disabled = !writable;
36
+
37
+ return (
38
+ <div className={clsx(name, theme)}>
39
+ {/* <span className={clsx(`${name}-cut-line`, theme)} /> */}
40
+ <Tippy
41
+ className="fastboard-tip"
42
+ content={i18n?.t("zoomOut")}
43
+ theme={theme}
44
+ disabled={disabled}
45
+ placement="top"
46
+ duration={300}
47
+ offset={TopOffset}
48
+ >
49
+ <button
50
+ className={clsx(`${name}-btn`, "minus", theme)}
51
+ disabled={disabled}
52
+ onClick={zoomOut}
53
+ >
54
+ <Icon
55
+ fallback={<Minus theme={theme} />}
56
+ src={disabled ? minusIconDisable : minusIcon}
57
+ alt="[minus]"
58
+ />
59
+ </button>
60
+ </Tippy>
61
+ <span className={clsx(`${name}-scale`, theme)}>
62
+ {Math.ceil(scale * 100)}
63
+ </span>
64
+ <span className={clsx(`${name}-percent`, theme)}>%</span>
65
+ <Tippy
66
+ className="fastboard-tip"
67
+ content={i18n?.t("zoomIn")}
68
+ theme={theme}
69
+ disabled={disabled}
70
+ placement="top"
71
+ duration={300}
72
+ offset={TopOffset}
73
+ >
74
+ <button
75
+ className={clsx(`${name}-btn`, "plus", theme)}
76
+ disabled={disabled}
77
+ onClick={zoomIn}
78
+ >
79
+ <Icon
80
+ fallback={<Plus theme={theme} />}
81
+ src={disabled ? plusIconDisable : plusIcon}
82
+ alt="[plus]"
83
+ />
84
+ </button>
85
+ </Tippy>
86
+ <Tippy
87
+ className="fastboard-tip"
88
+ content={i18n?.t("reset")}
89
+ theme={theme}
90
+ disabled={disabled}
91
+ placement="top"
92
+ duration={300}
93
+ offset={TopOffset}
94
+ >
95
+ <button
96
+ className={clsx(`${name}-btn`, "reset", theme)}
97
+ disabled={disabled}
98
+ onClick={resetCamera}
99
+ >
100
+ <Icon
101
+ fallback={<Reset theme={theme} />}
102
+ src={disabled ? resetIconDisable : resetIcon}
103
+ alt="[reset]"
104
+ />
105
+ </button>
106
+ </Tippy>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,111 @@
1
+ import type { Room, RoomState } from "white-web-sdk";
2
+ import type { WindowManager } from "@netless/window-manager";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { clamp } from "../../internal";
5
+
6
+ export const ScalePoints: readonly number[] = [
7
+ 0.10737418240000011, 0.13421772800000012, 0.16777216000000014,
8
+ 0.20971520000000016, 0.26214400000000015, 0.3276800000000002,
9
+ 0.4096000000000002, 0.5120000000000001, 0.6400000000000001, 0.8, 1, 1.26,
10
+ 1.5876000000000001, 2.000376, 2.5204737600000002, 3.1757969376000004,
11
+ 4.001504141376, 5.041895218133761, 6.352787974848539, 8.00451284830916, 10,
12
+ ];
13
+
14
+ function nextScale(scale: number, delta: 1 | -1) {
15
+ const { length } = ScalePoints;
16
+ const last = length - 1;
17
+ if (scale < ScalePoints[0]) return ScalePoints[0];
18
+ if (scale > ScalePoints[last]) return ScalePoints[last];
19
+ for (let i = 0; i < length; ++i) {
20
+ const curr = ScalePoints[i];
21
+ const prev = i === 0 ? -Infinity : (ScalePoints[i - 1] + curr) / 2;
22
+ const next = i === last ? Infinity : (ScalePoints[i + 1] + curr) / 2;
23
+ if (prev <= scale && scale <= next)
24
+ return ScalePoints[clamp(i + delta, 0, last)];
25
+ }
26
+ return 1;
27
+ }
28
+
29
+ export function useZoomControl(
30
+ room?: Room | null,
31
+ manager?: WindowManager | null
32
+ ) {
33
+ const [scale, setScale] = useState(1);
34
+
35
+ const resetCamera = useCallback(() => {
36
+ if (manager) {
37
+ manager.mainView.moveCamera({ scale: 1, centerX: 0, centerY: 0 });
38
+ } else if (room) {
39
+ const { scenes, index } = room.state.sceneState;
40
+ if (scenes[index].ppt) {
41
+ room.scalePptToFit();
42
+ } else {
43
+ room.moveCamera({ scale: 1, centerX: 0, centerY: 0 });
44
+ }
45
+ }
46
+ }, [room, manager]);
47
+
48
+ const zoomIn = useCallback(() => {
49
+ if (manager) {
50
+ manager.mainView.moveCamera({
51
+ scale: nextScale(scale, 1),
52
+ centerX: 0,
53
+ centerY: 0,
54
+ });
55
+ } else if (room) {
56
+ room.moveCamera({
57
+ scale: nextScale(scale, 1),
58
+ centerX: 0,
59
+ centerY: 0,
60
+ });
61
+ }
62
+ }, [room, manager, scale]);
63
+
64
+ const zoomOut = useCallback(() => {
65
+ if (manager) {
66
+ manager.mainView.moveCamera({
67
+ scale: nextScale(scale, -1),
68
+ centerX: 0,
69
+ centerY: 0,
70
+ });
71
+ } else if (room) {
72
+ room.moveCamera({
73
+ scale: nextScale(scale, -1),
74
+ centerX: 0,
75
+ centerY: 0,
76
+ });
77
+ }
78
+ }, [room, manager, scale]);
79
+
80
+ useEffect(() => {
81
+ if (manager) {
82
+ setScale(manager.mainView.camera.scale);
83
+
84
+ const onCameraUpdated = ({ scale }: { scale: number }) => setScale(scale);
85
+
86
+ manager.mainView.callbacks.on("onCameraUpdated", onCameraUpdated);
87
+
88
+ return () => {
89
+ manager.mainView.callbacks.off("onCameraUpdated", onCameraUpdated);
90
+ };
91
+ }
92
+
93
+ if (room) {
94
+ setScale(room.state.cameraState.scale);
95
+
96
+ const onRoomStateChanged = (modifyState: Partial<RoomState>) => {
97
+ if (modifyState.cameraState) {
98
+ setScale(modifyState.cameraState.scale);
99
+ }
100
+ };
101
+
102
+ room.callbacks.on("onRoomStateChanged", onRoomStateChanged);
103
+
104
+ return () => {
105
+ room.callbacks.off("onRoomStateChanged", onRoomStateChanged);
106
+ };
107
+ }
108
+ }, [room, manager]);
109
+
110
+ return { scale, resetCamera, zoomIn, zoomOut };
111
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export { name, ZoomControl, type ZoomControlProps } from "./ZoomControl";