-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathPath.ele
242 lines (217 loc) · 8.24 KB
/
Path.ele
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
constraint PathFunc(u:Num):Vector3
struct Bounds(lower:Num, upper:Num)
#[[ A Path is defined by a parametric equation of a path,
# and a domain over which the function is valid.
#]]
struct Path(at:PathFunc, domain:Bounds)
{
constraint ScalarTransform(x:Num):Num
constraint Vector3Unary(v:Vector3):Vector3
constraint Vector3Binary(v1:Vector3, v2:Vector3):Vector3
#[[ Change the path domain from the current domain "newDomain".
#
# This function returns a new path where the "newDomain" maps
# onto the original domain.
#
# This returned path, "newPath" has the properties:
# newPath.domain = newDomain
# newPath.at(newDomain.lower) = this.at(this.domain.lower)
# newPath.at(newDomain.upper) = this.at(this.domain.upper)
#]]
changeDomain(this:Path, newDomain:Bounds):Path
{
uTransform(u:Num):Num
{
return = Num.lerp(u.sub(newDomain.lower).div(newDomain.upper.sub(newDomain.lower)), this.domain.lower, this.domain.upper)
}
transformedPathFunc(u:Num):Vector3 = this.at(uTransform(u))
return = Path(transformedPathFunc, newDomain)
}
#[[ Returns a path which pingPongs along the original path
# in the original domain.
#
# Eg, if the original domain is (0, 1) then the returned
# path also has a domain (0, 1). Over this domain, the returned
# path travels to the end of the original path and back again.
#
# newPath.domain = oldPath.domain
# newPath.at(0) = oldPath.at(0)
# newPath.at(0.5) = oldPath.at(1)
# newPath.at(1) = oldPath.at(0)
#
# This means that the path length is double the original path length.
#]]
pingPong(this:Path):Path
{
unitIntervalTransform(u:Num):Num = if(u.gt(0.5), 1.sub(u).mul(2), u.mul(2))
return = this.applyUnitIntervalTransformToDomain(unitIntervalTransform)
}
#[[ Returns the same path repeated "repetitions" times over
# the same domain.
#
# This changes the path length.
#]]
repeat(this:Path, repetitions:Num):Path
{
unitIntervalTransform(u:Num):Num = u.mul(repetitions).rem(1)
return = this.applyUnitIntervalTransformToDomain(unitIntervalTransform)
}
reverse(this:Path):Path
{
uTransform(u:Num):Num
{
w = u.sub(this.domain.lower)
return = this.domain.upper.sub(w)
}
transformedPathFunc(u:Num):Vector3 = this.at(uTransform(u))
return = Path(transformedPathFunc, this.domain)
}
oscillate(this:Path):Path
{
unitIntervalTransform(u:Num):Num = Num.sub(0.5, Num.cos(u.mul(Num.tau)).div(2))
return = this.applyUnitIntervalTransformToDomain(unitIntervalTransform)
}
#[[ Map 0 to 1 to the same range, but more smoothly ]]
easing(this:Path):Path
{
smoothstep(t:Num):Num = 3.mul(t).mul(t).sub(2.mul(t).mul(t).mul(t))
return = this.applyUnitIntervalTransformToDomain(smoothstep)
}
#[[ Quantise the positions on the path with nSteps ]]
quantise(this:Path, nSteps:Num):Path
{
uiTransform(u:Num):Num
{
return = u.mul(nSteps).floor.div(nSteps.sub(1))
}
return = this.applyUnitIntervalTransformToDomain(uiTransform)
}
#[[ Concatenate multiple paths
#
# Supply a list of paths and their corresponding lengths.
#
# The input needs to be a list of anonymous blocks, each
# with a 'path' and a 'length' field (see example below).
#
# Note that the absolute values of the lengths do not need to
# be accurate, only their relative values are used. All pathLengths
# must be positive numbers.
#
# Usage:
# path1 = Path(...)
# path2 = Path(...)
# length1 = (...)
# length2 = (...)
# result = concatAll(
# list(
# {path=path1, length=length1},
# {path=path2, length=length2}
# )
# )
#
#]]
concatAll(pathsAndLengths:List):Path
{
firstPathAndLength = pathsAndLengths.at(0)
otherPathsAndLengths = pathsAndLengths.skip(1)
accumulator(current, next)
{
lengthRatio = next.length.div(current.length)
newPath = current.path.concatByLengthRatio(next.path, lengthRatio)
newLength = current.length.add(next.length)
return = {path=newPath, length=newLength}
}
return = otherPathsAndLengths.fold(firstPathAndLength, accumulator).path
}
#[[ Concatenate two paths by specifying the length ratio
#
# The "lengthRatio" needs to be (path2Length / path1Length)
#
# The new path has a domain (0, 1), and goes along path1 and
# then path2. The lengthRatio is used to determine where in this
# domain path1 should end and path2 should begin.
#]]
concatByLengthRatio(path1:Path, path2:Path, lengthRatio:Num):Path
{
# The domain boundary is calculated by
# boundary = length1 / (length1 + length2)
boundary = 1.div(1.add(lengthRatio))
return = concatByDomainBoundary(path1, path2, boundary)
}
#[[ Concatenate two paths by specifying the new domain boundary
#
# The output path has a domain (0, 1)
#
# The "boundary" parameter defines at which point in this domain
# "path1" should end and "path1" should begin.
#
# The domain (0, boundary) maps to the domain of path1, and
# the domain (boundary, 1) maps to the domain of path2
#]]
concatByDomainBoundary(path1:Path, path2:Path, boundary:Num):Path
{
pathA = path1.changeDomain(Bounds(0, boundary))
pathB = path2.changeDomain(Bounds(boundary, 1))
newPathFunction(u:Num):Vector3 = if(u.leq(boundary), pathA.at(u), pathB.at(u))
return = Path(newPathFunction, Bounds(0, 1))
}
#[[ Combine the two paths by adding the positions at each point
#
# The two paths must have the same domain for this operation to be valid.
# If they are not the same, the domain of path1 will be used.
#]]
add(path1:Path, path2:Path):Path
{
return = Path.applyBinaryVectorOperation(path1, path2, Vector3.add)
}
#[[ Apply a Transform to all points on the path ]]
applyTransform(this:Path, transform:Transform):Path
{
return = applyUnaryVectorOperation(this, transform.applyToPosition)
}
#[[ Apply an offset to the path ]]
applyOffset(this:Path, offset:Vector3):Path
{
adder = _(v:Vector3):Vector3 = v.add(offset)
return = applyUnaryVectorOperation(this, adder)
}
#[[ Apply an operation to all points on the path ]]
applyUnaryVectorOperation(this:Path, operation:Vector3Unary):Path
{
newPathFunction(u:Num):Vector3 = operation(this.at(u))
return = Path(newPathFunction, this.domain)
}
#[[ Combine the two paths by applying the binary operation at each point
#
# The two paths must have the same domain for this operation to be valid.
# If they are not the same, the domain of path1 will be used.
#]]
applyBinaryVectorOperation(path1:Path, path2:Path, operation:Vector3Binary):Path
{
newPathFunction(u:Num):Vector3
{
path1Pos = path1.at(u)
path2Pos = path2.at(u)
return = operation(path1Pos, path2Pos)
}
return = Path(newPathFunction, path1.domain)
}
#[[ Apply the unitIntervalTransform to the path inputs over the domain
#
# The unitIntervalTransform must map a point in the range (0, 1) to another point in this range.
# The returned Path has the same domain at the original Path, but with the transform applied
# to all inputs.
#]]
applyUnitIntervalTransformToDomain(this:Path, unitIntervalTransform:ScalarTransform):Path
{
domainTransform(u:Num):Num
{
domainLength = this.domain.upper.sub(this.domain.lower)
w = u.sub(this.domain.lower).div(domainLength)
wNew = unitIntervalTransform(w)
return = wNew.mul(domainLength).add(this.domain.lower)
}
transformedPathFunc(u:Num):Vector3 = this.at(domainTransform(u))
return = Path(transformedPathFunc, this.domain)
}
}