UNPKG

@onesy/ui-react

Version:
1,406 lines (1,349 loc) 51.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _utils = require("@onesy/utils"); var _styleReact = require("@onesy/style-react"); var _date = require("@onesy/date"); var _Line = _interopRequireDefault(require("../Line")); var _utils2 = require("../utils"); var _jsxRuntime = require("react/jsx-runtime"); const _excluded = ["ref", "valueDefault", "onChange", "minZoom", "maxZoom", "grid", "settings", "className"]; 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; } 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) { (0, _defineProperty2.default)(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; } const useStyle = (0, _styleReact.style)(theme => ({ root: { position: 'relative' }, canvas: { position: 'absolute', inset: 0, width: '100%', height: '100%', imageRendering: 'pixelated', background: '#fff', appearance: 'none', border: 'none', userSelect: 'none', transition: theme.methods.transitions.make('opacity'), '&[disabled]': { opacity: 0.7, pointerEvents: 'none' } }, ui: { zIndex: 0 }, object: { cursor: 'crosshair' }, pen: { cursor: 'crosshair' }, pan: { cursor: 'grab' }, panning: { cursor: 'grabbing' }, zoom: { cursor: 'zoom-in' }, eraser: { cursor: 'not-allowed' }, image: { cursor: 'copy' }, text: { cursor: 'text' } }), { name: 'onesy-Whiteboard' }); const colorSelect = 'hsl(244deg 64% 64%)'; const colorSelectBackground = 'hsla(244deg 64% 64% / 4%)'; const Whiteboard = props_ => { var _theme$ui, _theme$ui2, _theme$elements; const theme = (0, _styleReact.useOnesyTheme)(); const props = _objectSpread(_objectSpread(_objectSpread({}, theme === null || theme === void 0 || (_theme$ui = theme.ui) === null || _theme$ui === void 0 || (_theme$ui = _theme$ui.elements) === null || _theme$ui === void 0 || (_theme$ui = _theme$ui.all) === null || _theme$ui === void 0 || (_theme$ui = _theme$ui.props) === null || _theme$ui === void 0 ? void 0 : _theme$ui.default), theme === null || theme === void 0 || (_theme$ui2 = theme.ui) === null || _theme$ui2 === void 0 || (_theme$ui2 = _theme$ui2.elements) === null || _theme$ui2 === void 0 || (_theme$ui2 = _theme$ui2.onesyWhiteboard) === null || _theme$ui2 === void 0 || (_theme$ui2 = _theme$ui2.props) === null || _theme$ui2 === void 0 ? void 0 : _theme$ui2.default), props_); const Line = (theme === null || theme === void 0 || (_theme$elements = theme.elements) === null || _theme$elements === void 0 ? void 0 : _theme$elements.Line) || _Line.default; const { ref, valueDefault, onChange: onChangeProps, // 10% minZoom = 10, // 400% maxZoom = 4000, grid: gridProps = true, settings = { lineCap: 'round', lineJoin: 'round', lineWidth: 10, fillStyle: 'lightgreen', strokeStyle: 'lightgreen', globalAlpha: 0.44 }, className } = props, other = (0, _objectWithoutProperties2.default)(props, _excluded); const { classes } = useStyle(); const [size, setSize] = _react.default.useState({}); const [tool, setTool] = _react.default.useState('select'); const [mouseDown, setMouseDown] = _react.default.useState(false); const [grid, setGrid] = _react.default.useState(gridProps); const [loaded, setLoaded] = _react.default.useState(false); const refs = { root: _react.default.useRef(null), ui: _react.default.useRef(null), interactive: _react.default.useRef(null), on: _react.default.useRef(false), items: _react.default.useRef(valueDefault || []), previous: _react.default.useRef({ x: 0, y: 0 }), previousMouse: _react.default.useRef({ x: 0, y: 0 }), moveStarted: _react.default.useRef(false), undo: _react.default.useRef([]), redo: _react.default.useRef([]), move: _react.default.useRef({ x: 0, y: 0 }), offset: _react.default.useRef({ x: 0, y: 0 }), start: _react.default.useRef({ x: 0, y: 0 }), end: _react.default.useRef({ x: 0, y: 0 }), scale: _react.default.useRef(1), mouseDown: _react.default.useRef(mouseDown), mouseMove: _react.default.useRef({ current: { x: 0, y: 0 }, previous: undefined, delta: { x: 0, y: 0 } }), tool: _react.default.useRef(tool), previousTool: _react.default.useRef(tool), toolUpdateAuto: _react.default.useRef(false), remove: _react.default.useRef([]), grid: _react.default.useRef(grid), typing: _react.default.useRef(false), image: _react.default.useRef((0, _utils.isEnvironment)('browser') && new Image()), aspectRatio: _react.default.useRef(1), select: _react.default.useRef(null), textActive: _react.default.useRef(null), textSettings: _react.default.useRef({ lineHeight: 20, padding: 5, fillStyle: 'black' }) }; refs.mouseDown.current = mouseDown; refs.tool.current = tool; refs.grid.current = grid; const init = () => { // Todo // items // load all of the images in memory and attach theme to items as image elements // once it's all done, setLoaded(true), render setTimeout(() => { render(); setLoaded(true); }, 40); }; _react.default.useEffect(() => { if (!['zoom'].includes(tool)) refs.previousTool.current = tool; }, [tool]); const onChange = () => { if ((0, _utils.is)('function', onChangeProps)) onChangeProps(refs.items.current); }; const getItems = (selected = undefined) => refs.items.current.filter(item => selected === undefined || item.se === selected); const getItem = () => refs.items.current[refs.items.current.length - 1]; const filterItems = () => { const toRemove = refs.items.current.filter(item_0 => { var _item_0$s; if (refs.tool.current === 'text' && item_0 !== refs.textActive.current) item_0.se = false; const lines = ((_item_0$s = item_0.s) === null || _item_0$s === void 0 ? void 0 : _item_0$s.lines) || []; return !(item_0.v !== 't' || item_0 === refs.textActive.current || lines.length > 1 || lines[0].length); }); if (toRemove.length) remove(toRemove); }; const add = toAdd => { const itemsAdd = (Array.isArray(toAdd) ? toAdd : [toAdd]).filter(Boolean); const items = getItems(); // add to undo stack snapshot of current state refs.undo.current.push([...items]); // clear redo stack refs.redo.current = []; refs.items.current.push(...itemsAdd); }; const remove = toRemove_0 => { const itemsRemove = (Array.isArray(toRemove_0) ? toRemove_0 : [toRemove_0]).filter(Boolean); const items_0 = getItems(); const IDs = itemsRemove === null || itemsRemove === void 0 ? void 0 : itemsRemove.map(item_1 => item_1.i); const toRemoveIDs = items_0.filter(item_2 => IDs.includes(item_2.i)).map(item_3 => item_3.i); if (!toRemoveIDs) return; // add to undo stack snapshot of current state refs.undo.current.push([...items_0]); // clear redo stack refs.redo.current = []; refs.items.current = refs.items.current.filter(item_4 => !toRemoveIDs.includes(item_4.i)); }; const reset = () => { refs.on.current = false; refs.moveStarted.current = false; refs.image.current = null; // new move start refs.offset.current.x = refs.move.current.x; refs.offset.current.y = refs.move.current.y; }; const transform = coordinate => coordinate / refs.scale.current; const selectAll = () => { return [...refs.items.current].filter(Boolean).map(item_5 => { item_5.se = true; return item_5; }); }; const unselectAll = eventReact => { const event = (eventReact === null || eventReact === void 0 ? void 0 : eventReact.nativeEvent) || eventReact; const shift = event === null || event === void 0 ? void 0 : event.shiftKey; return [...refs.items.current].filter(Boolean).map(item_6 => { if (shift) return item_6; item_6.se = false; return item_6; }); }; const onInteractionDown = (body, eventReact_0) => { const event_0 = (eventReact_0 === null || eventReact_0 === void 0 ? void 0 : eventReact_0.nativeEvent) || eventReact_0; const { offsetX: x, offsetY: y, clientX, clientY } = body; refs.on.current = true; refs.previous.current = { x, y }; refs.mouseMove.current = { current: { x: 0, y: 0 }, previous: undefined, delta: { x: 0, y: 0 } }; const shift_0 = event_0 === null || event_0 === void 0 ? void 0 : event_0.shiftKey; const ui = refs.ui.current.getContext('2d'); const rect = refs.ui.current.getBoundingClientRect(); refs.start.current.x = clientX - rect.left; refs.start.current.y = clientY - rect.top; refs.textActive.current = null; const start = refs.start.current; const startTransformed = { x: transform(start.x - refs.move.current.x), y: transform(start.y - refs.move.current.y) }; Object.keys(settings).forEach(item_ => ui[item_] = settings[item_]); let item_7; const items_1 = getItems(); const t = refs.tool.current; refs.select.current = null; if (t === 'select') { // z-index top to bottom order const itemsReversed = [...items_1].reverse(); const itemsClicked = itemsReversed.filter(item__0 => { return item__0.c && startTransformed.x >= item__0.c[0] && startTransformed.x <= item__0.c[0] + item__0.c[2] && startTransformed.y >= item__0.c[1] && startTransformed.y <= item__0.c[1] + item__0.c[3]; }); const itemsSelected = getItems(true); const clicked = itemsClicked[0]; if (!clicked) { unselectAll(); refs.select.current = { p: [], ar: [] }; } if (!shift_0 && !(clicked !== null && clicked !== void 0 && clicked.se && itemsSelected.length > 1)) unselectAll(); if (clicked) { clicked.se = shift_0 ? !clicked.se : true; } } else if (t === 'text') { const { lineHeight, padding } = refs.textSettings.current; const selectedText = items_1.find(item__1 => item__1.c && startTransformed.x >= item__1.c[0] && startTransformed.x <= item__1.c[0] + item__1.c[2] && startTransformed.y >= item__1.c[1] && startTransformed.y <= item__1.c[1] + item__1.c[3]); if (selectedText) { var _selectedText$s; refs.textActive.current = selectedText; selectedText.se = true; const relativeY = startTransformed.y - selectedText.c[1] - padding; const lineIndex = Math.floor(relativeY / lineHeight); const clickedLine = ((_selectedText$s = selectedText.s) === null || _selectedText$s === void 0 || (_selectedText$s = _selectedText$s.lines) === null || _selectedText$s === void 0 ? void 0 : _selectedText$s[lineIndex]) || ''; const relativeX = startTransformed.x - selectedText.c[0] - padding; let charIndex = clickedLine.length; for (let i = 0; i < clickedLine.length; i++) { if (relativeX < ui.measureText(clickedLine.slice(0, i + 1)).width) { charIndex = i; break; } } selectedText.s.cursor = { line: lineIndex, char: charIndex }; } else { item_7 = { i: (0, _utils.getID)(), v: 't', p: [startTransformed.x, startTransformed.y], ar: [15, lineHeight + padding * 2], s: _objectSpread(_objectSpread({}, refs.textSettings.current), {}, { lines: [''], cursor: { line: 0, char: 0 } }), se: true, a: _date.OnesyDate.milliseconds }; refs.textActive.current = item_7; } } else { // pen if (t === 'pen') { // point item_7 = { i: (0, _utils.getID)(), v: 'dp', p: [transform(x - refs.move.current.x), transform(y - refs.move.current.y)], ar: [ui.lineWidth / 2, 0, Math.PI * 2], s: (0, _utils.copy)(settings), a: _date.OnesyDate.milliseconds }; } // circle, rectangle, line, line-arrow if (['circle', 'rectangle', 'triangle', 'line', 'line-arrow'].includes(t)) { item_7 = { i: (0, _utils.getID)(), p: [], ar: [], s: (0, _utils.copy)(settings), a: _date.OnesyDate.milliseconds }; } // pan if (t === 'pan') { // new move start refs.offset.current.x = refs.move.current.x; refs.offset.current.y = refs.move.current.y; } // image else if (t === 'image' && refs.image.current.complete && refs.image.current.src) { refs.aspectRatio.current = refs.image.current.width / refs.image.current.height; // Todo // add url of the image // instead of embeding the image item_7 = { i: (0, _utils.getID)(), v: 'i', p: [], ar: [], s: { // Todo // remove in the future image: refs.image.current, aspectRatio: refs.aspectRatio.current }, a: _date.OnesyDate.milliseconds }; } } if (item_7) add(item_7); filterItems(); // render render(); setMouseDown(true); }; const onMouseDown = event_1 => { const { offsetX, offsetY, clientX: clientX_0, clientY: clientY_0 } = event_1.nativeEvent; onInteractionDown({ offsetX, offsetY, clientX: clientX_0, clientY: clientY_0 }, event_1); }; const onTouchStart = event_2 => { // Get the first touch point const touch = event_2.touches[0]; const { clientX: clientX_1, clientY: clientY_1 } = touch; let { offsetX: offsetX_0, offsetY: offsetY_0 } = touch; const targetElement = touch.target; if (targetElement instanceof HTMLElement) { // Get the bounding rectangle of the target element const rect_0 = targetElement.getBoundingClientRect(); // Calculate the offsetX and offsetY offsetX_0 = touch.clientX - rect_0.left; offsetY_0 = touch.clientY - rect_0.top; } onInteractionDown({ offsetX: offsetX_0, offsetY: offsetY_0, clientX: clientX_1, clientY: clientY_1 }, event_2); }; const removeItems = () => { // remove if (refs.remove.current.length) { const toRemove_1 = []; for (const item_8 of refs.remove.current) { const index = refs.items.current.findIndex(itemItems => itemItems === item_8); if (index > -1) toRemove_1.push(item_8); } if (toRemove_1.length) remove(toRemove_1); refs.remove.current = []; } }; const onUpdateCoordinates = () => { const items_2 = getItems(); items_2.forEach(item_9 => { const p = (item_9 === null || item_9 === void 0 ? void 0 : item_9.p) || []; const ar = (item_9 === null || item_9 === void 0 ? void 0 : item_9.ar) || []; const s = (item_9 === null || item_9 === void 0 ? void 0 : item_9.s) || {}; if (p.length) { // cache // x1, y1, width. height for position on the screen const v = item_9.v; // draw point if (v === 'dp') { const lineWidth = s.lineWidth || 10; item_9.c = [p[0] - lineWidth / 2, p[1] - lineWidth / 2, lineWidth, lineWidth]; } // draw line else if (v === 'dl') { const x_0 = []; const y_0 = []; for (let i_0 = 0; i_0 < p.length; i_0 += 2) { x_0.push(p[i_0]); y_0.push(p[i_0 + 1]); } const xMin = Math.min(...x_0); const yMin = Math.min(...y_0); const xMax = Math.max(...x_0); const yMax = Math.max(...y_0); item_9.c = [xMin, yMin, xMax - xMin, yMax - yMin]; } // object line, object arrow else if (['ol', 'oa'].includes(v)) { const x_1 = [p[0], ar[0]]; const y_1 = [p[1], ar[1]]; const xMin_0 = Math.min(...x_1); const yMin_0 = Math.min(...y_1); const xMax_0 = Math.max(...x_1); const yMax_0 = Math.max(...y_1); item_9.c = [xMin_0, yMin_0, xMax_0 - xMin_0, yMax_0 - yMin_0]; } // object rectangle, object square else if (['or', 'os'].includes(v)) { item_9.c = [...p, ...ar]; } // object circle, object ellipse else if (['oc', 'oe'].includes(v)) { if (v === 'oc') { item_9.c = [p[0] - ar[0], p[1] - ar[0], ar[0] * 2, ar[0] * 2]; } else { item_9.c = [p[0] - ar[0], p[1] - ar[1], ar[0] * 2, ar[1] * 2]; } } // object triangle, object triangle equilateral else if (['ot', 'ote'].includes(v)) { const [x1, y1, x2] = p; const { height } = s; item_9.c = [x1, y1 - height, x2 - x1, height]; } // image else if (['i', 't'].includes(v)) { item_9.c = [...p, ...ar]; } } }); }; const onSelect = () => { const select = refs.select.current; if (!select) return; const px1 = select.p[0]; const px2 = px1 + select.ar[0]; const py1 = select.p[1]; const py2 = py1 + select.ar[1]; const min = { x: Math.min(px1, px2), y: Math.min(py1, py2) }; const max = { x: Math.max(px1, px2), y: Math.max(py1, py2) }; const items_3 = getItems(); items_3.forEach(item_10 => { const { c } = item_10; const [x1_0, y1_0] = c; let [x2_0, y2] = c; x2_0 = x1_0 + x2_0; y2 = y1_0 + y2; const minItem = { x: Math.min(x1_0, x2_0), y: Math.min(y1_0, y2) }; const maxItem = { x: Math.max(x1_0, x2_0), y: Math.max(y1_0, y2) }; const selected_0 = minItem.x >= min.x && maxItem.x <= max.x && minItem.y >= min.y && maxItem.y <= max.y; if (selected_0) item_10.se = true; }); }; const onMouseUp = event_3 => { if (refs.mouseDown.current) { refs.select.current = null; // update coordinates onUpdateCoordinates(); // reset reset(); // remove removeItems(); console.log('items', refs.items.current); // onChange onChange(); setMouseDown(false); if (['circle', 'rectangle', 'triangle', 'line', 'line-arrow', 'image'].includes(refs.previousTool.current)) { setTool('select'); } render(); } }; const updateTextBoxDimensions = item_11 => { const ui_0 = refs.ui.current.getContext('2d'); const { lineHeight: lineHeight_0, padding: padding_0 } = refs.textSettings.current; ui_0.font = '16px Arial'; const maxWidth = Math.max(...item_11.s.lines.map(line => ui_0.measureText(line).width)); item_11.ar[0] = maxWidth + padding_0 * 2.5; item_11.ar[1] = item_11.s.lines.length * lineHeight_0 + padding_0 * 2; }; const getPath = item_12 => { const path = new Path2D(); const { v: v_0, p: p_0, ar: ar_0 } = item_12; // draw line if (v_0 === 'dl') { const points = (0, _utils.arrayToParts)(p_0, 2); for (let i_1 = 0; i_1 < points.length - 1; i_1++) { const current = points[i_1]; const next = points[i_1 + 1]; // calculate the control point for the curve const midX = (current[0] + next[0]) / 2; const midY = (current[1] + next[1]) / 2; if (i_1 === 0) { // start from the first point path.moveTo(current[0], current[1]); } path.quadraticCurveTo(current[0], current[1], midX, midY); } } // draw point, object circle else if (['dp', 'oc'].includes(v_0) && ar_0.length === 3) { path.arc(p_0[0], p_0[1], ...ar_0); } // object ellipse else if (v_0 === 'oe' && ar_0.length === 5) { path.ellipse(p_0[0], p_0[1], ...ar_0); } // object rectangle else if (['or', 'os'].includes(v_0)) { path.roundRect(p_0[0], p_0[1], ...ar_0); } // object line else if (['ol', 'oa'].includes(v_0) && ar_0.length === 2) { path.moveTo(p_0[0], p_0[1]); path.lineTo(...ar_0); // draw an arrow if (v_0 === 'oa') { // Length of the arrowhead const headLength = 40; const angle = Math.atan2(ar_0[1] - p_0[1], ar_0[0] - p_0[0]); path.moveTo(ar_0[0], ar_0[1]); path.lineTo(ar_0[0] - headLength * Math.cos(angle - Math.PI / 6), ar_0[1] - headLength * Math.sin(angle - Math.PI / 6)); path.moveTo(ar_0[0], ar_0[1]); path.lineTo(ar_0[0] - headLength * Math.cos(angle + Math.PI / 6), ar_0[1] - headLength * Math.sin(angle + Math.PI / 6)); } } // object triangle else if (['ot', 'ote'].includes(v_0)) { path.moveTo(p_0[0], p_0[1]); path.lineTo(p_0[2], p_0[3]); path.lineTo(p_0[4], p_0[5]); path.closePath(); } return path; }; const draw = item_13 => { var _item_13$s2, _item_13$s3; const ui_1 = refs.ui.current.getContext('2d'); // settings Object.keys(item_13.s || {}).forEach(key => { var _item_13$s; return ui_1[key] = (_item_13$s = item_13.s) === null || _item_13$s === void 0 ? void 0 : _item_13$s[key]; }); ui_1.globalAlpha = refs.remove.current.includes(item_13) ? 0.25 : ((_item_13$s2 = item_13.s) === null || _item_13$s2 === void 0 ? void 0 : _item_13$s2.globalAlpha) !== undefined ? (_item_13$s3 = item_13.s) === null || _item_13$s3 === void 0 ? void 0 : _item_13$s3.globalAlpha : 1; ui_1.beginPath(); const path_0 = getPath(item_13); const v_1 = item_13.v; if (['dp'].includes(v_1)) ui_1.fill(path_0);else if (['dl', 'oc', 'oe', 'or', 'os', 'ol', 'oa', 'ot', 'ote'].includes(v_1)) ui_1.stroke(path_0); }; const drawGrid = () => { const uiCanvas = refs.ui.current; const ui_2 = refs.ui.current.getContext('2d'); const zoom = refs.scale.current; const gridSize = 70; const offsetX_1 = refs.move.current.x / zoom; const offsetY_1 = refs.move.current.y / zoom; // Calculate start positions based on offsets const startX = Math.floor(-offsetX_1 / gridSize) * gridSize; const startY = Math.floor(-offsetY_1 / gridSize) * gridSize; const width = uiCanvas.clientWidth * 1.5 / (zoom < 1 ? zoom : 1); const height_0 = uiCanvas.clientHeight * 1.5 / (zoom < 1 ? zoom : 1); if (gridSize < 30) return; // Draw main grid lines ui_2.globalAlpha = 1; ui_2.lineWidth = (zoom < 0.5 ? 0.3 : zoom <= 1 ? 0.5 : 0.7) / zoom; ui_2.strokeStyle = '#ccc'; // grid for (let x_2 = startX; x_2 < width + Math.abs(startX); x_2 += gridSize) { ui_2.beginPath(); ui_2.moveTo(x_2, startY); ui_2.lineTo(x_2, height_0 + startY); ui_2.stroke(); } for (let y_2 = startY; y_2 < height_0 + Math.abs(startY); y_2 += gridSize) { ui_2.beginPath(); ui_2.moveTo(startX, y_2); ui_2.lineTo(width + startX, y_2); ui_2.stroke(); } // subgrid if (gridSize * zoom > 100) { // Draw subgrid lines if zoomed in const subGridSize = gridSize / 5; ui_2.lineWidth = (zoom <= 5 ? 0.6 : zoom <= 10 ? 0.8 : 1) / zoom; ui_2.strokeStyle = '#ddd'; const dash = zoom < 1 ? 3 * zoom : 3 / zoom; ui_2.setLineDash([dash, dash]); for (let x_3 = startX; x_3 < width + Math.abs(startX); x_3 += subGridSize) { // without overlap if (!(x_3 % gridSize)) continue; ui_2.beginPath(); ui_2.moveTo(x_3, startY); ui_2.lineTo(x_3, height_0 + startY); ui_2.stroke(); } for (let y_3 = startY; y_3 < height_0 + Math.abs(startY); y_3 += subGridSize) { // without overlap if (!(y_3 % gridSize)) continue; ui_2.beginPath(); ui_2.moveTo(startX, y_3); ui_2.lineTo(width + startX, y_3); ui_2.stroke(); } ui_2.setLineDash([]); // Reset line dash } }; const drawImage = item_14 => { const ui_3 = refs.ui.current.getContext('2d'); ui_3.globalAlpha = 1; ui_3.drawImage(item_14.s.image || refs.image.current, ...item_14.p, ...item_14.ar); }; const drawCursor = item_15 => { if (!item_15 || !item_15.s.cursor) return; const ui_4 = refs.ui.current.getContext('2d'); const { line: line_0, char } = item_15.s.cursor; const currentLine = item_15.s.lines[line_0] || ''; const textWidth = ui_4.measureText(currentLine.slice(0, char)).width; const { padding: padding_1, lineHeight: lineHeight_1 } = refs.textSettings.current; const cursorX = item_15.p[0] + padding_1 + textWidth; const cursorY = item_15.p[1] + padding_1 + (line_0 + 1) * lineHeight_1; ui_4.fillStyle = 'black'; ui_4.fillRect(cursorX, cursorY - lineHeight_1 + 3, 2, lineHeight_1 - 5); }; const drawText = item_16 => { const ui_5 = refs.ui.current.getContext('2d'); const zoom_0 = refs.scale.current; const [x_4, y_4] = item_16.p; const [width_0, height_1] = item_16.ar; const { lineHeight: lineHeight_2, padding: padding_2, fillStyle } = refs.textSettings.current; const selected_1 = refs.tool.current === 'text' && item_16.se; // Draw the box ui_5.globalAlpha = 1; ui_5.fillStyle = 'transparent'; ui_5.fillRect(x_4, y_4, width_0, height_1); ui_5.lineWidth = 1 / zoom_0; ui_5.strokeStyle = selected_1 ? colorSelect : 'transparent'; ui_5.strokeRect(x_4, y_4, width_0, height_1); // Draw the text ui_5.fillStyle = fillStyle || 'black'; ui_5.font = '16px Arial'; item_16.s.lines.forEach((line_1, index_0) => { ui_5.fillText(line_1, x_4 + padding_2, y_4 + padding_2 + (index_0 + 1) * lineHeight_2 - 5); }); if (selected_1) drawCursor(item_16); }; const drawSelect = item_17 => { const ui_6 = refs.ui.current.getContext('2d'); const [x_5, y_5, width_1, height_2] = item_17.c || []; const path_1 = new Path2D(); path_1.rect(x_5, y_5, width_1, height_2); ui_6.globalAlpha = 1; ui_6.strokeStyle = colorSelect; ui_6.lineCap = 'square'; ui_6.lineJoin = 'bevel'; ui_6.lineWidth = 1 / refs.scale.current; ui_6.stroke(path_1); }; const drawSelection = () => { const ui_7 = refs.ui.current.getContext('2d'); const zoom_1 = refs.scale.current; // canvas selection const select_0 = refs.select.current; if (select_0) { ui_7.globalAlpha = 1; ui_7.globalCompositeOperation = 'source-over'; ui_7.lineWidth = 1 / zoom_1; ui_7.lineCap = 'square'; ui_7.lineJoin = 'bevel'; ui_7.strokeStyle = colorSelect; ui_7.fillStyle = colorSelectBackground; const path_2 = getPath(select_0); ui_7.fill(path_2); ui_7.stroke(path_2); } }; const render = () => { const ui_8 = refs.ui.current.getContext('2d'); const items_4 = refs.items.current.filter(Boolean); // methods ui_8.clearRect(0, 0, refs.ui.current.width, refs.ui.current.height); ui_8.save(); // pan ui_8.translate(refs.move.current.x, refs.move.current.y); // zoom ui_8.scale(refs.scale.current, refs.scale.current); // grid if (refs.grid.current) drawGrid(); // draw items_4.forEach(item_18 => { // image if (item_18.v === 'i' && item_18.ar.length === 2) drawImage(item_18); // text else if (item_18.v === 't') drawText(item_18); // other else draw(item_18); // select if (item_18.se) drawSelect(item_18); }); // canvas selection drawSelection(); ui_8.restore(); }; // Snap angle to nearest multiple of 15 degrees const snapToAngle = (dx, dy) => { // Current angle in radians const angle_0 = Math.atan2(dy, dx); // Snap to nearest 15 degrees const snappedAngle = Math.round(angle_0 / (Math.PI / 12)) * (Math.PI / 12); // Length of the vector const length = Math.sqrt(dx * dx + dy * dy); return { x: Math.cos(snappedAngle) * length, y: Math.sin(snappedAngle) * length }; }; const onMoveItems = (x_6, y_6) => { const itemsSelected_0 = getItems(true); itemsSelected_0.forEach(item_19 => { const v_2 = item_19.v; // draw line if (v_2 === 'dl') { item_19.p = item_19.p.map((value, index_1) => { return index_1 % 2 ? value + y_6 : value + x_6; }); } // rectangle, draw point, object circle, ellipse, object line, object arrow, object triangle, image, text if (['or', 'os', 'dp', 'oc', 'oe', 'ol', 'oa', 'ot', 'ote', 'i', 't'].includes(v_2)) { item_19.p[0] += x_6; item_19.p[1] += y_6; } // object line if (['ol', 'oa'].includes(v_2)) { item_19.ar[0] += x_6; item_19.ar[1] += y_6; } // object triangle if (['ot', 'ote'].includes(v_2)) { item_19.p[2] += x_6; item_19.p[4] += x_6; item_19.p[3] += y_6; item_19.p[5] += y_6; } }); onUpdateCoordinates(); }; const onMove = (body_0, event_4) => { if (!refs.on.current) return; const { offsetX: x_7, offsetY: y_7, clientX: clientX_2, clientY: clientY_2 } = body_0; const xo = transform(x_7 - refs.move.current.x); const yo = transform(y_7 - refs.move.current.y); const ui_9 = refs.ui.current.getContext('2d'); const rect_1 = refs.ui.current.getBoundingClientRect(); const currentX = clientX_2 - rect_1.left; const currentY = clientY_2 - rect_1.top; const start_0 = refs.start.current; const startTransformed_0 = { x: transform(start_0.x - refs.move.current.x), y: transform(start_0.y - refs.move.current.y) }; const item_20 = getItem(); const items_5 = getItems(); const t_0 = refs.tool.current; const shiftKey = event_4.shiftKey; const zoom_2 = refs.scale.current; refs.mouseMove.current.current = { x: clientX_2 / zoom_2, y: clientY_2 / zoom_2 }; 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 }; refs.mouseMove.current.previous = _objectSpread({}, refs.mouseMove.current.current); const delta = refs.mouseMove.current.delta; // select if (t_0 === 'select') { if (!refs.moveStarted.current) refs.moveStarted.current = true; if (refs.select.current) { unselectAll(); const width_2 = currentX - start_0.x; const height_3 = currentY - start_0.y; const isSquare = shiftKey; const radius = 0; if (isSquare) { const side = Math.min(Math.abs(width_2), Math.abs(height_3)); refs.select.current.v = 'os'; refs.select.current.p = [startTransformed_0.x, startTransformed_0.y]; refs.select.current.ar = [transform(Math.sign(width_2) * side), transform(Math.sign(height_3) * side), radius]; } else { refs.select.current.v = 'or'; refs.select.current.p = [startTransformed_0.x, startTransformed_0.y]; refs.select.current.ar = [transform(width_2), transform(height_3), radius]; } } else onMoveItems(delta.x, delta.y); } // pen else if (t_0 === 'pen' && item_20) { // same path from draw point, to move if (!refs.moveStarted.current) { item_20.v = 'dl'; refs.moveStarted.current = true; } // Add the current point to the path item_20.p.push(xo, yo); } // pan else if (t_0 === 'pan') { refs.move.current.x = x_7 - refs.previous.current.x + refs.offset.current.x; refs.move.current.y = y_7 - refs.previous.current.y + refs.offset.current.y; } // eraser else if (t_0 === 'eraser') { // find all items that x, y collides with, with certain radius for (const i_2 of items_5) { const isPointInStroke = ui_9.isPointInStroke(getPath(i_2), xo, yo); if (isPointInStroke) refs.remove.current.push(i_2); } } // object line, object arrow else if (['line', 'line-arrow'].includes(t_0)) { const snapAt15Degrees = shiftKey; let endX = currentX; let endY = currentY; if (snapAt15Degrees) { const snapped = snapToAngle(currentX - start_0.x, currentY - start_0.y); endX = start_0.x + snapped.x; endY = start_0.y + snapped.y; } item_20.v = t_0 === 'line' ? 'ol' : 'oa'; item_20.p = [startTransformed_0.x, startTransformed_0.y]; item_20.ar = [transform(endX - refs.move.current.x), transform(endY - refs.move.current.y)]; } // object circle else if (t_0 === 'circle') { const width_3 = currentX - start_0.x; const height_4 = currentY - start_0.y; const isCircle = shiftKey; if (isCircle) { const radius_0 = Math.min(Math.abs(width_3), Math.abs(height_4)) / 2; const centerX = start_0.x + Math.sign(width_3) * radius_0; const centerY = start_0.y + Math.sign(height_4) * radius_0; item_20.v = 'oc'; item_20.p = [transform(centerX - refs.move.current.x), transform(centerY - refs.move.current.y)]; item_20.ar = [transform(radius_0), 0, Math.PI * 2]; } else { item_20.v = 'oe'; item_20.p = [transform(start_0.x + width_3 / 2 - refs.move.current.x), transform(start_0.y + height_4 / 2 - refs.move.current.y)]; item_20.ar = [transform(Math.abs(width_3) / 2), transform(Math.abs(height_4) / 2), 0, 0, Math.PI * 2]; } } // object rectangle else if (t_0 === 'rectangle') { const width_4 = currentX - start_0.x; const height_5 = currentY - start_0.y; const isSquare_0 = shiftKey; const radius_1 = 0; if (isSquare_0) { const side_0 = Math.min(Math.abs(width_4), Math.abs(height_5)); item_20.v = 'os'; item_20.p = [startTransformed_0.x, startTransformed_0.y]; item_20.ar = [transform(Math.sign(width_4) * side_0), transform(Math.sign(height_5) * side_0), radius_1]; } else { item_20.v = 'or'; item_20.p = [startTransformed_0.x, startTransformed_0.y]; item_20.ar = [transform(width_4), transform(height_5), radius_1]; } } // object triangle else if (['triangle'].includes(t_0)) { const endX_0 = xo; const endY_0 = yo; const base = Math.abs(endX_0 - startTransformed_0.x); const height_6 = shiftKey ? base * Math.sqrt(3) / 2 : Math.abs(endY_0 - startTransformed_0.y); const points_0 = [startTransformed_0.x, startTransformed_0.y, endX_0, startTransformed_0.y, startTransformed_0.x + (endX_0 - startTransformed_0.x) / 2, startTransformed_0.y - height_6]; item_20.v = shiftKey ? 'ote' : 'ot'; item_20.p = points_0; item_20.s = _objectSpread(_objectSpread({}, item_20.s), {}, { height: height_6 }); } // image else if (t_0 === 'image' && refs.image.current.complete && refs.image.current.src) { const width_5 = transform(currentX - start_0.x); const height_7 = transform(currentY - start_0.y); const keepAspectRatio = !shiftKey; let currentWidth; let currentHeight; if (keepAspectRatio) { if (Math.abs(width_5 / refs.aspectRatio.current) <= Math.abs(height_7)) { currentWidth = width_5; currentHeight = width_5 / refs.aspectRatio.current; } else { currentWidth = height_7 * refs.aspectRatio.current; currentHeight = height_7; } } else { currentWidth = width_5; currentHeight = height_7; } if (keepAspectRatio) { if (width_5 < 0 && currentWidth > 0 || width_5 > 0 && currentWidth < 0) currentWidth *= -1; if (height_7 < 0 && currentHeight > 0 || height_7 > 0 && currentHeight < 0) currentHeight *= -1; } item_20.p = [startTransformed_0.x, startTransformed_0.y]; item_20.ar = [currentWidth, currentHeight]; } // select box onSelect(); // render render(); }; const onMouseMove = event_5 => { const { offsetX: offsetX_2, offsetY: offsetY_2, clientX: clientX_3, clientY: clientY_3 } = event_5; onMove({ offsetX: offsetX_2, offsetY: offsetY_2, clientX: clientX_3, clientY: clientY_3 }, event_5); }; const onTouchMove = event_6 => { // Get the first touch point const touch_0 = event_6.touches[0]; const { clientX: clientX_4, clientY: clientY_4 } = touch_0; let { offsetX: offsetX_3, offsetY: offsetY_3 } = touch_0; const targetElement_0 = touch_0.target; if (targetElement_0 instanceof HTMLElement) { // Get the bounding rectangle of the target element const rect_2 = targetElement_0.getBoundingClientRect(); // Calculate the offsetX and offsetY offsetX_3 = touch_0.clientX - rect_2.left; offsetY_3 = touch_0.clientY - rect_2.top; } onMove({ offsetX: offsetX_3, offsetY: offsetY_3, clientX: clientX_4, clientY: clientY_4 }, event_6); }; const undo = () => { if (!refs.undo.current.length) return; // add current state to redo refs.redo.current.push([...getItems()]); // restore the undo state refs.items.current = refs.undo.current.pop(); // render render(); }; const redo = () => { if (!refs.redo.current.length) return; // add current state to undo refs.undo.current.push([...getItems()]); // restore the redo state refs.items.current = refs.redo.current.pop(); // render render(); }; const onWheel = eventReact_1 => { const event_7 = eventReact_1.nativeEvent; // zoom if (event_7.metaKey || event_7.ctrlKey) { setTool('zoom'); refs.toolUpdateAuto.current = true; const zoomFactor = 1.054; const mouseX = event_7.offsetX; const mouseY = event_7.offsetY; const scale = refs.scale.current; // Convert mouse position to canvas coordinates const canvasX = (mouseX - refs.move.current.x) / scale; const canvasY = (mouseY - refs.move.current.y) / scale; // Adjust scale const zoomIn = event_7.deltaY < 0; const newScale = zoomIn ? scale * zoomFactor : scale / zoomFactor; if (newScale <= maxZoom / 100 && newScale >= minZoom / 100) { // Update origin to focus on mouse position refs.move.current.x -= canvasX * (newScale - scale); refs.move.current.y -= canvasY * (newScale - scale); refs.scale.current = newScale; render(); } } // pan else if (!refs.mouseDown.current) { refs.move.current.x -= event_7.deltaX; refs.move.current.y -= event_7.deltaY; render(); } }; const onPaste = event_8 => { event_8.preventDefault(); // Get clipboard data const items_6 = Array.from(event_8.clipboardData.items); // Loop through clipboard items to find an image for (const item_21 of items_6) { if (item_21.type.startsWith('image/')) { // Get the image file const blob = item_21.getAsFile(); refs.image.current = new Image(); // Load the image and draw it on the canvas refs.image.current.onload = () => { refs.aspectRatio.current = refs.image.current.width / refs.image.current.height; // Todo // 1) Upload the image first, than read it in image src // 2) Add url of the image // instead of embeding the image const item__2 = { i: (0, _utils.getID)(), v: 'i', p: [], ar: [], s: { // Todo // remove in the future image: refs.image.current, aspectRatio: refs.aspectRatio.current }, a: _date.OnesyDate.milliseconds }; add(item__2); setTool('image'); }; // Create an object URL for the blob and set it as the image source refs.image.current.src = URL.createObjectURL(blob); break; } } }; _react.default.useEffect(() => { const method = () => { const width_6 = refs.root.current.offsetWidth; const height_8 = refs.root.current.offsetHeight; setSize({ width: width_6, height: height_8 }); }; const onKeyUp = event_9 => { if (refs.toolUpdateAuto.current) setTool(refs.previousTool.current || 'pen'); }; const onKeyDown = async event_10 => { refs.toolUpdateAuto.current = false; const { key: key_0 } = event_10; const itemsAll = [...refs.items.current].filter(Boolean); const t_1 = refs.tool.current; const zoom_3 = refs.scale.current; if (['a', 'A'].includes(key_0) && (event_10.metaKey || event_10.ctrlKey)) { event_10.preventDefault(); selectAll(); render(); } else if (t_1 === 'select' && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Backspace'].includes(key_0)) { const value_0 = event_10.shiftKey ? 10 : 1; if (key_0 === 'ArrowUp') onMoveItems(0, -value_0 / zoom_3); if (key_0 === 'ArrowDown') onMoveItems(0, value_0 / zoom_3); if (key_0 === 'ArrowLeft') onMoveItems(-value_0 / zoom_3, 0); if (key_0 === 'ArrowRight') onMoveItems(value_0 / zoom_3, 0); if (key_0 === 'Backspace') { const toRemove_2 = []; refs.items.current.forEach(item_22 => { if (item_22.se) toRemove_2.push(item_22); }); if (toRemove_2.length) remove(toRemove_2); } render(); } else if (key_0 === 'Escape') { setTool('select'); refs.textActive.current = null; unselectAll(); filterItems(); render(); } else if (tool === 'text') { const selectedTextBox = itemsAll.find(item_23 => item_23.v === 't' && item_23.se); if (!selectedTextBox) return; const { line: line_2, char: char_0 } = selectedTextBox.s.cursor; const lines_0 = selectedTextBox.s.lines; const currentLine_0 = lines_0[line_2]; if (['ArrowLeft', 'ArrowRight'].includes(key_0)) { var _lines_0$line_, _lines_0$selectedText2; event_10.preventDefault(); selectedTextBox.s.cursor.char += key_0 === 'ArrowLeft' ? -1 : 1; if (selectedTextBox.s.cursor.char < 0) { var _lines_0$selectedText; selectedTextBox.s.cursor.line--; selectedTextBox.s.cursor.char = (_lines_0$selectedText = lines_0[selectedTextBox.s.cursor.line]) === null || _lines_0$selectedText === void 0 ? void 0 : _lines_0$selectedText.length; } else if (selectedTextBox.s.cursor.char > ((_lines_0$line_ = lines_0[line_2]) === null || _lines_0$line_ === void 0 ? void 0 : _lines_0$line_.length) && line_2 !== lines_0.length - 1) { selectedTextBox.s.cursor.line++; selectedTextBox.s.cursor.char = 0; } selectedTextBox.s.cursor.line = (0, _utils.clamp)(selectedTextBox.s.cursor.line, 0, lines_0.length - 1); selectedTextBox.s.cursor.char = (0, _utils.clamp)(selectedTextBox.s.cursor.char, 0, (_lines_0$selectedText2 = lines_0[selectedTextBox.s.cursor.line]) === null || _lines_0$selectedText2 === void 0 ? void 0 : _lines_0$selectedText2.length); } if (['ArrowUp', 'ArrowDown'].includes(key_0)) { var _lines_0$selectedText3; event_10.preventDefault(); selectedTextBox.s.cursor.line += key_0 === 'ArrowUp' ? -1 : 1; selectedTextBox.s.cursor.line = (0, _utils.clamp)(selectedTextBox.s.cursor.line, 0, lines_0.length - 1); selectedTextBox.s.cursor.char = (0, _utils.clamp)(selectedTextBox.s.cursor.char, 0, (_lines_0$selectedText3 = lines_0[selectedTextBox.s.cursor.line]) === null || _lines_0$selectedText3 === void 0 ? void 0 : _lines_0$selectedText3.length); } else if (key_0 === 'Enter') { event_10.preventDefault(); const newLine = currentLine_0.slice(char_0); lines_0[line_2] = currentLine_0.slice(0, char_0); lines_0.splice(line_2 + 1, 0, newLine); selectedTextBox.s.cursor.line++; selectedTextBox.s.cursor.char = 0; } else if (key_0 === 'Backspace') { event_10.preventDefault(); if (char_0 > 0) { lines_0[line_2] = currentLine_0.slice(0, char_0 - 1) + currentLine_0.slice(char_0); selectedTextBox.s.cursor.char--; } else if (line_2 > 0) { const prevLine = lines_0[line_2 - 1]; selectedTextBox.s.cursor.char = prevLine.length; lines_0[line_2 - 1] += lines_0[line_2]; lines_0.splice(line_2, 1); selectedTextBox.s.cursor.line--; } } else if (key_0.length === 1) { if ([' '].includes(key_0)) event_10.preventDefault(); let textClipboard = ''; const isPaste = (event_10.ctrlKey || event_10.metaKey) && ['v', 'V'].includes(key_0); if (isPaste) { try { textClipboard = await window.navigator.clipboard.readText(); } catch (error) {} } const text = isPaste ? textClipboard : key_0; selectedTextBox.s.lines[line_2] = currentLine_0.slice(0, char_0) + text + currentLine_0.slice(char_0); selectedTextBox.s.cursor.char += text.length; } updateTextBoxDimensions(selectedTextBox); selectedTextBox.c = [...selectedTextBox.p, ...selectedTextBox.ar]; render(); } else { if (event_10.metaKey && key_0 === 'z') { if (event_10.shiftKey) redo();else undo(); } if (key_0 === ' ') { refs.toolUpdateAuto.current = true; setTool('pan'); } // tools if (event_10.shiftKey) { if (['E', 'D', 'P', 'S', 'C', 'R', 'I', 'L', 'A', 'T', 'G'].includes(key_0)) refs.toolUpdateAuto.current = false; if (key_0 === 'E') setTool('eraser'); if (key_0 === 'D') setTool('pen'); if (key_0 === 'P') setTool('pan'); if (key_0 === 'S') setTool('select'); if (key_0 === 'C') setTool('circle'); if (key_0 === 'R') setTool('rectangle'); if (key_0 === 'I') setTool('triangle'); if (key_0 === 'L') setTool('line'); if (key_0 === 'A') setTool('line-arrow'); if (key_0 === 'T') setTool('text'); if (key_0 === 'G') { setGrid(previous => !previous); render(); } } } }; window.addEventListener('resize', method); window.document.addEventListener('mouseup', onMouseUp); window.document.addEventListener('touchend', onMouseUp); window.document.addEventListener('mousemove', onMouseMove); window.document.addEventListener('touchmove', onTouchMove); window.document.addEventListener('keyup', onKeyUp); window.document.addEventListener('keydown', onKeyDown); window.document.addEventListener('paste', onPaste); method(); init(); return () => { window.removeEventListener('resize', method); window.document.removeEventListener('mouseup', onMouseUp); window.document.removeEventListener('touchend', onMouseUp); window.document.removeEventListener('mousemove', onMouseMove); window.document.removeEventListener('touchmove', onTouchMove); window.document.removeEventListener('keyup', onKeyUp); window.document.removeEventListener('keydown', onKeyDown); window.document.removeEventListener('paste', onPaste); }; }, []); const onChangeInputFile = event_11 => { const file = event_11.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = eventReader => { refs.image.current = new Image(); refs.image.current.src = eventReader.target.result; refs.toolUpdateAuto.current = true; event_11.target.value = ''; setTool('image'); }; reader.readAsDataURL(file); } }; const propsCanvas = { width: size.width, height: size.height, disabled: !loaded, style: { width: size.width, height: size.height } }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(Line, _objectSpread(_objectSpread({ ref: item_24 => { if (ref) { if ((0, _utils.is)('function', ref)) ref(item_24);else ref.current = item_24; } refs.root.current = item_24; }, flex