jellies-draw 0.1.11 → 0.2.0
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
|
@@ -35,8 +35,13 @@ 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
|
+
});
|
|
40
45
|
},
|
|
41
46
|
generateEventHandlers() {
|
|
42
47
|
this.resizeHandler = this._handleResize.bind(this);
|
|
@@ -44,7 +49,17 @@ export default {
|
|
|
44
49
|
this.keyupHandler = this._handleKeyup.bind(this);
|
|
45
50
|
this.pointerDownHandler = this.handlePointerDown.bind(this);
|
|
46
51
|
this.pointerMoveHandler = this.handlePointerMove.bind(this);
|
|
47
|
-
this.
|
|
52
|
+
this.handleCanvasPointerUp = (event) => {
|
|
53
|
+
if (event.target.getStage()) {
|
|
54
|
+
this.handlePointerUp(event);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
this.handleGlobalPointerUp = (event) => {
|
|
58
|
+
const container = this.stage.container();
|
|
59
|
+
if (!container.contains(event.target)) {
|
|
60
|
+
this.handlePointerUp(event);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
48
63
|
this.clickHandler = this.handleClick.bind(this);
|
|
49
64
|
},
|
|
50
65
|
clear() {
|
|
@@ -54,7 +69,7 @@ export default {
|
|
|
54
69
|
destroy() {
|
|
55
70
|
this.stage.off('pointerdown', this.pointerDownHandler);
|
|
56
71
|
this.stage.off('pointermove', this.pointerMoveHandler);
|
|
57
|
-
this.stage.off('pointerup', this.
|
|
72
|
+
this.stage.off('pointerup', this.handleCanvasPointerUp);
|
|
58
73
|
this.stage.off('click tap', this.clickHandler);
|
|
59
74
|
document.removeEventListener('keydown', this.keydownHandler);
|
|
60
75
|
document.removeEventListener('keyup', this.keyupHandler);
|
|
@@ -180,3 +195,4 @@ export default {
|
|
|
180
195
|
Transformer.selectNode(targetNode, metaPressed);
|
|
181
196
|
}
|
|
182
197
|
}
|
|
198
|
+
|
|
@@ -1,51 +1,157 @@
|
|
|
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._updateEraserPath(offsetX, offsetY)
|
|
30
|
+
this._checkAndMarkIntersections(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
|
+
Canvas.stage.find('.node').forEach(node => {
|
|
64
|
+
if (node === this.temporalShape) return
|
|
65
|
+
|
|
66
|
+
if (this._checkIntersection(node, { x, y })) {
|
|
67
|
+
if (!this.erasedNodes.has(node)) {
|
|
68
|
+
this._markNodeForErasure(node)
|
|
69
|
+
}
|
|
31
70
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
_markNodeForErasure(node) {
|
|
75
|
+
node.opacity(0.2)
|
|
76
|
+
this.erasedNodes.add(node)
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
_destroyErasedNodes() {
|
|
80
|
+
this.erasedNodes.forEach(node => node.destroy())
|
|
81
|
+
this.erasedNodes.clear()
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
_cleanupTemporalShape() {
|
|
85
|
+
if (this.temporalShape) {
|
|
86
|
+
this.temporalShape.destroy()
|
|
87
|
+
this.temporalShape = null
|
|
37
88
|
}
|
|
38
89
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
90
|
+
|
|
91
|
+
_checkIntersection(node, point) {
|
|
92
|
+
if (node.attrs.nodeType === 'pen') {
|
|
93
|
+
return this._checkPenIntersection(node, point)
|
|
94
|
+
}
|
|
95
|
+
return this._checkDefaultIntersection(node)
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
_checkPenIntersection(node, point) {
|
|
99
|
+
const localPoint = this._transformPointToLocal(node, point)
|
|
100
|
+
const { points: pts, widths } = node.attrs
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
103
|
+
const distance = this._getDistanceToLineSegment(
|
|
104
|
+
localPoint.x,
|
|
105
|
+
localPoint.y,
|
|
106
|
+
pts[i][0],
|
|
107
|
+
pts[i][1],
|
|
108
|
+
pts[i + 1][0],
|
|
109
|
+
pts[i + 1][1]
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if (distance < (widths[i] + Properties.strokeWidth) / 2) {
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false
|
|
44
117
|
},
|
|
45
|
-
|
|
118
|
+
|
|
119
|
+
_checkDefaultIntersection(node) {
|
|
46
120
|
return Konva.Util.haveIntersection(
|
|
47
121
|
this.temporalShape.getClientRect(),
|
|
48
122
|
node.getClientRect()
|
|
49
|
-
)
|
|
123
|
+
)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
_transformPointToLocal(node, point) {
|
|
127
|
+
const transform = node.getTransform()
|
|
128
|
+
return transform.copy().invert().point(point)
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
_getDistanceToLineSegment(px, py, x1, y1, x2, y2) {
|
|
132
|
+
const A = px - x1
|
|
133
|
+
const B = py - y1
|
|
134
|
+
const C = x2 - x1
|
|
135
|
+
const D = y2 - y1
|
|
136
|
+
|
|
137
|
+
const dot = A * C + B * D
|
|
138
|
+
const lenSq = C * C + D * D
|
|
139
|
+
const param = lenSq !== 0 ? dot / lenSq : -1
|
|
140
|
+
|
|
141
|
+
if (param < 0) return this._getDistance(px, py, x1, y1)
|
|
142
|
+
if (param > 1) return this._getDistance(px, py, x2, y2)
|
|
143
|
+
|
|
144
|
+
return this._getDistance(
|
|
145
|
+
px,
|
|
146
|
+
py,
|
|
147
|
+
x1 + param * C,
|
|
148
|
+
y1 + param * D
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
_getDistance(x1, y1, x2, y2) {
|
|
153
|
+
const dx = x1 - x2
|
|
154
|
+
const dy = y1 - y2
|
|
155
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
50
156
|
}
|
|
51
157
|
}
|
|
@@ -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,8 @@ export default {
|
|
|
39
39
|
return this.selector !== null
|
|
40
40
|
},
|
|
41
41
|
_isInSelector(node) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
42
|
+
const nodeRect = node.getClientRect()
|
|
43
|
+
const selectorRect = this.selector.getClientRect()
|
|
44
|
+
return Konva.Util.haveIntersection(selectorRect, nodeRect)
|
|
46
45
|
}
|
|
47
46
|
}
|