lcms-pdf-annotation 0.0.0-stage â 1.0.3
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.
- package/README.md +455 -2
- package/dist/TwoDshapes.d.ts +20 -0
- package/dist/TwoDshapes.js +148 -0
- package/dist/TwoDshapes.js.map +1 -0
- package/dist/assets/cursor-highlighter.svg +5 -0
- package/dist/assets/cursor-pencil.svg +6 -0
- package/dist/constants.d.ts +80 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.css +515 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +1099 -0
- package/dist/index.js.map +1 -0
- package/dist/interface.d.ts +105 -0
- package/dist/interface.js +2 -0
- package/dist/interface.js.map +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +225 -0
- package/dist/utils.js.map +1 -0
- package/package.json +72 -4
package/README.md
CHANGED
|
@@ -1,3 +1,456 @@
|
|
|
1
|
-
#
|
|
1
|
+
# crystal-draw
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A multi-board drawing and whiteboard React component with PDF import and export functionality.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/crystal-draw)
|
|
6
|
+
[](https://www.npmjs.com/package/crystal-draw)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- đī¸ **Drawing tools** â Pen, Highlighter, Eraser, and Text
|
|
13
|
+
- đ **Shape tools** â Lines, Arrows, Rectangles, Circles, Triangles, Polygons, 2D/3D Graphs, Cones, Cylinders, Cuboids
|
|
14
|
+
- đī¸ **Multi-board** â Add, remove, and navigate between multiple boards
|
|
15
|
+
- âŠī¸ **Undo / Redo** â Full history with keyboard shortcuts (`Ctrl+Z` / `Ctrl+Y` / `Ctrl+Shift+Z`)
|
|
16
|
+
- đ **PDF import** â Upload a PDF and each page becomes a board background
|
|
17
|
+
- đ¤ **PDF export** â Export all boards as a single PDF file
|
|
18
|
+
- đą **Touch support** â Works on touch devices
|
|
19
|
+
- đ¨ **Customisable** â Accepts `className` and `style` props for easy layout integration
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install crystal-draw
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **Peer dependencies** â React 16 or higher is required and must be installed in your project.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import CrystalDraw from 'crystal-draw';
|
|
37
|
+
import 'crystal-draw/style.css';
|
|
38
|
+
|
|
39
|
+
export default function App() {
|
|
40
|
+
return (
|
|
41
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
42
|
+
<CrystalDraw />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> **Important:** Always import `crystal-draw/style.css` so that cursor and tool styles are applied correctly.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Component Props
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
|-------------|-------------------------|-------------|--------------------------------------------------|
|
|
56
|
+
| `className` | `string` | `undefined` | CSS class(es) applied to the root container div |
|
|
57
|
+
| `style` | `React.CSSProperties` | `undefined` | Inline styles applied to the root container div |
|
|
58
|
+
|
|
59
|
+
The component fills its parent container. Give the parent a defined width and height to control the canvas size.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<CrystalDraw
|
|
63
|
+
className="my-whiteboard"
|
|
64
|
+
style={{ border: '1px solid #ccc', borderRadius: '8px' }}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## API â Imperative Controls
|
|
71
|
+
|
|
72
|
+
All state is managed by an internal [Zustand](https://github.com/pmndrs/zustand) store. You can control the whiteboard programmatically using the exported helper functions â ideal for building your own toolbar.
|
|
73
|
+
|
|
74
|
+
### Tool Selection
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { setTool } from 'crystal-draw';
|
|
78
|
+
|
|
79
|
+
// Switch the active drawing tool
|
|
80
|
+
setTool('pen'); // freehand pen
|
|
81
|
+
setTool('highlighter'); // semi-transparent highlighter
|
|
82
|
+
setTool('eraser'); // erase drawn strokes
|
|
83
|
+
setTool('text'); // click on canvas to place text
|
|
84
|
+
setTool('laser'); // laser pointer (no persistent marks)
|
|
85
|
+
setTool('select'); // selection mode
|
|
86
|
+
|
|
87
|
+
// Shape tools (2D)
|
|
88
|
+
setTool('line');
|
|
89
|
+
setTool('arrowline');
|
|
90
|
+
setTool('rectangle');
|
|
91
|
+
setTool('circle');
|
|
92
|
+
setTool('triangle');
|
|
93
|
+
setTool('pentagon');
|
|
94
|
+
setTool('hexagon');
|
|
95
|
+
setTool('octagon');
|
|
96
|
+
setTool('parabola');
|
|
97
|
+
setTool('twoDgraph');
|
|
98
|
+
setTool('threeDgraph');
|
|
99
|
+
|
|
100
|
+
// Shape tools (3D)
|
|
101
|
+
setTool('cuboid');
|
|
102
|
+
setTool('cone');
|
|
103
|
+
setTool('cylinder');
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Color & Line Width
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { setColor, setHighlighterColor, setLineWidth } from 'crystal-draw';
|
|
110
|
+
|
|
111
|
+
// Set pen / shape color (any valid CSS color string)
|
|
112
|
+
setColor('#ff0000');
|
|
113
|
+
setColor('rgb(0, 128, 255)');
|
|
114
|
+
|
|
115
|
+
// Set highlighter color (use rgba for transparency)
|
|
116
|
+
setHighlighterColor('rgba(255, 255, 0, 0.5)');
|
|
117
|
+
|
|
118
|
+
// Set line width for a specific tool type
|
|
119
|
+
setLineWidth(2, 'PEN'); // thin pen stroke
|
|
120
|
+
setLineWidth(16, 'HIGHLIGHTER'); // default highlighter width
|
|
121
|
+
setLineWidth(60, 'ERASER'); // large eraser
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Undo & Redo
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { undo, redo } from 'crystal-draw';
|
|
128
|
+
|
|
129
|
+
undo(); // remove last drawn stroke
|
|
130
|
+
redo(); // re-apply last undone stroke
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Keyboard shortcuts are enabled automatically when the canvas is mounted:
|
|
134
|
+
- **Undo:** `Ctrl+Z` (Windows/Linux) or `â+Z` (macOS)
|
|
135
|
+
- **Redo:** `Ctrl+Y` or `Ctrl+Shift+Z` (Windows/Linux) / `â+Shift+Z` (macOS)
|
|
136
|
+
|
|
137
|
+
### Board Management
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { addBoard, removeBoard, clearBoard, setCurrentBoardIndex } from 'crystal-draw';
|
|
141
|
+
|
|
142
|
+
addBoard(); // append a new blank board
|
|
143
|
+
removeBoard(2); // remove board at index 2
|
|
144
|
+
clearBoard(0); // erase all strokes on board at index 0
|
|
145
|
+
setCurrentBoardIndex(1); // navigate to board at index 1
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Board Rotation
|
|
149
|
+
|
|
150
|
+
Rotate the active board (or any specific board by index) in 90° increments. Supports rotation in both clockwise and anti-clockwise directions:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import {
|
|
154
|
+
rotateClockwise,
|
|
155
|
+
rotateAntiClockwise,
|
|
156
|
+
rotateBoard,
|
|
157
|
+
getBoardRotation
|
|
158
|
+
} from 'crystal-draw';
|
|
159
|
+
|
|
160
|
+
// Rotate the current board by 90 degrees
|
|
161
|
+
await rotateClockwise(); // rotates 90° clockwise (e.g. 0° -> 90° -> 180° -> 270° -> 0°)
|
|
162
|
+
await rotateAntiClockwise(); // rotates 90° anti-clockwise
|
|
163
|
+
|
|
164
|
+
// Rotate a specific board by board index
|
|
165
|
+
await rotateClockwise(0); // rotate board 0 clockwise by 90°
|
|
166
|
+
await rotateAntiClockwise(2); // rotate board 2 anti-clockwise by 90°
|
|
167
|
+
|
|
168
|
+
// Or use the generic rotateBoard function
|
|
169
|
+
await rotateBoard('clockwise');
|
|
170
|
+
await rotateBoard('anti-clockwise');
|
|
171
|
+
|
|
172
|
+
// Get the current rotation angle (0, 90, 180, or 270)
|
|
173
|
+
const rotation = getBoardRotation(); // e.g. 90
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### PDF Import
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { uploadPDF } from 'crystal-draw';
|
|
180
|
+
|
|
181
|
+
async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
182
|
+
const file = event.target.files?.[0];
|
|
183
|
+
if (file) {
|
|
184
|
+
await uploadPDF(file); // each page becomes a new board background
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Usage in JSX
|
|
189
|
+
<input type="file" accept="application/pdf" onChange={handleFileChange} />
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If the whiteboard currently has only one empty board, `uploadPDF` replaces it with the PDF pages. Otherwise the pages are appended as additional boards.
|
|
193
|
+
|
|
194
|
+
### PDF Export
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { exportPDF } from 'crystal-draw';
|
|
198
|
+
import { useRef } from 'react';
|
|
199
|
+
|
|
200
|
+
function App() {
|
|
201
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
202
|
+
|
|
203
|
+
async function handleExport() {
|
|
204
|
+
const boardsContainer = document.getElementById('crystal--draw-container') as HTMLDivElement;
|
|
205
|
+
if (!boardsContainer) return;
|
|
206
|
+
|
|
207
|
+
const file = await exportPDF(boardsContainer);
|
|
208
|
+
if (file) {
|
|
209
|
+
const url = URL.createObjectURL(file);
|
|
210
|
+
const a = document.createElement('a');
|
|
211
|
+
a.href = url;
|
|
212
|
+
a.download = 'whiteboard.pdf';
|
|
213
|
+
a.click();
|
|
214
|
+
URL.revokeObjectURL(url);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
<button onClick={handleExport}>Export PDF</button>
|
|
221
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
222
|
+
<CrystalDraw />
|
|
223
|
+
</div>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> The root element rendered by `<CrystalDraw />` always has the id `crystal--draw-container`.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Advanced â Zustand Store
|
|
234
|
+
|
|
235
|
+
For reactive access to the internal state (e.g. building a live toolbar that reflects the current tool), use the exported `useBoundStore` hook directly:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { useBoundStore } from 'crystal-draw';
|
|
239
|
+
|
|
240
|
+
function Toolbar() {
|
|
241
|
+
const tool = useBoundStore((state) => state.tool);
|
|
242
|
+
const boards = useBoundStore((state) => state.boards);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div>
|
|
246
|
+
<p>Active tool: {tool}</p>
|
|
247
|
+
<p>Total boards: {boards.length}</p>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Store State Shape
|
|
254
|
+
|
|
255
|
+
| Field | Type | Description |
|
|
256
|
+
|---------------------|----------------------------------------------|----------------------------------------------|
|
|
257
|
+
| `tool` | `ToolTypes` | Currently active tool |
|
|
258
|
+
| `color` | `string` | Current pen / shape color |
|
|
259
|
+
| `highlighterColor` | `string` | Current highlighter color |
|
|
260
|
+
| `lineWidths` | `{ PEN: number, HIGHLIGHTER: number, ERASER: number }` | Line widths per tool type |
|
|
261
|
+
| `boards` | `Array<{ id, shapes, page, rotation, zoom }>` | All board data (zoom defaults to 1) |
|
|
262
|
+
| `currentBoardIndex` | `number` | Index of the currently active board |
|
|
263
|
+
| `history` | `historyItem[]` | Undo/redo history stack |
|
|
264
|
+
| `undoIndex` | `number` | Current position in the history stack |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### Zoom (Page Scale)
|
|
269
|
+
|
|
270
|
+
Each board has an independent zoom level (default `1.0`, range `0.1â5`).
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import {
|
|
274
|
+
zoomIn,
|
|
275
|
+
zoomOut,
|
|
276
|
+
setZoom,
|
|
277
|
+
resetZoom,
|
|
278
|
+
getZoom,
|
|
279
|
+
getBoardZoom,
|
|
280
|
+
} from 'crystal-draw';
|
|
281
|
+
|
|
282
|
+
// Zoom in/out by a step (default step = 0.1). Returns the new zoom level.
|
|
283
|
+
const z1 = zoomIn(); // 1.0 â 1.1
|
|
284
|
+
const z2 = zoomOut(0.25); // 1.1 â 0.85
|
|
285
|
+
|
|
286
|
+
// Set an exact zoom level (clamped to [0.1, 5])
|
|
287
|
+
setZoom(1.5); // 150 %
|
|
288
|
+
setZoom(0.5); // 50 %
|
|
289
|
+
|
|
290
|
+
// Reset zoom to 100 %
|
|
291
|
+
resetZoom();
|
|
292
|
+
|
|
293
|
+
// Read the current zoom level
|
|
294
|
+
const zoom = getZoom(); // e.g. 1.5
|
|
295
|
+
const zoom2 = getBoardZoom(2); // zoom of board at index 2
|
|
296
|
+
|
|
297
|
+
// All functions accept an optional boardIndex (defaults to currentBoardIndex)
|
|
298
|
+
zoomIn(0.1, 0); // zoom board 0 in by 0.1
|
|
299
|
+
zoomOut(0.2, 1); // zoom board 1 out by 0.2
|
|
300
|
+
setZoom(2, 0); // set board 0 to 200 %
|
|
301
|
+
resetZoom(1); // reset board 1
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Via `ref` (imperative API)
|
|
305
|
+
|
|
306
|
+
```tsx
|
|
307
|
+
import CrystalDraw, { CrystalDrawRef } from 'crystal-draw';
|
|
308
|
+
import { useRef, useState } from 'react';
|
|
309
|
+
|
|
310
|
+
function App() {
|
|
311
|
+
const boardRef = useRef<CrystalDrawRef>(null);
|
|
312
|
+
const [zoom, setCurrentZoom] = useState(1);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<>
|
|
316
|
+
<button onClick={() => { const z = boardRef.current?.zoomIn(); if (z != null) setCurrentZoom(z); }}>
|
|
317
|
+
Zoom In
|
|
318
|
+
</button>
|
|
319
|
+
<button onClick={() => { const z = boardRef.current?.zoomOut(); if (z != null) setCurrentZoom(z); }}>
|
|
320
|
+
Zoom Out
|
|
321
|
+
</button>
|
|
322
|
+
<button onClick={() => { boardRef.current?.resetZoom(); setCurrentZoom(1); }}>
|
|
323
|
+
Reset
|
|
324
|
+
</button>
|
|
325
|
+
<span>{Math.round(zoom * 100)}%</span>
|
|
326
|
+
|
|
327
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
328
|
+
<CrystalDraw ref={boardRef} onZoomChange={(z) => setCurrentZoom(z)} />
|
|
329
|
+
</div>
|
|
330
|
+
</>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
#### Via `getZoomCallbacks` prop
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import CrystalDraw, { ZoomCallbacks } from 'crystal-draw';
|
|
339
|
+
|
|
340
|
+
function App() {
|
|
341
|
+
let controls: ZoomCallbacks | null = null;
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<>
|
|
345
|
+
<button onClick={() => controls?.zoomIn()}>+</button>
|
|
346
|
+
<button onClick={() => controls?.zoomOut()}>-</button>
|
|
347
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
348
|
+
<CrystalDraw
|
|
349
|
+
onZoomChange={(zoom, boardIndex) =>
|
|
350
|
+
console.log(`Board ${boardIndex} zoom: ${zoom}`)
|
|
351
|
+
}
|
|
352
|
+
getZoomCallbacks={(cb) => { controls = cb; }}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
</>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Full Example â Custom Toolbar
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import CrystalDraw, {
|
|
366
|
+
setTool,
|
|
367
|
+
setColor,
|
|
368
|
+
setLineWidth,
|
|
369
|
+
undo,
|
|
370
|
+
redo,
|
|
371
|
+
addBoard,
|
|
372
|
+
clearBoard,
|
|
373
|
+
uploadPDF,
|
|
374
|
+
exportPDF,
|
|
375
|
+
useBoundStore,
|
|
376
|
+
} from 'crystal-draw';
|
|
377
|
+
import 'crystal-draw/style.css';
|
|
378
|
+
|
|
379
|
+
export default function Whiteboard() {
|
|
380
|
+
const tool = useBoundStore((state) => state.tool);
|
|
381
|
+
const boards = useBoundStore((state) => state.boards);
|
|
382
|
+
|
|
383
|
+
async function handleExport() {
|
|
384
|
+
const el = document.getElementById('crystal--draw-container') as HTMLDivElement;
|
|
385
|
+
const file = await exportPDF(el);
|
|
386
|
+
if (file) {
|
|
387
|
+
const a = document.createElement('a');
|
|
388
|
+
a.href = URL.createObjectURL(file);
|
|
389
|
+
a.download = 'drawing.pdf';
|
|
390
|
+
a.click();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
396
|
+
{/* Toolbar */}
|
|
397
|
+
<div style={{ display: 'flex', gap: 8, padding: 8, background: '#1e1e2e' }}>
|
|
398
|
+
<button onClick={() => setTool('pen')} style={{ fontWeight: tool === 'pen' ? 'bold' : 'normal' }}>Pen</button>
|
|
399
|
+
<button onClick={() => setTool('highlighter')} style={{ fontWeight: tool === 'highlighter' ? 'bold' : 'normal' }}>Highlight</button>
|
|
400
|
+
<button onClick={() => setTool('eraser')} style={{ fontWeight: tool === 'eraser' ? 'bold' : 'normal' }}>Eraser</button>
|
|
401
|
+
<button onClick={() => setTool('text')}>Text</button>
|
|
402
|
+
<button onClick={() => setColor('#ff0000')}>Red</button>
|
|
403
|
+
<button onClick={() => setColor('#0000ff')}>Blue</button>
|
|
404
|
+
<button onClick={() => setLineWidth(2, 'PEN')}>Thin</button>
|
|
405
|
+
<button onClick={() => setLineWidth(6, 'PEN')}>Thick</button>
|
|
406
|
+
<button onClick={undo}>Undo</button>
|
|
407
|
+
<button onClick={redo}>Redo</button>
|
|
408
|
+
<button onClick={addBoard}>+ Board</button>
|
|
409
|
+
<button onClick={() => clearBoard(boards.length - 1)}>Clear</button>
|
|
410
|
+
<label style={{ cursor: 'pointer', color: 'white' }}>
|
|
411
|
+
Import PDF
|
|
412
|
+
<input
|
|
413
|
+
type="file"
|
|
414
|
+
accept="application/pdf"
|
|
415
|
+
style={{ display: 'none' }}
|
|
416
|
+
onChange={(e) => e.target.files?.[0] && uploadPDF(e.target.files[0])}
|
|
417
|
+
/>
|
|
418
|
+
</label>
|
|
419
|
+
<button onClick={handleExport}>Export PDF</button>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Canvas */}
|
|
423
|
+
<div style={{ flex: 1 }}>
|
|
424
|
+
<CrystalDraw />
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Built-in Color Palette
|
|
434
|
+
|
|
435
|
+
The package exports sensible defaults you can reference:
|
|
436
|
+
|
|
437
|
+
**Pen colors:** `#f8f8f8` (default), `#262626` (black), `#ff0000` (red), `#00ff00` (lime), `#0000ff` (blue), `#ffff00` (yellow), and more standard web colors.
|
|
438
|
+
|
|
439
|
+
**Highlighter colors:** Yellow `rgba(225,203,5,0.7)`, Blue `rgba(39,152,177,0.7)`, Purple `rgba(234,151,255,0.7)`, Green `rgba(100,255,75,0.7)`, Orange `rgba(255,172,75,0.7)`.
|
|
440
|
+
|
|
441
|
+
You can use any valid CSS color string with `setColor` / `setHighlighterColor`.
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Browser / Environment Requirements
|
|
446
|
+
|
|
447
|
+
- **React** 16+
|
|
448
|
+
- **Modern browser** with Canvas 2D API support
|
|
449
|
+
- **PDF.js** is used internally for PDF import (lazy-loaded, no extra setup needed)
|
|
450
|
+
- **jsPDF** and **html2canvas-pro** are used internally for PDF export (lazy-loaded)
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## License
|
|
455
|
+
|
|
456
|
+
ISC â Š Nakul Soral ([nakulsoral@aesl.in](mailto:nakulsoral@aesl.in))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LineElement, Point } from "./interface";
|
|
2
|
+
export declare const rotatePoint: (x: number, y: number, cx: number, cy: number, angle: number) => Point;
|
|
3
|
+
export declare function calculateBoundingBox(shape: LineElement, canvas: HTMLCanvasElement): {
|
|
4
|
+
minX: number;
|
|
5
|
+
maxX: number;
|
|
6
|
+
minY: number;
|
|
7
|
+
maxY: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const isShapeSelected: (shape: LineElement, point: Point, canvas: HTMLCanvasElement) => boolean;
|
|
10
|
+
export declare function normalizeCoordinates(start: Point, current: Point): {
|
|
11
|
+
topLeft: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
};
|
|
15
|
+
dimensions: {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function getHandleUnderCursor(position: Point, shape: LineElement, canvas: HTMLCanvasElement): "top-left" | "top-right" | "bottom-left" | "bottom-right" | null;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const padding = 5;
|
|
2
|
+
export const rotatePoint = (x, y, cx, cy, angle) => {
|
|
3
|
+
return {
|
|
4
|
+
x: (x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
|
|
5
|
+
y: (x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
export function calculateBoundingBox(shape, canvas) {
|
|
9
|
+
let minX = Infinity;
|
|
10
|
+
let maxX = -Infinity;
|
|
11
|
+
let minY = Infinity;
|
|
12
|
+
let maxY = -Infinity;
|
|
13
|
+
if (shape.type === 'text' && shape.text) {
|
|
14
|
+
const startPoint = shape.points[0];
|
|
15
|
+
const fontSizematch = shape.font?.match(/(\d+)px/);
|
|
16
|
+
const fontSize = fontSizematch ? parseInt(fontSizematch[1], 10) : 20;
|
|
17
|
+
let widthInPixels = 0;
|
|
18
|
+
let heightInPixels = 0;
|
|
19
|
+
const ctx = canvas.getContext('2d');
|
|
20
|
+
if (ctx) {
|
|
21
|
+
ctx.save();
|
|
22
|
+
if (shape.font)
|
|
23
|
+
ctx.font = shape.font;
|
|
24
|
+
const maxWidth = 400;
|
|
25
|
+
const rawLines = shape.text.split('\n');
|
|
26
|
+
const wrappedLines = [];
|
|
27
|
+
rawLines.forEach((line) => {
|
|
28
|
+
if (ctx.measureText(line).width <= maxWidth) {
|
|
29
|
+
wrappedLines.push(line);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const words = line.split(/(\s+)/);
|
|
33
|
+
let currentLine = '';
|
|
34
|
+
words.forEach((word) => {
|
|
35
|
+
const testLine = currentLine + word;
|
|
36
|
+
if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
|
|
37
|
+
wrappedLines.push(currentLine);
|
|
38
|
+
currentLine = word;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
currentLine = testLine;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (currentLine)
|
|
45
|
+
wrappedLines.push(currentLine);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const maxLineWidth = wrappedLines.length > 0
|
|
49
|
+
? Math.max(...wrappedLines.map(l => ctx.measureText(l).width))
|
|
50
|
+
: 0;
|
|
51
|
+
widthInPixels = maxLineWidth;
|
|
52
|
+
heightInPixels = wrappedLines.length * (fontSize * 1.2);
|
|
53
|
+
ctx.restore();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const lines = shape.text.split('\n');
|
|
57
|
+
const maxLineLength = Math.max(...lines.map(l => l.length));
|
|
58
|
+
widthInPixels = maxLineLength * fontSize * 0.6;
|
|
59
|
+
heightInPixels = lines.length * fontSize * 1.2;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
minX: startPoint.x + (4 / canvas.width),
|
|
63
|
+
maxX: startPoint.x + (4 + widthInPixels) / canvas.width,
|
|
64
|
+
minY: startPoint.y + (2 / canvas.height),
|
|
65
|
+
maxY: startPoint.y + (2 + heightInPixels) / canvas.height
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
shape.points.forEach((point) => {
|
|
69
|
+
if (point.x < minX)
|
|
70
|
+
minX = point.x;
|
|
71
|
+
if (point.x > maxX)
|
|
72
|
+
maxX = point.x;
|
|
73
|
+
if (point.y < minY)
|
|
74
|
+
minY = point.y;
|
|
75
|
+
if (point.y > maxY)
|
|
76
|
+
maxY = point.y;
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
minX,
|
|
80
|
+
maxX,
|
|
81
|
+
minY,
|
|
82
|
+
maxY
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export const isShapeSelected = (shape, point, canvas) => {
|
|
86
|
+
const boundingBox = calculateBoundingBox(shape, canvas);
|
|
87
|
+
const width = boundingBox.maxX - boundingBox.minX;
|
|
88
|
+
const height = boundingBox.maxY - boundingBox.minY;
|
|
89
|
+
const centerX = (boundingBox.minX + width / 2) * canvas.width;
|
|
90
|
+
const centerY = (boundingBox.minY + height / 2) * canvas.height;
|
|
91
|
+
// Rotate the point in the opposite direction of the shape's rotation
|
|
92
|
+
const rotatedPoint = rotatePoint(point.x * canvas.width, point.y * canvas.height, centerX, centerY, 0);
|
|
93
|
+
return (rotatedPoint.x >= boundingBox.minX * canvas.width - padding &&
|
|
94
|
+
rotatedPoint.x <= boundingBox.maxX * canvas.width + padding &&
|
|
95
|
+
rotatedPoint.y >= boundingBox.minY * canvas.height - padding &&
|
|
96
|
+
rotatedPoint.y <= boundingBox.maxY * canvas.height + padding);
|
|
97
|
+
};
|
|
98
|
+
export function normalizeCoordinates(start, current) {
|
|
99
|
+
return {
|
|
100
|
+
topLeft: {
|
|
101
|
+
x: Math.min(start.x, current.x),
|
|
102
|
+
y: Math.min(start.y, current.y)
|
|
103
|
+
},
|
|
104
|
+
dimensions: {
|
|
105
|
+
width: current.x - start.x,
|
|
106
|
+
height: current.y - start.y
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function getHandleUnderCursor(position, shape, canvas) {
|
|
111
|
+
position = { x: position.x * canvas.width, y: position.y * canvas.height };
|
|
112
|
+
const startPoint = shape.points[0];
|
|
113
|
+
const endPoint = shape.points[shape.points.length - 1];
|
|
114
|
+
const { dimensions } = normalizeCoordinates(startPoint, endPoint);
|
|
115
|
+
const centerX = (startPoint.x + dimensions.width / 2) * canvas.width;
|
|
116
|
+
const centerY = (startPoint.y + dimensions.height / 2) * canvas.height;
|
|
117
|
+
const handleSize = 8;
|
|
118
|
+
const topLeft = rotatePoint(startPoint.x * canvas.width, startPoint.y * canvas.height, centerX, centerY, 0);
|
|
119
|
+
const topRight = rotatePoint((startPoint.x + dimensions.width) * canvas.width, startPoint.y * canvas.height, centerX, centerY, 0);
|
|
120
|
+
const bottomLeft = rotatePoint(startPoint.x * canvas.width, (startPoint.y + dimensions.height) * canvas.height, centerX, centerY, 0);
|
|
121
|
+
const bottomRight = rotatePoint((startPoint.x + dimensions.width) * canvas.width, (startPoint.y + dimensions.height) * canvas.height, centerX, centerY, 0);
|
|
122
|
+
if (position.x >= topLeft.x - handleSize &&
|
|
123
|
+
position.x <= topLeft.x + handleSize &&
|
|
124
|
+
position.y >= topLeft.y - handleSize &&
|
|
125
|
+
position.y <= topLeft.y + handleSize) {
|
|
126
|
+
return 'top-left';
|
|
127
|
+
}
|
|
128
|
+
if (position.x >= topRight.x - handleSize &&
|
|
129
|
+
position.x <= topRight.x + handleSize &&
|
|
130
|
+
position.y >= topRight.y - handleSize &&
|
|
131
|
+
position.y <= topRight.y + handleSize) {
|
|
132
|
+
return 'top-right';
|
|
133
|
+
}
|
|
134
|
+
if (position.x >= bottomLeft.x - handleSize &&
|
|
135
|
+
position.x <= bottomLeft.x + handleSize &&
|
|
136
|
+
position.y >= bottomLeft.y - handleSize &&
|
|
137
|
+
position.y <= bottomLeft.y + handleSize) {
|
|
138
|
+
return 'bottom-left';
|
|
139
|
+
}
|
|
140
|
+
if (position.x >= bottomRight.x - handleSize &&
|
|
141
|
+
position.x <= bottomRight.x + handleSize &&
|
|
142
|
+
position.y >= bottomRight.y - handleSize &&
|
|
143
|
+
position.y <= bottomRight.y + handleSize) {
|
|
144
|
+
return 'bottom-right';
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=TwoDshapes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TwoDshapes.js","sourceRoot":"","sources":["../src/TwoDshapes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#000" stroke="#fff"
|
|
2
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<path d="m9 11-6 6v3h9l3-3" />
|
|
4
|
+
<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="#000" stroke="#fff"
|
|
2
|
+
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
3
|
+
<path
|
|
4
|
+
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
|
|
5
|
+
<path d="m15 5 4 4" />
|
|
6
|
+
</svg>
|