Browse Source

feat: update whiteboard

wxyyxc1992 5 years ago
parent
commit
40c92270a2

example/index.less → example/drawboard-fullscreen/index.ts View File


+ 0
- 0
example/drawboard/index.ts View File


+ 20
- 12
example/index.ts View File

@@ -1,19 +1,27 @@
1
-import { eventHub } from './../src/event/EventHub';
2
-import { WhitePage } from './../src/board/WhitePage/index';
1
+import { EventHub } from './../src/event/EventHub';
2
+import { Whiteboard } from './../src/board/Whiteboard/index';
3 3
 
4
-const page1 = new WhitePage(document.getElementById('image1') as HTMLImageElement, { eventHub });
4
+const eventHub = new EventHub();
5 5
 
6
-page1.drawboard.show((dataUrl: string) => {
7
-  const res = document.getElementById('resultImage') as HTMLImageElement;
8
-  res.src = dataUrl;
9
-});
6
+eventHub.on('sync', (changeEv: SyncEvent) => {});
7
+
8
+const images = [
9
+  'https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
10
+  'http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
11
+  'http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'
12
+];
10 13
 
11
-const page2 = new WhitePage(document.getElementById('image2') as HTMLImageElement, {
12
-  mode: 'mirror',
14
+const whiteboard = new Whiteboard(document.getElementById('root') as HTMLDivElement, {
15
+  sources: images,
13 16
   eventHub
14 17
 });
15 18
 
16
-page2.drawboard.show((dataUrl: string) => {
17
-  const res = document.getElementById('resultImage') as HTMLImageElement;
18
-  res.src = dataUrl;
19
+whiteboard.open();
20
+
21
+const mirrorWhiteboard = new Whiteboard(document.getElementById('root-mirror') as HTMLDivElement, {
22
+  sources: images,
23
+  eventHub,
24
+  mode: 'mirror'
19 25
 });
26
+
27
+mirrorWhiteboard.open();

+ 0
- 0
example/mirror-whiteboard/index.ts View File


+ 0
- 0
example/mirror/index.less View File


+ 32
- 0
example/mirror/index.ts View File

@@ -0,0 +1,32 @@
1
+import { EventHub } from './../../src/event/EventHub';
2
+import { WhitePage } from '../../src/board/WhitePage/index';
3
+import { SyncEvent } from '../../src/event/Event';
4
+
5
+const eventHub = new EventHub();
6
+
7
+eventHub.on('sync', (changeEv: SyncEvent) => {
8
+  console.log(changeEv);
9
+});
10
+
11
+const page1 = new WhitePage(
12
+  { imgEle: document.getElementById('image1') as HTMLImageElement },
13
+  { eventHub }
14
+);
15
+
16
+page1.drawboard.open((dataUrl: string) => {
17
+  const res = document.getElementById('resultImage') as HTMLImageElement;
18
+  res.src = dataUrl;
19
+});
20
+
21
+const page2 = new WhitePage(
22
+  { imgEle: document.getElementById('image2') as HTMLImageElement },
23
+  {
24
+    mode: 'mirror',
25
+    eventHub
26
+  }
27
+);
28
+
29
+page2.drawboard.open((dataUrl: string) => {
30
+  const res = document.getElementById('resultImage') as HTMLImageElement;
31
+  res.src = dataUrl;
32
+});

+ 13
- 0
example/whiteboard/index.ts View File

@@ -0,0 +1,13 @@
1
+import { Whiteboard } from '../../src/board/Whiteboard/index';
2
+
3
+const images = [
4
+  'https://upload-images.jianshu.io/upload_images/1647496-6bede989c09af527.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
5
+  'http://upload-images.jianshu.io/upload_images/1647496-d281090a702045e5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240',
6
+  'http://upload-images.jianshu.io/upload_images/1647496-611a416be07d7ca3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240'
7
+];
8
+
9
+const whiteboard = new Whiteboard(document.getElementById('root') as HTMLDivElement, {
10
+  sources: images
11
+});
12
+
13
+whiteboard.open();

+ 1
- 0
src/assets/bx-left-arrow.svg View File

@@ -0,0 +1 @@
1
+<svg t="1553418887330" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2264" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M787.797333 90.197333a42.581333 42.581333 0 0 0-44.074666 2.688l-554.666667 384a42.794667 42.794667 0 0 0 0 70.186667l554.666667 384A42.666667 42.666667 0 0 0 810.666667 896V128a42.666667 42.666667 0 0 0-22.869334-37.802667zM725.333333 814.549333L288.298667 512 725.333333 209.450667v605.098666z" p-id="2265"></path></svg>

+ 1
- 0
src/assets/bx-right-arrow.svg View File

@@ -0,0 +1 @@
1
+<svg t="1553418900235" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2709" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M236.202667 933.802667a42.837333 42.837333 0 0 0 44.074666-2.730667l554.666667-384a42.666667 42.666667 0 0 0 0-70.144l-554.666667-384A42.581333 42.581333 0 0 0 213.333333 128v768a42.666667 42.666667 0 0 0 22.869334 37.802667zM298.666667 209.450667L735.701333 512 298.666667 814.549333V209.450667z" p-id="2710"></path></svg>

+ 0
- 0
src/board/Baseboard/index.less View File


+ 68
- 0
src/board/Baseboard/index.ts View File

@@ -0,0 +1,68 @@
1
+import { uuid } from './../../utils/uuid';
2
+import { WhitePageSource } from './../types';
3
+import { SvgHelper } from './../../renderer/SvgHelper/index';
4
+
5
+/** 基础的绘制版 */
6
+export class Baseboard {
7
+  id: string = uuid();
8
+
9
+  /** 元素 */
10
+  source: WhitePageSource;
11
+
12
+  // 目前使用 Image 元素作为输出源
13
+  target: HTMLImageElement;
14
+  targetRect: ClientRect;
15
+
16
+  boardCanvas: SVGSVGElement;
17
+  boardHolder: HTMLDivElement;
18
+  defs: SVGDefsElement;
19
+
20
+  width: number;
21
+  height: number;
22
+
23
+  constructor(source: WhitePageSource) {
24
+    this.source = source;
25
+
26
+    if (source.imgEle) {
27
+      this.target = source.imgEle!;
28
+
29
+      // 如果仅传入图片地址或者 Blob,则必须为全屏模式
30
+      this.width = this.target.clientWidth;
31
+      this.height = this.target.clientHeight;
32
+    }
33
+  }
34
+
35
+  protected initBoard = () => {
36
+    // init holder
37
+    this.boardHolder = document.createElement('div');
38
+    this.boardHolder.id = `fcw-board-holder-${this.id}`;
39
+    // fix for Edge's touch behavior
40
+    this.boardHolder.style.setProperty('touch-action', 'none');
41
+    this.boardHolder.style.setProperty('-ms-touch-action', 'none');
42
+    document.body.appendChild(this.boardHolder);
43
+
44
+    // init canvas
45
+    this.boardCanvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
46
+    this.boardCanvas.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
47
+    this.boardCanvas.setAttribute('width', this.width.toString());
48
+    this.boardCanvas.setAttribute('height', this.height.toString());
49
+    this.boardCanvas.setAttribute(
50
+      'viewBox',
51
+      '0 0 ' + this.width.toString() + ' ' + this.height.toString()
52
+    );
53
+
54
+    this.boardHolder.style.position = 'absolute';
55
+    this.boardHolder.style.width = `${this.width}px`;
56
+    this.boardHolder.style.height = `${this.height}px`;
57
+    this.boardHolder.style.transformOrigin = 'top left';
58
+    this.positionBoard();
59
+    this.defs = SvgHelper.createDefs();
60
+    this.boardCanvas.appendChild(this.defs);
61
+    this.boardHolder.appendChild(this.boardCanvas);
62
+  };
63
+
64
+  protected positionBoard = () => {
65
+    this.boardHolder.style.top = this.targetRect.top + 'px';
66
+    this.boardHolder.style.left = this.targetRect.left + 'px';
67
+  };
68
+}

+ 95
- 157
src/board/Drawboard/index.ts View File

@@ -1,42 +1,23 @@
1
+import { WhitePageSource } from './../types';
2
+import { Baseboard } from './../Baseboard/index';
1 3
 import { BaseMarker } from './../../markers/BaseMarker/index';
2
-import { lineMarkerToolbarItem } from './../../toolbar/toolbar-items';
4
+import { getToolbars } from './../../toolbar/toolbar-items';
3 5
 import { WhitePage } from './../WhitePage/index';
4
-import { onChangeFunc } from './../../event/Event';
5
-import { uuid } from './../../utils/uuid';
6
-import { SvgHelper } from '../../renderer/SvgHelper';
6
+import { onSyncFunc } from './../../event/Event';
7
+
7 8
 import { Synthetizer } from '../../renderer/Synthetizer';
8 9
 import { Toolbar } from '../../toolbar/Toolbar';
9 10
 import { ToolbarItem } from '../../toolbar/ToolbarItem';
10 11
 
11 12
 import './index.less';
12
-import {
13
-  arrowMarkerToolbarItem,
14
-  highlightMarkerToolbarItem,
15
-  textMarkerToolbarItem,
16
-  coverMarkerToolbarItem,
17
-  rectMarkerToolbarItem
18
-} from '../../toolbar/toolbar-items';
19
-
20
-const OkIcon = require('../../assets/check.svg');
21
-const DeleteIcon = require('../../assets/eraser.svg');
22
-const PointerIcon = require('../../assets/mouse-pointer.svg');
23
-const CloseIcon = require('../../assets/times.svg');
24
-
25
-export class Drawboard {
26
-  id: string = uuid();
13
+
14
+export class Drawboard extends Baseboard {
15
+  /** Options */
16
+  private scale = 1.0;
27 17
 
28 18
   /** 句柄 */
29 19
   page: WhitePage;
30 20
 
31
-  private target: HTMLImageElement;
32
-  private markerImage: SVGSVGElement;
33
-  private markerImageHolder: HTMLDivElement;
34
-  private defs: SVGDefsElement;
35
-
36
-  private targetRect: ClientRect;
37
-  private width: number;
38
-  private height: number;
39
-
40 21
   private markers: BaseMarker[];
41 22
   get markerMap(): { [key: string]: BaseMarker } {
42 23
     const map = {};
@@ -48,125 +29,107 @@ export class Drawboard {
48 29
   private activeMarker: BaseMarker | null;
49 30
 
50 31
   private toolbar: Toolbar;
32
+  private toolbars: ToolbarItem[];
51 33
   private toolbarUI: HTMLElement;
52 34
 
53
-  private completeCallback: (dataUrl: string) => void;
54
-  private cancelCallback: () => void;
55
-  private onChange: onChangeFunc = () => {};
56
-
57
-  private toolbars: ToolbarItem[] = [
58
-    {
59
-      icon: PointerIcon,
60
-      name: 'pointer',
61
-      tooltipText: 'Pointer'
62
-    },
63
-    {
64
-      icon: DeleteIcon,
65
-      name: 'delete',
66
-      tooltipText: 'Delete'
67
-    },
68
-    {
69
-      name: 'separator',
70
-      tooltipText: ''
71
-    },
72
-    rectMarkerToolbarItem,
73
-    coverMarkerToolbarItem,
74
-    highlightMarkerToolbarItem,
75
-    lineMarkerToolbarItem,
76
-    arrowMarkerToolbarItem,
77
-    textMarkerToolbarItem,
78
-    {
79
-      name: 'separator',
80
-      tooltipText: ''
81
-    },
82
-    {
83
-      icon: OkIcon,
84
-      name: 'ok',
85
-      tooltipText: 'OK'
86
-    },
87
-    {
88
-      icon: CloseIcon,
89
-      name: 'close',
90
-      tooltipText: 'Close'
91
-    }
92
-  ];
93
-
94
-  private scale = 1.0;
35
+  /** 回调 */
36
+  private onComplete: (dataUrl: string) => void = () => {};
37
+  private onChange: onSyncFunc = () => {};
38
+  private onCancel: () => void;
95 39
 
96 40
   constructor(
97
-    target: HTMLImageElement,
98
-    { page, onChange }: { page?: WhitePage; onChange?: onChangeFunc } = {}
41
+    source: WhitePageSource,
42
+    { page, onChange }: { page?: WhitePage; onChange?: onSyncFunc } = {}
99 43
   ) {
44
+    super(source);
45
+
100 46
     if (page) {
101 47
       this.page = page;
102 48
     }
103 49
 
104
-    this.target = target;
105
-
106
-    // 如果仅传入图片地址或者 Blob,则必须为全屏模式
107
-    this.width = target.clientWidth;
108
-    this.height = target.clientHeight;
109
-
110 50
     this.markers = [];
111 51
     this.activeMarker = null;
52
+    this.toolbars = getToolbars(page);
112 53
 
113 54
     if (onChange) {
114 55
       this.onChange = onChange;
115 56
     }
116 57
   }
117 58
 
118
-  public show = (completeCallback: (dataUrl: string) => void, cancelCallback?: () => void) => {
119
-    this.completeCallback = completeCallback;
120
-
121
-    if (cancelCallback) {
122
-      this.cancelCallback = cancelCallback;
59
+  /** @region LifeCycle open - hide - show - ... - close */
60
+  /** 打开画板 */
61
+  public open = (onComplete?: (dataUrl: string) => void, onCancel?: () => void) => {
62
+    if (onComplete) {
63
+      this.onComplete = onComplete;
123 64
     }
124 65
 
125
-    this.open();
126
-
127
-    if (this.page.mode !== 'mirror') {
128
-      this.showUI();
66
+    if (onCancel) {
67
+      this.onCancel = onCancel;
129 68
     }
130
-  };
131 69
 
132
-  public open = () => {
133 70
     this.setTargetRect();
134 71
 
135
-    this.initMarkerCanvas();
72
+    this.initBoard();
136 73
     this.attachEvents();
137 74
     this.setStyles();
138 75
 
139 76
     window.addEventListener('resize', this.adjustUI);
77
+
78
+    if (this.page.mode === 'master') {
79
+      this.showUI();
80
+    }
140 81
   };
141 82
 
142
-  public render = (completeCallback: (dataUrl: string) => void, cancelCallback?: () => void) => {
143
-    this.completeCallback = completeCallback;
83
+  public hide = () => {
84
+    if (this.source.imgSrc) {
85
+      this.target.style.display = 'none';
86
+    }
87
+    // 这里不使用 display:none,是为了保证在隐藏时候仍然可以执行更新
88
+    this.boardHolder.style.opacity = '0';
89
+    this.boardHolder.style.zIndex = '-1';
90
+
91
+    if (this.toolbar) {
92
+      this.toolbar.hide();
93
+    }
94
+  };
144 95
 
145
-    if (cancelCallback) {
146
-      this.cancelCallback = cancelCallback;
96
+  public show = () => {
97
+    if (this.source.imgSrc) {
98
+      this.target.style.display = 'block';
147 99
     }
148 100
 
149
-    this.selectMarker(null);
150
-    this.startRender(this.renderFinished);
101
+    this.boardHolder.style.opacity = '1';
102
+    this.boardHolder.style.zIndex = '9999';
103
+
104
+    if (this.toolbar) {
105
+      this.toolbar.show();
106
+    }
151 107
   };
152 108
 
153 109
   public close = () => {
154 110
     if (this.toolbarUI) {
155 111
       document.body.removeChild(this.toolbarUI);
156 112
     }
157
-    if (this.markerImage) {
158
-      document.body.removeChild(this.markerImageHolder);
113
+    if (this.boardCanvas) {
114
+      document.body.removeChild(this.boardHolder);
159 115
     }
160 116
   };
161 117
 
162
-  public addMarker = (markerType: typeof BaseMarker, { id }: { id?: string } = {}) => {
163
-    const marker = markerType.createMarker();
118
+  public render = (onComplete: (dataUrl: string) => void, onCancel?: () => void) => {
119
+    this.onComplete = onComplete;
164 120
 
165
-    // 假如 Drawboard 存在 Page 引用,则传导给 Marker
166
-    if (this.page) {
167
-      marker.page = this.page;
121
+    if (onCancel) {
122
+      this.onCancel = onCancel;
168 123
     }
169 124
 
125
+    this.selectMarker(null);
126
+    this.startRender(this.renderFinished);
127
+  };
128
+
129
+  public addMarker = (markerType: typeof BaseMarker, { id }: { id?: string } = {}) => {
130
+    // 假如 Drawboard 存在 Page 引用,则传导给 Marker
131
+    const marker = markerType.createMarker(this.page);
132
+
170 133
     if (id) {
171 134
       marker.id = id;
172 135
     }
@@ -176,7 +139,7 @@ export class Drawboard {
176 139
 
177 140
     if (marker.defs && marker.defs.length > 0) {
178 141
       for (const d of marker.defs) {
179
-        if (d.id && !this.markerImage.getElementById(d.id)) {
142
+        if (d.id && !this.boardCanvas.getElementById(d.id)) {
180 143
           this.defs.appendChild(d);
181 144
         }
182 145
       }
@@ -184,8 +147,8 @@ export class Drawboard {
184 147
 
185 148
     // 触发事件流
186 149
     this.onChange({
187
-      target: 'drawboard',
188
-      id: this.id,
150
+      target: 'marker',
151
+      parentId: this.page ? this.page.id : this.id,
189 152
       event: 'add',
190 153
       data: { type: marker.type, id: marker.id }
191 154
     });
@@ -194,7 +157,7 @@ export class Drawboard {
194 157
 
195 158
     this.selectMarker(marker);
196 159
 
197
-    this.markerImage.appendChild(marker.visual);
160
+    this.boardCanvas.appendChild(marker.visual);
198 161
 
199 162
     const bbox = marker.visual.getBBox();
200 163
     const x = this.width / 2 / this.scale - bbox.width / 2;
@@ -207,6 +170,15 @@ export class Drawboard {
207 170
 
208 171
   public deleteActiveMarker = () => {
209 172
     if (this.activeMarker) {
173
+      // 触发事件
174
+      if (this.onChange) {
175
+        this.onChange({
176
+          event: 'remove',
177
+          id: this.activeMarker.id,
178
+          target: 'marker',
179
+          data: { id: this.activeMarker.id }
180
+        });
181
+      }
210 182
       this.deleteMarker(this.activeMarker);
211 183
     }
212 184
   };
@@ -222,13 +194,13 @@ export class Drawboard {
222 194
 
223 195
   private startRender = (done: (dataUrl: string) => void) => {
224 196
     const renderer = new Synthetizer();
225
-    renderer.rasterize(this.target, this.markerImage, done);
197
+    renderer.rasterize(this.target, this.boardCanvas, done);
226 198
   };
227 199
 
228 200
   private attachEvents = () => {
229
-    this.markerImage.addEventListener('mousedown', this.mouseDown);
230
-    this.markerImage.addEventListener('mousemove', this.mouseMove);
231
-    this.markerImage.addEventListener('mouseup', this.mouseUp);
201
+    this.boardCanvas.addEventListener('mousedown', this.mouseDown);
202
+    this.boardCanvas.addEventListener('mousemove', this.mouseMove);
203
+    this.boardCanvas.addEventListener('mouseup', this.mouseUp);
232 204
   };
233 205
 
234 206
   private mouseDown = (ev: MouseEvent) => {
@@ -252,35 +224,6 @@ export class Drawboard {
252 224
     }
253 225
   };
254 226
 
255
-  private initMarkerCanvas = () => {
256
-    this.markerImageHolder = document.createElement('div');
257
-    // fix for Edge's touch behavior
258
-    this.markerImageHolder.style.setProperty('touch-action', 'none');
259
-    this.markerImageHolder.style.setProperty('-ms-touch-action', 'none');
260
-
261
-    this.markerImage = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
262
-    this.markerImage.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
263
-    this.markerImage.setAttribute('width', this.width.toString());
264
-    this.markerImage.setAttribute('height', this.height.toString());
265
-    this.markerImage.setAttribute(
266
-      'viewBox',
267
-      '0 0 ' + this.width.toString() + ' ' + this.height.toString()
268
-    );
269
-
270
-    this.markerImageHolder.style.position = 'absolute';
271
-    this.markerImageHolder.style.width = `${this.width}px`;
272
-    this.markerImageHolder.style.height = `${this.height}px`;
273
-    this.markerImageHolder.style.transformOrigin = 'top left';
274
-    this.positionMarkerImage();
275
-
276
-    this.defs = SvgHelper.createDefs();
277
-    this.markerImage.appendChild(this.defs);
278
-
279
-    this.markerImageHolder.appendChild(this.markerImage);
280
-
281
-    document.body.appendChild(this.markerImageHolder);
282
-  };
283
-
284 227
   private adjustUI = (ev: UIEvent) => {
285 228
     this.adjustSize();
286 229
     this.positionUI();
@@ -290,27 +233,22 @@ export class Drawboard {
290 233
     this.width = this.target.clientWidth;
291 234
     this.height = this.target.clientHeight;
292 235
 
293
-    const scale = this.target.clientWidth / this.markerImageHolder.clientWidth;
236
+    const scale = this.target.clientWidth / this.boardHolder.clientWidth;
294 237
     if (scale !== 1.0) {
295 238
       this.scale *= scale;
296
-      this.markerImageHolder.style.width = `${this.width}px`;
297
-      this.markerImageHolder.style.height = `${this.height}px`;
239
+      this.boardHolder.style.width = `${this.width}px`;
240
+      this.boardHolder.style.height = `${this.height}px`;
298 241
 
299
-      this.markerImageHolder.style.transform = `scale(${this.scale})`;
242
+      this.boardHolder.style.transform = `scale(${this.scale})`;
300 243
     }
301 244
   };
302 245
 
303 246
   private positionUI = () => {
304 247
     this.setTargetRect();
305
-    this.positionMarkerImage();
248
+    this.positionBoard();
306 249
     this.positionToolbar();
307 250
   };
308 251
 
309
-  private positionMarkerImage = () => {
310
-    this.markerImageHolder.style.top = this.targetRect.top + 'px';
311
-    this.markerImageHolder.style.left = this.targetRect.left + 'px';
312
-  };
313
-
314 252
   private positionToolbar = () => {
315 253
     this.toolbarUI.style.left = `${this.targetRect.left +
316 254
       this.target.offsetWidth -
@@ -378,7 +316,7 @@ export class Drawboard {
378 316
             }
379 317
         `;
380 318
 
381
-    this.markerImage.appendChild(editorStyleSheet);
319
+    this.boardCanvas.appendChild(editorStyleSheet);
382 320
   };
383 321
 
384 322
   private toolbarClick = (ev: MouseEvent, toolbarItem: ToolbarItem) => {
@@ -418,8 +356,8 @@ export class Drawboard {
418 356
     this.activeMarker = marker;
419 357
   };
420 358
 
421
-  private deleteMarker = (marker: BaseMarker) => {
422
-    this.markerImage.removeChild(marker.visual);
359
+  public deleteMarker = (marker: BaseMarker) => {
360
+    this.boardCanvas.removeChild(marker.visual);
423 361
     if (this.activeMarker === marker) {
424 362
       this.activeMarker = null;
425 363
     }
@@ -433,17 +371,17 @@ export class Drawboard {
433 371
 
434 372
   private cancel = () => {
435 373
     this.close();
436
-    if (this.cancelCallback) {
437
-      this.cancelCallback();
374
+    if (this.onCancel) {
375
+      this.onCancel();
438 376
     }
439 377
   };
440 378
 
441 379
   private renderFinished = (dataUrl: string) => {
442
-    this.completeCallback(dataUrl);
380
+    this.onComplete(dataUrl);
443 381
   };
444 382
 
445 383
   private renderFinishedClose = (dataUrl: string) => {
446 384
     this.close();
447
-    this.completeCallback(dataUrl);
385
+    this.onComplete(dataUrl);
448 386
   };
449 387
 }

+ 6
- 0
src/board/WhitePage/index.less View File

@@ -0,0 +1,6 @@
1
+.fcw-page {
2
+  img {
3
+    width: 100%;
4
+    height: 100%;
5
+  }
6
+}

+ 162
- 28
src/board/WhitePage/index.ts View File

@@ -1,58 +1,192 @@
1
+import { TextMarker } from './../../markers/TextMarker/index';
1 2
 import { MarkerType } from './../../markers/types';
2
-import { ChangeEvent } from './../../event/Event';
3
+import { SyncEvent } from './../../event/Event';
3 4
 import { EventHub } from '../../event/EventHub';
4
-import { WhiteboardMode } from './../types';
5
+import { WhiteboardMode, WhitePageSource } from './../types';
5 6
 import { Drawboard } from './../Drawboard/index';
6 7
 import { uuid } from './../../utils/uuid';
7 8
 import { getMarkerByType } from '../../markers/types';
8 9
 
10
+import './index.less';
11
+import { createDivWithClassName } from 'fc-whiteboard/src/utils/dom';
12
+
13
+const prefix = 'fcw-page';
14
+
9 15
 /** 白板中的每一页 */
10 16
 export class WhitePage {
11 17
   id: string = uuid();
12 18
 
13
-  drawboard: Drawboard;
19
+  source: WhitePageSource;
20
+  target: HTMLImageElement;
21
+
22
+  /** UI Options */
23
+  container: HTMLDivElement;
24
+  // 父容器指针
25
+  parentContainer?: HTMLDivElement;
14 26
   mode: WhiteboardMode = 'master';
15 27
 
28
+  /** Handlers */
29
+  drawboard: Drawboard;
30
+  eventHub?: EventHub;
31
+
16 32
   constructor(
17
-    target: HTMLImageElement,
18
-    { mode, eventHub }: { mode?: WhiteboardMode; eventHub?: EventHub } = {}
33
+    source: WhitePageSource,
34
+    {
35
+      mode,
36
+      eventHub,
37
+      parentContainer
38
+    }: { mode?: WhiteboardMode; eventHub?: EventHub; parentContainer?: HTMLDivElement } = {}
19 39
   ) {
20 40
     if (mode) {
21 41
       this.mode = mode;
22 42
     }
43
+    this.eventHub = eventHub;
44
+    this.parentContainer = parentContainer;
45
+
46
+    this.initSource(source);
23 47
 
24 48
     if (this.mode === 'master') {
25
-      if (eventHub) {
26
-        this.drawboard = new Drawboard(target, {
27
-          page: this,
28
-          onChange: ev => eventHub.emit('change', ev)
29
-        });
30
-      } else {
31
-        this.drawboard = new Drawboard(target, { page: this });
32
-      }
49
+      this.initMaster();
33 50
     }
34 51
 
35 52
     if (this.mode === 'mirror') {
36
-      if (!eventHub) {
37
-        throw new Error('Invalid eventHub');
38
-      }
53
+      this.initMirror();
54
+    }
55
+  }
56
+
57
+  /** LifeCycle open - close */
58
+  public open() {
59
+    this.drawboard.open();
60
+  }
61
+
62
+  public hide() {
63
+    this.drawboard.hide();
64
+  }
65
+
66
+  public show() {
67
+    this.drawboard.show();
68
+  }
69
+
70
+  public close() {
71
+    this.drawboard.close();
72
+  }
73
+
74
+  /** 初始化源 */
75
+  private initSource(source: WhitePageSource) {
76
+    // 判断 Source 的类型是否符合要求
77
+    if (typeof source.imgSrc === 'string' && !this.parentContainer) {
78
+      throw new Error('Invalid source, If you set image url, you must also set parentContainer');
79
+    }
80
+
81
+    this.source = source;
82
+
83
+    // 如果传入的 imgEle,则直接使用
84
+    if (source.imgEle) {
85
+      this.target = source.imgEle!;
86
+    }
87
+
88
+    // 如果是图片,则需要创建 Image 元素
89
+    if (typeof source.imgSrc === 'string') {
90
+      this.container = createDivWithClassName(prefix, this.parentContainer!);
91
+      this.container.id = this.id;
92
+
93
+      this.target = document.createElement('img');
94
+      this.target.src = source.imgSrc;
95
+      this.target.alt = 'Siema image';
96
+
97
+      this.container.appendChild(this.target);
98
+    }
99
+  }
100
+
101
+  /** 以 Master 模式启动 */
102
+  protected initMaster() {
103
+    if (this.eventHub) {
104
+      // 对于 WhitePage 中加载的 Drawboard,必须是传入自身可控的 Image 元素
105
+      this.drawboard = new Drawboard(
106
+        { imgEle: this.target },
107
+        {
108
+          page: this,
109
+          onChange: ev => this.eventHub!.emit('sync', ev)
110
+        }
111
+      );
112
+    } else {
113
+      this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
114
+    }
115
+  }
39 116
 
40
-      this.drawboard = new Drawboard(target, { page: this });
117
+  /** 以 Mirror 模式启动 */
118
+  protected initMirror() {
119
+    if (!this.eventHub) {
120
+      throw new Error('Invalid eventHub');
121
+    }
122
+
123
+    this.drawboard = new Drawboard({ imgEle: this.target }, { page: this });
41 124
 
42
-      eventHub.on('change', (ev: ChangeEvent) => {
43
-        if (ev.event === 'add') {
44
-          const data: { id: string; type: MarkerType } = ev.data as {
45
-            id: string;
46
-            type: MarkerType;
47
-          };
48
-          this.drawboard.addMarker(getMarkerByType(data.type), { id: data.id });
125
+    this.eventHub.on('sync', (ev: SyncEvent) => {
126
+      try {
127
+        // 判断是否为 WhitePage 的同步
128
+        if (ev.target === 'page' && ev.id === this.id) {
129
+          this.onPageSync();
49 130
         }
50 131
 
51
-        if (ev.event === 'move' || ev.event === 'resize') {
52
-          const marker = this.drawboard.markerMap[ev.id];
53
-          marker.reactToManipulation(ev.event, ev.data as any);
132
+        // 处理 Marker 的同步事件
133
+        if (ev.target === 'marker') {
134
+          this.onMarkerSync(ev);
54 135
         }
55
-      });
136
+      } catch (e) {
137
+        console.warn(e);
138
+      }
139
+    });
140
+  }
141
+
142
+  /** 处理 Page 的同步事件 */
143
+  private onPageSync() {}
144
+
145
+  /** 处理 Marker 的同步事件 */
146
+  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];
155
+      if (!marker) {
156
+        this.drawboard.addMarker(getMarkerByType(data.type), { id: data.id });
157
+      }
158
+    }
159
+
160
+    // 其余的情况,不存在 id 则直接返回空
161
+    if (!ev.id) {
162
+      return;
163
+    }
164
+
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];
171
+      if (marker) {
172
+        this.drawboard.deleteMarker(marker);
173
+      }
174
+    }
175
+
176
+    if (ev.event === 'move' || ev.event === 'resize') {
177
+      const marker = this.drawboard.markerMap[ev.id];
178
+
179
+      if (marker) {
180
+        marker.reactToManipulation(ev.event, ev.data as any);
181
+      }
182
+    }
183
+
184
+    // 响应文本变化事件
185
+    if (ev.event === 'changeText') {
186
+      const marker = this.drawboard.markerMap[ev.id] as TextMarker;
187
+      if (marker) {
188
+        marker.setText(ev.data as string);
189
+      }
56 190
     }
57 191
   }
58 192
 }

+ 39
- 0
src/board/Whiteboard/index.less View File

@@ -0,0 +1,39 @@
1
+.fcw-board {
2
+  // 设置需要元素的绝对布局
3
+  &-imgs,
4
+  &-pages,
5
+  .fcw-page {
6
+    position: absolute;
7
+    top: 0;
8
+    left: 0;
9
+    width: 100%;
10
+    height: 100%;
11
+  }
12
+
13
+  &-img {
14
+    width: 100%;
15
+    height: 100%;
16
+  }
17
+
18
+  &-flip-arrow {
19
+    height: 20px;
20
+    width: 20px;
21
+
22
+    cursor: pointer;
23
+  }
24
+
25
+  &-controller {
26
+    position: absolute;
27
+    display: flex;
28
+    width: 50px;
29
+    height: 30px;
30
+    justify-content: space-between;
31
+    align-items: center;
32
+    top: -30px;
33
+    background-color: #cccccc;
34
+    padding: 0px 5px;
35
+    margin: 0px;
36
+    border-top-left-radius: 10px;
37
+    border-top-right-radius: 10px;
38
+  }
39
+}

+ 311
- 6
src/board/Whiteboard/index.ts View File

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

+ 6
- 2
src/board/types.ts View File

@@ -1,6 +1,10 @@
1 1
 // 是主动绘制模式,还是镜像模式
2 2
 export type WhiteboardMode = 'master' | 'mirror';
3 3
 
4
-export type RenderCallback = (container: HTMLElement) => void;
4
+export type WhitePageSource = {
5
+  // 需要展示的图片元素
6
+  imgEle?: HTMLImageElement;
5 7
 
6
-export type WhitePageSource = string | RenderCallback;
8
+  // 需要展示的图片地址
9
+  imgSrc?: string;
10
+};

+ 21
- 6
src/event/Event.ts View File

@@ -1,5 +1,19 @@
1
-export type TargetType = 'page' | 'drawboard' | 'marker';
2
-export type EventType = 'add' | 'resize' | 'move' | 'remove';
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';
3 17
 export type PositionType =
4 18
   | 'left'
5 19
   | 'right'
@@ -12,11 +26,12 @@ export type PositionType =
12 26
   | 'topCenter'
13 27
   | 'bottomCenter';
14 28
 
15
-export interface ChangeEvent {
29
+export interface SyncEvent {
16 30
   target: TargetType;
17
-  id: string;
31
+  id?: string;
32
+  parentId?: string;
18 33
   event: EventType;
19
-  data?: object | string;
34
+  data?: object | string | number;
20 35
 }
21 36
 
22
-export type onChangeFunc = (ev: ChangeEvent) => void;
37
+export type onSyncFunc = (ev: SyncEvent) => void;

+ 1
- 8
src/event/EventHub.ts View File

@@ -1,10 +1,3 @@
1
-import { ChangeEvent } from './Event';
2 1
 import * as EventEmitter from 'eventemitter3';
3 2
 
4
-export class EventHub extends EventEmitter<'change'> {}
5
-
6
-export const eventHub = new EventHub();
7
-
8
-eventHub.on('change', (changeEv: ChangeEvent) => {
9
-  console.log(changeEv);
10
-});
3
+export class EventHub extends EventEmitter<'sync'> {}

+ 3
- 1
src/markers/ArrowMarker/index.ts View File

@@ -1,12 +1,14 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { LinearMarker } from '../LinearMarker';
3 3
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 5
 
5 6
 export class ArrowMarker extends LinearMarker {
6 7
   type: MarkerType = 'arrow';
7 8
 
8
-  public static createMarker = (): LinearMarker => {
9
+  public static createMarker = (page?: WhitePage): LinearMarker => {
9 10
     const marker = new ArrowMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };

+ 12
- 6
src/markers/BaseMarker/index.ts View File

@@ -1,6 +1,6 @@
1 1
 import { WhitePage } from './../../board/WhitePage/index';
2 2
 import { PositionType } from 'fc-whiteboard/src/event/Event';
3
-import { onChangeFunc, EventType } from '../../event/Event';
3
+import { onSyncFunc, EventType } from '../../event/Event';
4 4
 import { MarkerType } from '../types';
5 5
 import * as uuid from 'uuid/v1';
6 6
 import { SvgHelper } from '../../renderer/SvgHelper';
@@ -9,12 +9,13 @@ export class BaseMarker {
9 9
   id: string = uuid();
10 10
   type: MarkerType = 'base';
11 11
   // 归属的
12
-  page: WhitePage;
12
+  page?: WhitePage;
13 13
   // Marker 的属性发生变化后的回调
14
-  onChange: onChangeFunc = () => {};
14
+  onChange: onSyncFunc = () => {};
15 15
 
16
-  public static createMarker = (): BaseMarker => {
16
+  public static createMarker = (page?: WhitePage): BaseMarker => {
17 17
     const marker = new BaseMarker();
18
+    marker.page = page;
18 19
     marker.setup();
19 20
     return marker;
20 21
   };
@@ -30,13 +31,12 @@ export class BaseMarker {
30 31
   protected height: number = 50;
31 32
 
32 33
   protected isActive: boolean = true;
34
+  protected isDragging: boolean = false;
33 35
   protected isResizing: boolean = false;
34 36
 
35 37
   protected previousMouseX: number = 0;
36 38
   protected previousMouseY: number = 0;
37 39
 
38
-  private isDragging: boolean = false;
39
-
40 40
   public reactToManipulation(
41 41
     type: EventType,
42 42
     { dx, dy, pos }: { dx: number; dy: number; pos: PositionType }
@@ -50,6 +50,7 @@ export class BaseMarker {
50 50
     }
51 51
   }
52 52
 
53
+  /** 响应元素视图状态变化 */
53 54
   public manipulate = (ev: MouseEvent) => {
54 55
     const scale = this.visual.getScreenCTM()!.a;
55 56
     const dx = (ev.screenX - this.previousMouseX) / scale;
@@ -165,6 +166,11 @@ export class BaseMarker {
165 166
 
166 167
   private mouseDown = (ev: MouseEvent) => {
167 168
     ev.stopPropagation();
169
+
170
+    if (this.page && this.page.mode === 'mirror') {
171
+      return;
172
+    }
173
+
168 174
     this.select();
169 175
     this.isDragging = true;
170 176
     this.previousMouseX = ev.screenX;

+ 3
- 1
src/markers/CoverMarker/index.ts View File

@@ -1,12 +1,14 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 5
 
5 6
 export class CoverMarker extends RectBaseMarker {
6 7
   type: MarkerType = 'cover';
7 8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9 10
     const marker = new CoverMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };

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

@@ -1,12 +1,14 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectBaseMarker } from '../RectMarker/RectBaseMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 5
 
5 6
 export class HighlightMarker extends RectBaseMarker {
6 7
   type: MarkerType = 'highlight';
7 8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9 10
     const marker = new HighlightMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };

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

@@ -1,12 +1,14 @@
1 1
 import { MarkerType } from './../types';
2 2
 import { LinearMarker } from '../LinearMarker';
3 3
 import { SvgHelper } from './../../renderer/SvgHelper/index';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 5
 
5 6
 export class LineMarker extends LinearMarker {
6 7
   type: MarkerType = 'line';
7 8
 
8
-  public static createMarker = (): LinearMarker => {
9
+  public static createMarker = (page?: WhitePage): LinearMarker => {
9 10
     const marker = new LineMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };

+ 7
- 1
src/markers/LinearMarker/index.ts View File

@@ -1,11 +1,13 @@
1
+import { WhitePage } from './../../board/WhitePage/index';
1 2
 import { BaseMarker } from '../BaseMarker';
2 3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
3 4
 import { SvgHelper } from '../../renderer/SvgHelper';
4 5
 import { PositionType } from 'fc-whiteboard/src/event/Event';
5 6
 
6 7
 export class LinearMarker extends BaseMarker {
7
-  public static createMarker = (): LinearMarker => {
8
+  public static createMarker = (page?: WhitePage): LinearMarker => {
8 9
     const marker = new LinearMarker();
10
+    marker.page = page;
9 11
     marker.setup();
10 12
     return marker;
11 13
   };
@@ -54,6 +56,10 @@ export class LinearMarker extends BaseMarker {
54 56
     this.addToRenderVisual(this.markerLine);
55 57
 
56 58
     this.addControlBox();
59
+
60
+    if (this.page && this.page.mode === 'mirror') {
61
+      this.controlBox.style.display = 'none';
62
+    }
57 63
   }
58 64
 
59 65
   protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {

+ 3
- 1
src/markers/RectMarker/RectBaseMarker.ts View File

@@ -1,10 +1,12 @@
1 1
 import { PositionType } from '../../event/Event';
2 2
 import { SvgHelper } from '../../renderer/SvgHelper';
3 3
 import { RectangularMarker } from '../RectangularMarker';
4
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 5
 
5 6
 export class RectBaseMarker extends RectangularMarker {
6
-  public static createMarker = (): RectBaseMarker => {
7
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
7 8
     const marker = new RectBaseMarker();
9
+    marker.page = page;
8 10
     marker.setup();
9 11
     return marker;
10 12
   };

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

@@ -1,3 +1,4 @@
1
+import { WhitePage } from './../../board/WhitePage/index';
1 2
 import { MarkerType } from './../types';
2 3
 import { RectBaseMarker } from './RectBaseMarker';
3 4
 import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
@@ -5,8 +6,9 @@ import { SvgHelper } from 'fc-whiteboard/src/renderer/SvgHelper';
5 6
 export class RectMarker extends RectBaseMarker {
6 7
   type: MarkerType = 'rect';
7 8
 
8
-  public static createMarker = (): RectBaseMarker => {
9
+  public static createMarker = (page?: WhitePage): RectBaseMarker => {
9 10
     const marker = new RectMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };

+ 7
- 1
src/markers/RectangularMarker/index.ts View File

@@ -3,10 +3,12 @@ import { BaseMarker } from '../BaseMarker';
3 3
 import { ResizeGrip } from '../BaseMarker/ResizeGrip';
4 4
 import { PositionType } from '../../event/Event';
5 5
 import { SvgHelper } from '../../renderer/SvgHelper';
6
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
6 7
 
7 8
 export class RectangularMarker extends BaseMarker {
8
-  public static createMarker = (): RectangularMarker => {
9
+  public static createMarker = (page?: WhitePage): RectangularMarker => {
9 10
     const marker = new RectangularMarker();
11
+    marker.page = page;
10 12
     marker.setup();
11 13
     return marker;
12 14
   };
@@ -40,6 +42,10 @@ export class RectangularMarker extends BaseMarker {
40 42
     super.setup();
41 43
 
42 44
     this.addControlBox();
45
+
46
+    if (this.page && this.page.mode === 'mirror') {
47
+      this.controlBox.style.display = 'none';
48
+    }
43 49
   }
44 50
 
45 51
   protected resizeByEvent(x: number, y: number, pos: PositionType) {

+ 18
- 3
src/markers/TextMarker/index.ts View File

@@ -1,6 +1,8 @@
1 1
 import { MarkerType } from '../types';
2 2
 import { RectangularMarker } from '../RectangularMarker';
3 3
 import { SvgHelper } from '../../renderer/SvgHelper';
4
+import { PositionType } from 'fc-whiteboard/src/event/Event';
5
+import { WhitePage } from 'fc-whiteboard/src/board/WhitePage';
4 6
 
5 7
 const OkIcon = require('../../assets/check.svg');
6 8
 const CancelIcon = require('../../assets/times.svg');
@@ -8,12 +10,19 @@ const CancelIcon = require('../../assets/times.svg');
8 10
 export class TextMarker extends RectangularMarker {
9 11
   type: MarkerType = 'text';
10 12
 
11
-  public static createMarker = (): TextMarker => {
13
+  public static createMarker = (page?: WhitePage): TextMarker => {
12 14
     const marker = new TextMarker();
15
+    marker.page = page;
13 16
     marker.setup();
14 17
     return marker;
15 18
   };
16 19
 
20
+  /** 设置文本 */
21
+  public setText(text: string) {
22
+    this.text = text;
23
+    this.renderText();
24
+  }
25
+
17 26
   protected readonly MIN_SIZE = 50;
18 27
 
19 28
   private readonly DEFAULT_TEXT = 'Double-click to edit text';
@@ -41,8 +50,9 @@ export class TextMarker extends RectangularMarker {
41 50
     this.visual.addEventListener('touchstart', this.onTap);
42 51
   }
43 52
 
44
-  protected resize(x: number, y: number) {
45
-    super.resize(x, y);
53
+  protected resize(x: number, y: number, onPosition?: (pos: PositionType) => void) {
54
+    super.resize(x, y, onPosition);
55
+
46 56
     this.sizeText();
47 57
   }
48 58
 
@@ -128,12 +138,17 @@ export class TextMarker extends RectangularMarker {
128 138
     buttons.appendChild(cancelButton);
129 139
   };
130 140
 
141
+  /** 响应文本输入的事件 */
131 142
   private onEditorOkClick = (ev: MouseEvent | null) => {
132 143
     if (this.editorTextArea.value.trim()) {
133 144
       this.text = this.editorTextArea.value;
134 145
     } else {
135 146
       this.text = this.DEFAULT_TEXT;
136 147
     }
148
+
149
+    // 触发文本修改时间
150
+    this.onChange({ target: 'marker', id: this.id, event: 'changeText', data: this.text });
151
+
137 152
     this.renderText();
138 153
     this.closeEditor();
139 154
   };

+ 5
- 2
src/markers/types.ts View File

@@ -1,3 +1,4 @@
1
+import { HighlightMarker } from './HighlightMarker/index';
1 2
 import { TextMarker } from './TextMarker/index';
2 3
 import { ArrowMarker } from './ArrowMarker/index';
3 4
 import { BaseMarker } from './BaseMarker/index';
@@ -9,12 +10,14 @@ export type MarkerType = 'base' | 'arrow' | 'cover' | 'line' | 'rect' | 'text' |
9 10
 
10 11
 export function getMarkerByType(type: MarkerType): typeof BaseMarker {
11 12
   switch (type) {
12
-    case 'base':
13
-      return BaseMarker;
14 13
     case 'arrow':
15 14
       return ArrowMarker;
15
+    case 'base':
16
+      return BaseMarker;
16 17
     case 'cover':
17 18
       return CoverMarker;
19
+    case 'highlight':
20
+      return HighlightMarker;
18 21
     case 'line':
19 22
       return LineMarker;
20 23
     case 'rect':

+ 9
- 2
src/renderer/Synthetizer/index.ts View File

@@ -1,9 +1,16 @@
1
+import { isHTMLImageElement } from 'fc-whiteboard/src/utils/validator';
2
+
3
+/** 图片导出 */
1 4
 export class Synthetizer {
2 5
   public rasterize(
3
-    target: HTMLImageElement,
6
+    target: HTMLImageElement | HTMLDivElement,
4 7
     markerImage: SVGSVGElement,
5 8
     done: (dataUrl: string) => void
6 9
   ) {
10
+    if (!isHTMLImageElement(target)) {
11
+      throw new Error('Error: only support export HTMLImageElement');
12
+    }
13
+
7 14
     const canvas = document.createElement('canvas');
8 15
     canvas.width = markerImage.width.baseVal.value;
9 16
     canvas.height = markerImage.height.baseVal.value;
@@ -16,7 +23,7 @@ export class Synthetizer {
16 23
       throw new Error('Invalid ctx');
17 24
     }
18 25
 
19
-    ctx.drawImage(target, 0, 0, canvas.width, canvas.height);
26
+    ctx.drawImage(target as HTMLImageElement, 0, 0, canvas.width, canvas.height);
20 27
 
21 28
     const DOMURL = window.URL; // || window.webkitURL || window;
22 29
 

+ 15
- 0
src/toolbar/Toolbar.ts View File

@@ -1,7 +1,10 @@
1 1
 import { ToolbarButton } from './ToolbarButton';
2 2
 import { ToolbarItem } from './ToolbarItem';
3
+import { uuid } from '../utils/uuid';
3 4
 
4 5
 export class Toolbar {
6
+  id: string = uuid();
7
+
5 8
   private toolbarItems: ToolbarItem[];
6 9
   private toolbarUI: HTMLElement;
7 10
 
@@ -15,8 +18,10 @@ export class Toolbar {
15 18
     this.clickHandler = clickHandler;
16 19
   }
17 20
 
21
+  /** 获取 UI 元素 */
18 22
   public getUI = (): HTMLElement => {
19 23
     this.toolbarUI = document.createElement('div');
24
+    this.toolbarUI.id = `fcw-toolbar-${this.id}`;
20 25
     this.toolbarUI.className = 'fc-whiteboard-toolbar';
21 26
 
22 27
     for (const toolbarItem of this.toolbarItems) {
@@ -26,4 +31,14 @@ export class Toolbar {
26 31
 
27 32
     return this.toolbarUI;
28 33
   };
34
+
35
+  public hide() {
36
+    this.toolbarUI.style.opacity = '0';
37
+    this.toolbarUI.style.zIndex = '-1';
38
+  }
39
+
40
+  public show() {
41
+    this.toolbarUI.style.opacity = '1';
42
+    this.toolbarUI.style.zIndex = '999';
43
+  }
29 44
 }

+ 55
- 0
src/toolbar/toolbar-items.ts View File

@@ -1,3 +1,4 @@
1
+import { WhitePage } from './../board/WhitePage/index';
1 2
 import { RectMarker } from './../markers/RectMarker/index';
2 3
 import { CoverMarker } from './../markers/CoverMarker/index';
3 4
 import { TextMarker } from './../markers/TextMarker/index';
@@ -6,6 +7,11 @@ import { HighlightMarker } from './../markers/HighlightMarker/index';
6 7
 import { ToolbarItem } from './ToolbarItem';
7 8
 import { LineMarker } from '../markers/LineMarker';
8 9
 
10
+const OkIcon = require('../assets/check.svg');
11
+const DeleteIcon = require('../assets/eraser.svg');
12
+const PointerIcon = require('../assets/mouse-pointer.svg');
13
+const CloseIcon = require('../assets/times.svg');
14
+
9 15
 export const highlightMarkerToolbarItem = new ToolbarItem({
10 16
   name: 'cover-marker',
11 17
   tooltipText: 'Cover',
@@ -47,3 +53,52 @@ export const lineMarkerToolbarItem = new ToolbarItem({
47 53
   icon: require('../assets/line.svg'),
48 54
   markerType: LineMarker
49 55
 });
56
+
57
+export function getToolbars(page?: WhitePage) {
58
+  const toolbars = [
59
+    {
60
+      icon: PointerIcon,
61
+      name: 'pointer',
62
+      tooltipText: 'Pointer'
63
+    },
64
+    {
65
+      icon: DeleteIcon,
66
+      name: 'delete',
67
+      tooltipText: 'Delete'
68
+    },
69
+    {
70
+      name: 'separator',
71
+      tooltipText: ''
72
+    },
73
+    rectMarkerToolbarItem,
74
+    coverMarkerToolbarItem,
75
+    highlightMarkerToolbarItem,
76
+    lineMarkerToolbarItem,
77
+    arrowMarkerToolbarItem,
78
+    textMarkerToolbarItem,
79
+    {
80
+      name: 'separator',
81
+      tooltipText: ''
82
+    }
83
+  ];
84
+
85
+  if (!page) {
86
+    toolbars.push(
87
+      ...[
88
+        {
89
+          icon: OkIcon,
90
+          name: 'ok',
91
+          tooltipText: 'OK'
92
+        }
93
+      ]
94
+    );
95
+  }
96
+
97
+  toolbars.push({
98
+    icon: CloseIcon,
99
+    name: 'close',
100
+    tooltipText: 'Close'
101
+  });
102
+
103
+  return toolbars;
104
+}

+ 2
- 0
src/types.d.ts View File

@@ -11,3 +11,5 @@ declare module '*.svg' {
11 11
   const content: string;
12 12
   export default content;
13 13
 }
14
+
15
+declare module 'siema';

+ 23
- 0
src/utils/dom.ts View File

@@ -0,0 +1,23 @@
1
+/** 创建包含样式类名的 Div 元素 */
2
+export function createDivWithClassName(className?: string, parent?: HTMLElement) {
3
+  const ele = document.createElement('div');
4
+
5
+  if (parent) {
6
+    parent.appendChild(ele);
7
+  }
8
+
9
+  if (className) {
10
+    addClassName(ele, className);
11
+  }
12
+
13
+  return ele;
14
+}
15
+
16
+/** 添加样式类名 */
17
+export function addClassName(ele: HTMLElement, className: string) {
18
+  if (!ele) {
19
+    return;
20
+  }
21
+
22
+  ele.className = `${ele.className || ''} ${className}`.trim();
23
+}

+ 8
- 0
src/utils/validator.ts View File

@@ -0,0 +1,8 @@
1
+/** 判断是否为有效的 HTMLImageElement */
2
+export function isHTMLImageElement(ele: any) {
3
+  if (typeof ele === 'object' && ele instanceof HTMLImageElement) {
4
+    return true;
5
+  }
6
+
7
+  return false;
8
+}