Browse Source

feat: add multiple whiteboard & snap sync

wxyyxc1992 5 years ago
parent
commit
862308c2de
38 changed files with 983 additions and 585 deletions
  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 View File

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 View File


src/board/Baseboard/index.ts → src/drawboard/Baseboard/index.ts View File

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


src/board/Drawboard/index.ts → src/drawboard/Drawboard/index.ts View File

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

+ 0
- 37
src/event/Event.ts View File

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 View File

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 View File

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

+ 27
- 0
src/event/marker-events.ts View File

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 View File

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 View File

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

+ 1
- 0
src/markers/BaseMarker/ResizeGrip.ts View File

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

+ 85
- 24
src/markers/BaseMarker/index.ts View File

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
 import { MarkerType } from '../types';
4
 import { MarkerType } from '../types';
5
 import * as uuid from 'uuid/v1';
5
 import * as uuid from 'uuid/v1';
6
 import { SvgHelper } from '../../renderer/SvgHelper';
6
 import { SvgHelper } from '../../renderer/SvgHelper';
7
+import { MarkerSnap } from '../../whiteboard/AbstractWhiteboard/snap';
8
+import { Drawboard } from '../../drawboard/Drawboard/index';
7
 
9
 
8
 export class BaseMarker {
10
 export class BaseMarker {
9
   id: string = uuid();
11
   id: string = uuid();
10
   type: MarkerType = 'base';
12
   type: MarkerType = 'base';
11
-  // 归属的
13
+  // 归属的 WhitePage
12
   page?: WhitePage;
14
   page?: WhitePage;
15
+  // 归属的 Drawboard
16
+  drawboard?: Drawboard;
13
   // Marker 的属性发生变化后的回调
17
   // Marker 的属性发生变化后的回调
14
   onChange: onSyncFunc = () => {};
18
   onChange: onSyncFunc = () => {};
15
 
19
 
27
 
31
 
28
   public defs: SVGElement[] = [];
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
   protected isActive: boolean = true;
39
   protected isActive: boolean = true;
34
   protected isDragging: boolean = false;
40
   protected isDragging: boolean = false;
39
 
45
 
40
   public reactToManipulation(
46
   public reactToManipulation(
41
     type: EventType,
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
       this.move(dx, dy);
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
       this.resizeByEvent(dx, dy, pos);
63
       this.resizeByEvent(dx, dy, pos);
50
     }
64
     }
51
   }
65
   }
56
     const dx = (ev.screenX - this.previousMouseX) / scale;
70
     const dx = (ev.screenX - this.previousMouseX) / scale;
57
     const dy = (ev.screenY - this.previousMouseY) / scale;
71
     const dy = (ev.screenY - this.previousMouseY) / scale;
58
 
72
 
73
+    // 如果在拖拽
59
     if (this.isDragging) {
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
       this.move(dx, dy);
76
       this.move(dx, dy);
62
     }
77
     }
63
 
78
 
