-
Notifications
You must be signed in to change notification settings - Fork 113
/
patternUtils.js
117 lines (103 loc) · 3.87 KB
/
patternUtils.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
// functional re-impl of L.Point.distanceTo,
// with no dependency on Leaflet for easier testing
function pointDistance(ptA, ptB) {
const x = ptB.x - ptA.x;
const y = ptB.y - ptA.y;
return Math.sqrt(x * x + y * y);
}
const computeSegmentHeading = (a, b) =>
((Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI) + 90 + 360) % 360;
const asRatioToPathLength = ({ value, isInPixels }, totalPathLength) =>
isInPixels ? value / totalPathLength : value;
function parseRelativeOrAbsoluteValue(value) {
if (typeof value === 'string' && value.indexOf('%') !== -1) {
return {
value: parseFloat(value) / 100,
isInPixels: false,
};
}
const parsedValue = value ? parseFloat(value) : 0;
return {
value: parsedValue,
isInPixels: parsedValue > 0,
};
}
const pointsEqual = (a, b) => a.x === b.x && a.y === b.y;
function pointsToSegments(pts) {
return pts.reduce((segments, b, idx, points) => {
// this test skips same adjacent points
if (idx > 0 && !pointsEqual(b, points[idx - 1])) {
const a = points[idx - 1];
const distA = segments.length > 0 ? segments[segments.length - 1].distB : 0;
const distAB = pointDistance(a, b);
segments.push({
a,
b,
distA,
distB: distA + distAB,
heading: computeSegmentHeading(a, b),
});
}
return segments;
}, []);
}
function projectPatternOnPointPath(pts, pattern) {
// 1. split the path into segment infos
const segments = pointsToSegments(pts);
const nbSegments = segments.length;
if (nbSegments === 0) { return []; }
const totalPathLength = segments[nbSegments - 1].distB;
const offset = asRatioToPathLength(pattern.offset, totalPathLength);
const endOffset = asRatioToPathLength(pattern.endOffset, totalPathLength);
const repeat = asRatioToPathLength(pattern.repeat, totalPathLength);
const repeatIntervalPixels = totalPathLength * repeat;
const startOffsetPixels = offset > 0 ? totalPathLength * offset : 0;
const endOffsetPixels = endOffset > 0 ? totalPathLength * endOffset : 0;
// 2. generate the positions of the pattern as offsets from the path start
const positionOffsets = [];
let positionOffset = startOffsetPixels;
do {
positionOffsets.push(positionOffset);
positionOffset += repeatIntervalPixels;
} while(repeatIntervalPixels > 0 && positionOffset < totalPathLength - endOffsetPixels);
// 3. projects offsets to segments
let segmentIndex = 0;
let segment = segments[0];
return positionOffsets.map(positionOffset => {
// find the segment matching the offset,
// starting from the previous one as offsets are ordered
while (positionOffset > segment.distB && segmentIndex < nbSegments - 1) {
segmentIndex++;
segment = segments[segmentIndex];
}
const segmentRatio = (positionOffset - segment.distA) / (segment.distB - segment.distA);
return {
pt: interpolateBetweenPoints(segment.a, segment.b, segmentRatio),
heading: segment.heading,
};
});
}
/**
* Finds the point which lies on the segment defined by points A and B,
* at the given ratio of the distance from A to B, by linear interpolation.
*/
function interpolateBetweenPoints(ptA, ptB, ratio) {
if (ptB.x !== ptA.x) {
return {
x: ptA.x + ratio * (ptB.x - ptA.x),
y: ptA.y + ratio * (ptB.y - ptA.y),
};
}
// special case where points lie on the same vertical axis
return {
x: ptA.x,
y: ptA.y + (ptB.y - ptA.y) * ratio,
};
}
export {
projectPatternOnPointPath,
parseRelativeOrAbsoluteValue,
// the following function are exported only for unit testing purpose
computeSegmentHeading,
asRatioToPathLength,
};