@teachinglab/omd 0.3.8 → 0.3.9

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.
@@ -1,438 +1,463 @@
1
- export class Cursor {
2
- /**
3
- * @param {OMDCanvas} canvas - Canvas instance
4
- */
5
- constructor(canvas) {
6
- this.canvas = canvas;
7
- this.isVisible = true;
8
- this.currentShape = 'pencil';
9
- this.size = 20;
10
- this.color = '#007bff';
11
-
12
- // Create cursor element
13
- this._createElement();
14
-
15
- // Add to UI layer
16
- this.canvas.uiLayer.appendChild(this.element);
17
- }
18
-
19
- /**
20
- * Create the cursor SVG element
21
- * @private
22
- */
23
- _createElement() {
24
- this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
25
- this.element.setAttribute('class', 'omd-cursor');
26
- this.element.style.pointerEvents = 'none';
27
- this.element.style.opacity = '0.8';
28
-
29
- // Create different cursor shapes
30
- this._createShapes();
31
-
32
- // Initially hidden
33
- this.hide();
34
- }
35
-
36
- /**
37
- * Create different cursor shape elements
38
- * @private
39
- */
40
- _createShapes() {
41
- this.shapes = {};
42
-
43
- // Default cursor (crosshair)
44
- this.shapes.default = this._createCrosshair();
45
-
46
- // Pencil cursor
47
- this.shapes.pencil = this._createPencilCursor();
48
-
49
- // Eraser cursor
50
- this.shapes.eraser = this._createEraserCursor();
51
-
52
- // Select cursor
53
- this.shapes.select = this._createSelectCursor();
54
-
55
- // Add all shapes to cursor element
56
- Object.values(this.shapes).forEach(shape => {
57
- this.element.appendChild(shape);
58
- });
59
-
60
- // Show default shape initially
61
- this.setShape('default');
62
- }
63
-
64
- /**
65
- * Create crosshair cursor
66
- * @private
67
- */
68
- _createCrosshair() {
69
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
70
- group.setAttribute('data-shape', 'default');
71
-
72
- // Horizontal line
73
- const hLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
74
- hLine.setAttribute('x1', '-10');
75
- hLine.setAttribute('y1', '0');
76
- hLine.setAttribute('x2', '10');
77
- hLine.setAttribute('y2', '0');
78
- hLine.setAttribute('stroke', this.color);
79
- hLine.setAttribute('stroke-width', '1');
80
-
81
- // Vertical line
82
- const vLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
83
- vLine.setAttribute('x1', '0');
84
- vLine.setAttribute('y1', '-10');
85
- vLine.setAttribute('x2', '0');
86
- vLine.setAttribute('y2', '10');
87
- vLine.setAttribute('stroke', this.color);
88
- vLine.setAttribute('stroke-width', '1');
89
-
90
- group.appendChild(hLine);
91
- group.appendChild(vLine);
92
-
93
- return group;
94
- }
95
-
96
- /**
97
- * Create pencil cursor
98
- * @private
99
- */
100
- _createPencilCursor() {
101
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
102
- group.setAttribute('data-shape', 'pencil');
103
-
104
- // Solid dot cursor
105
- this.brushCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
106
- this.brushCircle.setAttribute('cx', '0');
107
- this.brushCircle.setAttribute('cy', '0');
108
- this.brushCircle.setAttribute('r', this.size / 2);
109
- this.brushCircle.setAttribute('fill', this.color);
110
- this.brushCircle.setAttribute('stroke', 'none');
111
-
112
- group.appendChild(this.brushCircle);
113
-
114
- return group;
115
- }
116
-
117
- /**
118
- * Create eraser cursor
119
- * @private
120
- */
121
- _createEraserCursor() {
122
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
123
- group.setAttribute('data-shape', 'eraser');
124
-
125
- // Circle eraser indicator
126
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
127
- circle.setAttribute('r', this.size / 2);
128
- circle.setAttribute('fill', 'none');
129
- circle.setAttribute('stroke', '#dc3545');
130
- circle.setAttribute('stroke-width', '1.5');
131
- circle.setAttribute('class', 'eraser-circle');
132
-
133
- // Mode indicator (inner fill for radius mode)
134
- const modeIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
135
- modeIndicator.setAttribute('r', this.size / 3);
136
- modeIndicator.setAttribute('fill', 'rgba(220, 53, 69, 0.15)');
137
- modeIndicator.setAttribute('class', 'eraser-mode-indicator');
138
- modeIndicator.style.display = 'none';
139
-
140
- // X mark inside (for stroke mode)
141
- const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
142
- line1.setAttribute('x1', -this.size / 5);
143
- line1.setAttribute('y1', -this.size / 5);
144
- line1.setAttribute('x2', this.size / 5);
145
- line1.setAttribute('y2', this.size / 5);
146
- line1.setAttribute('stroke', '#dc3545');
147
- line1.setAttribute('stroke-width', '1.5');
148
- line1.setAttribute('class', 'eraser-x1');
149
-
150
- const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
151
- line2.setAttribute('x1', this.size / 5);
152
- line2.setAttribute('y1', -this.size / 5);
153
- line2.setAttribute('x2', -this.size / 5);
154
- line2.setAttribute('y2', this.size / 5);
155
- line2.setAttribute('stroke', '#dc3545');
156
- line2.setAttribute('stroke-width', '1.5');
157
- line2.setAttribute('class', 'eraser-x2');
158
-
159
- group.appendChild(circle);
160
- group.appendChild(modeIndicator);
161
- group.appendChild(line1);
162
- group.appendChild(line2);
163
-
164
- return group;
165
- }
166
-
167
- /**
168
- * Create select cursor
169
- * @private
170
- */
171
- _createSelectCursor() {
172
- const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
173
- group.setAttribute('data-shape', 'select');
174
-
175
- // Arrow pointer
176
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
177
- path.setAttribute('d', 'M 0,0 L 0,12 L 4,8 L 8,12 L 12,8 L 4,4 Z');
178
- path.setAttribute('fill', this.color);
179
- path.setAttribute('stroke', 'white');
180
- path.setAttribute('stroke-width', '1');
181
-
182
- group.appendChild(path);
183
-
184
- return group;
185
- }
186
-
187
- /**
188
- * Set cursor shape
189
- * @param {string} shape - Shape name ('default', 'pencil', 'eraser', 'select')
190
- */
191
- setShape(shape) {
192
- this.currentShape = shape;
193
-
194
- // Hide all shapes
195
- Object.values(this.shapes).forEach(shapeElement => {
196
- shapeElement.style.display = 'none';
197
- });
198
-
199
- // Show current shape
200
- if (this.shapes[shape]) {
201
- this.shapes[shape].style.display = 'block';
202
- } else {
203
- this.shapes.default.style.display = 'block';
204
- }
205
-
206
- // Update brush size for pencil
207
- if (shape === 'pencil' && this.brushCircle) {
208
- this._updateBrushSize();
209
- }
210
- }
211
-
212
- /**
213
- * Set cursor position
214
- * @param {number} x - X coordinate
215
- * @param {number} y - Y coordinate
216
- */
217
- setPosition(x, y) {
218
- this.element.setAttribute('transform', `translate(${x}, ${y})`);
219
- }
220
-
221
- /**
222
- * Show cursor
223
- */
224
- show() {
225
- this.isVisible = true;
226
- this.element.style.display = 'block';
227
- }
228
-
229
- /**
230
- * Hide cursor
231
- */
232
- hide() {
233
- this.isVisible = false;
234
- this.element.style.display = 'none';
235
- }
236
-
237
- /**
238
- * Set cursor size (for tools that support it)
239
- * @param {number} size - Cursor size
240
- */
241
- setSize(size) {
242
- this.size = size;
243
- this._updateBrushSize();
244
- }
245
-
246
- /**
247
- * Update brush size for pencil cursor
248
- * @private
249
- */
250
- _updateBrushSize() {
251
- if (this.brushCircle) {
252
- this.brushCircle.setAttribute('r', this.size / 2);
253
- }
254
-
255
- // Update eraser size if applicable
256
- const eraserShape = this.shapes.eraser;
257
- if (eraserShape) {
258
- const circle = eraserShape.querySelector('.eraser-circle');
259
- const modeIndicator = eraserShape.querySelector('.eraser-mode-indicator');
260
- const x1 = eraserShape.querySelector('.eraser-x1');
261
- const x2 = eraserShape.querySelector('.eraser-x2');
262
-
263
- if (circle) {
264
- circle.setAttribute('r', this.size / 2);
265
- }
266
- if (modeIndicator) {
267
- modeIndicator.setAttribute('r', this.size / 3);
268
- }
269
- if (x1) {
270
- x1.setAttribute('x1', -this.size / 5);
271
- x1.setAttribute('y1', -this.size / 5);
272
- x1.setAttribute('x2', this.size / 5);
273
- x1.setAttribute('y2', this.size / 5);
274
- }
275
- if (x2) {
276
- x2.setAttribute('x1', this.size / 5);
277
- x2.setAttribute('y1', -this.size / 5);
278
- x2.setAttribute('x2', -this.size / 5);
279
- x2.setAttribute('y2', this.size / 5);
280
- }
281
- }
282
- }
283
-
284
- /**
285
- * Set cursor color
286
- * @param {string} color - CSS color value
287
- */
288
- setColor(color) {
289
- this.color = color;
290
-
291
- // Update all shape colors
292
- this.element.querySelectorAll('[stroke]').forEach(element => {
293
- if (element.getAttribute('stroke') === this.color) {
294
- element.setAttribute('stroke', color);
295
- }
296
- });
297
-
298
- this.element.querySelectorAll('[fill]').forEach(element => {
299
- if (element.getAttribute('fill') === this.color) {
300
- element.setAttribute('fill', color);
301
- }
302
- });
303
- }
304
-
305
- /**
306
- * Set cursor opacity
307
- * @param {number} opacity - Opacity value (0-1)
308
- */
309
- setOpacity(opacity) {
310
- this.element.style.opacity = opacity;
311
- }
312
-
313
- /**
314
- * Enable pressure feedback (for pressure-sensitive devices)
315
- * @param {number} pressure - Pressure value (0-1)
316
- */
317
- setPressureFeedback(pressure) {
318
- if (this.currentShape === 'pencil' && this.brushCircle) {
319
- // Scale brush circle based on pressure
320
- const scale = 0.5 + (pressure * 0.5); // Scale from 50% to 100%
321
- this.brushCircle.setAttribute('transform', `scale(${scale})`);
322
-
323
- // Adjust opacity based on pressure
324
- const opacity = 0.3 + (pressure * 0.5); // Opacity from 30% to 80%
325
- this.brushCircle.style.opacity = opacity;
326
- }
327
- }
328
-
329
- /**
330
- * Add temporary visual feedback
331
- * @param {string} type - Feedback type ('success', 'error', 'info')
332
- * @param {number} [duration=500] - Duration in milliseconds
333
- */
334
- showFeedback(type, duration = 500) {
335
- const colors = {
336
- success: '#28a745',
337
- error: '#dc3545',
338
- info: '#17a2b8'
339
- };
340
-
341
- const originalColor = this.color;
342
- this.setColor(colors[type] || colors.info);
343
-
344
- // Pulse animation
345
- this.element.style.animation = 'pulse 0.3s ease-in-out';
346
-
347
- setTimeout(() => {
348
- this.setColor(originalColor);
349
- this.element.style.animation = '';
350
- }, duration);
351
- }
352
-
353
- /**
354
- * Update cursor based on tool configuration
355
- * @param {Object} toolConfig - Tool configuration
356
- */
357
- updateFromToolConfig(toolConfig) {
358
- if (toolConfig.strokeWidth) {
359
- // Scale the cursor size for better visibility - multiply by 4 for pencil to make it clearly visible
360
- const scaledSize = this.currentShape === 'pencil' ? toolConfig.strokeWidth * 4 : toolConfig.strokeWidth;
361
- this.setSize(scaledSize);
362
- }
363
-
364
- if (toolConfig.strokeColor) {
365
- this.setColor(toolConfig.strokeColor);
366
- }
367
-
368
- if (toolConfig.size) {
369
- this.setSize(toolConfig.size);
370
- }
371
-
372
- // Update eraser mode indicator
373
- if (toolConfig.mode !== undefined && this.currentShape === 'eraser') {
374
- this._updateEraserMode(toolConfig.mode);
375
- }
376
- }
377
-
378
- /**
379
- * Update eraser mode visual indicator
380
- * @private
381
- */
382
- _updateEraserMode(mode) {
383
- const eraserShape = this.shapes.eraser;
384
- if (!eraserShape) return;
385
-
386
- const circle = eraserShape.querySelector('.eraser-circle');
387
- const modeIndicator = eraserShape.querySelector('.eraser-mode-indicator');
388
- const x1 = eraserShape.querySelector('.eraser-x1');
389
- const x2 = eraserShape.querySelector('.eraser-x2');
390
-
391
- if (mode === 'radius') {
392
- // Radius mode: show filled circle, hide X, use orange color
393
- if (circle) {
394
- circle.setAttribute('stroke', '#ff9f43');
395
- circle.setAttribute('stroke-dasharray', '3,3');
396
- }
397
- if (modeIndicator) {
398
- modeIndicator.style.display = 'block';
399
- modeIndicator.setAttribute('fill', 'rgba(255, 159, 67, 0.3)');
400
- }
401
- if (x1) x1.style.display = 'none';
402
- if (x2) x2.style.display = 'none';
403
- } else {
404
- // Stroke mode: show X, hide fill, use red color
405
- if (circle) {
406
- circle.setAttribute('stroke', '#dc3545');
407
- circle.setAttribute('stroke-dasharray', 'none');
408
- }
409
- if (modeIndicator) {
410
- modeIndicator.style.display = 'none';
411
- }
412
- if (x1) x1.style.display = 'block';
413
- if (x2) x2.style.display = 'block';
414
- }
415
- }
416
-
417
- /**
418
- * Get current cursor state
419
- * @returns {Object} Cursor state
420
- */
421
- getState() {
422
- return {
423
- isVisible: this.isVisible,
424
- shape: this.currentShape,
425
- size: this.size,
426
- color: this.color
427
- };
428
- }
429
-
430
- /**
431
- * Clean up cursor resources
432
- */
433
- destroy() {
434
- if (this.element.parentNode) {
435
- this.element.parentNode.removeChild(this.element);
436
- }
437
- }
1
+ export class Cursor {
2
+ /**
3
+ * @param {OMDCanvas} canvas - Canvas instance
4
+ */
5
+ constructor(canvas) {
6
+ this.canvas = canvas;
7
+ this.isVisible = true;
8
+ this.currentShape = 'pencil';
9
+ this.size = 20;
10
+ this.color = '#007bff';
11
+
12
+ // Create cursor element
13
+ this._createElement();
14
+
15
+ // Add to UI layer
16
+ this.canvas.uiLayer.appendChild(this.element);
17
+ }
18
+
19
+ /**
20
+ * Create the cursor SVG element
21
+ * @private
22
+ */
23
+ _createElement() {
24
+ this.element = document.createElementNS('http://www.w3.org/2000/svg', 'g');
25
+ this.element.setAttribute('class', 'omd-cursor');
26
+ this.element.style.pointerEvents = 'none';
27
+ this.element.style.opacity = '0.8';
28
+
29
+ // Create different cursor shapes
30
+ this._createShapes();
31
+
32
+ // Initially hidden
33
+ this.hide();
34
+ }
35
+
36
+ /**
37
+ * Create different cursor shape elements
38
+ * @private
39
+ */
40
+ _createShapes() {
41
+ this.shapes = {};
42
+
43
+ // Default cursor (crosshair)
44
+ this.shapes.default = this._createCrosshair();
45
+
46
+ // Pointer cursor (arrow)
47
+ this.shapes.pointer = this._createPointerCursor();
48
+
49
+ // Pencil cursor
50
+ this.shapes.pencil = this._createPencilCursor();
51
+
52
+ // Eraser cursor
53
+ this.shapes.eraser = this._createEraserCursor();
54
+
55
+ // Select cursor
56
+ this.shapes.select = this._createSelectCursor();
57
+
58
+ // Add all shapes to cursor element
59
+ Object.values(this.shapes).forEach(shape => {
60
+ this.element.appendChild(shape);
61
+ });
62
+
63
+ // Show default shape initially
64
+ this.setShape('default');
65
+ }
66
+
67
+ /**
68
+ * Create crosshair cursor
69
+ * @private
70
+ */
71
+ _createCrosshair() {
72
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
73
+ group.setAttribute('data-shape', 'default');
74
+
75
+ // Horizontal line
76
+ const hLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
77
+ hLine.setAttribute('x1', '-10');
78
+ hLine.setAttribute('y1', '0');
79
+ hLine.setAttribute('x2', '10');
80
+ hLine.setAttribute('y2', '0');
81
+ hLine.setAttribute('stroke', this.color);
82
+ hLine.setAttribute('stroke-width', '1');
83
+
84
+ // Vertical line
85
+ const vLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
86
+ vLine.setAttribute('x1', '0');
87
+ vLine.setAttribute('y1', '-10');
88
+ vLine.setAttribute('x2', '0');
89
+ vLine.setAttribute('y2', '10');
90
+ vLine.setAttribute('stroke', this.color);
91
+ vLine.setAttribute('stroke-width', '1');
92
+
93
+ group.appendChild(hLine);
94
+ group.appendChild(vLine);
95
+
96
+ return group;
97
+ }
98
+
99
+ /**
100
+ * Create pencil cursor
101
+ * @private
102
+ */
103
+ _createPencilCursor() {
104
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
105
+ group.setAttribute('data-shape', 'pencil');
106
+
107
+ // Solid dot cursor
108
+ this.brushCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
109
+ this.brushCircle.setAttribute('cx', '0');
110
+ this.brushCircle.setAttribute('cy', '0');
111
+ this.brushCircle.setAttribute('r', this.size / 2);
112
+ this.brushCircle.setAttribute('fill', this.color);
113
+ this.brushCircle.setAttribute('stroke', 'none');
114
+
115
+ group.appendChild(this.brushCircle);
116
+
117
+ return group;
118
+ }
119
+
120
+ /**
121
+ * Create eraser cursor
122
+ * @private
123
+ */
124
+ _createEraserCursor() {
125
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
126
+ group.setAttribute('data-shape', 'eraser');
127
+
128
+ // Circle eraser indicator
129
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
130
+ circle.setAttribute('r', this.size / 2);
131
+ circle.setAttribute('fill', 'none');
132
+ circle.setAttribute('stroke', '#dc3545');
133
+ circle.setAttribute('stroke-width', '1.5');
134
+ circle.setAttribute('class', 'eraser-circle');
135
+
136
+ // Mode indicator (inner fill for radius mode)
137
+ const modeIndicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
138
+ modeIndicator.setAttribute('r', this.size / 3);
139
+ modeIndicator.setAttribute('fill', 'rgba(220, 53, 69, 0.15)');
140
+ modeIndicator.setAttribute('class', 'eraser-mode-indicator');
141
+ modeIndicator.style.display = 'none';
142
+
143
+ // X mark inside (for stroke mode)
144
+ const line1 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
145
+ line1.setAttribute('x1', -this.size / 5);
146
+ line1.setAttribute('y1', -this.size / 5);
147
+ line1.setAttribute('x2', this.size / 5);
148
+ line1.setAttribute('y2', this.size / 5);
149
+ line1.setAttribute('stroke', '#dc3545');
150
+ line1.setAttribute('stroke-width', '1.5');
151
+ line1.setAttribute('class', 'eraser-x1');
152
+
153
+ const line2 = document.createElementNS('http://www.w3.org/2000/svg', 'line');
154
+ line2.setAttribute('x1', this.size / 5);
155
+ line2.setAttribute('y1', -this.size / 5);
156
+ line2.setAttribute('x2', -this.size / 5);
157
+ line2.setAttribute('y2', this.size / 5);
158
+ line2.setAttribute('stroke', '#dc3545');
159
+ line2.setAttribute('stroke-width', '1.5');
160
+ line2.setAttribute('class', 'eraser-x2');
161
+
162
+ group.appendChild(circle);
163
+ group.appendChild(modeIndicator);
164
+ group.appendChild(line1);
165
+ group.appendChild(line2);
166
+
167
+ return group;
168
+ }
169
+
170
+ /**
171
+ * Create select cursor
172
+ * @private
173
+ */
174
+ _createSelectCursor() {
175
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
176
+ group.setAttribute('data-shape', 'select');
177
+
178
+ // Better arrow pointer - classic selection cursor
179
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
180
+ path.setAttribute('d', 'M 2,2 L 2,14 L 6,10 L 10,14 L 14,10 L 6,6 Z');
181
+ path.setAttribute('fill', '#333333');
182
+ path.setAttribute('stroke', 'white');
183
+ path.setAttribute('stroke-width', '1');
184
+ path.setAttribute('stroke-linejoin', 'round');
185
+
186
+ group.appendChild(path);
187
+
188
+ return group;
189
+ }
190
+
191
+ /**
192
+ * Create pointer cursor (standard arrow)
193
+ * @private
194
+ */
195
+ _createPointerCursor() {
196
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
197
+ group.setAttribute('data-shape', 'pointer');
198
+
199
+ // Standard arrow pointer cursor
200
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
201
+ path.setAttribute('d', 'M 0,0 L 0,16 L 5,11 L 8,16 L 10,15 L 7,10 L 12,10 Z');
202
+ path.setAttribute('fill', '#000000');
203
+ path.setAttribute('stroke', 'white');
204
+ path.setAttribute('stroke-width', '1');
205
+ path.setAttribute('stroke-linejoin', 'round');
206
+
207
+ group.appendChild(path);
208
+
209
+ return group;
210
+ }
211
+
212
+ /**
213
+ * Set cursor shape
214
+ * @param {string} shape - Shape name ('default', 'pencil', 'eraser', 'select')
215
+ */
216
+ setShape(shape) {
217
+ this.currentShape = shape;
218
+
219
+ // Hide all shapes
220
+ Object.values(this.shapes).forEach(shapeElement => {
221
+ shapeElement.style.display = 'none';
222
+ });
223
+
224
+ // Show current shape
225
+ if (this.shapes[shape]) {
226
+ this.shapes[shape].style.display = 'block';
227
+ } else {
228
+ this.shapes.default.style.display = 'block';
229
+ }
230
+
231
+ // Update brush size for pencil
232
+ if (shape === 'pencil' && this.brushCircle) {
233
+ this._updateBrushSize();
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Set cursor position
239
+ * @param {number} x - X coordinate
240
+ * @param {number} y - Y coordinate
241
+ */
242
+ setPosition(x, y) {
243
+ this.element.setAttribute('transform', `translate(${x}, ${y})`);
244
+ }
245
+
246
+ /**
247
+ * Show cursor
248
+ */
249
+ show() {
250
+ this.isVisible = true;
251
+ this.element.style.display = 'block';
252
+ }
253
+
254
+ /**
255
+ * Hide cursor
256
+ */
257
+ hide() {
258
+ this.isVisible = false;
259
+ this.element.style.display = 'none';
260
+ }
261
+
262
+ /**
263
+ * Set cursor size (for tools that support it)
264
+ * @param {number} size - Cursor size
265
+ */
266
+ setSize(size) {
267
+ this.size = size;
268
+ this._updateBrushSize();
269
+ }
270
+
271
+ /**
272
+ * Update brush size for pencil cursor
273
+ * @private
274
+ */
275
+ _updateBrushSize() {
276
+ if (this.brushCircle) {
277
+ this.brushCircle.setAttribute('r', this.size / 2);
278
+ }
279
+
280
+ // Update eraser size if applicable
281
+ const eraserShape = this.shapes.eraser;
282
+ if (eraserShape) {
283
+ const circle = eraserShape.querySelector('.eraser-circle');
284
+ const modeIndicator = eraserShape.querySelector('.eraser-mode-indicator');
285
+ const x1 = eraserShape.querySelector('.eraser-x1');
286
+ const x2 = eraserShape.querySelector('.eraser-x2');
287
+
288
+ if (circle) {
289
+ circle.setAttribute('r', this.size / 2);
290
+ }
291
+ if (modeIndicator) {
292
+ modeIndicator.setAttribute('r', this.size / 3);
293
+ }
294
+ if (x1) {
295
+ x1.setAttribute('x1', -this.size / 5);
296
+ x1.setAttribute('y1', -this.size / 5);
297
+ x1.setAttribute('x2', this.size / 5);
298
+ x1.setAttribute('y2', this.size / 5);
299
+ }
300
+ if (x2) {
301
+ x2.setAttribute('x1', this.size / 5);
302
+ x2.setAttribute('y1', -this.size / 5);
303
+ x2.setAttribute('x2', -this.size / 5);
304
+ x2.setAttribute('y2', this.size / 5);
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Set cursor color
311
+ * @param {string} color - CSS color value
312
+ */
313
+ setColor(color) {
314
+ this.color = color;
315
+
316
+ // Update all shape colors
317
+ this.element.querySelectorAll('[stroke]').forEach(element => {
318
+ if (element.getAttribute('stroke') === this.color) {
319
+ element.setAttribute('stroke', color);
320
+ }
321
+ });
322
+
323
+ this.element.querySelectorAll('[fill]').forEach(element => {
324
+ if (element.getAttribute('fill') === this.color) {
325
+ element.setAttribute('fill', color);
326
+ }
327
+ });
328
+ }
329
+
330
+ /**
331
+ * Set cursor opacity
332
+ * @param {number} opacity - Opacity value (0-1)
333
+ */
334
+ setOpacity(opacity) {
335
+ this.element.style.opacity = opacity;
336
+ }
337
+
338
+ /**
339
+ * Enable pressure feedback (for pressure-sensitive devices)
340
+ * @param {number} pressure - Pressure value (0-1)
341
+ */
342
+ setPressureFeedback(pressure) {
343
+ if (this.currentShape === 'pencil' && this.brushCircle) {
344
+ // Scale brush circle based on pressure
345
+ const scale = 0.5 + (pressure * 0.5); // Scale from 50% to 100%
346
+ this.brushCircle.setAttribute('transform', `scale(${scale})`);
347
+
348
+ // Adjust opacity based on pressure
349
+ const opacity = 0.3 + (pressure * 0.5); // Opacity from 30% to 80%
350
+ this.brushCircle.style.opacity = opacity;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Add temporary visual feedback
356
+ * @param {string} type - Feedback type ('success', 'error', 'info')
357
+ * @param {number} [duration=500] - Duration in milliseconds
358
+ */
359
+ showFeedback(type, duration = 500) {
360
+ const colors = {
361
+ success: '#28a745',
362
+ error: '#dc3545',
363
+ info: '#17a2b8'
364
+ };
365
+
366
+ const originalColor = this.color;
367
+ this.setColor(colors[type] || colors.info);
368
+
369
+ // Pulse animation
370
+ this.element.style.animation = 'pulse 0.3s ease-in-out';
371
+
372
+ setTimeout(() => {
373
+ this.setColor(originalColor);
374
+ this.element.style.animation = '';
375
+ }, duration);
376
+ }
377
+
378
+ /**
379
+ * Update cursor based on tool configuration
380
+ * @param {Object} toolConfig - Tool configuration
381
+ */
382
+ updateFromToolConfig(toolConfig) {
383
+ if (toolConfig.strokeWidth) {
384
+ // Scale the cursor size for better visibility - multiply by 4 for pencil to make it clearly visible
385
+ const scaledSize = this.currentShape === 'pencil' ? toolConfig.strokeWidth * 4 : toolConfig.strokeWidth;
386
+ this.setSize(scaledSize);
387
+ }
388
+
389
+ if (toolConfig.strokeColor) {
390
+ this.setColor(toolConfig.strokeColor);
391
+ }
392
+
393
+ if (toolConfig.size) {
394
+ this.setSize(toolConfig.size);
395
+ }
396
+
397
+ // Update eraser mode indicator
398
+ if (toolConfig.mode !== undefined && this.currentShape === 'eraser') {
399
+ this._updateEraserMode(toolConfig.mode);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Update eraser mode visual indicator
405
+ * @private
406
+ */
407
+ _updateEraserMode(mode) {
408
+ const eraserShape = this.shapes.eraser;
409
+ if (!eraserShape) return;
410
+
411
+ const circle = eraserShape.querySelector('.eraser-circle');
412
+ const modeIndicator = eraserShape.querySelector('.eraser-mode-indicator');
413
+ const x1 = eraserShape.querySelector('.eraser-x1');
414
+ const x2 = eraserShape.querySelector('.eraser-x2');
415
+
416
+ if (mode === 'radius') {
417
+ // Radius mode: show filled circle, hide X, use orange color
418
+ if (circle) {
419
+ circle.setAttribute('stroke', '#ff9f43');
420
+ circle.setAttribute('stroke-dasharray', '3,3');
421
+ }
422
+ if (modeIndicator) {
423
+ modeIndicator.style.display = 'block';
424
+ modeIndicator.setAttribute('fill', 'rgba(255, 159, 67, 0.3)');
425
+ }
426
+ if (x1) x1.style.display = 'none';
427
+ if (x2) x2.style.display = 'none';
428
+ } else {
429
+ // Stroke mode: show X, hide fill, use red color
430
+ if (circle) {
431
+ circle.setAttribute('stroke', '#dc3545');
432
+ circle.setAttribute('stroke-dasharray', 'none');
433
+ }
434
+ if (modeIndicator) {
435
+ modeIndicator.style.display = 'none';
436
+ }
437
+ if (x1) x1.style.display = 'block';
438
+ if (x2) x2.style.display = 'block';
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Get current cursor state
444
+ * @returns {Object} Cursor state
445
+ */
446
+ getState() {
447
+ return {
448
+ isVisible: this.isVisible,
449
+ shape: this.currentShape,
450
+ size: this.size,
451
+ color: this.color
452
+ };
453
+ }
454
+
455
+ /**
456
+ * Clean up cursor resources
457
+ */
458
+ destroy() {
459
+ if (this.element.parentNode) {
460
+ this.element.parentNode.removeChild(this.element);
461
+ }
462
+ }
438
463
  }