79
+    // 如果是缩放
64
     if (this.isResizing) {
80
     if (this.isResizing) {
65
       this.resize(dx, dy, (pos: PositionType) => {
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
     return;
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
   protected setup() {
167
   protected setup() {
94
     this.visual = SvgHelper.createGroup();
168
     this.visual = SvgHelper.createGroup();
95
     // translate
169
     // translate
115
     this.renderVisual.appendChild(el);
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
   /** 截获 Touch 事件,并且转发为 Mouse 事件 */
192
   /** 截获 Touch 事件,并且转发为 Mouse 事件 */
126
   protected onTouch(ev: TouchEvent) {
193
   protected onTouch(ev: TouchEvent) {
127
     ev.preventDefault();
194
     ev.preventDefault();
186
     ev.stopPropagation();
253
     ev.stopPropagation();
187
     this.manipulate(ev);
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 View File

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

+ 1
- 1
src/markers/HighlightMarker/index.ts View File

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

+ 1
- 1
src/markers/LineMarker/index.ts View File

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

+ 80
- 35
src/markers/LinearMarker/index.ts View File

1
-import { WhitePage } from './../../board/WhitePage/index';
1
+import { WhitePage } from '../../whiteboard/WhitePage/index';
2
 import { BaseMarker } from '../BaseMarker';
2
 import { BaseMarker } from '../BaseMarker';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4
 import { SvgHelper } from '../../renderer/SvgHelper';
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
   public static createMarker = (page?: WhitePage): LinearMarker => {
13
   public static createMarker = (page?: WhitePage): LinearMarker => {
9
     const marker = new LinearMarker();
14
     const marker = new LinearMarker();
10
     marker.page = page;
15
     marker.page = page;
12
     return marker;
17
     return marker;
13
   };
18
   };
14
 
19
 
15
-  protected markerLine: SVGLineElement;
16
-
17
   private readonly MIN_LENGTH = 20;
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
   private markerBgLine: SVGLineElement; // touch target
29
   private markerBgLine: SVGLineElement; // touch target
20
-
21
   private controlBox: SVGGElement;
30
   private controlBox: SVGGElement;
22
 
31
 
23
   private controlGrips: { left: ResizeGrip; right: ResizeGrip };
32
   private controlGrips: { left: ResizeGrip; right: ResizeGrip };
24
   private activeGrip: ResizeGrip | null;
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
   public endManipulation() {
65
   public endManipulation() {
32
     super.endManipulation();
66
     super.endManipulation();
33
     this.isResizing = false;
67
     this.isResizing = false;
62
     }
96
     }
63
   }
97
   }
64
 
98
 
99
+  /** 主动伸缩操作 */
65
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
100
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
66
     if (this.activeGrip) {
101
     if (this.activeGrip) {
67
       if (
102
       if (
96
     this.adjustControlBox();
131
     this.adjustControlBox();
97
   }
132
   }
98
 
133
 
134
+  /** 根据事件进行伸缩操作 */
99
   protected resizeByEvent(x: number, y: number, pos?: PositionType) {
135
   protected resizeByEvent(x: number, y: number, pos?: PositionType) {
100
     if (pos === 'left') {
136
     if (pos === 'left') {
101
       this.activeGrip = this.controlGrips.left;
137
       this.activeGrip = this.controlGrips.left;
106
     this.resize(x, y);
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
   private addControlBox = () => {
147
   private addControlBox = () => {
117
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-line-control-box']]);
148
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-line-control-box']]);
149
     return grip;
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
   private gripMouseDown = (ev: MouseEvent) => {
184
   private gripMouseDown = (ev: MouseEvent) => {
171
     this.isResizing = true;
185
     this.isResizing = true;
172
     this.activeGrip =
186
     this.activeGrip =
189
       this.resize(ev.movementX, ev.movementY);
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 View File

1
-import { PositionType } from '../../event/Event';
1
+import { PositionType } from '../../utils/layout';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
2
 import { SvgHelper } from '../../renderer/SvgHelper';
3
 import { RectangularMarker } from '../RectangularMarker';
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
 export class RectBaseMarker extends RectangularMarker {
7
 export class RectBaseMarker extends RectangularMarker {
7
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
8
   public static createMarker = (page?: WhitePage): RectBaseMarker => {
13
 
14
 
14
   private markerRect: SVGRectElement;
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
   protected setup() {
28
   protected setup() {
17
     super.setup();
29
     super.setup();
18
 
30
 

+ 1
- 1
src/markers/RectMarker/index.ts View File

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

+ 101
- 47
src/markers/RectangularMarker/index.ts View File

1
 import { RectangularMarkerGrips } from './RectangularMarkerGrips';
1
 import { RectangularMarkerGrips } from './RectangularMarkerGrips';
2
 import { BaseMarker } from '../BaseMarker';
2
 import { BaseMarker } from '../BaseMarker';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4
-import { PositionType } from '../../event/Event';
5
 import { SvgHelper } from '../../renderer/SvgHelper';
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
 export class RectangularMarker extends BaseMarker {
9
 export class RectangularMarker extends BaseMarker {
9
   public static createMarker = (page?: WhitePage): RectangularMarker => {
10
   public static createMarker = (page?: WhitePage): RectangularMarker => {
22
   private controlGrips: RectangularMarkerGrips;
23
   private controlGrips: RectangularMarkerGrips;
23
   private activeGrip: ResizeGrip | null;
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
   public endManipulation() {
52
   public endManipulation() {
26
     super.endManipulation();
53
     super.endManipulation();
27
     this.isResizing = false;
54
     this.isResizing = false;
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
     this.activeGrip = this.controlGrips[pos];
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
     let translateX = 0;
84
     let translateX = 0;
58
     let translateY = 0;
85
     let translateY = 0;
59
 
86
 
60
     switch (this.activeGrip) {
87
     switch (this.activeGrip) {
61
       case this.controlGrips.topLeft:
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
         if (onPosition) {
97
         if (onPosition) {
67
           onPosition('topLeft');
98
           onPosition('topLeft');
68
         }
99
         }
69
         break;
100
         break;
101
+
70
       case this.controlGrips.bottomLeft:
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
         if (onPosition) {
109
         if (onPosition) {
75
           onPosition('bottomLeft');
110
           onPosition('bottomLeft');
76
         }
111
         }
77
         break;
112
         break;
113
+
78
       case this.controlGrips.topRight:
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
         if (onPosition) {
121
         if (onPosition) {
83
           onPosition('topRight');
122
           onPosition('topRight');
84
         }
123
         }
85
         break;
124
         break;
125
+
86
       case this.controlGrips.bottomRight:
126
       case this.controlGrips.bottomRight:
87
-        this.width += x;
88
-        this.height += y;
127
+        this.width += dx;
128
+        this.height += dy;
89
         if (onPosition) {
129
         if (onPosition) {
90
           onPosition('bottomRight');
130
           onPosition('bottomRight');
91
         }
131
         }
92
         break;
132
         break;
133
+
93
       case this.controlGrips.centerLeft:
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
         if (onPosition) {
140
         if (onPosition) {
97
           onPosition('centerLeft');
141
           onPosition('centerLeft');
98
         }
142
         }
99
         break;
143
         break;
144
+
100
       case this.controlGrips.centerRight:
145
       case this.controlGrips.centerRight:
101
-        this.width += x;
146
+        this.width += dx;
102
         if (onPosition) {
147
         if (onPosition) {
103
           onPosition('centerRight');
148
           onPosition('centerRight');
104
         }
149
         }
105
         break;
150
         break;
151
+
106
       case this.controlGrips.topCenter:
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
         if (onPosition) {
158
         if (onPosition) {
110
           onPosition('topCenter');
159
           onPosition('topCenter');
111
         }
160
         }
112
         break;
161
         break;
162
+
113
       case this.controlGrips.bottomCenter:
163
       case this.controlGrips.bottomCenter:
114
-        this.height += y;
164
+        this.height += dy;
115
         if (onPosition) {
165
         if (onPosition) {
116
           onPosition('bottomCenter');
166
           onPosition('bottomCenter');
117
         }
167
         }
118
         break;
168
         break;
169
+
119
       default:
170
       default:
120
         break;
171
         break;
121
     }
172
     }
144
     this.adjustControlBox();
195
     this.adjustControlBox();
145
   }
196
   }
146
 
197
 
147
-  protected onTouch(ev: TouchEvent) {
148
-    super.onTouch(ev);
149
-  }
150
-
198
+  /** Init Comps */
151
   private addControlBox = () => {
199
   private addControlBox = () => {
152
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-rect-control-box']]);
200
     this.controlBox = SvgHelper.createGroup([['class', 'fc-whiteboard-rect-control-box']]);
153
     const translate = SvgHelper.createTransform();
201
     const translate = SvgHelper.createTransform();
204
     return grip;
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
   private positionGrips = () => {
281
   private positionGrips = () => {
208
     const gripSize = this.controlGrips.topLeft.GRIP_SIZE;
282
     const gripSize = this.controlGrips.topLeft.GRIP_SIZE;
209
 
283
 
229
     translate.setTranslate(x, y);
303
     translate.setTranslate(x, y);
230
     grip.transform.baseVal.replaceItem(translate, 0);
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 View File

1
 import { MarkerType } from '../types';
1
 import { MarkerType } from '../types';
2
 import { RectangularMarker } from '../RectangularMarker';
2
 import { RectangularMarker } from '../RectangularMarker';
3
 import { SvgHelper } from '../../renderer/SvgHelper';
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
 const OkIcon = require('../../assets/check.svg');
8
 const OkIcon = require('../../assets/check.svg');
8
 const CancelIcon = require('../../assets/times.svg');
9
 const CancelIcon = require('../../assets/times.svg');
17
     return marker;
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
   public setText(text: string) {
33
   public setText(text: string) {
22
     this.text = text;
34
     this.text = text;
23
     this.renderText();
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
   protected setup() {
54
   protected setup() {
39
     super.setup();
55
     super.setup();
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
     this.renderText();
173
     this.renderText();
153
     this.closeEditor();
174
     this.closeEditor();

+ 7
- 0
src/markers/types.ts View File

8
 
8
 
9
 export type MarkerType = 'base' | 'arrow' | 'cover' | 'line' | 'rect' | 'text' | 'highlight';
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
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
18
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
12
   switch (type) {
19
   switch (type) {
13
     case 'arrow':
20
     case 'arrow':

+ 1
- 0
src/renderer/SvgHelper/index.ts View File

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

+ 4
- 3
src/renderer/Synthetizer/index.ts View File

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

+ 0
- 0
src/toolbar/EmbeddingToolbar/index.ts View File


+ 0
- 0
src/toolbar/FloatingToolbar/index.ts View File


+ 5
- 4
src/toolbar/Toolbar.ts View File

4
 
4
 
5
 export class Toolbar {
5
 export class Toolbar {
6
   id: string = uuid();
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
   constructor(
14
   constructor(
14
     toolbarItems: ToolbarItem[],
15
     toolbarItems: ToolbarItem[],
39
 
40
 
40
   public show() {
41
   public show() {
41
     this.toolbarUI.style.visibility = 'visible';
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 View File

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

+ 11
- 0
src/utils/layout.ts View File

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 View File

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
   imgEle?: HTMLImageElement;
6
   imgEle?: HTMLImageElement;
7
 
7
 

src/board/Whiteboard/index.less → src/whiteboard/AbstractWhiteboard/index.less View File


+ 189
- 0
src/whiteboard/AbstractWhiteboard/index.ts View File

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 View File

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 View File

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 View File


src/board/WhitePage/index.less → src/whiteboard/WhitePage/index.less View File


src/board/WhitePage/index.ts → src/whiteboard/WhitePage/index.ts View File

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
 import { getMarkerByType } from '../../markers/types';
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
 import './index.less';
11
 import './index.less';
11
-import { createDivWithClassName } from '../../utils/dom';
12
 
12
 
13
 const prefix = 'fcw-page';
13
 const prefix = 'fcw-page';
14
 
14
 
16
 export class WhitePage {
16
 export class WhitePage {
17
   id: string = uuid();
17
   id: string = uuid();
18
 
18
 
19
-  source: WhitePageSource;
19
+  source: Source;
20
   target: HTMLImageElement;
20
   target: HTMLImageElement;
21
 
21
 
22
   /** UI Options */
22
   /** UI Options */
23
   container: HTMLDivElement;
23
   container: HTMLDivElement;
24
   // 父容器指针
24
   // 父容器指针
25
   parentContainer?: HTMLDivElement;
25
   parentContainer?: HTMLDivElement;
26
-  mode: WhiteboardMode = 'master';
26
+  mode: Mode = 'master';
27
 
27
 
28
   /** Handlers */
28
   /** Handlers */
29
   drawboard: Drawboard;
29
   drawboard: Drawboard;
30
-  eventHub?: EventHub;
30
+  whiteboard?: AbstractWhiteboard;
31
 
31
 
32
   constructor(
32
   constructor(
33
-    source: WhitePageSource,
33
+    source: Source,
34
     {
34
     {
35
       mode,
35
       mode,
36
-      eventHub,
36
+      whiteboard,
37
       parentContainer
37
       parentContainer
38
-    }: { mode?: WhiteboardMode; eventHub?: EventHub; parentContainer?: HTMLDivElement } = {}
38
+    }: { mode?: Mode; whiteboard?: AbstractWhiteboard; parentContainer?: HTMLDivElement } = {}
39
   ) {
39
   ) {
40
     if (mode) {
40
     if (mode) {
41
       this.mode = mode;
41
       this.mode = mode;
42
     }
42
     }
43
-    this.eventHub = eventHub;
44
     this.parentContainer = parentContainer;
43
     this.parentContainer = parentContainer;
44
+    this.whiteboard = whiteboard;
45
 
45
 
46
     this.initSource(source);
46
     this.initSource(source);
47
 
47
 
71
     this.drawboard.close();
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
     // 判断 Source 的类型是否符合要求
103
     // 判断 Source 的类型是否符合要求
77
     if (typeof source.imgSrc === 'string' && !this.parentContainer) {
104
     if (typeof source.imgSrc === 'string' && !this.parentContainer) {
78
       throw new Error('Invalid source, If you set image url, you must also set parentContainer');
105
       throw new Error('Invalid source, If you set image url, you must also set parentContainer');
100
 
127
 
101
   /** 以 Master 模式启动 */
128
   /** 以 Master 模式启动 */
102
   protected initMaster() {
129
   protected initMaster() {
103
-    if (this.eventHub) {
130
+    if (this.whiteboard) {
104
       // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
131
       // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
105
       this.drawboard = new Drawboard(
132
       this.drawboard = new Drawboard(
106
         { imgEle: this.target },
133
         { imgEle: this.target },
107
         {
134
         {
108
           page: this,
135
           page: this,
109
-          onChange: ev => this.eventHub!.emit('sync', ev)
136
+          onChange: ev => this.whiteboard!.emit(ev)
110
         }
137
         }
111
       );
138
       );
112
     } else {
139
     } else {
116
 
143
 
117
   /** 以 Mirror 模式启动 */
144
   /** 以 Mirror 模式启动 */
118
   protected initMirror() {
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
     this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
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
       try {
153
       try {
127
         // 判断是否为 WhitePage 的同步
154
         // 判断是否为 WhitePage 的同步
128
         if (ev.target === 'page' && ev.id === this.id) {
155
         if (ev.target === 'page' && ev.id === this.id) {
144
 
171
 
145
   /** 处理 Marker 的同步事件 */
172
   /** 处理 Marker 的同步事件 */
146
   private onMarkerSync(ev: SyncEvent) {
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
       if (!marker) {
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
     // 其余的情况,不存在 id 则直接返回空
187
     // 其余的情况,不存在 id 则直接返回空
161
-    if (!ev.id) {
188
+    if (!id) {
162
       return;
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
       if (marker) {
194
       if (marker) {
172
         this.drawboard.deleteMarker(marker);
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
       if (marker) {
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
       if (marker) {
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 View File

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
+}