jellies-draw 0.1.11 → 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 +1 -1
- package/src/components/functions/canvas.js +57 -3
- package/src/components/functions/tools/eraser.js +256 -29
- package/src/components/functions/tools/pen.js +347 -36
- package/src/components/functions/tools/selector.js +38 -4
- package/src/components/functions/transformer.js +43 -0
package/package.json
CHANGED
|
@@ -35,8 +35,14 @@ export default {
|
|
|
35
35
|
Histories.record();
|
|
36
36
|
this.stage.on('pointerdown', this.pointerDownHandler);
|
|
37
37
|
this.stage.on('pointermove', this.pointerMoveHandler);
|
|
38
|
-
this.stage.on('pointerup', this.
|
|
38
|
+
this.stage.on('pointerup', this.handleCanvasPointerUp);
|
|
39
39
|
this.stage.on('click tap', this.clickHandler);
|
|
40
|
+
this.stage.on('pointerdown', () => {
|
|
41
|
+
if (Properties.isUsingDrawingTool) {
|
|
42
|
+
window.addEventListener('pointerup', this.handleGlobalPointerUp, { once: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
this.stage.on('mousemove', this._handleMouseMove.bind(this));
|
|
40
46
|
},
|
|
41
47
|
generateEventHandlers() {
|
|
42
48
|
this.resizeHandler = this._handleResize.bind(this);
|
|
@@ -44,7 +50,17 @@ export default {
|
|
|
44
50
|
this.keyupHandler = this._handleKeyup.bind(this);
|
|
45
51
|
this.pointerDownHandler = this.handlePointerDown.bind(this);
|
|
46
52
|
this.pointerMoveHandler = this.handlePointerMove.bind(this);
|
|
47
|
-
this.
|
|
53
|
+
this.handleCanvasPointerUp = (event) => {
|
|
54
|
+
if (event.target.getStage()) {
|
|
55
|
+
this.handlePointerUp(event);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
this.handleGlobalPointerUp = (event) => {
|
|
59
|
+
const container = this.stage.container();
|
|
60
|
+
if (!container.contains(event.target)) {
|
|
61
|
+
this.handlePointerUp(event);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
48
64
|
this.clickHandler = this.handleClick.bind(this);
|
|
49
65
|
},
|
|
50
66
|
clear() {
|
|
@@ -54,7 +70,7 @@ export default {
|
|
|
54
70
|
destroy() {
|
|
55
71
|
this.stage.off('pointerdown', this.pointerDownHandler);
|
|
56
72
|
this.stage.off('pointermove', this.pointerMoveHandler);
|
|
57
|
-
this.stage.off('pointerup', this.
|
|
73
|
+
this.stage.off('pointerup', this.handleCanvasPointerUp);
|
|
58
74
|
this.stage.off('click tap', this.clickHandler);
|
|
59
75
|
document.removeEventListener('keydown', this.keydownHandler);
|
|
60
76
|
document.removeEventListener('keyup', this.keyupHandler);
|
|
@@ -178,5 +194,43 @@ export default {
|
|
|
178
194
|
return;
|
|
179
195
|
}
|
|
180
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);
|
|
181
234
|
}
|
|
182
235
|
}
|
|
236
|
+
|
|
@@ -1,51 +1,278 @@
|
|
|
1
1
|
import Konva from 'konva'
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
|
+
|
|
4
5
|
export default {
|
|
5
|
-
temporalCurveTrajectory: null,
|
|
6
6
|
temporalShape: null,
|
|
7
7
|
startX: 0,
|
|
8
8
|
startY: 0,
|
|
9
|
+
lastX: 0,
|
|
10
|
+
lastY: 0,
|
|
11
|
+
erasedNodes: new Set(),
|
|
12
|
+
|
|
9
13
|
show({ offsetX, offsetY }) {
|
|
10
|
-
Canvas.isDrawing = true
|
|
14
|
+
Canvas.isDrawing = true
|
|
11
15
|
this.startX = offsetX
|
|
12
16
|
this.startY = offsetY
|
|
13
|
-
this.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
strokeWidth,
|
|
19
|
-
name: 'node',
|
|
20
|
-
bezier: true,
|
|
21
|
-
lineCap: 'round',
|
|
22
|
-
strokeScaleEnabled: false
|
|
23
|
-
});
|
|
17
|
+
this.lastX = offsetX
|
|
18
|
+
this.lastY = offsetY
|
|
19
|
+
this.erasedNodes.clear()
|
|
20
|
+
|
|
21
|
+
const newCurve = this._generateEraserLine(offsetX, offsetY)
|
|
24
22
|
this.temporalShape = newCurve
|
|
25
|
-
Canvas.layer.add(this.temporalShape)
|
|
23
|
+
Canvas.layer.add(this.temporalShape)
|
|
26
24
|
},
|
|
25
|
+
|
|
27
26
|
change({ offsetX, offsetY }) {
|
|
28
|
-
if (Canvas.isDrawing)
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
if (!Canvas.isDrawing) return
|
|
28
|
+
|
|
29
|
+
this._interpolateAndCheck(this.lastX, this.lastY, offsetX, offsetY)
|
|
30
|
+
this._updateEraserPath(offsetX, offsetY)
|
|
31
|
+
|
|
32
|
+
this.lastX = offsetX
|
|
33
|
+
this.lastY = offsetY
|
|
34
|
+
Canvas.layer.batchDraw()
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
finish() {
|
|
38
|
+
this._destroyErasedNodes()
|
|
39
|
+
this._cleanupTemporalShape()
|
|
40
|
+
Canvas.layer.batchDraw()
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
_generateEraserLine(x, y) {
|
|
44
|
+
return new Konva.Line({
|
|
45
|
+
points: [x, y],
|
|
46
|
+
stroke: '#000000',
|
|
47
|
+
strokeWidth: Properties.strokeWidth * 2,
|
|
48
|
+
opacity: 0,
|
|
49
|
+
lineCap: 'round',
|
|
50
|
+
lineJoin: 'round',
|
|
51
|
+
tension: 0.5,
|
|
52
|
+
strokeScaleEnabled: false
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
_updateEraserPath(x, y) {
|
|
57
|
+
const points = this.temporalShape.points()
|
|
58
|
+
points.push(x, y)
|
|
59
|
+
this.temporalShape.points(points)
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
_checkAndMarkIntersections(x, y) {
|
|
63
|
+
const eraserRadius = Properties.strokeWidth
|
|
64
|
+
|
|
65
|
+
Canvas.stage.find('.node').forEach(node => {
|
|
66
|
+
if (node === this.temporalShape) return
|
|
67
|
+
|
|
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)) {
|
|
78
|
+
this._markNodeForErasure(node)
|
|
79
|
+
}
|
|
31
80
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
_markNodeForErasure(node) {
|
|
85
|
+
node.opacity(0.2)
|
|
86
|
+
this.erasedNodes.add(node)
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
_destroyErasedNodes() {
|
|
90
|
+
this.erasedNodes.forEach(node => node.destroy())
|
|
91
|
+
this.erasedNodes.clear()
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
_cleanupTemporalShape() {
|
|
95
|
+
if (this.temporalShape) {
|
|
96
|
+
this.temporalShape.destroy()
|
|
97
|
+
this.temporalShape = null
|
|
37
98
|
}
|
|
38
99
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
100
|
+
|
|
101
|
+
_checkIntersection(node, point) {
|
|
102
|
+
if (node.attrs.nodeType === 'pen') {
|
|
103
|
+
return this._checkPenIntersection(node, point)
|
|
104
|
+
}
|
|
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
|
+
}
|
|
44
118
|
},
|
|
45
|
-
|
|
119
|
+
|
|
120
|
+
_checkPenIntersection(node, point) {
|
|
121
|
+
const localPoint = this._transformPointToLocal(node, point)
|
|
122
|
+
const { points, widths } = node.attrs
|
|
123
|
+
const eraserRadius = Properties.strokeWidth
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
126
|
+
const distance = this._getDistanceToLineSegment(
|
|
127
|
+
localPoint.x,
|
|
128
|
+
localPoint.y,
|
|
129
|
+
points[i][0],
|
|
130
|
+
points[i][1],
|
|
131
|
+
points[i + 1][0],
|
|
132
|
+
points[i + 1][1]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const combinedWidth = (widths[i] + eraserRadius) / 2
|
|
136
|
+
if (distance < combinedWidth) {
|
|
137
|
+
return true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
_checkDefaultIntersection(node) {
|
|
46
144
|
return Konva.Util.haveIntersection(
|
|
47
145
|
this.temporalShape.getClientRect(),
|
|
48
146
|
node.getClientRect()
|
|
49
|
-
)
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
_transformPointToLocal(node, point) {
|
|
151
|
+
const transform = node.getTransform()
|
|
152
|
+
return transform.copy().invert().point(point)
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
_getDistanceToLineSegment(px, py, x1, y1, x2, y2) {
|
|
156
|
+
const A = px - x1
|
|
157
|
+
const B = py - y1
|
|
158
|
+
const C = x2 - x1
|
|
159
|
+
const D = y2 - y1
|
|
160
|
+
|
|
161
|
+
const dot = A * C + B * D
|
|
162
|
+
const lenSq = C * C + D * D
|
|
163
|
+
const param = lenSq !== 0 ? dot / lenSq : -1
|
|
164
|
+
|
|
165
|
+
if (param < 0) return this._getDistance(px, py, x1, y1)
|
|
166
|
+
if (param > 1) return this._getDistance(px, py, x2, y2)
|
|
167
|
+
|
|
168
|
+
return this._getDistance(
|
|
169
|
+
px,
|
|
170
|
+
py,
|
|
171
|
+
x1 + param * C,
|
|
172
|
+
y1 + param * D
|
|
173
|
+
)
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
_getDistance(x1, y1, x2, y2) {
|
|
177
|
+
const dx = x1 - x2
|
|
178
|
+
const dy = y1 - y2
|
|
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)
|
|
50
277
|
}
|
|
51
278
|
}
|
|
@@ -2,62 +2,373 @@ import Konva from 'konva'
|
|
|
2
2
|
import Properties from '../properties'
|
|
3
3
|
import Canvas from '../canvas'
|
|
4
4
|
import Histories from '../histories'
|
|
5
|
+
|
|
5
6
|
export default {
|
|
6
|
-
temporalCurveTrajectory: null,
|
|
7
7
|
temporalShape: null,
|
|
8
8
|
startX: 0,
|
|
9
9
|
startY: 0,
|
|
10
|
+
lastX: 0,
|
|
11
|
+
lastY: 0,
|
|
12
|
+
lastTime: 0,
|
|
13
|
+
points: [],
|
|
14
|
+
widths: [],
|
|
15
|
+
boundMinX: 0,
|
|
16
|
+
boundMinY: 0,
|
|
17
|
+
boundMaxX: 0,
|
|
18
|
+
boundMaxY: 0,
|
|
19
|
+
|
|
10
20
|
show({ offsetX, offsetY }) {
|
|
11
|
-
Canvas.isDrawing = true
|
|
21
|
+
Canvas.isDrawing = true
|
|
12
22
|
this.startX = offsetX
|
|
13
23
|
this.startY = offsetY
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
this.lastX = offsetX
|
|
25
|
+
this.lastY = offsetY
|
|
26
|
+
this.lastTime = Date.now()
|
|
27
|
+
|
|
28
|
+
this.boundMinX = offsetX
|
|
29
|
+
this.boundMinY = offsetY
|
|
30
|
+
this.boundMaxX = offsetX
|
|
31
|
+
this.boundMaxY = offsetY
|
|
32
|
+
|
|
33
|
+
this.points = [[0, 0]]
|
|
34
|
+
this.widths = [Properties.strokeWidth]
|
|
35
|
+
|
|
17
36
|
const newCurve = this.generate({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
x: offsetX,
|
|
38
|
+
y: offsetY,
|
|
39
|
+
points: this.points,
|
|
40
|
+
widths: this.widths,
|
|
41
|
+
stroke: Properties.strokeColor
|
|
21
42
|
})
|
|
43
|
+
|
|
22
44
|
this.temporalShape = newCurve
|
|
23
45
|
Canvas.layer.add(this.temporalShape)
|
|
24
46
|
},
|
|
47
|
+
|
|
25
48
|
change({ offsetX, offsetY }) {
|
|
26
|
-
if (Canvas.isDrawing)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
if (!Canvas.isDrawing) return
|
|
50
|
+
|
|
51
|
+
const currentTime = Date.now()
|
|
52
|
+
const strokeWidth = this._calculateStrokeWidth(offsetX, offsetY, currentTime)
|
|
53
|
+
|
|
54
|
+
this._updateBoundaries(offsetX, offsetY)
|
|
55
|
+
|
|
56
|
+
const relativeX = offsetX - this.startX
|
|
57
|
+
const relativeY = offsetY - this.startY
|
|
58
|
+
|
|
59
|
+
this.points.push([relativeX, relativeY])
|
|
60
|
+
this.widths.push(strokeWidth)
|
|
61
|
+
|
|
62
|
+
Canvas.layer.batchDraw()
|
|
63
|
+
|
|
64
|
+
this.lastX = offsetX
|
|
65
|
+
this.lastY = offsetY
|
|
66
|
+
this.lastTime = currentTime
|
|
33
67
|
},
|
|
68
|
+
|
|
34
69
|
finish() {
|
|
70
|
+
if (!this.temporalShape) return
|
|
71
|
+
|
|
72
|
+
const maxStrokeWidth = Math.max(...this.widths)
|
|
73
|
+
const padding = maxStrokeWidth
|
|
74
|
+
|
|
75
|
+
this._adjustShapeBoundaries(padding)
|
|
76
|
+
Canvas.layer.batchDraw()
|
|
35
77
|
this.temporalShape = null
|
|
36
78
|
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
|
|
80
|
+
generate({ points, widths, stroke, x, y, width, height }) {
|
|
81
|
+
const tool = this
|
|
82
|
+
|
|
83
|
+
const shape = new Konva.Shape({
|
|
84
|
+
x: x || 0,
|
|
85
|
+
y: y || 0,
|
|
86
|
+
width: width || 0,
|
|
87
|
+
height: height || 0,
|
|
46
88
|
stroke,
|
|
47
|
-
|
|
89
|
+
points,
|
|
90
|
+
widths,
|
|
48
91
|
name: 'node',
|
|
49
92
|
nodeType: 'pen',
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
93
|
+
strokeScaleEnabled: false,
|
|
94
|
+
|
|
95
|
+
sceneFunc: (context, shape) => {
|
|
96
|
+
const smoothPoints = tool._calculateSmoothPoints(shape.attrs.points)
|
|
97
|
+
tool._drawSmoothLine(context, smoothPoints, shape.attrs.widths, shape.attrs.stroke)
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
hitFunc: (context, shape) => {
|
|
101
|
+
const smoothPoints = tool._calculateSmoothPoints(shape.attrs.points)
|
|
102
|
+
tool._drawHitPath(context, smoothPoints, shape.attrs.widths)
|
|
103
|
+
context.fillStrokeShape(shape)
|
|
104
|
+
}
|
|
53
105
|
})
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
106
|
+
|
|
107
|
+
this.bindEvents(shape)
|
|
108
|
+
return shape
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
bindEvents(shape) {
|
|
112
|
+
shape.off('transformend')
|
|
113
|
+
shape.off('dragend')
|
|
114
|
+
shape.on('transformend', Histories.record)
|
|
115
|
+
shape.on('dragend', Histories.record)
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
_calculateStrokeWidth(offsetX, offsetY, currentTime) {
|
|
119
|
+
const timeDiff = currentTime - this.lastTime
|
|
120
|
+
const distance = Math.sqrt(
|
|
121
|
+
Math.pow(offsetX - this.lastX, 2) +
|
|
122
|
+
Math.pow(offsetY - this.lastY, 2)
|
|
123
|
+
)
|
|
124
|
+
const velocity = distance / (timeDiff + 1)
|
|
125
|
+
|
|
126
|
+
const baseWidth = Properties.strokeWidth
|
|
127
|
+
const minWidth = baseWidth * 0.7
|
|
128
|
+
const maxWidth = baseWidth * 1.2
|
|
129
|
+
|
|
130
|
+
if (velocity > 0.7) return minWidth
|
|
131
|
+
if (velocity < 0.2) return maxWidth
|
|
132
|
+
return maxWidth - (velocity - 0.2) * (maxWidth - minWidth) / 0.5
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
_updateBoundaries(x, y) {
|
|
136
|
+
this.boundMinX = Math.min(this.boundMinX, x)
|
|
137
|
+
this.boundMinY = Math.min(this.boundMinY, y)
|
|
138
|
+
this.boundMaxX = Math.max(this.boundMaxX, x)
|
|
139
|
+
this.boundMaxY = Math.max(this.boundMaxY, y)
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
_adjustShapeBoundaries(padding) {
|
|
143
|
+
this.temporalShape.x(this.boundMinX - padding)
|
|
144
|
+
this.temporalShape.y(this.boundMinY - padding)
|
|
145
|
+
this.temporalShape.width(this.boundMaxX - this.boundMinX + padding * 2)
|
|
146
|
+
this.temporalShape.height(this.boundMaxY - this.boundMinY + padding * 2)
|
|
147
|
+
|
|
148
|
+
const adjustedPoints = this.points.map(point => [
|
|
149
|
+
point[0] - (this.boundMinX - this.startX - padding),
|
|
150
|
+
point[1] - (this.boundMinY - this.startY - padding)
|
|
151
|
+
])
|
|
152
|
+
this.temporalShape.attrs.points = adjustedPoints
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
_drawSmoothLine(context, points, widths, stroke) {
|
|
156
|
+
context.beginPath()
|
|
157
|
+
context.strokeStyle = stroke
|
|
158
|
+
context.lineCap = 'round'
|
|
159
|
+
context.lineJoin = 'round'
|
|
160
|
+
context.moveTo(points[0][0], points[0][1])
|
|
161
|
+
|
|
162
|
+
for (let i = 1; i < points.length; i++) {
|
|
163
|
+
const width = this._interpolateWidth(i, points.length, widths)
|
|
164
|
+
context.lineWidth = width
|
|
165
|
+
context.lineTo(points[i][0], points[i][1])
|
|
166
|
+
context.stroke()
|
|
167
|
+
context.beginPath()
|
|
168
|
+
context.moveTo(points[i][0], points[i][1])
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
_drawHitPath(context, points, widths) {
|
|
173
|
+
context.beginPath()
|
|
174
|
+
context.lineCap = 'round'
|
|
175
|
+
context.lineJoin = 'round'
|
|
176
|
+
context.moveTo(points[0][0], points[0][1])
|
|
177
|
+
|
|
178
|
+
this._drawForwardPath(context, points, widths)
|
|
179
|
+
this._drawReversePath(context, points, widths)
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
_drawForwardPath(context, points, widths) {
|
|
183
|
+
for (let i = 1; i < points.length; i++) {
|
|
184
|
+
const width = this._interpolateWidth(i, points.length, widths) + 10
|
|
185
|
+
context.lineWidth = width
|
|
186
|
+
context.lineTo(points[i][0], points[i][1])
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
_drawReversePath(context, points, widths) {
|
|
191
|
+
for (let i = points.length - 1; i >= 0; i--) {
|
|
192
|
+
const width = this._interpolateWidth(i, points.length, widths) + 10
|
|
193
|
+
context.lineWidth = width
|
|
194
|
+
context.lineTo(points[i][0], points[i][1])
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
_interpolateWidth(index, total, widths) {
|
|
199
|
+
const progress = index / total
|
|
200
|
+
const widthIndex = Math.floor(progress * (widths.length - 1))
|
|
201
|
+
const nextWidthIndex = Math.min(widthIndex + 1, widths.length - 1)
|
|
202
|
+
const widthProgress = progress * (widths.length - 1) - widthIndex
|
|
203
|
+
return widths[widthIndex] * (1 - widthProgress) +
|
|
204
|
+
widths[nextWidthIndex] * widthProgress
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
_calculateSmoothPoints(points) {
|
|
208
|
+
// 首先检查是否是近似直线
|
|
209
|
+
const isNearlyLine = this._checkIfNearlyLine(points)
|
|
210
|
+
|
|
211
|
+
// 如果是近似直线,使用 Douglas-Peucker 算法简化
|
|
212
|
+
if (isNearlyLine) {
|
|
213
|
+
return this._simplifyDouglasPeucker(points, 2)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const smoothPoints = []
|
|
217
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
218
|
+
const p0 = i > 0 ? points[i - 1] : points[i]
|
|
219
|
+
const p1 = points[i]
|
|
220
|
+
const p2 = points[i + 1]
|
|
221
|
+
const p3 = i < points.length - 2 ? points[i + 2] : p2
|
|
222
|
+
|
|
223
|
+
// 检查是否是锐角
|
|
224
|
+
if (i > 0 && i < points.length - 2) {
|
|
225
|
+
const angle = this._getAngle(p1, p2, p0)
|
|
226
|
+
|
|
227
|
+
if (angle < Math.PI * 0.6 && this._getSegmentLength(p1, p2) > 20) {
|
|
228
|
+
smoothPoints.push(p1)
|
|
229
|
+
smoothPoints.push(p2)
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._addCatmullRomPoints(smoothPoints, p0, p1, p2, p3)
|
|
235
|
+
}
|
|
236
|
+
smoothPoints.push(points[points.length - 1])
|
|
237
|
+
return smoothPoints
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
_checkIfNearlyLine(points) {
|
|
241
|
+
if (points.length < 3) return true
|
|
242
|
+
|
|
243
|
+
// 计算所有点到起点和终点连线的最大偏差
|
|
244
|
+
const start = points[0]
|
|
245
|
+
const end = points[points.length - 1]
|
|
246
|
+
let maxDeviation = 0
|
|
247
|
+
|
|
248
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
249
|
+
const deviation = this._getPointToLineDistance(
|
|
250
|
+
points[i],
|
|
251
|
+
start,
|
|
252
|
+
end
|
|
253
|
+
)
|
|
254
|
+
maxDeviation = Math.max(maxDeviation, deviation)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 检查所有相邻点之间的角度
|
|
258
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
259
|
+
const angle = this._getAngle(points[i-1], points[i], points[i+1])
|
|
260
|
+
// 176度 ≈ 3.07 弧度
|
|
261
|
+
if (angle < 3.07) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 如果最大偏差小于阈值且所有角度都接近180度,认为是近似直线
|
|
267
|
+
return maxDeviation < 5
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
_getPointToLineDistance(point, lineStart, lineEnd) {
|
|
271
|
+
const dx = lineEnd[0] - lineStart[0]
|
|
272
|
+
const dy = lineEnd[1] - lineStart[1]
|
|
273
|
+
const lineLengthSq = dx * dx + dy * dy
|
|
274
|
+
|
|
275
|
+
if (lineLengthSq === 0) return 0
|
|
276
|
+
|
|
277
|
+
const t = ((point[0] - lineStart[0]) * dx +
|
|
278
|
+
(point[1] - lineStart[1]) * dy) / lineLengthSq
|
|
279
|
+
|
|
280
|
+
if (t < 0) return this._getSegmentLength(point, lineStart)
|
|
281
|
+
if (t > 1) return this._getSegmentLength(point, lineEnd)
|
|
282
|
+
|
|
283
|
+
const projX = lineStart[0] + t * dx
|
|
284
|
+
const projY = lineStart[1] + t * dy
|
|
285
|
+
|
|
286
|
+
return Math.sqrt(
|
|
287
|
+
Math.pow(point[0] - projX, 2) +
|
|
288
|
+
Math.pow(point[1] - projY, 2)
|
|
289
|
+
)
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
_getAngle(p1, p2, p0) {
|
|
293
|
+
const v1 = [p1[0] - p0[0], p1[1] - p0[1]]
|
|
294
|
+
const v2 = [p2[0] - p1[0], p2[1] - p1[1]]
|
|
295
|
+
|
|
296
|
+
// 计算向量夹角
|
|
297
|
+
const dot = v1[0] * v2[0] + v1[1] * v2[1]
|
|
298
|
+
const len1 = Math.sqrt(v1[0] * v1[0] + v1[1] * v1[1])
|
|
299
|
+
const len2 = Math.sqrt(v2[0] * v2[0] + v2[1] * v2[1])
|
|
300
|
+
|
|
301
|
+
return Math.acos(dot / (len1 * len2))
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
_getSegmentLength(p1, p2) {
|
|
305
|
+
const dx = p2[0] - p1[0]
|
|
306
|
+
const dy = p2[1] - p1[1]
|
|
307
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
_addCatmullRomPoints(smoothPoints, p0, p1, p2, p3) {
|
|
311
|
+
for (let t = 0; t < 1; t += 0.1) {
|
|
312
|
+
const t2 = t * t
|
|
313
|
+
const t3 = t2 * t
|
|
314
|
+
|
|
315
|
+
const x = 0.5 * (
|
|
316
|
+
(2 * p1[0]) +
|
|
317
|
+
(-p0[0] + p2[0]) * t +
|
|
318
|
+
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
|
|
319
|
+
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3
|
|
320
|
+
)
|
|
321
|
+
const y = 0.5 * (
|
|
322
|
+
(2 * p1[1]) +
|
|
323
|
+
(-p0[1] + p2[1]) * t +
|
|
324
|
+
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
|
|
325
|
+
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
smoothPoints.push([x, y])
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
_simplifyDouglasPeucker(points, epsilon) {
|
|
333
|
+
if (points.length <= 2) return points
|
|
334
|
+
|
|
335
|
+
// 找到最远点
|
|
336
|
+
let maxDistance = 0
|
|
337
|
+
let maxIndex = 0
|
|
338
|
+
|
|
339
|
+
const firstPoint = points[0]
|
|
340
|
+
const lastPoint = points[points.length - 1]
|
|
341
|
+
|
|
342
|
+
// 找到距离首尾连线最远的点
|
|
343
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
344
|
+
const distance = this._getPointToLineDistance(
|
|
345
|
+
points[i],
|
|
346
|
+
firstPoint,
|
|
347
|
+
lastPoint
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if (distance > maxDistance) {
|
|
351
|
+
maxDistance = distance
|
|
352
|
+
maxIndex = i
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// 如果最大距离大于阈值,递归处理两个子段
|
|
357
|
+
if (maxDistance > epsilon) {
|
|
358
|
+
const firstHalf = this._simplifyDouglasPeucker(
|
|
359
|
+
points.slice(0, maxIndex + 1),
|
|
360
|
+
epsilon
|
|
361
|
+
)
|
|
362
|
+
const secondHalf = this._simplifyDouglasPeucker(
|
|
363
|
+
points.slice(maxIndex),
|
|
364
|
+
epsilon
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
// 合并两个子段,去掉重复的中间点
|
|
368
|
+
return firstHalf.slice(0, -1).concat(secondHalf)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 如果最大距离小于阈值,只保留首尾两点
|
|
372
|
+
return [firstPoint, lastPoint]
|
|
62
373
|
}
|
|
63
374
|
}
|
|
@@ -39,9 +39,43 @@ export default {
|
|
|
39
39
|
return this.selector !== null
|
|
40
40
|
},
|
|
41
41
|
_isInSelector(node) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const selectorRect = this.selector.getClientRect()
|
|
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
|
|
46
80
|
}
|
|
47
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
|
}
|