forked from pencil-js/pencil.js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
spline.js
118 lines (105 loc) · 3.89 KB
/
spline.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import Line from "@pencil.js/line";
import Position from "@pencil.js/position";
import { equals } from "@pencil.js/math";
/**
* Spline class
* @class
* @extends Line
*/
export default class Spline extends Line {
/**
* Spline constructor
* @param {PositionDefinition} positionDefinition - First point
* @param {Array<PositionDefinition>|PositionDefinition} points - Set of points to go through or a single target point
* @param {Number} [tension=Spline.defaultTension] - Ratio of tension between points (0 means straight line, can take any value, but with weird results above 1)
* @param {LineOptions} [options] - Drawing options
*/
constructor (positionDefinition, points, tension = Spline.defaultTension, options) {
super(positionDefinition, points, options);
/**
* @type {Number}
*/
this.tension = tension;
}
/**
* Draw the spline
* @param {Path2D} path - Current drawing path
* @return {Spline} Itself
*/
trace (path) {
if (this.points.length === 1 || equals(this.tension, 0)) {
super.trace(path);
}
else {
path.moveTo(0, 0);
Spline.splineThrough(path, [new Position(0, 0)].concat(this.points), this.tension);
}
return this;
}
/**
* @inheritDoc
*/
toJSON () {
return Object.assign(super.toJSON(), {
tension: this.tension,
});
}
/**
* @inheritDoc
* @param {Object} definition - Spline definition
* @return {Spline}
*/
static from (definition) {
return new Spline(definition.position, definition.points, definition.tension, definition.options);
}
/**
* Default ratio of tension
* @return {Number}
*/
static get defaultTension () {
return 0.2;
}
/**
* Draw a spline through points using a tension (first point should be current position)
* @param {Path2D} path - Current drawing path
* @param {Array<PositionDefinition>} points - Points to use (need at least 2 points)
* @param {Number} [tension=Spline.defaultTension] - Ratio of tension
*/
static splineThrough (path, points, tension = Spline.defaultTension) {
if (points.length < 2) {
throw new RangeError(`Need at least 2 points to spline, but only ${points.length} given.`);
}
const positions = points.map(point => Position.from(point));
if (positions.length === 2) {
path.lineTo(positions[1].x, positions[1].y);
return;
}
const getCtrlPts = Spline.getControlPoint;
let previousControls = [null, positions[0]];
for (let i = 1, l = positions.length; i < l; ++i) {
const controlPoints = i < l - 1 ? getCtrlPts(positions.slice(i - 1, i + 2), tension) : [positions[i], null];
path.bezierCurveTo(
previousControls[1].x, previousControls[1].y, controlPoints[0].x, controlPoints[0].y,
positions[i].x, positions[i].y,
);
previousControls = controlPoints;
}
}
/**
* Returns control points for a point in a spline (needs before and after, 3 points in total)
* @param {Array<PositionDefinition>} points - 3 points to use (before, target, after)
* @param {Number} [tension=Spline.defaultTension] - Ratio of tension
* @return {Array<Position>}
*/
static getControlPoint (points, tension = Spline.defaultTension) {
if (points.length < 3) {
throw new RangeError(`Need exactly 3 points to compute control points, but ${points.length} given.`);
}
const positions = points.map(point => Position.from(point));
const diff = positions[2].clone().subtract(positions[0]).multiply(tension);
return [
positions[1].clone().subtract(diff),
positions[1].clone().add(diff),
];
}
}