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