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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jellies-draw",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "A drawer for jellies",
5
5
  "private": false,
6
6
  "main": "./src/index.js",
@@ -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.pointerUpHandler);
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.pointerUpHandler = this.handlePointerUp.bind(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.pointerUpHandler);
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._handleKeyShortCuts(event);
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
- _handleKeyShortCuts(event) {
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: '#000000',
9
+ strokeColorPicked: '#f33b29',
10
10
  fontSizeSelected: 20,
11
- strokeWidthSelected: 4,
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.temporalCurveTrajectory = null
14
- const strokeWidth = Properties.strokeWidth
15
- const newCurve = new Konva.Line({
16
- points: [offsetX, offsetY, offsetX, offsetY],
17
- stroke: 'transparent',
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
- if (!this.temporalCurveTrajectory) {
30
- this.temporalCurveTrajectory = [this.startX, this.startY]
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
- this.temporalCurveTrajectory.push(offsetX, offsetY)
33
- this.temporalShape.points(this.temporalCurveTrajectory);
34
- Canvas.stage.find('.node')
35
- .filter(node => this._hasIntersectionOnEraserCurve(node))
36
- .forEach(node => node.opacity(0.1));
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
- finish() {
40
- Canvas.stage.find('.node')
41
- .filter(node => this._hasIntersectionOnEraserCurve(node))
42
- .forEach(node => node.remove())
43
- this.temporalShape = null
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
- _hasIntersectionOnEraserCurve(node) {
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.temporalCurveTrajectory = null
15
- const stroke = Properties.strokeColor
16
- const strokeWidth = Properties.strokeWidth
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
- points: [offsetX, offsetY, offsetX, offsetY],
19
- stroke,
20
- strokeWidth
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
- if (!this.temporalCurveTrajectory) {
28
- this.temporalCurveTrajectory = [this.startX, this.startY]
29
- }
30
- this.temporalCurveTrajectory.push(offsetX, offsetY)
31
- this.temporalShape.points(this.temporalCurveTrajectory);
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
- generate({ x, y, scaleX, scaleY, skewX, rotation, points, stroke, strokeWidth }) {
38
- const newLine = new Konva.Line({
39
- x,
40
- y,
41
- scaleX,
42
- scaleY,
43
- skewX,
44
- rotation,
45
- points,
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
- strokeWidth,
89
+ points,
90
+ widths,
48
91
  name: 'node',
49
92
  nodeType: 'pen',
50
- lineCap: 'round',
51
- bezier: false,
52
- strokeScaleEnabled: false
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
- this.bindEvents(newLine)
55
- return newLine
56
- },
57
- bindEvents(newLine) {
58
- newLine.off('transformend')
59
- newLine.off('dragend')
60
- newLine.on('transformend', Histories.record)
61
- newLine.on('dragend', Histories.record)
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
- return Konva.Util.haveIntersection(
43
- this.selector.getClientRect(),
44
- node.getClientRect()
45
- );
42
+ const nodeRect = node.getClientRect()
43
+ const selectorRect = this.selector.getClientRect()
44
+ return Konva.Util.haveIntersection(selectorRect, nodeRect)
46
45
  }
47
46
  }