jellies-draw 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jellies-draw",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A drawer for jellies",
5
5
  "private": false,
6
6
  "main": "./src/index.js",
@@ -42,6 +42,7 @@ export default {
42
42
  window.addEventListener('pointerup', this.handleGlobalPointerUp, { once: true });
43
43
  }
44
44
  });
45
+ this.stage.on('mousemove', this._handleMouseMove.bind(this));
45
46
  },
46
47
  generateEventHandlers() {
47
48
  this.resizeHandler = this._handleResize.bind(this);
@@ -193,6 +194,43 @@ export default {
193
194
  return;
194
195
  }
195
196
  Transformer.selectNode(targetNode, metaPressed);
197
+ },
198
+ _handleMouseMove(e) {
199
+ if (Properties.isUsingDrawingTool || Properties.isUsingText) {
200
+ return;
201
+ }
202
+
203
+ const target = e.target;
204
+
205
+ // Reset cursor to default when over stage
206
+ if (target === this.stage) {
207
+ this.stage.container().style.cursor = 'default';
208
+ return;
209
+ }
210
+
211
+ // Handle cursor for nodes
212
+ if (target.hasName('node')) {
213
+ if (this._shouldShowMoveCursor(target)) {
214
+ this.stage.container().style.cursor = 'move';
215
+ } else {
216
+ this.stage.container().style.cursor = 'default';
217
+ }
218
+ }
219
+ },
220
+ _shouldShowMoveCursor(node) {
221
+ // Always show move cursor for non-shape nodes
222
+ if (!['Rect', 'Circle', 'Ellipse'].includes(node.className)) {
223
+ return true;
224
+ }
225
+
226
+ // Show move cursor for filled shapes
227
+ if (node.attrs.fill && node.attrs.fill !== 'transparent') {
228
+ return true;
229
+ }
230
+
231
+ // For unfilled shapes, only show move cursor near border
232
+ const mousePos = this.stage.getPointerPosition();
233
+ return Transformer._isNearBorder(node, mousePos);
196
234
  }
197
235
  }
198
236
 
