jellies-draw 0.1.10 → 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 +1 -1
- package/src/components/PropertyPickers.vue +6 -16
- package/src/components/functions/canvas.js +55 -14
- package/src/components/functions/properties.js +38 -3
- package/src/components/functions/tools/eraser.js +135 -29
- package/src/components/functions/tools/pen.js +347 -36
- package/src/components/functions/tools/selector.js +3 -4
package/package.json
CHANGED
|
@@ -59,23 +59,13 @@
|
|
|
59
59
|
import Properties from './functions/properties';
|
|
60
60
|
export default {
|
|
61
61
|
name: 'PropertiesPicker',
|
|
62
|
-
data() {
|
|
63
|
-
return {
|
|
64
|
-
fontSizeOptions: {
|
|
65
|
-
15: 'S',
|
|
66
|
-
20: 'M',
|
|
67
|
-
35: 'L',
|
|
68
|
-
50: 'XL'
|
|
69
|
-
},
|
|
70
|
-
strokeWidthOptions: {
|
|
71
|
-
3: 'H',
|
|
72
|
-
4: 'HB',
|
|
73
|
-
5: 'B',
|
|
74
|
-
6: '2B'
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
62
|
computed: {
|
|
63
|
+
fontSizeOptions() {
|
|
64
|
+
return Properties.fontSizeOptions
|
|
65
|
+
},
|
|
66
|
+
strokeWidthOptions() {
|
|
67
|
+
return Properties.strokeWidthOptions
|
|
68
|
+
},
|
|
79
69
|
fillColor: {
|
|
80
70
|
get() {
|
|
81
71
|
return Properties.fillColor
|
|
@@ -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);
|
|
@@ -79,7 +94,11 @@ export default {
|
|
|
79
94
|
Transformer.removeSelectedNodes();
|
|
80
95
|
} else if (event.ctrlKey || event.metaKey) {
|
|
81
96
|
if (!this.isEditingText) {
|
|
82
|
-
this.
|
|
97
|
+
this._handleNodesShortCuts(event);
|
|
98
|
+
}
|
|
99
|
+
} else if (event.shiftKey) {
|
|
100
|
+
if (!this.isEditingText) {
|
|
101
|
+
this._handlePropertiesShortCuts(event);
|
|
83
102
|
}
|
|
84
103
|
} else if (event.altKey) {
|
|
85
104
|
Properties.isCanvasPenetrable = true;
|
|
@@ -93,11 +112,32 @@ export default {
|
|
|
93
112
|
Properties.latestTool = {
|
|
94
113
|
'tool': 'selector',
|
|
95
114
|
'isToolLocked': false
|
|
96
|
-
}
|
|
115
|
+
};
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
},
|
|
100
|
-
|
|
119
|
+
_handlePropertiesShortCuts(event) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
const colorsKeyMap = {
|
|
122
|
+
'Digit1': '#f33b29',
|
|
123
|
+
'Digit2': '#4ec53d',
|
|
124
|
+
'Digit3': '#ffb020',
|
|
125
|
+
'Digit4': '#399af4',
|
|
126
|
+
'Digit5': '#ffcaff',
|
|
127
|
+
'Digit6': '#ffde00',
|
|
128
|
+
'Digit7': '#ce82ff',
|
|
129
|
+
'Digit8': '#eeeeee',
|
|
130
|
+
'Digit9': '#333333'
|
|
131
|
+
};
|
|
132
|
+
if (event.code in colorsKeyMap) {
|
|
133
|
+
Properties.strokeColor = colorsKeyMap[event.code];
|
|
134
|
+
} else if (event.code === 'Equal') {
|
|
135
|
+
Properties.scalePropertyUp();
|
|
136
|
+
} else if (event.code === 'Minus') {
|
|
137
|
+
Properties.scalePropertyDown();
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
_handleNodesShortCuts(event) {
|
|
101
141
|
event.preventDefault();
|
|
102
142
|
if (event.key === 'c') {
|
|
103
143
|
Clipboard.copy();
|
|
@@ -109,9 +149,9 @@ export default {
|
|
|
109
149
|
Transformer.selectAllNodes();
|
|
110
150
|
} else if (event.key === 'z' || event.key === 'Z') {
|
|
111
151
|
if (event.shiftKey) {
|
|
112
|
-
Histories.redo()
|
|
152
|
+
Histories.redo();
|
|
113
153
|
} else {
|
|
114
|
-
Histories.undo()
|
|
154
|
+
Histories.undo();
|
|
115
155
|
}
|
|
116
156
|
}
|
|
117
157
|
},
|
|
@@ -121,20 +161,20 @@ export default {
|
|
|
121
161
|
return;
|
|
122
162
|
}
|
|
123
163
|
}
|
|
124
|
-
Tools[Properties.tool].show(event.evt)
|
|
164
|
+
Tools[Properties.tool].show(event.evt);
|
|
125
165
|
},
|
|
126
166
|
handlePointerMove(event) {
|
|
127
167
|
if (Properties.tool !== 'clear') {
|
|
128
|
-
Tools[Properties.tool].change(event.evt)
|
|
168
|
+
Tools[Properties.tool].change(event.evt);
|
|
129
169
|
}
|
|
130
170
|
},
|
|
131
171
|
handlePointerUp(event) {
|
|
132
|
-
Tools[Properties.tool].finish()
|
|
172
|
+
Tools[Properties.tool].finish();
|
|
133
173
|
if (this.isDrawing || this.isEditingText) {
|
|
134
|
-
Histories.record()
|
|
174
|
+
Histories.record();
|
|
135
175
|
}
|
|
136
|
-
this.isDrawing = false
|
|
137
|
-
this.isEditingText = false
|
|
176
|
+
this.isDrawing = false;
|
|
177
|
+
this.isEditingText = false;
|
|
138
178
|
},
|
|
139
179
|
handleClick(event) {
|
|
140
180
|
const metaPressed = event.evt.shiftKey || event.evt.ctrlKey || event.evt.metaKey;
|
|
@@ -155,3 +195,4 @@ export default {
|
|
|
155
195
|
Transformer.selectNode(targetNode, metaPressed);
|
|
156
196
|
}
|
|
157
197
|
}
|
|
198
|
+
|
|
@@ -6,9 +6,9 @@ export default new Vue({
|
|
|
6
6
|
data() {
|
|
7
7
|
return {
|
|
8
8
|
fillColorPicked: 'transparent',
|
|
9
|
-
strokeColorPicked: '#
|
|
9
|
+
strokeColorPicked: '#f33b29',
|
|
10
10
|
fontSizeSelected: 20,
|
|
11
|
-
strokeWidthSelected:
|
|
11
|
+
strokeWidthSelected: 3,
|
|
12
12
|
toolSelected: 'pen',
|
|
13
13
|
toolLatestSelected: {
|
|
14
14
|
'tool': 'pen',
|
|
@@ -17,7 +17,19 @@ export default new Vue({
|
|
|
17
17
|
isTextNodesSelected: false,
|
|
18
18
|
isMassivelyAssigningProperties: false,
|
|
19
19
|
isToolLocked: false,
|
|
20
|
-
isCanvasPenetrable: false
|
|
20
|
+
isCanvasPenetrable: false,
|
|
21
|
+
fontSizeOptions: {
|
|
22
|
+
15: 'S',
|
|
23
|
+
20: 'M',
|
|
24
|
+
35: 'L',
|
|
25
|
+
50: 'XL'
|
|
26
|
+
},
|
|
27
|
+
strokeWidthOptions: {
|
|
28
|
+
3: 'H',
|
|
29
|
+
4: 'HB',
|
|
30
|
+
5: 'B',
|
|
31
|
+
6: '2B'
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
34
|
},
|
|
23
35
|
computed: {
|
|
@@ -120,6 +132,29 @@ export default new Vue({
|
|
|
120
132
|
handleClearMoveCursor(){
|
|
121
133
|
Canvas.stage.container().style.cursor = null;
|
|
122
134
|
},
|
|
135
|
+
scaleProperty(direction) {
|
|
136
|
+
if (this.isUsingText) {
|
|
137
|
+
this.fontSize = this.setPropertyToScale(this.fontSize, this.fontSizeOptions, direction);
|
|
138
|
+
} else {
|
|
139
|
+
this.strokeWidth = this.setPropertyToScale(this.strokeWidth, this.strokeWidthOptions, direction);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
setPropertyToScale(currentValue, options, direction) {
|
|
143
|
+
const optionsKeys = Object.keys(options);
|
|
144
|
+
const currentIndex = optionsKeys.indexOf(currentValue.toString());
|
|
145
|
+
if (direction === 'down' && currentIndex > 0) {
|
|
146
|
+
return parseInt(optionsKeys[currentIndex - 1]);
|
|
147
|
+
} else if (direction === 'up' && currentIndex < optionsKeys.length - 1) {
|
|
148
|
+
return parseInt(optionsKeys[currentIndex + 1]);
|
|
149
|
+
}
|
|
150
|
+
return currentValue;
|
|
151
|
+
},
|
|
152
|
+
scalePropertyUp() {
|
|
153
|
+
this.scaleProperty('up');
|
|
154
|
+
},
|
|
155
|
+
scalePropertyDown() {
|
|
156
|
+
this.scaleProperty('down');
|
|
157
|
+
},
|
|
123
158
|
setProperties(properties) {
|
|
124
159
|
this.isMassivelyAssigningProperties = true;
|
|
125
160
|
Object.keys(properties).forEach(property => {
|
|
@@ -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
|
}
|