@onesy/ui-react 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1415 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+ import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties";
3
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
4
+ const _excluded = ["valueDefault", "onChange", "minZoom", "maxZoom", "grid", "settings", "className"];
5
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
6
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
7
+ import React from 'react';
8
+ import { arrayToParts, clamp, copy, getID, is, isEnvironment } from '@onesy/utils';
9
+ import { classNames, style as styleMethod, useOnesyTheme } from '@onesy/style-react';
10
+ import { OnesyDate } from '@onesy/date';
11
+ import LineElement from '../Line';
12
+ import { staticClassName } from '../utils';
13
+ const useStyle = styleMethod(theme => ({
14
+ root: {
15
+ position: 'relative'
16
+ },
17
+ canvas: {
18
+ position: 'absolute',
19
+ inset: 0,
20
+ width: '100%',
21
+ height: '100%',
22
+ imageRendering: 'pixelated',
23
+ background: '#fff',
24
+ appearance: 'none',
25
+ border: 'none',
26
+ userSelect: 'none',
27
+ transition: theme.methods.transitions.make('opacity'),
28
+ '&[disabled]': {
29
+ opacity: 0.7,
30
+ pointerEvents: 'none'
31
+ }
32
+ },
33
+ ui: {
34
+ zIndex: 0
35
+ },
36
+ object: {
37
+ cursor: 'crosshair'
38
+ },
39
+ pen: {
40
+ cursor: 'crosshair'
41
+ },
42
+ pan: {
43
+ cursor: 'grab'
44
+ },
45
+ panning: {
46
+ cursor: 'grabbing'
47
+ },
48
+ zoom: {
49
+ cursor: 'zoom-in'
50
+ },
51
+ eraser: {
52
+ cursor: 'not-allowed'
53
+ },
54
+ image: {
55
+ cursor: 'copy'
56
+ },
57
+ text: {
58
+ cursor: 'text'
59
+ }
60
+ }), {
61
+ name: 'onesy-Whiteboard'
62
+ });
63
+ const colorSelect = 'hsl(244deg 64% 64%)';
64
+ const colorSelectBackground = 'hsla(244deg 64% 64% / 4%)';
65
+ const Whiteboard = /*#__PURE__*/React.forwardRef((props_, ref) => {
66
+ const theme = useOnesyTheme();
67
+ const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.onesyWhiteboard?.props?.default), props_), [props_]);
68
+ const Line = React.useMemo(() => theme?.elements?.Line || LineElement, [theme]);
69
+ const {
70
+ valueDefault,
71
+ onChange: onChangeProps,
72
+ // 10%
73
+ minZoom = 10,
74
+ // 400%
75
+ maxZoom = 4000,
76
+ grid: gridProps = true,
77
+ settings = {
78
+ lineCap: 'round',
79
+ lineJoin: 'round',
80
+ lineWidth: 10,
81
+ fillStyle: 'lightgreen',
82
+ strokeStyle: 'lightgreen',
83
+ globalAlpha: 0.44
84
+ },
85
+ className
86
+ } = props,
87
+ other = _objectWithoutProperties(props, _excluded);
88
+ const {
89
+ classes
90
+ } = useStyle();
91
+ const [size, setSize] = React.useState({});
92
+ const [tool, setTool] = React.useState('select');
93
+ const [mouseDown, setMouseDown] = React.useState(false);
94
+ const [grid, setGrid] = React.useState(gridProps);
95
+ const [loaded, setLoaded] = React.useState(false);
96
+ const refs = {
97
+ root: React.useRef(null),
98
+ ui: React.useRef(null),
99
+ interactive: React.useRef(null),
100
+ on: React.useRef(false),
101
+ items: React.useRef(valueDefault || []),
102
+ previous: React.useRef({
103
+ x: 0,
104
+ y: 0
105
+ }),
106
+ previousMouse: React.useRef({
107
+ x: 0,
108
+ y: 0
109
+ }),
110
+ moveStarted: React.useRef(false),
111
+ undo: React.useRef([]),
112
+ redo: React.useRef([]),
113
+ move: React.useRef({
114
+ x: 0,
115
+ y: 0
116
+ }),
117
+ offset: React.useRef({
118
+ x: 0,
119
+ y: 0
120
+ }),
121
+ start: React.useRef({
122
+ x: 0,
123
+ y: 0
124
+ }),
125
+ end: React.useRef({
126
+ x: 0,
127
+ y: 0
128
+ }),
129
+ scale: React.useRef(1),
130
+ mouseDown: React.useRef(mouseDown),
131
+ mouseMove: React.useRef({
132
+ current: {
133
+ x: 0,
134
+ y: 0
135
+ },
136
+ previous: undefined,
137
+ delta: {
138
+ x: 0,
139
+ y: 0
140
+ }
141
+ }),
142
+ tool: React.useRef(tool),
143
+ previousTool: React.useRef(tool),
144
+ toolUpdateAuto: React.useRef(false),
145
+ remove: React.useRef([]),
146
+ grid: React.useRef(grid),
147
+ typing: React.useRef(false),
148
+ image: React.useRef(isEnvironment('browser') && new Image()),
149
+ aspectRatio: React.useRef(1),
150
+ select: React.useRef(null),
151
+ textActive: React.useRef(null),
152
+ textSettings: React.useRef({
153
+ lineHeight: 20,
154
+ padding: 5,
155
+ fillStyle: 'black'
156
+ })
157
+ };
158
+ refs.mouseDown.current = mouseDown;
159
+ refs.tool.current = tool;
160
+ refs.grid.current = grid;
161
+ const init = React.useCallback(() => {
162
+ // Todo
163
+ // items
164
+ // load all of the images in memory and attach theme to items as image elements
165
+ // once it's all done, setLoaded(true), render
166
+
167
+ setTimeout(() => {
168
+ render();
169
+ setLoaded(true);
170
+ }, 40);
171
+ }, []);
172
+ React.useEffect(() => {
173
+ if (!['zoom'].includes(tool)) refs.previousTool.current = tool;
174
+ }, [tool]);
175
+ const onChange = React.useCallback(() => {
176
+ if (is('function', onChangeProps)) onChangeProps(refs.items.current);
177
+ }, [onChangeProps]);
178
+ const getItems = React.useCallback(function () {
179
+ let selected = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
180
+ return refs.items.current.filter(item => selected === undefined || item.se === selected);
181
+ }, []);
182
+ const getItem = React.useCallback(() => refs.items.current[refs.items.current.length - 1], []);
183
+ const filterItems = React.useCallback(() => {
184
+ const toRemove = refs.items.current.filter(item => {
185
+ if (refs.tool.current === 'text' && item !== refs.textActive.current) item.se = false;
186
+ const lines = item.s?.lines || [];
187
+ return !(item.v !== 't' || item === refs.textActive.current || lines.length > 1 || lines[0].length);
188
+ });
189
+ if (toRemove.length) remove(toRemove);
190
+ }, []);
191
+ const add = React.useCallback(toAdd => {
192
+ const itemsAdd = (Array.isArray(toAdd) ? toAdd : [toAdd]).filter(Boolean);
193
+ const items = getItems();
194
+
195
+ // add to undo stack snapshot of current state
196
+ refs.undo.current.push([...items]);
197
+
198
+ // clear redo stack
199
+ refs.redo.current = [];
200
+ refs.items.current.push(...itemsAdd);
201
+ }, []);
202
+ const remove = React.useCallback(toRemove => {
203
+ const itemsRemove = (Array.isArray(toRemove) ? toRemove : [toRemove]).filter(Boolean);
204
+ const items = getItems();
205
+ const IDs = itemsRemove?.map(item => item.i);
206
+ const toRemoveIDs = items.filter(item => IDs.includes(item.i)).map(item => item.i);
207
+ if (!toRemoveIDs) return;
208
+
209
+ // add to undo stack snapshot of current state
210
+ refs.undo.current.push([...items]);
211
+
212
+ // clear redo stack
213
+ refs.redo.current = [];
214
+ refs.items.current = refs.items.current.filter(item => !toRemoveIDs.includes(item.i));
215
+ }, []);
216
+ const reset = React.useCallback(() => {
217
+ refs.on.current = false;
218
+ refs.moveStarted.current = false;
219
+ refs.image.current = null;
220
+
221
+ // new move start
222
+ refs.offset.current.x = refs.move.current.x;
223
+ refs.offset.current.y = refs.move.current.y;
224
+ }, []);
225
+ const transform = React.useCallback(coordinate => coordinate / refs.scale.current, []);
226
+ const selectAll = React.useCallback(() => {
227
+ return [...refs.items.current].filter(Boolean).map(item => {
228
+ item.se = true;
229
+ return item;
230
+ });
231
+ }, []);
232
+ const unselectAll = React.useCallback(eventReact => {
233
+ const event = eventReact?.nativeEvent || eventReact;
234
+ const shift = event?.shiftKey;
235
+ return [...refs.items.current].filter(Boolean).map(item => {
236
+ if (shift) return item;
237
+ item.se = false;
238
+ return item;
239
+ });
240
+ }, []);
241
+ const onInteractionDown = React.useCallback((body, eventReact) => {
242
+ const event = eventReact?.nativeEvent || eventReact;
243
+ const {
244
+ offsetX: x,
245
+ offsetY: y,
246
+ clientX,
247
+ clientY
248
+ } = body;
249
+ refs.on.current = true;
250
+ refs.previous.current = {
251
+ x,
252
+ y
253
+ };
254
+ refs.mouseMove.current = {
255
+ current: {
256
+ x: 0,
257
+ y: 0
258
+ },
259
+ previous: undefined,
260
+ delta: {
261
+ x: 0,
262
+ y: 0
263
+ }
264
+ };
265
+ const shift = event?.shiftKey;
266
+ const ui = refs.ui.current.getContext('2d');
267
+ const rect = refs.ui.current.getBoundingClientRect();
268
+ refs.start.current.x = clientX - rect.left;
269
+ refs.start.current.y = clientY - rect.top;
270
+ refs.textActive.current = null;
271
+ const start = refs.start.current;
272
+ const startTransformed = {
273
+ x: transform(start.x - refs.move.current.x),
274
+ y: transform(start.y - refs.move.current.y)
275
+ };
276
+ Object.keys(settings).forEach(item_ => ui[item_] = settings[item_]);
277
+ let item;
278
+ const items = getItems();
279
+ const t = refs.tool.current;
280
+ refs.select.current = null;
281
+ if (t === 'select') {
282
+ // z-index top to bottom order
283
+ const itemsReversed = [...items].reverse();
284
+ const itemsClicked = itemsReversed.filter(item_ => {
285
+ return item_.c && startTransformed.x >= item_.c[0] && startTransformed.x <= item_.c[0] + item_.c[2] && startTransformed.y >= item_.c[1] && startTransformed.y <= item_.c[1] + item_.c[3];
286
+ });
287
+ const itemsSelected = getItems(true);
288
+ const clicked = itemsClicked[0];
289
+ if (!clicked) {
290
+ unselectAll();
291
+ refs.select.current = {
292
+ p: [],
293
+ ar: []
294
+ };
295
+ }
296
+ if (!shift && !(clicked?.se && itemsSelected.length > 1)) unselectAll();
297
+ if (clicked) {
298
+ clicked.se = shift ? !clicked.se : true;
299
+ }
300
+ } else if (t === 'text') {
301
+ const {
302
+ lineHeight,
303
+ padding
304
+ } = refs.textSettings.current;
305
+ const selectedText = items.find(item_ => item_.c && startTransformed.x >= item_.c[0] && startTransformed.x <= item_.c[0] + item_.c[2] && startTransformed.y >= item_.c[1] && startTransformed.y <= item_.c[1] + item_.c[3]);
306
+ if (selectedText) {
307
+ refs.textActive.current = selectedText;
308
+ selectedText.se = true;
309
+ const relativeY = startTransformed.y - selectedText.c[1] - padding;
310
+ const lineIndex = Math.floor(relativeY / lineHeight);
311
+ const clickedLine = selectedText.s?.lines?.[lineIndex] || '';
312
+ const relativeX = startTransformed.x - selectedText.c[0] - padding;
313
+ let charIndex = clickedLine.length;
314
+ for (let i = 0; i < clickedLine.length; i++) {
315
+ if (relativeX < ui.measureText(clickedLine.slice(0, i + 1)).width) {
316
+ charIndex = i;
317
+ break;
318
+ }
319
+ }
320
+ selectedText.s.cursor = {
321
+ line: lineIndex,
322
+ char: charIndex
323
+ };
324
+ } else {
325
+ item = {
326
+ i: getID(),
327
+ v: 't',
328
+ p: [startTransformed.x, startTransformed.y],
329
+ ar: [15, lineHeight + padding * 2],
330
+ s: _objectSpread(_objectSpread({}, refs.textSettings.current), {}, {
331
+ lines: [''],
332
+ cursor: {
333
+ line: 0,
334
+ char: 0
335
+ }
336
+ }),
337
+ se: true,
338
+ a: OnesyDate.milliseconds
339
+ };
340
+ refs.textActive.current = item;
341
+ }
342
+ } else {
343
+ // pen
344
+ if (t === 'pen') {
345
+ // point
346
+ item = {
347
+ i: getID(),
348
+ v: 'dp',
349
+ p: [transform(x - refs.move.current.x), transform(y - refs.move.current.y)],
350
+ ar: [ui.lineWidth / 2, 0, Math.PI * 2],
351
+ s: copy(settings),
352
+ a: OnesyDate.milliseconds
353
+ };
354
+ }
355
+ // circle, rectangle, line, line-arrow
356
+ if (['circle', 'rectangle', 'triangle', 'line', 'line-arrow'].includes(t)) {
357
+ item = {
358
+ i: getID(),
359
+ p: [],
360
+ ar: [],
361
+ s: copy(settings),
362
+ a: OnesyDate.milliseconds
363
+ };
364
+ }
365
+ // pan
366
+ if (t === 'pan') {
367
+ // new move start
368
+ refs.offset.current.x = refs.move.current.x;
369
+ refs.offset.current.y = refs.move.current.y;
370
+ }
371
+ // image
372
+ else if (t === 'image' && refs.image.current.complete && refs.image.current.src) {
373
+ refs.aspectRatio.current = refs.image.current.width / refs.image.current.height;
374
+
375
+ // Todo
376
+ // add url of the image
377
+ // instead of embeding the image
378
+ item = {
379
+ i: getID(),
380
+ v: 'i',
381
+ p: [],
382
+ ar: [],
383
+ s: {
384
+ // Todo
385
+ // remove in the future
386
+ image: refs.image.current,
387
+ aspectRatio: refs.aspectRatio.current
388
+ },
389
+ a: OnesyDate.milliseconds
390
+ };
391
+ }
392
+ }
393
+ if (item) add(item);
394
+ filterItems();
395
+
396
+ // render
397
+ render();
398
+ setMouseDown(true);
399
+ }, []);
400
+ const onMouseDown = React.useCallback(event => {
401
+ const {
402
+ offsetX,
403
+ offsetY,
404
+ clientX,
405
+ clientY
406
+ } = event.nativeEvent;
407
+ onInteractionDown({
408
+ offsetX,
409
+ offsetY,
410
+ clientX,
411
+ clientY
412
+ }, event);
413
+ }, [onInteractionDown]);
414
+ const onTouchStart = React.useCallback(event => {
415
+ // Get the first touch point
416
+ const touch = event.touches[0];
417
+ const {
418
+ clientX,
419
+ clientY
420
+ } = touch;
421
+ let {
422
+ offsetX,
423
+ offsetY
424
+ } = touch;
425
+ const targetElement = touch.target;
426
+ if (targetElement instanceof HTMLElement) {
427
+ // Get the bounding rectangle of the target element
428
+ const rect = targetElement.getBoundingClientRect();
429
+
430
+ // Calculate the offsetX and offsetY
431
+ offsetX = touch.clientX - rect.left;
432
+ offsetY = touch.clientY - rect.top;
433
+ }
434
+ onInteractionDown({
435
+ offsetX,
436
+ offsetY,
437
+ clientX,
438
+ clientY
439
+ }, event);
440
+ }, [onInteractionDown]);
441
+ const removeItems = React.useCallback(() => {
442
+ // remove
443
+ if (refs.remove.current.length) {
444
+ const toRemove = [];
445
+ for (const item of refs.remove.current) {
446
+ const index = refs.items.current.findIndex(itemItems => itemItems === item);
447
+ if (index > -1) toRemove.push(item);
448
+ }
449
+ if (toRemove.length) remove(toRemove);
450
+ refs.remove.current = [];
451
+ }
452
+ }, []);
453
+ const onUpdateCoordinates = React.useCallback(() => {
454
+ const items = getItems();
455
+ items.forEach(item => {
456
+ const p = item?.p || [];
457
+ const ar = item?.ar || [];
458
+ const s = item?.s || {};
459
+ if (p.length) {
460
+ // cache
461
+ // x1, y1, width. height for position on the screen
462
+ const v = item.v;
463
+
464
+ // draw point
465
+ if (v === 'dp') {
466
+ const lineWidth = s.lineWidth || 10;
467
+ item.c = [p[0] - lineWidth / 2, p[1] - lineWidth / 2, lineWidth, lineWidth];
468
+ }
469
+ // draw line
470
+ else if (v === 'dl') {
471
+ const x = [];
472
+ const y = [];
473
+ for (let i = 0; i < p.length; i += 2) {
474
+ x.push(p[i]);
475
+ y.push(p[i + 1]);
476
+ }
477
+ const xMin = Math.min(...x);
478
+ const yMin = Math.min(...y);
479
+ const xMax = Math.max(...x);
480
+ const yMax = Math.max(...y);
481
+ item.c = [xMin, yMin, xMax - xMin, yMax - yMin];
482
+ }
483
+ // object line, object arrow
484
+ else if (['ol', 'oa'].includes(v)) {
485
+ const x = [p[0], ar[0]];
486
+ const y = [p[1], ar[1]];
487
+ const xMin = Math.min(...x);
488
+ const yMin = Math.min(...y);
489
+ const xMax = Math.max(...x);
490
+ const yMax = Math.max(...y);
491
+ item.c = [xMin, yMin, xMax - xMin, yMax - yMin];
492
+ }
493
+ // object rectangle, object square
494
+ else if (['or', 'os'].includes(v)) {
495
+ item.c = [...p, ...ar];
496
+ }
497
+ // object circle, object ellipse
498
+ else if (['oc', 'oe'].includes(v)) {
499
+ if (v === 'oc') {
500
+ item.c = [p[0] - ar[0], p[1] - ar[0], ar[0] * 2, ar[0] * 2];
501
+ } else {
502
+ item.c = [p[0] - ar[0], p[1] - ar[1], ar[0] * 2, ar[1] * 2];
503
+ }
504
+ }
505
+ // object triangle, object triangle equilateral
506
+ else if (['ot', 'ote'].includes(v)) {
507
+ const [x1, y1, x2] = p;
508
+ const {
509
+ height
510
+ } = s;
511
+ item.c = [x1, y1 - height, x2 - x1, height];
512
+ }
513
+ // image
514
+ else if (['i', 't'].includes(v)) {
515
+ item.c = [...p, ...ar];
516
+ }
517
+ }
518
+ });
519
+ }, []);
520
+ const onSelect = React.useCallback(() => {
521
+ const select = refs.select.current;
522
+ if (!select) return;
523
+ const px1 = select.p[0];
524
+ const px2 = px1 + select.ar[0];
525
+ const py1 = select.p[1];
526
+ const py2 = py1 + select.ar[1];
527
+ const min = {
528
+ x: Math.min(px1, px2),
529
+ y: Math.min(py1, py2)
530
+ };
531
+ const max = {
532
+ x: Math.max(px1, px2),
533
+ y: Math.max(py1, py2)
534
+ };
535
+ const items = getItems();
536
+ items.forEach(item => {
537
+ const {
538
+ c
539
+ } = item;
540
+ const [x1, y1] = c;
541
+ let [x2, y2] = c;
542
+ x2 = x1 + x2;
543
+ y2 = y1 + y2;
544
+ const minItem = {
545
+ x: Math.min(x1, x2),
546
+ y: Math.min(y1, y2)
547
+ };
548
+ const maxItem = {
549
+ x: Math.max(x1, x2),
550
+ y: Math.max(y1, y2)
551
+ };
552
+ const selected = minItem.x >= min.x && maxItem.x <= max.x && minItem.y >= min.y && maxItem.y <= max.y;
553
+ if (selected) item.se = true;
554
+ });
555
+ }, []);
556
+ const onMouseUp = React.useCallback(event => {
557
+ if (refs.mouseDown.current) {
558
+ refs.select.current = null;
559
+
560
+ // update coordinates
561
+ onUpdateCoordinates();
562
+
563
+ // reset
564
+ reset();
565
+
566
+ // remove
567
+ removeItems();
568
+ console.log('items', refs.items.current);
569
+
570
+ // onChange
571
+ onChange();
572
+ setMouseDown(false);
573
+ if (['circle', 'rectangle', 'triangle', 'line', 'line-arrow', 'image'].includes(refs.previousTool.current)) {
574
+ setTool('select');
575
+ }
576
+ render();
577
+ }
578
+ }, [onChange]);
579
+ const updateTextBoxDimensions = React.useCallback(item => {
580
+ const ui = refs.ui.current.getContext('2d');
581
+ const {
582
+ lineHeight,
583
+ padding
584
+ } = refs.textSettings.current;
585
+ ui.font = '16px Arial';
586
+ const maxWidth = Math.max(...item.s.lines.map(line => ui.measureText(line).width));
587
+ item.ar[0] = maxWidth + padding * 2.5;
588
+ item.ar[1] = item.s.lines.length * lineHeight + padding * 2;
589
+ }, []);
590
+ const getPath = React.useCallback(item => {
591
+ const path = new Path2D();
592
+ const {
593
+ v,
594
+ p,
595
+ ar
596
+ } = item;
597
+
598
+ // draw line
599
+ if (v === 'dl') {
600
+ const points = arrayToParts(p, 2);
601
+ for (let i = 0; i < points.length - 1; i++) {
602
+ const current = points[i];
603
+ const next = points[i + 1];
604
+
605
+ // calculate the control point for the curve
606
+ const midX = (current[0] + next[0]) / 2;
607
+ const midY = (current[1] + next[1]) / 2;
608
+ if (i === 0) {
609
+ // start from the first point
610
+ path.moveTo(current[0], current[1]);
611
+ }
612
+ path.quadraticCurveTo(current[0], current[1], midX, midY);
613
+ }
614
+ }
615
+ // draw point, object circle
616
+ else if (['dp', 'oc'].includes(v) && ar.length === 3) {
617
+ path.arc(p[0], p[1], ...ar);
618
+ }
619
+ // object ellipse
620
+ else if (v === 'oe' && ar.length === 5) {
621
+ path.ellipse(p[0], p[1], ...ar);
622
+ }
623
+ // object rectangle
624
+ else if (['or', 'os'].includes(v)) {
625
+ path.roundRect(p[0], p[1], ...ar);
626
+ }
627
+ // object line
628
+ else if (['ol', 'oa'].includes(v) && ar.length === 2) {
629
+ path.moveTo(p[0], p[1]);
630
+ path.lineTo(...ar);
631
+
632
+ // draw an arrow
633
+ if (v === 'oa') {
634
+ // Length of the arrowhead
635
+ const headLength = 40;
636
+ const angle = Math.atan2(ar[1] - p[1], ar[0] - p[0]);
637
+ path.moveTo(ar[0], ar[1]);
638
+ path.lineTo(ar[0] - headLength * Math.cos(angle - Math.PI / 6), ar[1] - headLength * Math.sin(angle - Math.PI / 6));
639
+ path.moveTo(ar[0], ar[1]);
640
+ path.lineTo(ar[0] - headLength * Math.cos(angle + Math.PI / 6), ar[1] - headLength * Math.sin(angle + Math.PI / 6));
641
+ }
642
+ }
643
+ // object triangle
644
+ else if (['ot', 'ote'].includes(v)) {
645
+ path.moveTo(p[0], p[1]);
646
+ path.lineTo(p[2], p[3]);
647
+ path.lineTo(p[4], p[5]);
648
+ path.closePath();
649
+ }
650
+ return path;
651
+ }, []);
652
+ const draw = React.useCallback(item => {
653
+ const ui = refs.ui.current.getContext('2d');
654
+
655
+ // settings
656
+ Object.keys(item.s || {}).forEach(key => ui[key] = item.s?.[key]);
657
+ ui.globalAlpha = refs.remove.current.includes(item) ? 0.25 : item.s?.globalAlpha !== undefined ? item.s?.globalAlpha : 1;
658
+ ui.beginPath();
659
+ const path = getPath(item);
660
+ const v = item.v;
661
+ if (['dp'].includes(v)) ui.fill(path);else if (['dl', 'oc', 'oe', 'or', 'os', 'ol', 'oa', 'ot', 'ote'].includes(v)) ui.stroke(path);
662
+ }, []);
663
+ const drawGrid = React.useCallback(() => {
664
+ const uiCanvas = refs.ui.current;
665
+ const ui = refs.ui.current.getContext('2d');
666
+ const zoom = refs.scale.current;
667
+ const gridSize = 70;
668
+ const offsetX = refs.move.current.x / zoom;
669
+ const offsetY = refs.move.current.y / zoom;
670
+
671
+ // Calculate start positions based on offsets
672
+ const startX = Math.floor(-offsetX / gridSize) * gridSize;
673
+ const startY = Math.floor(-offsetY / gridSize) * gridSize;
674
+ const width = uiCanvas.clientWidth * 1.5 / (zoom < 1 ? zoom : 1);
675
+ const height = uiCanvas.clientHeight * 1.5 / (zoom < 1 ? zoom : 1);
676
+ if (gridSize < 30) return;
677
+
678
+ // Draw main grid lines
679
+ ui.globalAlpha = 1;
680
+ ui.lineWidth = (zoom < 0.5 ? 0.3 : zoom <= 1 ? 0.5 : 0.7) / zoom;
681
+ ui.strokeStyle = '#ccc';
682
+
683
+ // grid
684
+ for (let x = startX; x < width + Math.abs(startX); x += gridSize) {
685
+ ui.beginPath();
686
+ ui.moveTo(x, startY);
687
+ ui.lineTo(x, height + startY);
688
+ ui.stroke();
689
+ }
690
+ for (let y = startY; y < height + Math.abs(startY); y += gridSize) {
691
+ ui.beginPath();
692
+ ui.moveTo(startX, y);
693
+ ui.lineTo(width + startX, y);
694
+ ui.stroke();
695
+ }
696
+
697
+ // subgrid
698
+ if (gridSize * zoom > 100) {
699
+ // Draw subgrid lines if zoomed in
700
+ const subGridSize = gridSize / 5;
701
+ ui.lineWidth = (zoom <= 5 ? 0.6 : zoom <= 10 ? 0.8 : 1) / zoom;
702
+ ui.strokeStyle = '#ddd';
703
+ const dash = zoom < 1 ? 3 * zoom : 3 / zoom;
704
+ ui.setLineDash([dash, dash]);
705
+ for (let x = startX; x < width + Math.abs(startX); x += subGridSize) {
706
+ // without overlap
707
+ if (!(x % gridSize)) continue;
708
+ ui.beginPath();
709
+ ui.moveTo(x, startY);
710
+ ui.lineTo(x, height + startY);
711
+ ui.stroke();
712
+ }
713
+ for (let y = startY; y < height + Math.abs(startY); y += subGridSize) {
714
+ // without overlap
715
+ if (!(y % gridSize)) continue;
716
+ ui.beginPath();
717
+ ui.moveTo(startX, y);
718
+ ui.lineTo(width + startX, y);
719
+ ui.stroke();
720
+ }
721
+ ui.setLineDash([]); // Reset line dash
722
+ }
723
+ }, []);
724
+ const drawImage = React.useCallback(item => {
725
+ const ui = refs.ui.current.getContext('2d');
726
+ ui.globalAlpha = 1;
727
+ ui.drawImage(item.s.image || refs.image.current, ...item.p, ...item.ar);
728
+ }, []);
729
+ const drawCursor = React.useCallback(item => {
730
+ if (!item || !item.s.cursor) return;
731
+ const ui = refs.ui.current.getContext('2d');
732
+ const {
733
+ line,
734
+ char
735
+ } = item.s.cursor;
736
+ const currentLine = item.s.lines[line] || '';
737
+ const textWidth = ui.measureText(currentLine.slice(0, char)).width;
738
+ const {
739
+ padding,
740
+ lineHeight
741
+ } = refs.textSettings.current;
742
+ const cursorX = item.p[0] + padding + textWidth;
743
+ const cursorY = item.p[1] + padding + (line + 1) * lineHeight;
744
+ ui.fillStyle = 'black';
745
+ ui.fillRect(cursorX, cursorY - lineHeight + 3, 2, lineHeight - 5);
746
+ }, []);
747
+ const drawText = React.useCallback(item => {
748
+ const ui = refs.ui.current.getContext('2d');
749
+ const zoom = refs.scale.current;
750
+ const [x, y] = item.p;
751
+ const [width, height] = item.ar;
752
+ const {
753
+ lineHeight,
754
+ padding,
755
+ fillStyle
756
+ } = refs.textSettings.current;
757
+ const selected = refs.tool.current === 'text' && item.se;
758
+
759
+ // Draw the box
760
+ ui.globalAlpha = 1;
761
+ ui.fillStyle = 'transparent';
762
+ ui.fillRect(x, y, width, height);
763
+ ui.lineWidth = 1 / zoom;
764
+ ui.strokeStyle = selected ? colorSelect : 'transparent';
765
+ ui.strokeRect(x, y, width, height);
766
+
767
+ // Draw the text
768
+ ui.fillStyle = fillStyle || 'black';
769
+ ui.font = '16px Arial';
770
+ item.s.lines.forEach((line, index) => {
771
+ ui.fillText(line, x + padding, y + padding + (index + 1) * lineHeight - 5);
772
+ });
773
+ if (selected) drawCursor(item);
774
+ }, []);
775
+ const drawSelect = React.useCallback(item => {
776
+ const ui = refs.ui.current.getContext('2d');
777
+ const [x, y, width, height] = item.c || [];
778
+ const path = new Path2D();
779
+ path.rect(x, y, width, height);
780
+ ui.globalAlpha = 1;
781
+ ui.strokeStyle = colorSelect;
782
+ ui.lineCap = 'square';
783
+ ui.lineJoin = 'bevel';
784
+ ui.lineWidth = 1 / refs.scale.current;
785
+ ui.stroke(path);
786
+ }, []);
787
+ const drawSelection = React.useCallback(() => {
788
+ const ui = refs.ui.current.getContext('2d');
789
+ const zoom = refs.scale.current;
790
+
791
+ // canvas selection
792
+ const select = refs.select.current;
793
+ if (select) {
794
+ ui.globalAlpha = 1;
795
+ ui.globalCompositeOperation = 'source-over';
796
+ ui.lineWidth = 1 / zoom;
797
+ ui.lineCap = 'square';
798
+ ui.lineJoin = 'bevel';
799
+ ui.strokeStyle = colorSelect;
800
+ ui.fillStyle = colorSelectBackground;
801
+ const path = getPath(select);
802
+ ui.fill(path);
803
+ ui.stroke(path);
804
+ }
805
+ }, []);
806
+ const render = React.useCallback(() => {
807
+ const ui = refs.ui.current.getContext('2d');
808
+ const items = refs.items.current.filter(Boolean);
809
+
810
+ // methods
811
+ ui.clearRect(0, 0, refs.ui.current.width, refs.ui.current.height);
812
+ ui.save();
813
+
814
+ // pan
815
+ ui.translate(refs.move.current.x, refs.move.current.y);
816
+ // zoom
817
+ ui.scale(refs.scale.current, refs.scale.current);
818
+
819
+ // grid
820
+ if (refs.grid.current) drawGrid();
821
+
822
+ // draw
823
+ items.forEach(item => {
824
+ // image
825
+ if (item.v === 'i' && item.ar.length === 2) drawImage(item);
826
+ // text
827
+ else if (item.v === 't') drawText(item);
828
+ // other
829
+ else draw(item);
830
+
831
+ // select
832
+ if (item.se) drawSelect(item);
833
+ });
834
+
835
+ // canvas selection
836
+ drawSelection();
837
+ ui.restore();
838
+ }, []);
839
+
840
+ // Snap angle to nearest multiple of 15 degrees
841
+ const snapToAngle = React.useCallback((dx, dy) => {
842
+ // Current angle in radians
843
+ const angle = Math.atan2(dy, dx);
844
+ // Snap to nearest 15 degrees
845
+ const snappedAngle = Math.round(angle / (Math.PI / 12)) * (Math.PI / 12);
846
+ // Length of the vector
847
+ const length = Math.sqrt(dx * dx + dy * dy);
848
+ return {
849
+ x: Math.cos(snappedAngle) * length,
850
+ y: Math.sin(snappedAngle) * length
851
+ };
852
+ }, []);
853
+ const onMoveItems = React.useCallback((x, y) => {
854
+ const itemsSelected = getItems(true);
855
+ itemsSelected.forEach(item => {
856
+ const v = item.v;
857
+
858
+ // draw line
859
+ if (v === 'dl') {
860
+ item.p = item.p.map((value, index) => {
861
+ return index % 2 ? value + y : value + x;
862
+ });
863
+ }
864
+
865
+ // rectangle, draw point, object circle, ellipse, object line, object arrow, object triangle, image, text
866
+ if (['or', 'os', 'dp', 'oc', 'oe', 'ol', 'oa', 'ot', 'ote', 'i', 't'].includes(v)) {
867
+ item.p[0] += x;
868
+ item.p[1] += y;
869
+ }
870
+
871
+ // object line
872
+ if (['ol', 'oa'].includes(v)) {
873
+ item.ar[0] += x;
874
+ item.ar[1] += y;
875
+ }
876
+
877
+ // object triangle
878
+ if (['ot', 'ote'].includes(v)) {
879
+ item.p[2] += x;
880
+ item.p[4] += x;
881
+ item.p[3] += y;
882
+ item.p[5] += y;
883
+ }
884
+ });
885
+ onUpdateCoordinates();
886
+ }, []);
887
+ const onMove = React.useCallback((body, event) => {
888
+ if (!refs.on.current) return;
889
+ const {
890
+ offsetX: x,
891
+ offsetY: y,
892
+ clientX,
893
+ clientY
894
+ } = body;
895
+ const xo = transform(x - refs.move.current.x);
896
+ const yo = transform(y - refs.move.current.y);
897
+ const ui = refs.ui.current.getContext('2d');
898
+ const rect = refs.ui.current.getBoundingClientRect();
899
+ const currentX = clientX - rect.left;
900
+ const currentY = clientY - rect.top;
901
+ const start = refs.start.current;
902
+ const startTransformed = {
903
+ x: transform(start.x - refs.move.current.x),
904
+ y: transform(start.y - refs.move.current.y)
905
+ };
906
+ const item = getItem();
907
+ const items = getItems();
908
+ const t = refs.tool.current;
909
+ const shiftKey = event.shiftKey;
910
+ const zoom = refs.scale.current;
911
+ refs.mouseMove.current.current = {
912
+ x: clientX / zoom,
913
+ y: clientY / zoom
914
+ };
915
+ refs.mouseMove.current.delta = {
916
+ x: refs.mouseMove.current.previous ? refs.mouseMove.current.current.x - refs.mouseMove.current.previous.x : 0,
917
+ y: refs.mouseMove.current.previous ? refs.mouseMove.current.current.y - refs.mouseMove.current.previous.y : 0
918
+ };
919
+ refs.mouseMove.current.previous = _objectSpread({}, refs.mouseMove.current.current);
920
+ const delta = refs.mouseMove.current.delta;
921
+
922
+ // select
923
+ if (t === 'select') {
924
+ if (!refs.moveStarted.current) refs.moveStarted.current = true;
925
+ if (refs.select.current) {
926
+ unselectAll();
927
+ const width = currentX - start.x;
928
+ const height = currentY - start.y;
929
+ const isSquare = shiftKey;
930
+ const radius = 0;
931
+ if (isSquare) {
932
+ const side = Math.min(Math.abs(width), Math.abs(height));
933
+ refs.select.current.v = 'os';
934
+ refs.select.current.p = [startTransformed.x, startTransformed.y];
935
+ refs.select.current.ar = [transform(Math.sign(width) * side), transform(Math.sign(height) * side), radius];
936
+ } else {
937
+ refs.select.current.v = 'or';
938
+ refs.select.current.p = [startTransformed.x, startTransformed.y];
939
+ refs.select.current.ar = [transform(width), transform(height), radius];
940
+ }
941
+ } else onMoveItems(delta.x, delta.y);
942
+ }
943
+ // pen
944
+ else if (t === 'pen' && item) {
945
+ // same path from draw point, to move
946
+ if (!refs.moveStarted.current) {
947
+ item.v = 'dl';
948
+ refs.moveStarted.current = true;
949
+ }
950
+
951
+ // Add the current point to the path
952
+ item.p.push(xo, yo);
953
+ }
954
+ // pan
955
+ else if (t === 'pan') {
956
+ refs.move.current.x = x - refs.previous.current.x + refs.offset.current.x;
957
+ refs.move.current.y = y - refs.previous.current.y + refs.offset.current.y;
958
+ }
959
+ // eraser
960
+ else if (t === 'eraser') {
961
+ // find all items that x, y collides with, with certain radius
962
+ for (const i of items) {
963
+ const isPointInStroke = ui.isPointInStroke(getPath(i), xo, yo);
964
+ if (isPointInStroke) refs.remove.current.push(i);
965
+ }
966
+ }
967
+ // object line, object arrow
968
+ else if (['line', 'line-arrow'].includes(t)) {
969
+ const snapAt15Degrees = shiftKey;
970
+ let endX = currentX;
971
+ let endY = currentY;
972
+ if (snapAt15Degrees) {
973
+ const snapped = snapToAngle(currentX - start.x, currentY - start.y);
974
+ endX = start.x + snapped.x;
975
+ endY = start.y + snapped.y;
976
+ }
977
+ item.v = t === 'line' ? 'ol' : 'oa';
978
+ item.p = [startTransformed.x, startTransformed.y];
979
+ item.ar = [transform(endX - refs.move.current.x), transform(endY - refs.move.current.y)];
980
+ }
981
+ // object circle
982
+ else if (t === 'circle') {
983
+ const width = currentX - start.x;
984
+ const height = currentY - start.y;
985
+ const isCircle = shiftKey;
986
+ if (isCircle) {
987
+ const radius = Math.min(Math.abs(width), Math.abs(height)) / 2;
988
+ const centerX = start.x + Math.sign(width) * radius;
989
+ const centerY = start.y + Math.sign(height) * radius;
990
+ item.v = 'oc';
991
+ item.p = [transform(centerX - refs.move.current.x), transform(centerY - refs.move.current.y)];
992
+ item.ar = [transform(radius), 0, Math.PI * 2];
993
+ } else {
994
+ item.v = 'oe';
995
+ item.p = [transform(start.x + width / 2 - refs.move.current.x), transform(start.y + height / 2 - refs.move.current.y)];
996
+ item.ar = [transform(Math.abs(width) / 2), transform(Math.abs(height) / 2), 0, 0, Math.PI * 2];
997
+ }
998
+ }
999
+ // object rectangle
1000
+ else if (t === 'rectangle') {
1001
+ const width = currentX - start.x;
1002
+ const height = currentY - start.y;
1003
+ const isSquare = shiftKey;
1004
+ const radius = 0;
1005
+ if (isSquare) {
1006
+ const side = Math.min(Math.abs(width), Math.abs(height));
1007
+ item.v = 'os';
1008
+ item.p = [startTransformed.x, startTransformed.y];
1009
+ item.ar = [transform(Math.sign(width) * side), transform(Math.sign(height) * side), radius];
1010
+ } else {
1011
+ item.v = 'or';
1012
+ item.p = [startTransformed.x, startTransformed.y];
1013
+ item.ar = [transform(width), transform(height), radius];
1014
+ }
1015
+ }
1016
+ // object triangle
1017
+ else if (['triangle'].includes(t)) {
1018
+ const endX = xo;
1019
+ const endY = yo;
1020
+ const base = Math.abs(endX - startTransformed.x);
1021
+ const height = shiftKey ? base * Math.sqrt(3) / 2 : Math.abs(endY - startTransformed.y);
1022
+ const points = [startTransformed.x, startTransformed.y, endX, startTransformed.y, startTransformed.x + (endX - startTransformed.x) / 2, startTransformed.y - height];
1023
+ item.v = shiftKey ? 'ote' : 'ot';
1024
+ item.p = points;
1025
+ item.s = _objectSpread(_objectSpread({}, item.s), {}, {
1026
+ height
1027
+ });
1028
+ }
1029
+ // image
1030
+ else if (t === 'image' && refs.image.current.complete && refs.image.current.src) {
1031
+ const width = transform(currentX - start.x);
1032
+ const height = transform(currentY - start.y);
1033
+ const keepAspectRatio = !shiftKey;
1034
+ let currentWidth;
1035
+ let currentHeight;
1036
+ if (keepAspectRatio) {
1037
+ if (Math.abs(width / refs.aspectRatio.current) <= Math.abs(height)) {
1038
+ currentWidth = width;
1039
+ currentHeight = width / refs.aspectRatio.current;
1040
+ } else {
1041
+ currentWidth = height * refs.aspectRatio.current;
1042
+ currentHeight = height;
1043
+ }
1044
+ } else {
1045
+ currentWidth = width;
1046
+ currentHeight = height;
1047
+ }
1048
+ if (keepAspectRatio) {
1049
+ if (width < 0 && currentWidth > 0 || width > 0 && currentWidth < 0) currentWidth *= -1;
1050
+ if (height < 0 && currentHeight > 0 || height > 0 && currentHeight < 0) currentHeight *= -1;
1051
+ }
1052
+ item.p = [startTransformed.x, startTransformed.y];
1053
+ item.ar = [currentWidth, currentHeight];
1054
+ }
1055
+
1056
+ // select box
1057
+ onSelect();
1058
+
1059
+ // render
1060
+ render();
1061
+ }, []);
1062
+ const onMouseMove = React.useCallback(event => {
1063
+ const {
1064
+ offsetX,
1065
+ offsetY,
1066
+ clientX,
1067
+ clientY
1068
+ } = event;
1069
+ onMove({
1070
+ offsetX,
1071
+ offsetY,
1072
+ clientX,
1073
+ clientY
1074
+ }, event);
1075
+ }, [onMove]);
1076
+ const onTouchMove = React.useCallback(event => {
1077
+ // Get the first touch point
1078
+ const touch = event.touches[0];
1079
+ const {
1080
+ clientX,
1081
+ clientY
1082
+ } = touch;
1083
+ let {
1084
+ offsetX,
1085
+ offsetY
1086
+ } = touch;
1087
+ const targetElement = touch.target;
1088
+ if (targetElement instanceof HTMLElement) {
1089
+ // Get the bounding rectangle of the target element
1090
+ const rect = targetElement.getBoundingClientRect();
1091
+
1092
+ // Calculate the offsetX and offsetY
1093
+ offsetX = touch.clientX - rect.left;
1094
+ offsetY = touch.clientY - rect.top;
1095
+ }
1096
+ onMove({
1097
+ offsetX,
1098
+ offsetY,
1099
+ clientX,
1100
+ clientY
1101
+ }, event);
1102
+ }, [onInteractionDown]);
1103
+ const undo = React.useCallback(() => {
1104
+ if (!refs.undo.current.length) return;
1105
+
1106
+ // add current state to redo
1107
+ refs.redo.current.push([...getItems()]);
1108
+
1109
+ // restore the undo state
1110
+ refs.items.current = refs.undo.current.pop();
1111
+
1112
+ // render
1113
+ render();
1114
+ }, []);
1115
+ const redo = React.useCallback(() => {
1116
+ if (!refs.redo.current.length) return;
1117
+
1118
+ // add current state to undo
1119
+ refs.undo.current.push([...getItems()]);
1120
+
1121
+ // restore the redo state
1122
+ refs.items.current = refs.redo.current.pop();
1123
+
1124
+ // render
1125
+ render();
1126
+ }, []);
1127
+ const onWheel = React.useCallback(eventReact => {
1128
+ const event = eventReact.nativeEvent;
1129
+
1130
+ // zoom
1131
+ if (event.metaKey || event.ctrlKey) {
1132
+ setTool('zoom');
1133
+ refs.toolUpdateAuto.current = true;
1134
+ const zoomFactor = 1.054;
1135
+ const mouseX = event.offsetX;
1136
+ const mouseY = event.offsetY;
1137
+ const scale = refs.scale.current;
1138
+
1139
+ // Convert mouse position to canvas coordinates
1140
+ const canvasX = (mouseX - refs.move.current.x) / scale;
1141
+ const canvasY = (mouseY - refs.move.current.y) / scale;
1142
+
1143
+ // Adjust scale
1144
+ const zoomIn = event.deltaY < 0;
1145
+ const newScale = zoomIn ? scale * zoomFactor : scale / zoomFactor;
1146
+ if (newScale <= maxZoom / 100 && newScale >= minZoom / 100) {
1147
+ // Update origin to focus on mouse position
1148
+ refs.move.current.x -= canvasX * (newScale - scale);
1149
+ refs.move.current.y -= canvasY * (newScale - scale);
1150
+ refs.scale.current = newScale;
1151
+ render();
1152
+ }
1153
+ }
1154
+ // pan
1155
+ else if (!refs.mouseDown.current) {
1156
+ refs.move.current.x -= event.deltaX;
1157
+ refs.move.current.y -= event.deltaY;
1158
+ render();
1159
+ }
1160
+ }, [minZoom, maxZoom]);
1161
+ const onPaste = React.useCallback(event => {
1162
+ event.preventDefault();
1163
+
1164
+ // Get clipboard data
1165
+ const items = Array.from(event.clipboardData.items);
1166
+
1167
+ // Loop through clipboard items to find an image
1168
+ for (const item of items) {
1169
+ if (item.type.startsWith('image/')) {
1170
+ // Get the image file
1171
+ const blob = item.getAsFile();
1172
+ refs.image.current = new Image();
1173
+
1174
+ // Load the image and draw it on the canvas
1175
+ refs.image.current.onload = () => {
1176
+ refs.aspectRatio.current = refs.image.current.width / refs.image.current.height;
1177
+
1178
+ // Todo
1179
+ // 1) Upload the image first, than read it in image src
1180
+ // 2) Add url of the image
1181
+ // instead of embeding the image
1182
+ const item_ = {
1183
+ i: getID(),
1184
+ v: 'i',
1185
+ p: [],
1186
+ ar: [],
1187
+ s: {
1188
+ // Todo
1189
+ // remove in the future
1190
+ image: refs.image.current,
1191
+ aspectRatio: refs.aspectRatio.current
1192
+ },
1193
+ a: OnesyDate.milliseconds
1194
+ };
1195
+ add(item_);
1196
+ setTool('image');
1197
+ };
1198
+
1199
+ // Create an object URL for the blob and set it as the image source
1200
+ refs.image.current.src = URL.createObjectURL(blob);
1201
+ break;
1202
+ }
1203
+ }
1204
+ }, []);
1205
+ React.useEffect(() => {
1206
+ const method = () => {
1207
+ const width = refs.root.current.offsetWidth;
1208
+ const height = refs.root.current.offsetHeight;
1209
+ setSize({
1210
+ width,
1211
+ height
1212
+ });
1213
+ };
1214
+ const onKeyUp = event => {
1215
+ if (refs.toolUpdateAuto.current) setTool(refs.previousTool.current || 'pen');
1216
+ };
1217
+ const onKeyDown = async event => {
1218
+ refs.toolUpdateAuto.current = false;
1219
+ const {
1220
+ key
1221
+ } = event;
1222
+ const itemsAll = [...refs.items.current].filter(Boolean);
1223
+ const t = refs.tool.current;
1224
+ const zoom = refs.scale.current;
1225
+ if (['a', 'A'].includes(key) && (event.metaKey || event.ctrlKey)) {
1226
+ event.preventDefault();
1227
+ selectAll();
1228
+ render();
1229
+ } else if (t === 'select' && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace'].includes(key)) {
1230
+ const value = event.shiftKey ? 10 : 1;
1231
+ if (key === 'ArrowUp') onMoveItems(0, -value / zoom);
1232
+ if (key === 'ArrowDown') onMoveItems(0, value / zoom);
1233
+ if (key === 'ArrowLeft') onMoveItems(-value / zoom, 0);
1234
+ if (key === 'ArrowRight') onMoveItems(value / zoom, 0);
1235
+ if (key === 'Backspace') {
1236
+ const toRemove = [];
1237
+ refs.items.current.forEach(item => {
1238
+ if (item.se) toRemove.push(item);
1239
+ });
1240
+ if (toRemove.length) remove(toRemove);
1241
+ }
1242
+ render();
1243
+ } else if (key === 'Escape') {
1244
+ setTool('select');
1245
+ refs.textActive.current = null;
1246
+ unselectAll();
1247
+ filterItems();
1248
+ render();
1249
+ } else if (tool === 'text') {
1250
+ const selectedTextBox = itemsAll.find(item => item.v === 't' && item.se);
1251
+ if (!selectedTextBox) return;
1252
+ const {
1253
+ line,
1254
+ char
1255
+ } = selectedTextBox.s.cursor;
1256
+ const lines = selectedTextBox.s.lines;
1257
+ const currentLine = lines[line];
1258
+ if (['ArrowLeft', 'ArrowRight'].includes(key)) {
1259
+ event.preventDefault();
1260
+ selectedTextBox.s.cursor.char += key === 'ArrowLeft' ? -1 : 1;
1261
+ if (selectedTextBox.s.cursor.char < 0) {
1262
+ selectedTextBox.s.cursor.line--;
1263
+ selectedTextBox.s.cursor.char = lines[selectedTextBox.s.cursor.line]?.length;
1264
+ } else if (selectedTextBox.s.cursor.char > lines[line]?.length && line !== lines.length - 1) {
1265
+ selectedTextBox.s.cursor.line++;
1266
+ selectedTextBox.s.cursor.char = 0;
1267
+ }
1268
+ selectedTextBox.s.cursor.line = clamp(selectedTextBox.s.cursor.line, 0, lines.length - 1);
1269
+ selectedTextBox.s.cursor.char = clamp(selectedTextBox.s.cursor.char, 0, lines[selectedTextBox.s.cursor.line]?.length);
1270
+ }
1271
+ if (['ArrowUp', 'ArrowDown'].includes(key)) {
1272
+ event.preventDefault();
1273
+ selectedTextBox.s.cursor.line += key === 'ArrowUp' ? -1 : 1;
1274
+ selectedTextBox.s.cursor.line = clamp(selectedTextBox.s.cursor.line, 0, lines.length - 1);
1275
+ selectedTextBox.s.cursor.char = clamp(selectedTextBox.s.cursor.char, 0, lines[selectedTextBox.s.cursor.line]?.length);
1276
+ } else if (key === 'Enter') {
1277
+ event.preventDefault();
1278
+ const newLine = currentLine.slice(char);
1279
+ lines[line] = currentLine.slice(0, char);
1280
+ lines.splice(line + 1, 0, newLine);
1281
+ selectedTextBox.s.cursor.line++;
1282
+ selectedTextBox.s.cursor.char = 0;
1283
+ } else if (key === 'Backspace') {
1284
+ event.preventDefault();
1285
+ if (char > 0) {
1286
+ lines[line] = currentLine.slice(0, char - 1) + currentLine.slice(char);
1287
+ selectedTextBox.s.cursor.char--;
1288
+ } else if (line > 0) {
1289
+ const prevLine = lines[line - 1];
1290
+ selectedTextBox.s.cursor.char = prevLine.length;
1291
+ lines[line - 1] += lines[line];
1292
+ lines.splice(line, 1);
1293
+ selectedTextBox.s.cursor.line--;
1294
+ }
1295
+ } else if (key.length === 1) {
1296
+ if ([' '].includes(key)) event.preventDefault();
1297
+ let textClipboard = '';
1298
+ const isPaste = (event.ctrlKey || event.metaKey) && ['v', 'V'].includes(key);
1299
+ if (isPaste) {
1300
+ try {
1301
+ textClipboard = await window.navigator.clipboard.readText();
1302
+ } catch (error) {}
1303
+ }
1304
+ const text = isPaste ? textClipboard : key;
1305
+ selectedTextBox.s.lines[line] = currentLine.slice(0, char) + text + currentLine.slice(char);
1306
+ selectedTextBox.s.cursor.char += text.length;
1307
+ }
1308
+ updateTextBoxDimensions(selectedTextBox);
1309
+ selectedTextBox.c = [...selectedTextBox.p, ...selectedTextBox.ar];
1310
+ render();
1311
+ } else {
1312
+ if (event.metaKey && key === 'z') {
1313
+ if (event.shiftKey) redo();else undo();
1314
+ }
1315
+ if (key === ' ') {
1316
+ refs.toolUpdateAuto.current = true;
1317
+ setTool('pan');
1318
+ }
1319
+
1320
+ // tools
1321
+ if (event.shiftKey) {
1322
+ if (['E', 'D', 'P', 'S', 'C', 'R', 'I', 'L', 'A', 'T', 'G'].includes(key)) refs.toolUpdateAuto.current = false;
1323
+ if (key === 'E') setTool('eraser');
1324
+ if (key === 'D') setTool('pen');
1325
+ if (key === 'P') setTool('pan');
1326
+ if (key === 'S') setTool('select');
1327
+ if (key === 'C') setTool('circle');
1328
+ if (key === 'R') setTool('rectangle');
1329
+ if (key === 'I') setTool('triangle');
1330
+ if (key === 'L') setTool('line');
1331
+ if (key === 'A') setTool('line-arrow');
1332
+ if (key === 'T') setTool('text');
1333
+ if (key === 'G') {
1334
+ setGrid(previous => !previous);
1335
+ render();
1336
+ }
1337
+ }
1338
+ }
1339
+ };
1340
+ window.addEventListener('resize', method);
1341
+ window.document.addEventListener('mouseup', onMouseUp);
1342
+ window.document.addEventListener('touchend', onMouseUp);
1343
+ window.document.addEventListener('mousemove', onMouseMove);
1344
+ window.document.addEventListener('touchmove', onTouchMove);
1345
+ window.document.addEventListener('keyup', onKeyUp);
1346
+ window.document.addEventListener('keydown', onKeyDown);
1347
+ window.document.addEventListener('paste', onPaste);
1348
+ method();
1349
+ init();
1350
+ return () => {
1351
+ window.removeEventListener('resize', method);
1352
+ window.document.removeEventListener('mouseup', onMouseUp);
1353
+ window.document.removeEventListener('touchend', onMouseUp);
1354
+ window.document.removeEventListener('mousemove', onMouseMove);
1355
+ window.document.removeEventListener('touchmove', onTouchMove);
1356
+ window.document.removeEventListener('keyup', onKeyUp);
1357
+ window.document.removeEventListener('keydown', onKeyDown);
1358
+ window.document.removeEventListener('paste', onPaste);
1359
+ };
1360
+ }, []);
1361
+ const onChangeInputFile = React.useCallback(event => {
1362
+ const file = event.target.files[0];
1363
+ if (file) {
1364
+ const reader = new FileReader();
1365
+ reader.onload = eventReader => {
1366
+ refs.image.current = new Image();
1367
+ refs.image.current.src = eventReader.target.result;
1368
+ refs.toolUpdateAuto.current = true;
1369
+ event.target.value = '';
1370
+ setTool('image');
1371
+ };
1372
+ reader.readAsDataURL(file);
1373
+ }
1374
+ }, []);
1375
+ const propsCanvas = {
1376
+ width: size.width,
1377
+ height: size.height,
1378
+ disabled: !loaded,
1379
+ style: {
1380
+ width: size.width,
1381
+ height: size.height
1382
+ }
1383
+ };
1384
+ return /*#__PURE__*/React.createElement(Line, _extends({
1385
+ ref: item => {
1386
+ if (ref) {
1387
+ if (is('function', ref)) ref(item);else ref.current = item;
1388
+ }
1389
+ refs.root.current = item;
1390
+ },
1391
+ flex: true,
1392
+ fullWidth: true,
1393
+ className: classNames([staticClassName('Whiteboard', theme) && ['onesy-Whiteboard-root'], className, classes.root])
1394
+ }, other), /*#__PURE__*/React.createElement("div", {
1395
+ id: "controls",
1396
+ style: {
1397
+ position: 'absolute',
1398
+ zIndex: 14,
1399
+ top: 12,
1400
+ left: '50%'
1401
+ }
1402
+ }, /*#__PURE__*/React.createElement("input", {
1403
+ type: "file",
1404
+ accept: "image/*",
1405
+ onChange: onChangeInputFile
1406
+ })), /*#__PURE__*/React.createElement("canvas", _extends({
1407
+ ref: refs.ui,
1408
+ onMouseDown: onMouseDown,
1409
+ onTouchStart: onTouchStart,
1410
+ onWheel: onWheel,
1411
+ className: classNames([classes.canvas, classes.interactive, tool === 'pan' && classes[!mouseDown ? 'pan' : 'panning'], tool === 'image' && classes.image, tool === 'text' && classes.text, ['pen', 'eraser', 'zoom'].includes(tool) && classes[tool], ['circle', 'rectangle', 'triangle', 'line', 'arrow'].includes(tool) && classes.object])
1412
+ }, propsCanvas)));
1413
+ });
1414
+ Whiteboard.displayName = 'onesy-Whiteboard';
1415
+ export default Whiteboard;