@@ -26,8 +26,8 @@ export default {
26
26
  change({ offsetX, offsetY }) {
27
27
  if (!Canvas.isDrawing) return
28
28
 
29
+ this._interpolateAndCheck(this.lastX, this.lastY, offsetX, offsetY)
29
30
  this._updateEraserPath(offsetX, offsetY)
30
- this._checkAndMarkIntersections(offsetX, offsetY)
31
31
 
32
32
  this.lastX = offsetX
33
33
  this.lastY = offsetY
@@ -60,11 +60,21 @@ export default {
60
60
  },
61
61
 
62
62
  _checkAndMarkIntersections(x, y) {
63
+ const eraserRadius = Properties.strokeWidth
64
+
63
65
  Canvas.stage.find('.node').forEach(node => {
64
66
  if (node === this.temporalShape) return
65
67
 
66
- if (this._checkIntersection(node, { x, y })) {
67
- if (!this.erasedNodes.has(node)) {
68
+ const nodeRect = node.getClientRect()
69
+ const eraserRect = {
70
+ x: x - eraserRadius,
71
+ y: y - eraserRadius,
72
+ width: eraserRadius * 2,
73
+ height: eraserRadius * 2
74
+ }
75
+
76
+ if (this._checkRectOverlap(nodeRect, eraserRect)) {
77
+ if (this._checkIntersection(node, { x, y }) && !this.erasedNodes.has(node)) {
68
78
  this._markNodeForErasure(node)
69
79
  }
70
80
  }
@@ -92,24 +102,38 @@ export default {
92
102
  if (node.attrs.nodeType === 'pen') {
93
103
  return this._checkPenIntersection(node, point)
94
104
  }
95
- return this._checkDefaultIntersection(node)
105
+
106
+ switch (node.className) {
107
+ case 'Line':
108
+ case 'Arrow':
109
+ return this._checkLineIntersection(node, point)
110
+ case 'Circle':
111
+ case 'Ellipse':
112
+ return this._checkEllipseIntersection(node, point)
113
+ case 'Rect':
114
+ return this._checkRectIntersection(node, point)
115
+ default:
116
+ return this._checkDefaultIntersection(node)
117
+ }
96
118
  },
97
119
 
98
120
  _checkPenIntersection(node, point) {
99
121
  const localPoint = this._transformPointToLocal(node, point)
100
- const { points: pts, widths } = node.attrs
122
+ const { points, widths } = node.attrs
123
+ const eraserRadius = Properties.strokeWidth
101
124
 
102
- for (let i = 0; i < pts.length - 1; i++) {
125
+ for (let i = 0; i < points.length - 1; i++) {
103
126
  const distance = this._getDistanceToLineSegment(
104
127
  localPoint.x,
105
128
  localPoint.y,
106
- pts[i][0],
107
- pts[i][1],
108
- pts[i + 1][0],
109
- pts[i + 1][1]
129
+ points[i][0],
130
+ points[i][1],
131
+ points[i + 1][0],
132
+ points[i + 1][1]
110
133
  )
111
134
 
112
- if (distance < (widths[i] + Properties.strokeWidth) / 2) {
135
+ const combinedWidth = (widths[i] + eraserRadius) / 2
136
+ if (distance < combinedWidth) {
113
137
  return true
114
138
  }
115
139
  }
@@ -153,5 +177,102 @@ export default {
153
177
  const dx = x1 - x2
154
178
  const dy = y1 - y2
155
179
  return Math.sqrt(dx * dx + dy * dy)
180
+ },
181
+
182
+ _checkLineIntersection(node, point) {
183
+ const points = node.points()
184
+ const transform = node.getTransform()
185
+ const localPoint = transform.copy().invert().point(point)
186
+ const eraserRadius = Properties.strokeWidth
187
+
188
+ for (let i = 0; i < points.length - 2; i += 2) {
189
+ const distance = this._getDistanceToLineSegment(
190
+ localPoint.x,
191
+ localPoint.y,
192
+ points[i],
193
+ points[i + 1],
194
+ points[i + 2],
195
+ points[i + 3]
196
+ )
197
+
198
+ const combinedWidth = (node.strokeWidth() + eraserRadius) / 2
199
+ if (distance < combinedWidth) {
200
+ return true
201
+ }
202
+ }
203
+ return false
204
+ },
205
+
206
+ _checkEllipseIntersection(node, point) {
207
+ const transform = node.getTransform()
208
+ const localPoint = transform.copy().invert().point(point)
209
+
210
+ const radiusX = node.radiusX()
211
+ const radiusY = node.radiusY()
212
+
213
+ // Normalize point to unit circle
214
+ const normalizedX = localPoint.x / radiusX
215
+ const normalizedY = localPoint.y / radiusY
216
+
217
+ // Check if point is within ellipse border
218
+ const distanceFromCenter = Math.sqrt(
219
+ normalizedX * normalizedX + normalizedY * normalizedY
220
+ )
221
+
222
+ if (node.attrs.fill && node.attrs.fill !== 'transparent') {
223
+ // For filled ellipses, allow erasing anywhere inside
224
+ return distanceFromCenter <= 1
225
+ }
226
+ // For unfilled ellipses, only erase near the border
227
+ return Math.abs(distanceFromCenter - 1) < Properties.strokeWidth / Math.min(radiusX, radiusY)
228
+ },
229
+
230
+ _checkRectIntersection(node, point) {
231
+ const transform = node.getTransform()
232
+ const localPoint = transform.copy().invert().point(point)
233
+
234
+ const width = node.width()
235
+ const height = node.height()
236
+ const strokeWidth = Properties.strokeWidth
237
+
238
+ if (node.attrs.fill && node.attrs.fill !== 'transparent') {
239
+ // For filled rectangles, allow erasing anywhere inside
240
+ return localPoint.x >= -strokeWidth &&
241
+ localPoint.x <= width + strokeWidth &&
242
+ localPoint.y >= -strokeWidth &&
243
+ localPoint.y <= height + strokeWidth
244
+ }
245
+ // For unfilled rectangles, only erase near the border
246
+ return (Math.abs(localPoint.x) < strokeWidth ||
247
+ Math.abs(localPoint.x - width) < strokeWidth ||
248
+ Math.abs(localPoint.y) < strokeWidth ||
249
+ Math.abs(localPoint.y - height) < strokeWidth) &&
250
+ localPoint.x >= -strokeWidth &&
251
+ localPoint.x <= width + strokeWidth &&
252
+ localPoint.y >= -strokeWidth &&
253
+ localPoint.y <= height + strokeWidth
254
+ },
255
+
256
+ _interpolateAndCheck(x1, y1, x2, y2) {
257
+ const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
258
+ const steps = Math.ceil(distance / (Properties.strokeWidth / 2))
259
+
260
+ if (steps > 1) {
261
+ for (let i = 1; i < steps; i++) {
262
+ const ratio = i / steps
263
+ const x = x1 + (x2 - x1) * ratio
264
+ const y = y1 + (y2 - y1) * ratio
265
+ this._checkAndMarkIntersections(x, y)
266
+ }
267
+ } else {
268
+ this._checkAndMarkIntersections(x2, y2)
269
+ }
270
+ },
271
+
272
+ _checkRectOverlap(rect1, rect2) {
273
+ return !(rect1.x > rect2.x + rect2.width ||
274
+ rect1.x + rect1.width < rect2.x ||
275
+ rect1.y > rect2.y + rect2.height ||
276
+ rect1.y + rect1.height < rect2.y)
156
277
  }
157
278
  }
@@ -39,8 +39,43 @@ export default {
39
39
  return this.selector !== null
40
40
  },
41
41
  _isInSelector(node) {
42
- const nodeRect = node.getClientRect()
43
42
  const selectorRect = this.selector.getClientRect()
44
- return Konva.Util.haveIntersection(selectorRect, nodeRect)
43
+
44
+ if (node.className === 'Line' || node.className === 'Arrow') {
45
+ return this._isLineFullyContained(node, selectorRect)
46
+ }
47
+
48
+ return this._isShapeFullyContained(node, selectorRect)
49
+ },
50
+ _isLineFullyContained(node, selectorRect) {
51
+ const points = node.points()
52
+ for (let i = 0; i < points.length; i += 2) {
53
+ const point = node.getAbsoluteTransform().point({
54
+ x: points[i],
55
+ y: points[i + 1]
56
+ })
57
+
58
+ if (!this._isPointInRect(point, selectorRect)) {
59
+ return false
60
+ }
61
+ }
62
+ return true
63
+ },
64
+ _isShapeFullyContained(node, selectorRect) {
65
+ const nodeRect = node.getClientRect()
66
+ const corners = [
67
+ { x: nodeRect.x, y: nodeRect.y }, // top-left
68
+ { x: nodeRect.x + nodeRect.width, y: nodeRect.y }, // top-right
69
+ { x: nodeRect.x, y: nodeRect.y + nodeRect.height }, // bottom-left
70
+ { x: nodeRect.x + nodeRect.width, y: nodeRect.y + nodeRect.height } // bottom-right
71
+ ]
72
+
73
+ return corners.every(point => this._isPointInRect(point, selectorRect))
74
+ },
75
+ _isPointInRect(point, rect) {
76
+ return point.x >= rect.x &&
77
+ point.x <= rect.x + rect.width &&
78
+ point.y >= rect.y &&
79
+ point.y <= rect.y + rect.height
45
80
  }
46
81
  }
@@ -39,6 +39,10 @@ export default {
39
39
  });
40
40
  },
41
41
  selectNode(targetNode, isAppending = false) {
42
+ if (!this._shouldSelectNode(targetNode)) {
43
+ return;
44
+ }
45
+
42
46
  const isSelected = this._nodes().indexOf(targetNode) >= 0 || this.linearTransformer.node === targetNode;
43
47
  if (!isAppending && !isSelected) {
44
48
  this.selectNodes([targetNode]);
@@ -211,5 +215,44 @@ export default {
211
215
  return hasOnlyTextNodes;
212
216
  }
213
217
  return false;
218
+ },
219
+ _shouldSelectNode(node) {
220
+ if ((node.className === 'Rect' || node.className === 'Circle' || node.className === 'Ellipse') &&
221
+ (!node.attrs.fill || node.attrs.fill === 'transparent')) {
222
+ const stage = Canvas.stage;
223
+ const mousePos = stage.getPointerPosition();
224
+ return this._isNearBorder(node, mousePos);
225
+ }
226
+ return true;
227
+ },
228
+ _isNearBorder(node, point) {
229
+ const localPoint = node.getAbsoluteTransform().copy().invert().point(point);
230
+ const threshold = Properties.strokeWidth * 2;
231
+
232
+ if (node.className === 'Rect') {
233
+ const width = node.width();
234
+ const height = node.height();
235
+
236
+ return (Math.abs(localPoint.x) < threshold ||
237
+ Math.abs(localPoint.x - width) < threshold ||
238
+ Math.abs(localPoint.y) < threshold ||
239
+ Math.abs(localPoint.y - height) < threshold) &&
240
+ localPoint.x >= -threshold &&
241
+ localPoint.x <= width + threshold &&
242
+ localPoint.y >= -threshold &&
243
+ localPoint.y <= height + threshold;
244
+ } else if (node.className === 'Circle' || node.className === 'Ellipse') {
245
+ const radiusX = node.radiusX ? node.radiusX() : node.radius();
246
+ const radiusY = node.radiusY ? node.radiusY() : node.radius();
247
+
248
+ const normalizedX = localPoint.x / radiusX;
249
+ const normalizedY = localPoint.y / radiusY;
250
+
251
+ const distanceFromCenter = Math.sqrt(
252
+ normalizedX * normalizedX + normalizedY * normalizedY
253
+ );
254
+ return Math.abs(distanceFromCenter - 1) < threshold / Math.min(radiusX, radiusY);
255
+ }
256
+ return true;
214
257
  }
215
258
  }