Quellcode durchsuchen

feat: add multiple whiteboard & snap sync

wxyyxc1992 vor 5 Jahren
Ursprung
Commit
862308c2de
38 geänderte Dateien mit 983 neuen und 585 gelöschten Zeilen
  1. 0
    340
      src/board/Whiteboard/index.ts
  2. 0
    0
      src/drawboard/Baseboard/index.less
  3. 18
    4
      src/drawboard/Baseboard/index.ts
  4. 0
    0
      src/drawboard/Drawboard/index.less
  5. 34
    24
      src/drawboard/Drawboard/index.ts
  6. 0
    37
      src/event/Event.ts
  7. 21
    0
      src/event/SyncEvent.ts
  8. 5
    0
      src/event/border-events.ts
  9. 27
    0
      src/event/marker-events.ts
  10. 4
    3
      src/index.ts
  11. 1
    1
      src/markers/ArrowMarker/index.ts
  12. 1
    0
      src/markers/BaseMarker/ResizeGrip.ts
  13. 85
    24
      src/markers/BaseMarker/index.ts
  14. 1
    1
      src/markers/CoverMarker/index.ts
  15. 1
    1
      src/markers/HighlightMarker/index.ts
  16. 1
    1
      src/markers/LineMarker/index.ts
  17. 80
    35
      src/markers/LinearMarker/index.ts
  18. 14
    2
      src/markers/RectMarker/RectBaseMarker.ts
  19. 1
    1
      src/markers/RectMarker/index.ts
  20. 101
    47
      src/markers/RectangularMarker/index.ts
  21. 32
    11
      src/markers/TextMarker/index.ts
  22. 7
    0
      src/markers/types.ts
  23. 1
    0
      src/renderer/SvgHelper/index.ts
  24. 4
    3
      src/renderer/Synthetizer/index.ts
  25. 0
    0
      src/toolbar/EmbeddingToolbar/index.ts
  26. 0
    0
      src/toolbar/FloatingToolbar/index.ts
  27. 5
    4
      src/toolbar/Toolbar.ts
  28. 1
    1
      src/toolbar/toolbar-items.ts
  29. 11
    0
      src/utils/layout.ts
  30. 2
    2
      src/utils/types.ts
  31. 0
    0
      src/whiteboard/AbstractWhiteboard/index.less
  32. 189
    0
      src/whiteboard/AbstractWhiteboard/index.ts
  33. 42
    0
      src/whiteboard/AbstractWhiteboard/snap.ts
  34. 114
    0
      src/whiteboard/MirrorWhiteboard/index.ts
  35. 0
    0
      src/whiteboard/ReplayWhiteboard/index.ts
  36. 0
    0
      src/whiteboard/WhitePage/index.less
  37. 66
    43
      src/whiteboard/WhitePage/index.ts
  38. 114
    0
      src/whiteboard/Whiteboard/index.ts

+ 0
- 340
src/board/Whiteboard/index.ts Datei anzeigen

@@ -1,340 +0,0 @@
1
-import { SyncEvent } from './../../event/Event';
2
-import * as Siema from 'siema';
3
-
4
-import { WhiteboardMode } from '../types';
5
-import { WhitePage } from '../WhitePage/index';
6
-import { EventHub } from '../../event/EventHub';
7
-import { uuid } from '../../utils/uuid';
8
-import { addClassName, createDivWithClassName } from '../../utils/dom';
9
-
10
-import './index.less';
11
-
12
-const LeftArrowIcon = require('../../assets/bx-left-arrow.svg');
13
-const RightArrowIcon = require('../../assets/bx-right-arrow.svg');
14
-
15
-const prefix = 'fcw-board';
16
-
17
-export class SerializableWhiteboard {
18
-  id: string;
19
-  sources: string[];
20
-  pageIds: string[];
21
-  visiblePageIndex: number;
22
-}
23
-
24
-export class Whiteboard {
25
-  id: string = uuid();
26
-  sources: string[] = [];
27
-
28
-  /** 元素 */
29
-  // 如果传入的是图片地址,则需要挂载到该 Target 元素下
30
-  target: HTMLDivElement;
31
-  imgsContainer: HTMLDivElement;
32
-  pagesContainer: HTMLDivElement;
33
-
34
-  /** UI Options */
35
-  // 事件中心
36
-  eventHub?: EventHub;
37
-  // 编辑模式
38
-  mode: WhiteboardMode = 'master';
39
-  // 是否为全屏模式
40
-  isFullscreen: boolean = false;
41
-
42
-  /** 句柄 */
43
-  pages: WhitePage[] = [];
44
-  get activePage() {
45
-    return this.pages[this.visiblePageIndex];
46
-  }
47
-  siema: any;
48
-
49
-  /** State | 内部状态 */
50
-  // 是否被初始化过,如果尚未被初始化,则等待来自于 Master 的同步消息
51
-  isInitialized: boolean = false;
52
-  isSyncing: boolean = false;
53
-  visiblePageIndex: number = 0;
54
-  emitInterval: any;
55
-
56
-  constructor(
57
-    target: HTMLDivElement,
58
-    {
59
-      sources,
60
-      eventHub,
61
-      mode,
62
-      visiblePageIndex
63
-    }: {
64
-      sources?: string[];
65
-      eventHub?: EventHub;
66
-      mode?: WhiteboardMode;
67
-      visiblePageIndex?: number;
68
-    } = {}
69
-  ) {
70
-    if (target) {
71
-      this.target = target;
72
-    } else {
73
-      this.target = document.createElement('div');
74
-      document.body.appendChild(this.target);
75
-    }
76
-
77
-    if (!this.target.id) {
78
-      this.target.id = this.id;
79
-    }
80
-
81
-    addClassName(this.target, prefix);
82
-
83
-    if (sources) {
84
-      this.sources = sources;
85
-    }
86
-
87
-    this.eventHub = eventHub;
88
-
89
-    if (mode) {
90
-      this.mode = mode;
91
-    }
92
-
93
-    // set inner state
94
-    if (typeof visiblePageIndex !== 'undefined') {
95
-      this.visiblePageIndex = visiblePageIndex;
96
-    }
97
-
98
-    this.init();
99
-  }
100
-
101
-  /** LifeCycle */
102
-  public open() {
103
-    // 依次渲染所有的页,隐藏非当前页之外的其他页
104
-    this.pages.forEach((page, i) => {
105
-      page.open();
106
-
107
-      if (i !== this.visiblePageIndex) {
108
-        page.hide();
109
-      }
110
-    });
111
-  }
112
-
113
-  /** 关闭当前的 Whiteboard */
114
-  public close() {
115
-    if (this.emitInterval) {
116
-      clearInterval(this.emitInterval);
117
-    }
118
-  }
119
-
120
-  /** 展示当前的 WhitePage */
121
-  public show() {
122
-    if (this.activePage) {
123
-      this.activePage.show();
124
-    }
125
-  }
126
-
127
-  public hide() {
128
-    if (this.activePage) {
129
-      this.activePage.hide();
130
-    }
131
-  }
132
-
133
-  /** 获取当前快照 */
134
-  public snap(): SerializableWhiteboard {
135
-    return {
136
-      id: this.id,
137
-      sources: this.sources,
138
-      pageIds: this.pages.map(page => page.id),
139
-      visiblePageIndex: this.visiblePageIndex
140
-    };
141
-  }
142
-
143
-  /** 初始化操作 */
144
-  private init() {
145
-    // 为 target 添加子 imgs 容器
146
-    this.imgsContainer = createDivWithClassName(`${prefix}-imgs`, this.target);
147
-    // 为 target 添加子 pages 容器
148
-    this.pagesContainer = createDivWithClassName(`${prefix}-pages`, this.target);
149
-
150
-    if (this.mode === 'master') {
151
-      this.initMaster();
152
-
153
-      this.emitSnapshot();
154
-    }
155
-
156
-    if (this.mode === 'mirror') {
157
-      this.initMirror();
158
-    }
159
-  }
160
-
161
-  /** 以主模式启动 */
162
-  private initMaster() {
163
-    // 初始化所有的 WhitePages
164
-    this.sources.forEach(source => {
165
-      const page = new WhitePage(
166
-        { imgSrc: source },
167
-        {
168
-          mode: this.mode,
169
-          eventHub: this.eventHub,
170
-          parentContainer: this.pagesContainer
171
-        }
172
-      );
173
-
174
-      // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
175
-      page.container.style.visibility = 'hidden';
176
-
177
-      this.pages.push(page);
178
-    });
179
-
180
-    this.initSiema();
181
-
182
-    // 初始化控制节点
183
-    const controller = createDivWithClassName(`${prefix}-controller`, this.target);
184
-
185
-    const prevEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
186
-    prevEle.innerHTML = LeftArrowIcon;
187
-
188
-    const nextEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
189
-    nextEle.innerHTML = RightArrowIcon;
190
-
191
-    nextEle!.addEventListener('click', () => {
192
-      const nextPageIndex =
193
-        this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
194
-      this.onPageChange(nextPageIndex);
195
-    });
196
-    prevEle!.addEventListener('click', () => {
197
-      const nextPageIndex =
198
-        this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
199
-
200
-      this.onPageChange(nextPageIndex);
201
-    });
202
-  }
203
-
204
-  /** 以镜像模式启动 */
205
-  private initMirror() {
206
-    if (!this.eventHub) {
207
-      throw new Error('Invalid eventHub');
208
-    }
209
-
210
-    this.eventHub.on('sync', (ev: SyncEvent) => {
211
-      if (ev.target !== 'whiteboard') {
212
-        return;
213
-      }
214
-
215
-      if (ev.event === 'snap') {
216
-        // 如果已经初始化完毕,则直接跳过
217
-        if (this.isInitialized) {
218
-          return;
219
-        }
220
-
221
-        this.onSnapshot(ev.data as SerializableWhiteboard);
222
-      }
223
-
224
-      if (ev.event === 'changeIndex' && ev.id === this.id) {
225
-        if (this.isInitialized) {
226
-          this.onPageChange(ev.data as number);
227
-        }
228
-      }
229
-    });
230
-  }
231
-
232
-  /** 初始化 Siema */
233
-  private initSiema() {
234
-    // 初始化所有的占位图片,用于给 Siema 播放使用
235
-    this.sources.forEach(source => {
236
-      const imgEle = document.createElement('img');
237
-      addClassName(imgEle, `${prefix}-img`);
238
-      imgEle.src = source;
239
-      imgEle.alt = 'Siema image';
240
-
241
-      this.imgsContainer.appendChild(imgEle);
242
-    });
243
-
244
-    // 初始化 Siema,并且添加控制节点
245
-    this.siema = new Siema({
246
-      selector: this.imgsContainer,
247
-      duration: 200,
248
-      easing: 'ease-out',
249
-      perPage: 1,
250
-      startIndex: 0,
251
-      draggable: false,
252
-      multipleDrag: true,
253
-      threshold: 20,
254
-      loop: false,
255
-      rtl: false
256
-    });
257
-  }
258
-
259
-  /** 响应页面切换的事件 */
260
-  private onPageChange(nextPageIndex: number) {
261
-    this.siema.goTo(nextPageIndex);
262
-    this.visiblePageIndex = nextPageIndex;
263
-
264
-    // 将所有的 Page 隐藏
265
-    this.pages.forEach((page, i) => {
266
-      if (nextPageIndex === i) {
267
-        page.show();
268
-      } else {
269
-        page.hide();
270
-      }
271
-    });
272
-
273
-    if (this.mode === 'master' && this.eventHub) {
274
-      this.eventHub.emit('sync', {
275
-        event: 'changeIndex',
276
-        id: this.id,
277
-        target: 'whiteboard',
278
-        data: nextPageIndex
279
-      });
280
-    }
281
-  }
282
-
283
-  private emitSnapshot() {
284
-    const innerFunc = () => {
285
-      if (this.eventHub) {
286
-        this.eventHub.emit('sync', {
287
-          event: 'snap',
288
-          id: this.id,
289
-          target: 'whiteboard',
290
-          data: this.snap()
291
-        });
292
-      }
293
-    };
294
-
295
-    // 定期触发事件
296
-    this.emitInterval = setInterval(() => {
297
-      innerFunc();
298
-    }, 5 * 1000);
299
-
300
-    // 首次事件,延时 500ms 发出
301
-    setTimeout(innerFunc, 500);
302
-  }
303
-
304
-  /** 响应获取到的快照事件 */
305
-  private onSnapshot(snap: SerializableWhiteboard) {
306
-    const { id, sources, pageIds, visiblePageIndex } = snap;
307
-
308
-    if (!this.isInitialized && !this.isSyncing) {
309
-      this.id = id;
310
-      this.sources = sources;
311
-      this.isSyncing = true;
312
-
313
-      // 初始化所有的 WhitePages
314
-      this.sources.forEach((source, i) => {
315
-        const page = new WhitePage(
316
-          { imgSrc: source },
317
-          {
318
-            mode: this.mode,
319
-            eventHub: this.eventHub,
320
-            parentContainer: this.pagesContainer
321
-          }
322
-        );
323
-        page.id = pageIds[i];
324
-
325
-        // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
326
-        page.container.style.visibility = 'hidden';
327
-
328
-        this.pages.push(page);
329
-
330
-        page.open();
331
-      });
332
-
333
-      this.initSiema();
334
-    }
335
-
336
-    this.isInitialized = true;
337
-    this.isSyncing = false;
338
-    this.onPageChange(visiblePageIndex);
339
-  }
340
-}

src/board/Baseboard/index.less → src/drawboard/Baseboard/index.less Datei anzeigen


src/board/Baseboard/index.ts → src/drawboard/Baseboard/index.ts Datei anzeigen

@@ -1,13 +1,16 @@
1
+import { Source } from './../../utils/types';
1 2
 import { uuid } from './../../utils/uuid';
2
-import { WhitePageSource } from './../types';
3 3
 import { SvgHelper } from './../../renderer/SvgHelper/index';
4 4
 
5 5
 /** 基础的绘制版 */
6 6
 export class Baseboard {
7 7
   id: string = uuid();
8 8
 
9
+  // 状态
10
+  isFullscreen: boolean = false;
11
+
9 12
   /** 元素 */
10
-  source: WhitePageSource;
13
+  source: Source;
11 14
 
12 15
   // 目前使用 Image 元素作为输出源
13 16
   target: HTMLImageElement;
@@ -20,16 +23,18 @@ export class Baseboard {
20 23
   width: number;
21 24
   height: number;
22 25
 
23
-  constructor(source: WhitePageSource) {
26
+  constructor(source: Source) {
24 27
     this.source = source;
25 28
 
29
+    // 如果传入的是某个元素,则直接附着
26 30
     if (source.imgEle) {
27 31
       this.target = source.imgEle!;
28 32
 
29
-      // 如果仅传入图片地址或者 Blob,则必须为全屏模式
30 33
       this.width = this.target.clientWidth;
31 34
       this.height = this.target.clientHeight;
32 35
     }
36
+
37
+    // 如果仅传入图片地址或者 Blob,则必须为全屏模式
33 38
   }
34 39
 
35 40
   protected initBoard = () => {
@@ -61,8 +66,17 @@ export class Baseboard {
61 66
     this.boardHolder.appendChild(this.boardCanvas);
62 67
   };
63 68
 
69
+  /** 放置 Board */
64 70
   protected positionBoard = () => {
65 71
     this.boardHolder.style.top = this.targetRect.top + 'px';
66 72
     this.boardHolder.style.left = this.targetRect.left + 'px';
67 73
   };
74
+
75
+  protected initEmbeddingToolbar = () => {};
76
+
77
+  protected positionEmbeddingToolbar = () => {};
78
+
79
+  protected initFloatingToolbar = () => {};
80
+
81
+  protected positionFloatingToolbar = () => {};
68 82
 }

src/board/Drawboard/index.less → src/drawboard/Drawboard/index.less Datei anzeigen


src/board/Drawboard/index.ts → src/drawboard/Drawboard/index.ts Datei anzeigen

@@ -1,9 +1,9 @@
1
-import { WhitePageSource } from './../types';
1
+import { Source } from './../../utils/types';
2 2
 import { Baseboard } from './../Baseboard/index';
3 3
 import { BaseMarker } from './../../markers/BaseMarker/index';
4 4
 import { getToolbars } from './../../toolbar/toolbar-items';
5
-import { WhitePage } from './../WhitePage/index';
6
-import { onSyncFunc } from './../../event/Event';
5
+import { WhitePage } from '../../whiteboard/WhitePage';
6
+import { onSyncFunc } from '../../event/SyncEvent';
7 7
 
8 8
 import { Synthetizer } from '../../renderer/Synthetizer';
9 9
 import { Toolbar } from '../../toolbar/Toolbar';
@@ -13,12 +13,13 @@ import './index.less';
13 13
 
14 14
 export class Drawboard extends Baseboard {
15 15
   /** Options */
16
-  private scale = 1.0;
16
+  scale = 1.0;
17
+  zIndex: number = 999;
17 18
 
18 19
   /** 句柄 */
19 20
   page: WhitePage;
20 21
 
21
-  private markers: BaseMarker[];
22
+  markers: BaseMarker[];
22 23
   get markerMap(): { [key: string]: BaseMarker } {
23 24
     const map = {};
24 25
     this.markers.forEach(marker => {
@@ -26,20 +27,20 @@ export class Drawboard extends Baseboard {
26 27
     });
27 28
     return map;
28 29
   }
29
-  private activeMarker: BaseMarker | null;
30
+  activeMarker: BaseMarker | null;
30 31
 
31
-  private toolbar: Toolbar;
32
-  private toolbars: ToolbarItem[];
33
-  private toolbarUI: HTMLElement;
32
+  toolbar: Toolbar;
33
+  toolbars: ToolbarItem[];
34
+  toolbarUI: HTMLElement;
34 35
 
35 36
   /** 回调 */
36
-  private onComplete: (dataUrl: string) => void = () => {};
37
-  private onChange: onSyncFunc = () => {};
38
-  private onCancel: () => void;
37
+  onComplete: (dataUrl: string) => void = () => {};
38
+  onChange: onSyncFunc = () => {};
39
+  onCancel: () => void;
39 40
 
40 41
   constructor(
41
-    source: WhitePageSource,
42
-    { page, onChange }: { page?: WhitePage; onChange?: onSyncFunc } = {}
42
+    source: Source,
43
+    { page, zIndex, onChange }: { page?: WhitePage; zIndex?: number; onChange?: onSyncFunc } = {}
43 44
   ) {
44 45
     super(source);
45 46
 
@@ -47,6 +48,10 @@ export class Drawboard extends Baseboard {
47 48
       this.page = page;
48 49
     }
49 50
 
51
+    if (zIndex) {
52
+      this.zIndex = zIndex;
53
+    }
54
+
50 55
     this.markers = [];
51 56
     this.activeMarker = null;
52 57
     this.toolbars = getToolbars(page);
@@ -99,7 +104,7 @@ export class Drawboard extends Baseboard {
99 104
     }
100 105
 
101 106
     this.boardHolder.style.visibility = 'visible';
102
-    this.boardHolder.style.zIndex = '9999';
107
+    this.boardHolder.style.zIndex = `${this.zIndex}`;
103 108
 
104 109
     if (this.toolbar) {
105 110
       this.toolbar.show();
@@ -126,6 +131,7 @@ export class Drawboard extends Baseboard {
126 131
     this.startRender(this.renderFinished);
127 132
   };
128 133
 
134
+  /** 添加某个 Marker */
129 135
   public addMarker = (markerType: typeof BaseMarker, { id }: { id?: string } = {}) => {
130 136
     // 假如 Drawboard 存在 Page 引用,则传导给 Marker
131 137
     const marker = markerType.createMarker(this.page);
@@ -134,6 +140,7 @@ export class Drawboard extends Baseboard {
134 140
       marker.id = id;
135 141
     }
136 142
 
143
+    marker.drawboard = this;
137 144
     marker.onSelected = this.selectMarker;
138 145
     marker.onChange = this.onChange;
139 146
 
@@ -149,23 +156,22 @@ export class Drawboard extends Baseboard {
149 156
     this.onChange({
150 157
       target: 'marker',
151 158
       parentId: this.page ? this.page.id : this.id,
152
-      event: 'add',
153
-      data: { type: marker.type, id: marker.id }
159
+      event: 'addMarker',
160
+      marker: { type: marker.type, id: marker.id }
154 161
     });
155 162
 
156 163
     this.markers.push(marker);
157
-
158 164
     this.selectMarker(marker);
159
-
160 165
     this.boardCanvas.appendChild(marker.visual);
161 166
 
167
+    // 默认居中
162 168
     const bbox = marker.visual.getBBox();
163 169
     const x = this.width / 2 / this.scale - bbox.width / 2;
164 170
     const y = this.height / 2 / this.scale - bbox.height / 2;
165 171
 
166
-    const translate = marker.visual.transform.baseVal.getItem(0);
167
-    translate.setMatrix(translate.matrix.translate(x, y));
168
-    marker.visual.transform.baseVal.replaceItem(translate, 0);
172
+    marker.moveTo(x, y);
173
+
174
+    return marker;
169 175
   };
170 176
 
171 177
   public deleteActiveMarker = () => {
@@ -173,10 +179,10 @@ export class Drawboard extends Baseboard {
173 179
       // 触发事件
174 180
       if (this.onChange) {
175 181
         this.onChange({
176
-          event: 'remove',
182
+          event: 'removeMarker',
177 183
           id: this.activeMarker.id,
178 184
           target: 'marker',
179
-          data: { id: this.activeMarker.id }
185
+          marker: { id: this.activeMarker.id }
180 186
         });
181 187
       }
182 188
       this.deleteMarker(this.activeMarker);
@@ -258,9 +264,13 @@ export class Drawboard extends Baseboard {
258 264
 
259 265
   private showUI = () => {
260 266
     this.toolbar = new Toolbar(this.toolbars, this.toolbarClick);
267
+    this.toolbar.zIndex = this.zIndex;
268
+
261 269
     this.toolbarUI = this.toolbar.getUI();
270
+
262 271
     document.body.appendChild(this.toolbarUI);
263 272
     this.toolbarUI.style.position = 'absolute';
273
+
264 274
     this.positionToolbar();
265 275
   };
266 276
 

+ 0
- 37
src/event/Event.ts Datei anzeigen

@@ -1,37 +0,0 @@
1
-export type TargetType = 'whiteboard' | 'page' | 'marker';
2
-export type EventType =
3
-  // 完全的状态同步,FCW 支持两种状态的同步交换:Snapshot(Snap) 与 KeyActions(KA) 方式
4
-  | 'snap'
5
-  // 添加
6
-  | 'add'
7
-  // 尺寸重置
8
-  | 'resize'
9
-  // 移动
10
-  | 'move'
11
-  // 移除
12
-  | 'remove'
13
-  // 下标改变
14
-  | 'changeIndex'
15
-  // 文本改变
16
-  | 'changeText';
17
-export type PositionType =
18
-  | 'left'
19
-  | 'right'
20
-  | 'topLeft'
21
-  | 'bottomLeft'
22
-  | 'topRight'
23
-  | 'bottomRight'
24
-  | 'centerLeft'
25
-  | 'centerRight'
26
-  | 'topCenter'
27
-  | 'bottomCenter';
28
-
29
-export interface SyncEvent {
30
-  target: TargetType;
31
-  id?: string;
32
-  parentId?: string;
33
-  event: EventType;
34
-  data?: object | string | number;
35
-}
36
-
37
-export type onSyncFunc = (ev: SyncEvent) => void;

+ 21
- 0
src/event/SyncEvent.ts Datei anzeigen

@@ -0,0 +1,21 @@
1
+import { BorderEventType } from './border-events';
2
+import { MarkerEventType, MarkerData } from './marker-events';
3
+import { WhiteboardSnap } from '../whiteboard/AbstractWhiteboard/snap';
4
+
5
+export type TargetType = 'whiteboard' | 'page' | 'marker';
6
+
7
+export type EventType = MarkerEventType | BorderEventType;
8
+
9
+export type onSyncFunc = (ev: SyncEvent) => void;
10
+
11
+export interface SyncEvent {
12
+  target: TargetType;
13
+
14
+  // 当前事件触发者的 ID
15
+  id?: string;
16
+  parentId?: string;
17
+  event: EventType;
18
+  marker?: MarkerData;
19
+  border?: WhiteboardSnap;
20
+  timestamp?: number;
21
+}

+ 5
- 0
src/event/border-events.ts Datei anzeigen

@@ -0,0 +1,5 @@
1
+export type BorderEventType =
2
+  // 完全的状态同步,FCW 支持两种状态的同步交换:Snapshot(Snap) 与 KeyActions(KA) 方式
3
+  | 'borderSnap'
4
+  // 下标改变
5
+  | 'borderChangePage';

+ 27
- 0
src/event/marker-events.ts Datei anzeigen

@@ -0,0 +1,27 @@
1
+import { MarkerType } from './../markers/types';
2
+import { PositionType } from '../utils/layout';
3
+
4
+export type MarkerEventType =
5
+  // 添加
6
+  | 'addMarker'
7
+  // 尺寸重置
8
+  | 'resizeMarker'
9
+  // 移动
10
+  | 'moveMarker'
11
+  // 移除
12
+  | 'removeMarker'
13
+  // 改变 Marker 文本
14
+  | 'inputMarker';
15
+
16
+export interface MarkerData {
17
+  id?: string;
18
+  type?: MarkerType;
19
+
20
+  // 内部数据
21
+  text?: string;
22
+
23
+  // 位置信息
24
+  dx?: number;
25
+  dy?: number;
26
+  pos?: PositionType;
27
+}

+ 4
- 3
src/index.ts Datei anzeigen

@@ -1,3 +1,4 @@
1
-export { Drawboard } from './board/Drawboard';
2
-export { Whiteboard } from './board/Whiteboard';
3
-export { WhiteboardMode } from './board/types';
1
+export { EventHub } from './event/EventHub';
2
+export { Drawboard } from './drawboard/Drawboard';
3
+export { Whiteboard } from './whiteboard/Whiteboard';
4
+export { Mode } from './utils/types';

+ 1
- 1
src/markers/ArrowMarker/index.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { LinearMarker } from '../LinearMarker';
3 3
 import { SvgHelper } from '../../renderer/SvgHelper';
4
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5 5
 
6 6
 export class ArrowMarker extends LinearMarker {
7 7
   type: MarkerType = 'arrow';

+ 1
- 0
src/markers/BaseMarker/ResizeGrip.ts Datei anzeigen

@@ -1,5 +1,6 @@
1 1
 import { SvgHelper } from '../../renderer/SvgHelper';
2 2
 
3
+/** 操作小圆点 */
3 4
 export class ResizeGrip {
4 5
   public visual: SVGGraphicsElement;
5 6
 

+ 85
- 24
src/markers/BaseMarker/index.ts Datei anzeigen

@@ -1,15 +1,19 @@
1
-import { WhitePage } from './../../board/WhitePage/index';
2
-import { PositionType } from '../../event/Event';
3
-import { onSyncFunc, EventType } from '../../event/Event';
1
+import { WhitePage } from '../../whiteboard/WhitePage/index';
2
+import { PositionType } from '../../utils/layout';
3
+import { onSyncFunc, EventType } from '../../event/SyncEvent';
4 4
 import { MarkerType } from '../types';
5 5
 import * as uuid from 'uuid/v1';
6 6
 import { SvgHelper } from '../../renderer/SvgHelper';
7
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
8
+import { Drawboard } from '../../drawboard/Drawboard/index';
7 9
 
8 10
 export class BaseMarker {
9 11
   id: string = uuid();
10 12
   type: MarkerType = 'base';
11
-  // 归属的
13
+  // 归属的 WhitePage
12 14
   page?: WhitePage;
15
+  // 归属的 Drawboard
16
+  drawboard?: Drawboard;
13 17
   // Marker 的属性发生变化后的回调
14 18
   onChange: onSyncFunc = () => {};
15 19
 
@@ -27,8 +31,10 @@ export class BaseMarker {
27 31
 
28 32
   public defs: SVGElement[] = [];
29 33
 
30
-  protected width: number = 200;
31
-  protected height: number = 50;
34
+  x: number = 0;
35
+  y: number = 0;
36
+  width: number = 200;
37
+  height: number = 50;
32 38
 
33 39
   protected isActive: boolean = true;
34 40
   protected isDragging: boolean = false;
@@ -39,13 +45,21 @@ export class BaseMarker {
39 45
 
40 46
   public reactToManipulation(
41 47
     type: EventType,
42
-    { dx, dy, pos }: { dx: number; dy: number; pos: PositionType }
48
+    { dx, dy, pos }: { dx?: number; dy?: number; pos?: PositionType } = {}
43 49
   ) {
44
-    if (type === 'move') {
50
+    if (type === 'moveMarker') {
51
+      if (!dx || !dy) {
52
+        return;
53
+      }
54
+
45 55
       this.move(dx, dy);
46 56
     }
47 57
 
48
-    if (type === 'resize') {
58
+    if (type === 'resizeMarker') {
59
+      if (!dx || !dy) {
60
+        return;
61
+      }
62
+
49 63
       this.resizeByEvent(dx, dy, pos);
50 64
     }
51 65
   }
@@ -56,14 +70,21 @@ export class BaseMarker {
56 70
     const dx = (ev.screenX - this.previousMouseX) / scale;
57 71
     const dy = (ev.screenY - this.previousMouseY) / scale;
58 72
 
73
+    // 如果在拖拽
59 74
     if (this.isDragging) {
60
-      this.onChange({ target: 'marker', id: this.id, event: 'move', data: { dx, dy } });
75
+      this.onChange({ target: 'marker', id: this.id, event: 'moveMarker', marker: { dx, dy } });
61 76
       this.move(dx, dy);
62 77
     }
63 78
 
79
+    // 如果是缩放
64 80
     if (this.isResizing) {
65 81
       this.resize(dx, dy, (pos: PositionType) => {
66
-        this.onChange({ target: 'marker', id: this.id, event: 'resize', data: { dx, dy, pos } });
82
+        this.onChange({
83
+          target: 'marker',
84
+          id: this.id,
85
+          event: 'resizeMarker',
86
+          marker: { dx, dy, pos }
87
+        });
67 88
       });
68 89
     }
69 90
 
@@ -90,6 +111,59 @@ export class BaseMarker {
90 111
     return;
91 112
   }
92 113
 
114
+  /** 生成某个快照 */
115
+  public captureSnap(): MarkerSnap {
116
+    return {
117
+      id: this.id,
118
+      type: this.type,
119
+      isActive: this.isActive,
120
+      x: this.x,
121
+      y: this.y
122
+    };
123
+  }
124
+
125
+  /** 应用某个快照 */
126
+  public applySnap(snap: MarkerSnap): void {
127
+    this.id = snap.id;
128
+    this.type = snap.type;
129
+
130
+    if (snap.x && snap.y) {
131
+      // 移动当前位置
132
+      this.moveTo(snap.x, snap.y);
133
+    }
134
+
135
+    // 判断是否为激活
136
+    if (this.isActive) {
137
+      this.select();
138
+    }
139
+  }
140
+
141
+  protected resize(x: number, y: number, cb?: Function) {
142
+    return;
143
+  }
144
+  protected resizeByEvent(x: number, y: number, pos?: PositionType) {
145
+    return;
146
+  }
147
+
148
+  public move = (dx: number, dy: number) => {
149
+    const translate = this.visual.transform.baseVal.getItem(0);
150
+    translate.setMatrix(translate.matrix.translate(dx, dy));
151
+    this.visual.transform.baseVal.replaceItem(translate, 0);
152
+
153
+    this.x += dx;
154
+    this.y += dy;
155
+  };
156
+
157
+  public moveTo = (x: number, y: number) => {
158
+    const translate = this.visual.transform.baseVal.getItem(0);
159
+    translate.setMatrix(translate.matrix.translate(x - this.x, y - this.y));
160
+    this.visual.transform.baseVal.replaceItem(translate, 0);
161
+
162
+    this.x = x;
163
+    this.y = y;
164
+  };
165
+
166
+  /** Init */
93 167
   protected setup() {
94 168
     this.visual = SvgHelper.createGroup();
95 169
     // translate
@@ -115,13 +189,6 @@ export class BaseMarker {
115 189
     this.renderVisual.appendChild(el);
116 190
   };
117 191
 
118
-  protected resize(x: number, y: number, cb?: Function) {
119
-    return;
120
-  }
121
-  protected resizeByEvent(x: number, y: number, pos?: PositionType) {
122
-    return;
123
-  }
124
-
125 192
   /** 截获 Touch 事件,并且转发为 Mouse 事件 */
126 193
   protected onTouch(ev: TouchEvent) {
127 194
     ev.preventDefault();
@@ -186,10 +253,4 @@ export class BaseMarker {
186 253
     ev.stopPropagation();
187 254
     this.manipulate(ev);
188 255
   };
189
-
190
-  private move = (dx: number, dy: number) => {
191
-    const translate = this.visual.transform.baseVal.getItem(0);
192
-    translate.setMatrix(translate.matrix.translate(dx, dy));
193
-    this.visual.transform.baseVal.replaceItem(translate, 0);
194
-  };
195 256
 }

+ 1
- 1
src/markers/CoverMarker/index.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5 5
 
6 6
 export class CoverMarker extends RectBaseMarker {
7 7
   type: MarkerType = 'cover';

+ 1
- 1
src/markers/HighlightMarker/index.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5 5
 
6 6
 export class HighlightMarker extends RectBaseMarker {
7 7
   type: MarkerType = 'highlight';

+ 1
- 1
src/markers/LineMarker/index.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { LinearMarker } from '../LinearMarker';
3 3
 import { SvgHelper } from './../../renderer/SvgHelper/index';
4
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5 5
 
6 6
 export class LineMarker extends LinearMarker {
7 7
   type: MarkerType = 'line';

+ 80
- 35
src/markers/LinearMarker/index.ts Datei anzeigen

@@ -1,10 +1,15 @@
1
-import { WhitePage } from './../../board/WhitePage/index';
1
+import { WhitePage } from '../../whiteboard/WhitePage/index';
2 2
 import { BaseMarker } from '../BaseMarker';
3 3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4 4
 import { SvgHelper } from '../../renderer/SvgHelper';
5
-import { PositionType } from '../../event/Event';
6
-
7
-export class LinearMarker extends BaseMarker {
5
+import { PositionType } from '../../utils/layout';
6
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
7
+import { LinearBound } from '../types';
8
+
9
+/**
10
+ * 线性标识
11
+ */
12
+export class LinearMarker extends BaseMarker implements LinearBound {
8 13
   public static createMarker = (page?: WhitePage): LinearMarker => {
9 14
     const marker = new LinearMarker();
10 15
     marker.page = page;
@@ -12,22 +17,51 @@ export class LinearMarker extends BaseMarker {
12 17
     return marker;
13 18
   };
14 19
 
15
-  protected markerLine: SVGLineElement;
16
-
17 20
   private readonly MIN_LENGTH = 20;
21
+  // 线的左端点与右端点
22
+  x1: number = 0;
23
+  y1: number = 0;
24
+  x2: number = this.width;
25
+  y2: number = 0;
18 26
 
27
+  /** @region UI Handlers */
28
+  protected markerLine: SVGLineElement;
19 29
   private markerBgLine: SVGLineElement; // touch target
20
-
21 30
   private controlBox: SVGGElement;
22 31
 
23 32
   private controlGrips: { left: ResizeGrip; right: ResizeGrip };
24 33
   private activeGrip: ResizeGrip | null;
25 34
 
26
-  private x1: number = 0;
27
-  private y1: number = 0;
28
-  private x2: number = this.width;
29
-  private y2: number = 0;
35
+  /** Getter & Setter */
36
+  public getLineLength = (x1: number, y1: number, x2: number, y2: number): number => {
37
+    const dx = Math.abs(x1 - x2);
38
+    const dy = Math.abs(y1 - y2);
39
+
40
+    return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
41
+  };
42
+
43
+  public captureSnap(): MarkerSnap {
44
+    const baseSnap = super.captureSnap();
45
+
46
+    baseSnap.linearSnap = {
47
+      x1: this.x1,
48
+      y1: this.y1,
49
+      x2: this.x2,
50
+      y2: this.y2
51
+    };
52
+
53
+    return baseSnap;
54
+  }
55
+
56
+  public applySnap(snap: MarkerSnap): void {
57
+    super.applySnap(snap);
30 58
 
59
+    if (snap.linearSnap) {
60
+      this.positionLine(snap.linearSnap);
61
+    }
62
+  }
63
+
64
+  /** 复写操作事件 */
31 65
   public endManipulation() {
32 66
     super.endManipulation();
33 67
     this.isResizing = false;
@@ -62,6 +96,7 @@ export class LinearMarker extends BaseMarker {
62 96
     }
63 97
   }
64 98
 
99
+  /** 主动伸缩操作 */
65 100
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
66 101
     if (this.activeGrip) {
67 102
       if (
@@ -96,6 +131,7 @@ export class LinearMarker extends BaseMarker {
96 131
     this.adjustControlBox();
97 132
   }
98 133
 
134
+  /** 根据事件进行伸缩操作 */
99 135
   protected resizeByEvent(x: number, y: number, pos?: PositionType) {
100 136
     if (pos === 'left') {
101 137
       this.activeGrip = this.controlGrips.left;
@@ -106,12 +142,7 @@ export class LinearMarker extends BaseMarker {
106 142
     this.resize(x, y);
107 143
   }
108 144
 
109
-  private getLineLength = (x1: number, y1: number, x2: number, y2: number): number => {
110
-    const dx = Math.abs(x1 - x2);
111
-    const dy = Math.abs(y1 - y2);
112
-
113
-    return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
114
-  };
145
+  /** Init */
115 146
 
116 147
   private addControlBox = () => {
117 148
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-line-control-box']]);
@@ -149,24 +180,7 @@ export class LinearMarker extends BaseMarker {
149 180
     return grip;
150 181
   };
151 182
 
152
-  private positionGrips = () => {
153
-    const gripSize = this.controlGrips.left.GRIP_SIZE;
154
-
155
-    const x1 = this.x1 - gripSize / 2;
156
-    const y1 = this.y1 - gripSize / 2;
157
-    const x2 = this.x2 - gripSize / 2;
158
-    const y2 = this.y2 - gripSize / 2;
159
-
160
-    this.positionGrip(this.controlGrips.left.visual, x1, y1);
161
-    this.positionGrip(this.controlGrips.right.visual, x2, y2);
162
-  };
163
-
164
-  private positionGrip = (grip: SVGGraphicsElement, x: number, y: number) => {
165
-    const translate = grip.transform.baseVal.getItem(0);
166
-    translate.setTranslate(x, y);
167
-    grip.transform.baseVal.replaceItem(translate, 0);
168
-  };
169
-
183
+  /** Event Handlers */
170 184
   private gripMouseDown = (ev: MouseEvent) => {
171 185
     this.isResizing = true;
172 186
     this.activeGrip =
@@ -189,4 +203,35 @@ export class LinearMarker extends BaseMarker {
189 203
       this.resize(ev.movementX, ev.movementY);
190 204
     }
191 205
   };
206
+
207
+  /** UI Position */
208
+  private positionLine = (bound: LinearBound) => {
209
+    this.x1 = bound.x1;
210
+    this.y1 = bound.y1;
211
+    this.x2 = bound.x2;
212
+    this.y2 = bound.y2;
213
+
214
+    this.markerBgLine.setAttribute('x1', this.x1.toString());
215
+    this.markerBgLine.setAttribute('y1', this.y1.toString());
216
+    this.markerLine.setAttribute('x2', this.x2.toString());
217
+    this.markerLine.setAttribute('y2', this.y2.toString());
218
+  };
219
+
220
+  private positionGrips = () => {
221
+    const gripSize = this.controlGrips.left.GRIP_SIZE;
222
+
223
+    const x1 = this.x1 - gripSize / 2;
224
+    const y1 = this.y1 - gripSize / 2;
225
+    const x2 = this.x2 - gripSize / 2;
226
+    const y2 = this.y2 - gripSize / 2;
227
+
228
+    this.positionGrip(this.controlGrips.left.visual, x1, y1);
229
+    this.positionGrip(this.controlGrips.right.visual, x2, y2);
230
+  };
231
+
232
+  private positionGrip = (grip: SVGGraphicsElement, x: number, y: number) => {
233
+    const translate = grip.transform.baseVal.getItem(0);
234
+    translate.setTranslate(x, y);
235
+    grip.transform.baseVal.replaceItem(translate, 0);
236
+  };
192 237
 }

+ 14
- 2
src/markers/RectMarker/RectBaseMarker.ts Datei anzeigen

@@ -1,7 +1,8 @@
1
-import { PositionType } from '../../event/Event';
1
+import { PositionType } from '../../utils/layout';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectangularMarker } from '../RectangularMarker';
4
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
5 6
 
6 7
 export class RectBaseMarker extends RectangularMarker {
7 8
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
@@ -13,6 +14,17 @@ export class RectBaseMarker extends RectangularMarker {
13 14
 
14 15
   private markerRect: SVGRectElement;
15 16
 
17
+  /** Getter & Setter */
18
+
19
+  public applySnap(snap: MarkerSnap) {
20
+    super.applySnap(snap);
21
+
22
+    if (snap.rectSnap) {
23
+      this.markerRect.setAttribute('width', this.width.toString());
24
+      this.markerRect.setAttribute('height', this.height.toString());
25
+    }
26
+  }
27
+
16 28
   protected setup() {
17 29
     super.setup();
18 30
 

+ 1
- 1
src/markers/RectMarker/index.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { WhitePage } from './../../board/WhitePage/index';
1
+import { WhitePage } from '../../whiteboard/WhitePage/index';
2 2
 import { MarkerType } from './../types';
3 3
 import { RectBaseMarker } from './RectBaseMarker';
4 4
 import { SvgHelper } from '../../renderer/SvgHelper';

+ 101
- 47
src/markers/RectangularMarker/index.ts Datei anzeigen

@@ -1,9 +1,10 @@
1 1
 import { RectangularMarkerGrips } from './RectangularMarkerGrips';
2 2
 import { BaseMarker } from '../BaseMarker';
3 3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4
-import { PositionType } from '../../event/Event';
5 4
 import { SvgHelper } from '../../renderer/SvgHelper';
6
-import { WhitePage } from '../../board/WhitePage';
5
+import { WhitePage } from '../../whiteboard/WhitePage';
6
+import { PositionType } from '../../utils/layout';
7
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
7 8
 
8 9
 export class RectangularMarker extends BaseMarker {
9 10
   public static createMarker = (page?: WhitePage): RectangularMarker => {
@@ -22,6 +23,32 @@ export class RectangularMarker extends BaseMarker {
22 23
   private controlGrips: RectangularMarkerGrips;
23 24
   private activeGrip: ResizeGrip | null;
24 25
 
26
+  /** Getter & Setter */
27
+  public captureSnap(): MarkerSnap {
28
+    const snap = super.captureSnap();
29
+
30
+    snap.rectSnap = {
31
+      width: this.width,
32
+      height: this.height
33
+    };
34
+
35
+    return snap;
36
+  }
37
+
38
+  public applySnap(snap: MarkerSnap) {
39
+    super.applySnap(snap);
40
+
41
+    if (snap.rectSnap) {
42
+      const { width, height } = snap.rectSnap;
43
+
44
+      if (width && height) {
45
+        this.width = width;
46
+        this.height = height;
47
+        this.adjustControlBox();
48
+      }
49
+    }
50
+  }
51
+
25 52
   public endManipulation() {
26 53
     super.endManipulation();
27 54
     this.isResizing = false;
@@ -48,74 +75,98 @@ export class RectangularMarker extends BaseMarker {
48 75
     }
49 76
   }
50 77
 
51
-  protected resizeByEvent(x: number, y: number, pos: PositionType) {
78
+  protected resizeByEvent(dx: number, dy: number, pos: PositionType) {
52 79
     this.activeGrip = this.controlGrips[pos];
53
-    this.resize(x, y);
80
+    this.resize(dx, dy);
54 81
   }
55 82
 
56
-  protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
83
+  protected resize(dx: number, dy: number, onPosition?: (pos: PositionType) => void) {
57 84
     let translateX = 0;
58 85
     let translateY = 0;
59 86
 
60 87
     switch (this.activeGrip) {
61 88
       case this.controlGrips.topLeft:
62
-        this.width -= x;
63
-        this.height -= y;
64
-        translateX += x;
65
-        translateY += y;
89
+        this.width -= dx;
90
+        this.height -= dy;
91
+        translateX += dx;
92
+        translateY += dy;
93
+
94
+        this.x += dx;
95
+        this.y += dy;
96
+
66 97
         if (onPosition) {
67 98
           onPosition('topLeft');
68 99
         }
69 100
         break;
101
+
70 102
       case this.controlGrips.bottomLeft:
71
-        this.width -= x;
72
-        this.height += y;
73
-        translateX += x;
103
+        this.width -= dx;
104
+        this.height += dy;
105
+        translateX += dx;
106
+
107
+        this.x += dx;
108
+
74 109
         if (onPosition) {
75 110
           onPosition('bottomLeft');
76 111
         }
77 112
         break;
113
+
78 114
       case this.controlGrips.topRight:
79
-        this.width += x;
80
-        this.height -= y;
81
-        translateY += y;
115
+        this.width += dx;
116
+        this.height -= dy;
117
+        translateY += dy;
118
+
119
+        this.y += dy;
120
+
82 121
         if (onPosition) {
83 122
           onPosition('topRight');
84 123
         }
85 124
         break;
125
+
86 126
       case this.controlGrips.bottomRight:
87
-        this.width += x;
88
-        this.height += y;
127
+        this.width += dx;
128
+        this.height += dy;
89 129
         if (onPosition) {
90 130
           onPosition('bottomRight');
91 131
         }
92 132
         break;
133
+
93 134
       case this.controlGrips.centerLeft:
94
-        this.width -= x;
95
-        translateX += x;
135
+        this.width -= dx;
136
+        translateX += dx;
137
+
138
+        this.x += dx;
139
+
96 140
         if (onPosition) {
97 141
           onPosition('centerLeft');
98 142
         }
99 143
         break;
144
+
100 145
       case this.controlGrips.centerRight:
101
-        this.width += x;
146
+        this.width += dx;
102 147
         if (onPosition) {
103 148
           onPosition('centerRight');
104 149
         }
105 150
         break;
151
+
106 152
       case this.controlGrips.topCenter:
107
-        this.height -= y;
108
-        translateY += y;
153
+        this.height -= dy;
154
+        translateY += dy;
155
+
156
+        this.y += dy;
157
+
109 158
         if (onPosition) {
110 159
           onPosition('topCenter');
111 160
         }
112 161
         break;
162
+
113 163
       case this.controlGrips.bottomCenter:
114
-        this.height += y;
164
+        this.height += dy;
115 165
         if (onPosition) {
116 166
           onPosition('bottomCenter');
117 167
         }
118 168
         break;
169
+
119 170
       default:
120 171
         break;
121 172
     }
@@ -144,10 +195,7 @@ export class RectangularMarker extends BaseMarker {
144 195
     this.adjustControlBox();
145 196
   }
146 197
 
147
-  protected onTouch(ev: TouchEvent) {
148
-    super.onTouch(ev);
149
-  }
150
-
198
+  /** Init Comps */
151 199
   private addControlBox = () => {
152 200
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-rect-control-box']]);
153 201
     const translate = SvgHelper.createTransform();
@@ -204,6 +252,32 @@ export class RectangularMarker extends BaseMarker {
204 252
     return grip;
205 253
   };
206 254
 
255
+  /** Event Handlers */
256
+  protected onTouch(ev: TouchEvent) {
257
+    super.onTouch(ev);
258
+  }
259
+
260
+  private gripMouseDown = (ev: MouseEvent) => {
261
+    this.isResizing = true;
262
+    this.activeGrip = this.controlGrips.findGripByVisual(ev.target as SVGGraphicsElement) || null;
263
+    this.previousMouseX = ev.screenX;
264
+    this.previousMouseY = ev.screenY;
265
+    ev.stopPropagation();
266
+  };
267
+
268
+  private gripMouseUp = (ev: MouseEvent) => {
269
+    this.isResizing = false;
270
+    this.activeGrip = null;
271
+    ev.stopPropagation();
272
+  };
273
+
274
+  private gripMouseMove = (ev: MouseEvent) => {
275
+    if (this.isResizing) {
276
+      this.manipulate(ev);
277
+    }
278
+  };
279
+
280
+  /** UI Position */
207 281
   private positionGrips = () => {
208 282
     const gripSize = this.controlGrips.topLeft.GRIP_SIZE;
209 283
 
@@ -229,24 +303,4 @@ export class RectangularMarker extends BaseMarker {
229 303
     translate.setTranslate(x, y);
230 304
     grip.transform.baseVal.replaceItem(translate, 0);
231 305
   };
232
-
233
-  private gripMouseDown = (ev: MouseEvent) => {
234
-    this.isResizing = true;
235
-    this.activeGrip = this.controlGrips.findGripByVisual(ev.target as SVGGraphicsElement) || null;
236
-    this.previousMouseX = ev.screenX;
237
-    this.previousMouseY = ev.screenY;
238
-    ev.stopPropagation();
239
-  };
240
-
241
-  private gripMouseUp = (ev: MouseEvent) => {
242
-    this.isResizing = false;
243
-    this.activeGrip = null;
244
-    ev.stopPropagation();
245
-  };
246
-
247
-  private gripMouseMove = (ev: MouseEvent) => {
248
-    if (this.isResizing) {
249
-      this.manipulate(ev);
250
-    }
251
-  };
252 306
 }

+ 32
- 11
src/markers/TextMarker/index.ts Datei anzeigen

@@ -1,8 +1,9 @@
1 1
 import { MarkerType } from '../types';
2 2
 import { RectangularMarker } from '../RectangularMarker';
3 3
 import { SvgHelper } from '../../renderer/SvgHelper';
4
-import { PositionType } from '../../event/Event';
5
-import { WhitePage } from '../../board/WhitePage';
4
+import { WhitePage } from '../../whiteboard/WhitePage';
5
+import { PositionType } from '../../utils/layout';
6
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
6 7
 
7 8
 const OkIcon = require('../../assets/check.svg');
8 9
 const CancelIcon = require('../../assets/times.svg');
@@ -17,23 +18,38 @@ export class TextMarker extends RectangularMarker {
17 18
     return marker;
18 19
   };
19 20
 
20
-  /** 设置文本 */
21
+  /** UI Options */
22
+  protected readonly MIN_SIZE = 50;
23
+  private readonly DEFAULT_TEXT = 'Double-click to edit text';
24
+  private text: string = this.DEFAULT_TEXT;
25
+  private inDoubleTap = false;
26
+
27
+  /** UI Handlers */
28
+  private textElement: SVGTextElement;
29
+  private editor: HTMLDivElement;
30
+  private editorTextArea: HTMLTextAreaElement;
31
+
32
+  /** Getter & Setter */
21 33
   public setText(text: string) {
22 34
     this.text = text;
23 35
     this.renderText();
24 36
   }
25 37
 
26
-  protected readonly MIN_SIZE = 50;
38
+  public captureSnap(): MarkerSnap {
39
+    const baseSnap = super.captureSnap();
27 40
 
28
-  private readonly DEFAULT_TEXT = 'Double-click to edit text';
29
-  private text: string = this.DEFAULT_TEXT;
30
-  private textElement: SVGTextElement;
41
+    baseSnap.textSnap = { text: this.text };
31 42
 
32
-  private inDoubleTap = false;
43
+    return baseSnap;
44
+  }
33 45
 
34
-  private editor: HTMLDivElement;
46
+  public applySnap(snap: MarkerSnap) {
47
+    super.applySnap(snap);
35 48
 
36
-  private editorTextArea: HTMLTextAreaElement;
49
+    if (snap.textSnap && snap.textSnap.text !== this.text) {
50
+      this.setText(snap.textSnap.text);
51
+    }
52
+  }
37 53
 
38 54
   protected setup() {
39 55
     super.setup();
@@ -147,7 +163,12 @@ export class TextMarker extends RectangularMarker {
147 163
     }
148 164
 
149 165
     // 触发文本修改时间
150
-    this.onChange({ target: 'marker', id: this.id, event: 'changeText', data: this.text });
166
+    this.onChange({
167
+      target: 'marker',
168
+      id: this.id,
169
+      event: 'inputMarker',
170
+      marker: { text: this.text }
171
+    });
151 172
 
152 173
     this.renderText();
153 174
     this.closeEditor();

+ 7
- 0
src/markers/types.ts Datei anzeigen

@@ -8,6 +8,13 @@ import { RectMarker } from './RectMarker';
8 8
 
9 9
 export type MarkerType = 'base' | 'arrow' | 'cover' | 'line' | 'rect' | 'text' | 'highlight';
10 10
 
11
+export interface LinearBound {
12
+  x1: number;
13
+  y1: number;
14
+  x2: number;
15
+  y2: number;
16
+}
17
+
11 18
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
12 19
   switch (type) {
13 20
     case 'arrow':

+ 1
- 0
src/renderer/SvgHelper/index.ts Datei anzeigen

@@ -1,3 +1,4 @@
1
+/** 通用的 Svg 助手 */
1 2
 export class SvgHelper {
2 3
   public static createRect = (
3 4
     width: number | string,

+ 4
- 3
src/renderer/Synthetizer/index.ts Datei anzeigen

@@ -1,14 +1,15 @@
1 1
 import { isHTMLImageElement } from '../../utils/validator';
2 2
 
3
-/** 图片导出 */
3
+/** 图片导出,将原图片与 Svgs 绘制到某个 Canvas 中 */
4 4
 export class Synthetizer {
5 5
   public rasterize(
6
-    target: HTMLImageElement | HTMLDivElement,
6
+    // target 是原图片
7
+    target: HTMLImageElement,
7 8
     markerImage: SVGSVGElement,
8 9
     done: (dataUrl: string) => void
9 10
   ) {
10 11
     if (!isHTMLImageElement(target)) {
11
-      throw new Error('Error: only support export HTMLImageElement');
12
+      throw new Error('Error: only support export to HTMLImageElement');
12 13
     }
13 14
 
14 15
     const canvas = document.createElement('canvas');

+ 0
- 0
src/toolbar/EmbeddingToolbar/index.ts Datei anzeigen


+ 0
- 0
src/toolbar/FloatingToolbar/index.ts Datei anzeigen


+ 5
- 4
src/toolbar/Toolbar.ts Datei anzeigen

@@ -4,11 +4,12 @@ import { uuid } from '../utils/uuid';
4 4
 
5 5
 export class Toolbar {
6 6
   id: string = uuid();
7
+  zIndex: number = 999;
7 8
 
8
-  private toolbarItems: ToolbarItem[];
9
-  private toolbarUI: HTMLElement;
9
+  toolbarItems: ToolbarItem[];
10
+  toolbarUI: HTMLElement;
10 11
 
11
-  private clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void;
12
+  clickHandler: (ev: MouseEvent, toolbarItem: ToolbarItem) => void;
12 13
 
13 14
   constructor(
14 15
     toolbarItems: ToolbarItem[],
@@ -39,6 +40,6 @@ export class Toolbar {
39 40
 
40 41
   public show() {
41 42
     this.toolbarUI.style.visibility = 'visible';
42
-    this.toolbarUI.style.zIndex = '999';
43
+    this.toolbarUI.style.zIndex = `${this.zIndex}`;
43 44
   }
44 45
 }

+ 1
- 1
src/toolbar/toolbar-items.ts Datei anzeigen

@@ -1,4 +1,4 @@
1
-import { WhitePage } from './../board/WhitePage/index';
1
+import { WhitePage } from '../whiteboard/WhitePage/index';
2 2
 import { RectMarker } from './../markers/RectMarker/index';
3 3
 import { CoverMarker } from './../markers/CoverMarker/index';
4 4
 import { TextMarker } from './../markers/TextMarker/index';

+ 11
- 0
src/utils/layout.ts Datei anzeigen

@@ -0,0 +1,11 @@
1
+export type PositionType =
2
+  | 'left'
3
+  | 'right'
4
+  | 'topLeft'
5
+  | 'bottomLeft'
6
+  | 'topRight'
7
+  | 'bottomRight'
8
+  | 'centerLeft'
9
+  | 'centerRight'
10
+  | 'topCenter'
11
+  | 'bottomCenter';

src/board/types.ts → src/utils/types.ts Datei anzeigen

@@ -1,7 +1,7 @@
1 1
 // 是主动绘制模式,还是镜像模式
2
-export type WhiteboardMode = 'master' | 'mirror';
2
+export type Mode = 'master' | 'mirror' | 'replay';
3 3
 
4
-export type WhitePageSource = {
4
+export type Source = {
5 5
   // 需要展示的图片元素
6 6
   imgEle?: HTMLImageElement;
7 7
 

src/board/Whiteboard/index.less → src/whiteboard/AbstractWhiteboard/index.less Datei anzeigen


+ 189
- 0
src/whiteboard/AbstractWhiteboard/index.ts Datei anzeigen

@@ -0,0 +1,189 @@
1
+import { Mode } from './../../utils/types';
2
+import { SyncEvent } from '../../event/SyncEvent';
3
+
4
+import { WhitePage } from '../WhitePage/index';
5
+import { EventHub } from '../../event/EventHub';
6
+import { uuid } from '../../utils/uuid';
7
+import { addClassName } from '../../utils/dom';
8
+
9
+import './index.less';
10
+import { WhiteboardSnap } from '../AbstractWhiteboard/snap';
11
+import * as Siema from 'siema';
12
+
13
+const prefix = 'fcw-board';
14
+
15
+export abstract class AbstractWhiteboard {
16
+  id: string = uuid();
17
+  sources: string[] = [];
18
+
19
+  /** 元素 */
20
+  // 如果传入的是图片地址,则需要挂载到该 Target 元素下
21
+  target: HTMLDivElement;
22
+  imgsContainer: HTMLDivElement;
23
+  pagesContainer: HTMLDivElement;
24
+
25
+  /** Options */
26
+  // 是否仅同步快照数据,用于弱网状态下
27
+  onlyEmitSnap: boolean = false;
28
+  snapInterval: number = 15 * 1000;
29
+
30
+  /** UI Options */
31
+  // 事件中心
32
+  eventHub?: EventHub;
33
+  // 编辑模式
34
+  mode: Mode = 'master';
35
+  // 是否为全屏模式
36
+  isFullscreen: boolean = false;
37
+
38
+  /** 句柄 */
39
+  pages: WhitePage[] = [];
40
+  get activePage() {
41
+    return this.pages[this.visiblePageIndex];
42
+  }
43
+  get pageMap(): Record<string, WhitePage> {
44
+    const map = {};
45
+    this.pages.forEach(p => (map[p.id] = p));
46
+
47
+    return map;
48
+  }
49
+  siema: any;
50
+
51
+  /** State | 内部状态 */
52
+  // 是否被初始化过,如果尚未被初始化,则等待来自于 Master 的同步消息
53
+  isInitialized: boolean = false;
54
+  isSyncing: boolean = false;
55
+  visiblePageIndex: number = 0;
56
+  emitInterval: any;
57
+
58
+  constructor(
59
+    target: HTMLDivElement,
60
+    { sources, eventHub, visiblePageIndex, onlyEmitSnap }: Partial<AbstractWhiteboard> = {}
61
+  ) {
62
+    if (target) {
63
+      this.target = target;
64
+    } else {
65
+      this.target = document.createElement('div');
66
+      document.body.appendChild(this.target);
67
+    }
68
+
69
+    if (!this.target.id) {
70
+      this.target.id = this.id;
71
+    }
72
+
73
+    addClassName(this.target, prefix);
74
+
75
+    if (sources) {
76
+      this.sources = sources;
77
+    }
78
+
79
+    this.eventHub = eventHub;
80
+
81
+    // set inner state
82
+    if (typeof visiblePageIndex !== 'undefined') {
83
+      this.visiblePageIndex = visiblePageIndex;
84
+    }
85
+
86
+    this.onlyEmitSnap = !!onlyEmitSnap;
87
+
88
+    this.init();
89
+  }
90
+
91
+  /** LifeCycle */
92
+  public open() {
93
+    // 依次渲染所有的页,隐藏非当前页之外的其他页
94
+    this.pages.forEach((page, i) => {
95
+      page.open();
96
+
97
+      if (i !== this.visiblePageIndex) {
98
+        page.hide();
99
+      }
100
+    });
101
+  }
102
+
103
+  /** 关闭当前的 Whiteboard */
104
+  public close() {
105
+    if (this.emitInterval) {
106
+      clearInterval(this.emitInterval);
107
+    }
108
+  }
109
+
110
+  /** 展示当前的 WhitePage */
111
+  public show() {
112
+    if (this.activePage) {
113
+      this.activePage.show();
114
+    }
115
+  }
116
+
117
+  public hide() {
118
+    if (this.activePage) {
119
+      this.activePage.hide();
120
+    }
121
+  }
122
+
123
+  /** 触发事件 */
124
+  public emit(borderEvent: SyncEvent) {
125
+    if (this.mode !== 'master' || !this.eventHub) {
126
+      return;
127
+    }
128
+
129
+    // 在快照模式下,仅同步快照消息
130
+    if (this.onlyEmitSnap) {
131
+      if (borderEvent.event !== 'borderSnap') {
132
+        return;
133
+      }
134
+    }
135
+
136
+    borderEvent.timestamp = Math.floor(Date.now() / 1000);
137
+    this.eventHub.emit('sync', borderEvent);
138
+  }
139
+
140
+  /** 获取当前快照 */
141
+  public captureSnap(shadow: boolean = true): WhiteboardSnap {
142
+    if (shadow) {
143
+      return {
144
+        id: this.id,
145
+        sources: this.sources,
146
+        pageIds: this.pages.map(page => page.id),
147
+        visiblePageIndex: this.visiblePageIndex
148
+      };
149
+    }
150
+
151
+    return {
152
+      id: this.id,
153
+      sources: this.sources,
154
+      pageIds: this.pages.map(page => page.id),
155
+      visiblePageIndex: this.visiblePageIndex,
156
+      pages: this.pages.map(p => p.captureSnap())
157
+    };
158
+  }
159
+
160
+  /** 初始化操作 */
161
+  protected abstract init(): void;
162
+
163
+  /** 初始化 Siema */
164
+  protected initSiema() {
165
+    // 初始化所有的占位图片,用于给 Siema 播放使用
166
+    this.sources.forEach(source => {
167
+      const imgEle = document.createElement('img');
168
+      addClassName(imgEle, `${prefix}-img`);
169
+      imgEle.src = source;
170
+      imgEle.alt = 'Siema image';
171
+
172
+      this.imgsContainer.appendChild(imgEle);
173
+    });
174
+
175
+    // 初始化 Siema,并且添加控制节点
176
+    this.siema = new Siema({
177
+      selector: this.imgsContainer,
178
+      duration: 200,
179
+      easing: 'ease-out',
180
+      perPage: 1,
181
+      startIndex: 0,
182
+      draggable: false,
183
+      multipleDrag: true,
184
+      threshold: 20,
185
+      loop: false,
186
+      rtl: false
187
+    });
188
+  }
189
+}

+ 42
- 0
src/whiteboard/AbstractWhiteboard/snap.ts Datei anzeigen

@@ -0,0 +1,42 @@
1
+import { MarkerType } from '../../markers/types';
2
+
3
+export class WhiteboardSnap {
4
+  id: string;
5
+  sources: string[];
6
+  pageIds: string[];
7
+  visiblePageIndex: number;
8
+
9
+  // 页面信息
10
+  pages?: WhitepageSnap[];
11
+}
12
+
13
+export class WhitepageSnap {
14
+  id: string;
15
+  markers: MarkerSnap[];
16
+}
17
+
18
+export class MarkerSnap {
19
+  id: string;
20
+  type: MarkerType;
21
+  isActive: boolean;
22
+  x: number;
23
+  y: number;
24
+
25
+  // 线性元素的快照
26
+  linearSnap?: {
27
+    x1: number;
28
+    y1: number;
29
+    x2: number;
30
+    y2: number;
31
+  };
32
+
33
+  // 矩形元素的快照
34
+  rectSnap?: {
35
+    width: number;
36
+    height: number;
37
+  };
38
+
39
+  textSnap?: {
40
+    text: string;
41
+  };
42
+}

+ 114
- 0
src/whiteboard/MirrorWhiteboard/index.ts Datei anzeigen

@@ -0,0 +1,114 @@
1
+import { SyncEvent } from '../../event/SyncEvent';
2
+import { WhitePage } from '../WhitePage/index';
3
+import { createDivWithClassName } from '../../utils/dom';
4
+import { WhiteboardSnap } from '../AbstractWhiteboard/snap';
5
+import { AbstractWhiteboard } from '../AbstractWhiteboard/index';
6
+import { Mode } from '../../utils/types';
7
+
8
+const prefix = 'fcw-board';
9
+
10
+export class MirrorWhiteboard extends AbstractWhiteboard {
11
+  mode: Mode = 'mirror';
12
+
13
+  /** 初始化操作 */
14
+  protected init() {
15
+    // 为 target 添加子 imgs 容器
16
+    this.imgsContainer = createDivWithClassName(`${prefix}-imgs`, this.target);
17
+    // 为 target 添加子 pages 容器
18
+    this.pagesContainer = createDivWithClassName(`${prefix}-pages`, this.target);
19
+
20
+    if (!this.eventHub) {
21
+      throw new Error('Invalid eventHub');
22
+    }
23
+
24
+    this.eventHub.on('sync', (ev: SyncEvent) => {
25
+      if (ev.target !== 'whiteboard' || !ev.border) {
26
+        return;
27
+      }
28
+
29
+      if (ev.event === 'borderSnap') {
30
+        this.applySnap(ev.border);
31
+      }
32
+
33
+      if (ev.event === 'borderChangePage' && ev.id === this.id) {
34
+        if (this.isInitialized) {
35
+          this.onPageChange(ev.border.visiblePageIndex);
36
+        }
37
+      }
38
+    });
39
+  }
40
+
41
+  /** 响应页面切换的事件 */
42
+  private onPageChange(nextPageIndex: number) {
43
+    if (this.visiblePageIndex === nextPageIndex) {
44
+      return;
45
+    }
46
+
47
+    this.siema.goTo(nextPageIndex);
48
+    this.visiblePageIndex = nextPageIndex;
49
+
50
+    // 将所有的 Page 隐藏
51
+    this.pages.forEach((page, i) => {
52
+      if (nextPageIndex === i) {
53
+        page.show();
54
+      } else {
55
+        page.hide();
56
+      }
57
+    });
58
+
59
+    this.emit({
60
+      event: 'borderChangePage',
61
+      id: this.id,
62
+      target: 'whiteboard',
63
+      border: this.captureSnap()
64
+    });
65
+  }
66
+
67
+  /** 响应获取到的快照事件 */
68
+  private applySnap(snap: WhiteboardSnap) {
69
+    const { id, sources, pageIds, visiblePageIndex } = snap;
70
+
71
+    if (!this.isInitialized && !this.isSyncing) {
72
+      this.id = id;
73
+      this.sources = sources;
74
+      this.isSyncing = true;
75
+
76
+      // 初始化所有的 WhitePages
77
+      this.sources.forEach((source, i) => {
78
+        const page = new WhitePage(
79
+          { imgSrc: source },
80
+          {
81
+            mode: this.mode,
82
+            whiteboard: this,
83
+            parentContainer: this.pagesContainer
84
+          }
85
+        );
86
+        page.id = pageIds[i];
87
+
88
+        // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
89
+        page.container.style.visibility = 'hidden';
90
+
91
+        this.pages.push(page);
92
+
93
+        page.open();
94
+      });
95
+
96
+      this.initSiema();
97
+      this.isInitialized = true;
98
+      this.isSyncing = false;
99
+      this.onPageChange(visiblePageIndex);
100
+    }
101
+
102
+    // 如果已经初始化完毕,则进行状态同步
103
+    this.onPageChange(snap.visiblePageIndex);
104
+
105
+    // 同步 Pages
106
+    (snap.pages || []).forEach(pageSnap => {
107
+      const page = this.pageMap[pageSnap.id];
108
+
109
+      if (page) {
110
+        page.applySnap(pageSnap);
111
+      }
112
+    });
113
+  }
114
+}

+ 0
- 0
src/whiteboard/ReplayWhiteboard/index.ts Datei anzeigen


src/board/WhitePage/index.less → src/whiteboard/WhitePage/index.less Datei anzeigen


src/board/WhitePage/index.ts → src/whiteboard/WhitePage/index.ts Datei anzeigen

@@ -1,14 +1,14 @@
1
-import { TextMarker } from './../../markers/TextMarker/index';
2
-import { MarkerType } from './../../markers/types';
3
-import { SyncEvent } from './../../event/Event';
4
-import { EventHub } from '../../event/EventHub';
5
-import { WhiteboardMode, WhitePageSource } from './../types';
6
-import { Drawboard } from './../Drawboard/index';
7
-import { uuid } from './../../utils/uuid';
1
+import { Source, Mode } from './../../utils/types';
2
+import { Drawboard } from './../../drawboard/Drawboard/index';
3
+import { TextMarker } from '../../markers/TextMarker/index';
4
+import { SyncEvent } from '../../event/SyncEvent';
5
+import { uuid } from '../../utils/uuid';
8 6
 import { getMarkerByType } from '../../markers/types';
7
+import { createDivWithClassName } from '../../utils/dom';
8
+import { WhitepageSnap } from '../AbstractWhiteboard/snap';
9
+import { AbstractWhiteboard } from '../AbstractWhiteboard/index';
9 10
 
10 11
 import './index.less';
11
-import { createDivWithClassName } from '../../utils/dom';
12 12
 
13 13
 const prefix = 'fcw-page';
14 14
 
@@ -16,32 +16,32 @@ const prefix = 'fcw-page';
16 16
 export class WhitePage {
17 17
   id: string = uuid();
18 18
 
19
-  source: WhitePageSource;
19
+  source: Source;
20 20
   target: HTMLImageElement;
21 21
 
22 22
   /** UI Options */
23 23
   container: HTMLDivElement;
24 24
   // 父容器指针
25 25
   parentContainer?: HTMLDivElement;
26
-  mode: WhiteboardMode = 'master';
26
+  mode: Mode = 'master';
27 27
 
28 28
   /** Handlers */
29 29
   drawboard: Drawboard;
30
-  eventHub?: EventHub;
30
+  whiteboard?: AbstractWhiteboard;
31 31
 
32 32
   constructor(
33
-    source: WhitePageSource,
33
+    source: Source,
34 34
     {
35 35
       mode,
36
-      eventHub,
36
+      whiteboard,
37 37
       parentContainer
38
-    }: { mode?: WhiteboardMode; eventHub?: EventHub; parentContainer?: HTMLDivElement } = {}
38
+    }: { mode?: Mode; whiteboard?: AbstractWhiteboard; parentContainer?: HTMLDivElement } = {}
39 39
   ) {
40 40
     if (mode) {
41 41
       this.mode = mode;
42 42
     }
43
-    this.eventHub = eventHub;
44 43
     this.parentContainer = parentContainer;
44
+    this.whiteboard = whiteboard;
45 45
 
46 46
     this.initSource(source);
47 47
 
@@ -71,8 +71,35 @@ export class WhitePage {
71 71
     this.drawboard.close();
72 72
   }
73 73
 
74
+  /** 生成快照 */
75
+  public captureSnap(): WhitepageSnap {
76
+    const markerSnaps = this.drawboard.markers.map(m => m.captureSnap());
77
+
78
+    return {
79
+      id: this.id,
80
+      markers: markerSnaps
81
+    };
82
+  }
83
+
84
+  /** 应用快照 */
85
+  public applySnap(snap: WhitepageSnap) {
86
+    snap.markers.forEach(markerSnap => {
87
+      // 判断是否存在,存在则同步,否则创建
88
+      const marker = this.drawboard.markerMap[markerSnap.id];
89
+
90
+      if (marker) {
91
+        marker.applySnap(markerSnap);
92
+      } else {
93
+        const newMarker = this.drawboard.addMarker(getMarkerByType(markerSnap.type), {
94
+          id: markerSnap.id
95
+        });
96
+        newMarker.applySnap(markerSnap);
97
+      }
98
+    });
99
+  }
100
+
74 101
   /** 初始化源 */
75
-  private initSource(source: WhitePageSource) {
102
+  private initSource(source: Source) {
76 103
     // 判断 Source 的类型是否符合要求
77 104
     if (typeof source.imgSrc === 'string' && !this.parentContainer) {
78 105
       throw new Error('Invalid source, If you set image url, you must also set parentContainer');
@@ -100,13 +127,13 @@ export class WhitePage {
100 127
 
101 128
   /** 以 Master 模式启动 */
102 129
   protected initMaster() {
103
-    if (this.eventHub) {
130
+    if (this.whiteboard) {
104 131
       // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
105 132
       this.drawboard = new Drawboard(
106 133
         { imgEle: this.target },
107 134
         {
108 135
           page: this,
109
-          onChange: ev => this.eventHub!.emit('sync', ev)
136
+          onChange: ev => this.whiteboard!.emit(ev)
110 137
         }
111 138
       );
112 139
     } else {
@@ -116,13 +143,13 @@ export class WhitePage {
116 143
 
117 144
   /** 以 Mirror 模式启动 */
118 145
   protected initMirror() {
119
-    if (!this.eventHub) {
120
-      throw new Error('Invalid eventHub');
146
+    if (!this.whiteboard) {
147
+      throw new Error('Invalid whiteboard');
121 148
     }
122 149
 
123 150
     this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
124 151
 
125
-    this.eventHub.on('sync', (ev: SyncEvent) => {
152
+    this.whiteboard!.eventHub!.on('sync', (ev: SyncEvent) => {
126 153
       try {
127 154
         // 判断是否为 WhitePage 的同步
128 155
         if (ev.target === 'page' && ev.id === this.id) {
@@ -144,48 +171,44 @@ export class WhitePage {
144 171
 
145 172
   /** 处理 Marker 的同步事件 */
146 173
   private onMarkerSync(ev: SyncEvent) {
147
-    if (ev.event === 'add' && ev.parentId === this.id) {
148
-      const data: { id: string; type: MarkerType } = ev.data as {
149
-        id: string;
150
-        type: MarkerType;
151
-      };
152
-
153
-      // 这里判断该 Marker 是否已经添加过;如果已经存在则忽略
154
-      const marker = this.drawboard.markerMap[data.id];
174
+    if (!ev.marker) {
175
+      return;
176
+    }
177
+
178
+    const id = ev.id;
179
+
180
+    if (ev.event === 'addMarker' && ev.parentId === this.id) {
181
+      const marker = this.drawboard.markerMap[id!];
155 182
       if (!marker) {
156
-        this.drawboard.addMarker(getMarkerByType(data.type), { id: data.id });
183
+        this.drawboard.addMarker(getMarkerByType(ev.marker.type!), { id: ev.marker.id });
157 184
       }
158 185
     }
159 186
 
160 187
     // 其余的情况,不存在 id 则直接返回空
161
-    if (!ev.id) {
188
+    if (!id) {
162 189
       return;
163 190
     }
164 191
 
165
-    if (ev.event === 'remove') {
166
-      const data: { id: string } = ev.data as {
167
-        id: string;
168
-      };
169
-
170
-      const marker = this.drawboard.markerMap[data.id];
192
+    if (ev.event === 'removeMarker') {
193
+      const marker = this.drawboard.markerMap[id];
171 194
       if (marker) {
172 195
         this.drawboard.deleteMarker(marker);
173 196
       }
174 197
     }
175 198
 
176
-    if (ev.event === 'move' || ev.event === 'resize') {
177
-      const marker = this.drawboard.markerMap[ev.id];
199
+    if (ev.event === 'moveMarker' || ev.event === 'resizeMarker') {
200
+      const marker = this.drawboard.markerMap[id];
178 201
 
179 202
       if (marker) {
180
-        marker.reactToManipulation(ev.event, ev.data as any);
203
+        marker.reactToManipulation(ev.event, ev.marker);
181 204
       }
182 205
     }
183 206
 
184 207
     // 响应文本变化事件
185
-    if (ev.event === 'changeText') {
186
-      const marker = this.drawboard.markerMap[ev.id] as TextMarker;
208
+    if (ev.event === 'inputMarker') {
209
+      const marker = this.drawboard.markerMap[id] as TextMarker;
187 210
       if (marker) {
188
-        marker.setText(ev.data as string);
211
+        marker.setText(ev.marker.text!);
189 212
       }
190 213
     }
191 214
   }

+ 114
- 0
src/whiteboard/Whiteboard/index.ts Datei anzeigen

@@ -0,0 +1,114 @@
1
+import { WhitePage } from '../WhitePage/index';
2
+import { createDivWithClassName } from '../../utils/dom';
3
+import { AbstractWhiteboard } from '../AbstractWhiteboard/index';
4
+import { Mode } from '../../utils/types';
5
+
6
+const LeftArrowIcon = require('../../assets/bx-left-arrow.svg');
7
+const RightArrowIcon = require('../../assets/bx-right-arrow.svg');
8
+
9
+const prefix = 'fcw-board';
10
+
11
+export class Whiteboard extends AbstractWhiteboard {
12
+  mode: Mode = 'master';
13
+
14
+  /** 初始化操作 */
15
+  protected init() {
16
+    // 为 target 添加子 imgs 容器
17
+    this.imgsContainer = createDivWithClassName(`${prefix}-imgs`, this.target);
18
+    // 为 target 添加子 pages 容器
19
+    this.pagesContainer = createDivWithClassName(`${prefix}-pages`, this.target);
20
+
21
+    this.initMaster();
22
+
23
+    this.emitSnapshot();
24
+  }
25
+
26
+  /** 以主模式启动 */
27
+  private initMaster() {
28
+    // 初始化所有的 WhitePages
29
+    this.sources.forEach(source => {
30
+      const page = new WhitePage(
31
+        { imgSrc: source },
32
+        {
33
+          mode: this.mode,
34
+          whiteboard: this,
35
+          parentContainer: this.pagesContainer
36
+        }
37
+      );
38
+
39
+      // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
40
+      page.container.style.visibility = 'hidden';
41
+
42
+      this.pages.push(page);
43
+    });
44
+
45
+    this.initSiema();
46
+
47
+    // 初始化控制节点
48
+    const controller = createDivWithClassName(`${prefix}-controller`, this.target);
49
+
50
+    const prevEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
51
+    prevEle.innerHTML = LeftArrowIcon;
52
+
53
+    const nextEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
54
+    nextEle.innerHTML = RightArrowIcon;
55
+
56
+    nextEle!.addEventListener('click', () => {
57
+      const nextPageIndex =
58
+        this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
59
+      this.onPageChange(nextPageIndex);
60
+    });
61
+    prevEle!.addEventListener('click', () => {
62
+      const nextPageIndex =
63
+        this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
64
+
65
+      this.onPageChange(nextPageIndex);
66
+    });
67
+  }
68
+
69
+  /** 响应页面切换的事件 */
70
+  private onPageChange(nextPageIndex: number) {
71
+    if (this.visiblePageIndex === nextPageIndex) {
72
+      return;
73
+    }
74
+
75
+    this.siema.goTo(nextPageIndex);
76
+    this.visiblePageIndex = nextPageIndex;
77
+
78
+    // 将所有的 Page 隐藏
79
+    this.pages.forEach((page, i) => {
80
+      if (nextPageIndex === i) {
81
+        page.show();
82
+      } else {
83
+        page.hide();
84
+      }
85
+    });
86
+
87
+    this.emit({
88
+      event: 'borderChangePage',
89
+      id: this.id,
90
+      target: 'whiteboard',
91
+      border: this.captureSnap()
92
+    });
93
+  }
94
+
95
+  /** 触发快照事件 */
96
+  private emitSnapshot() {
97
+    const innerFunc = () => {
98
+      this.emit({
99
+        event: 'borderSnap',
100
+        id: this.id,
101
+        target: 'whiteboard',
102
+        border: this.captureSnap(false)
103
+      });
104
+    };
105
+
106
+    // 定期触发事件
107
+    this.emitInterval = setInterval(() => {
108
+      innerFunc();
109
+    }, this.snapInterval);
110
+
111
+    // 首次事件,延时 500ms 发出
112
+    setTimeout(innerFunc, 500);
113
+  }
114
+}