/
Interpolation.scala
169 lines (154 loc) · 5.98 KB
/
Interpolation.scala
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
/*
* Copyright 2015 Creative Scala
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package doodle
package interact
package animation
import cats._
import cats.syntax.all._
import doodle.interact.easing.Easing
import scala.concurrent.duration.Duration
/** An Interpolation represents a range of values between a starting and ending
* value. The interpolation is also optionally transformed by an easing
* function. The starting value is mapped to 0.0 and the ending value to 1.0.
* When a number of steps is specified the interpolation is transformed into a
* transducer, which may be run or composed with other transducers.
*
* Interpolations may be transformed by map and product (functor and
* semigroupal) to construct more complex interpolations.
*
* The differences between an interpolation and a transducer are as follows:
*
* - An interpolation specifies a start and end value. When a number of steps
* is given it becomes a transducer. When interpolations are combined in
* parallel, with product, they always take the same amount of time when
* converted to a transducer. Transducers combined in parallel may take
* differing amounts of time.
*
* - A transducer can represent arbitrary FSMs, while an interpolation moves
* from start to end value.
*
* - Transducers can be run. Interpolations must be transformed to
* transducers to run.
*/
sealed trait Interpolation[A] {
import Interpolation._
/** Transform the output of this interpolation with the given function.
*/
def map[B](f: A => B): Interpolation[B] =
Map(this, f)
/** Combine this Interpolation in parallel with that Interpolation.
*/
def product[B](that: Interpolation[B]): Interpolation[(A, B)] =
Product(this, that)
/** Apply an easing function to this interpolation.
*
* Map the range in this interpolation to 0.0 and 1.0, pass through the given
* easing function, and then map back to the original domain.
*/
def withEasing(easing: Easing): Interpolation[A] =
WithEasing(this, easing)
/** Create a transducer that will produce the given number of values before it
* stops. So, for example, calling `forSteps(2)` will create a transducer
* that produces 2 values before it stops.
*
* The number of steps must be non-negative. 0 steps means a transducer that
* stops immediately. 1 step will produce the start value for a half-open
* interval and the stop value for a closed interval.
*/
def forSteps(steps: Long): Transducer[A] = {
// This method serves to make type inference happier by introducing the new type variable C.
def loop[C](
interpolation: Interpolation[C],
easing: Option[Easing]
): Transducer[C] =
interpolation match {
case WithEasing(source, e) =>
// Use outermost easing
if (easing.isEmpty) loop(source, Some(e))
else loop(source, easing)
case Map(source, f) => loop(source, easing).map(f)
case Product(l, r) => loop(l, easing).product(loop(r, easing))
case HalfOpen(start, stop, i) =>
easing match {
case Some(e) => i.halfOpen(start, stop, steps, e)
case None => i.halfOpen(start, stop, steps)
}
case Closed(start, stop, i) =>
easing match {
case Some(e) => i.closed(start, stop, steps, e)
case None => i.closed(start, stop, steps)
}
case Constant(value) =>
// Easing is irrelevant when we're generating a constant
Transducer
.scanLeftUntil(0L)(x => x + 1)(x => x >= steps)
.as(value)
}
loop(this, None)
}
def forDuration(duration: Duration): Transducer[A] =
forSteps(duration.toMillis * 60 / 1000)
}
object Interpolation {
final case class HalfOpen[A](
start: A,
stop: A,
interpolator: Interpolator[A]
) extends Interpolation[A]
final case class Closed[A](
start: A,
stop: A,
interpolator: Interpolator[A]
) extends Interpolation[A]
final case class WithEasing[A](source: Interpolation[A], easing: Easing)
extends Interpolation[A]
// Essentially a free applicative
final case class Map[A, B](source: Interpolation[A], f: A => B)
extends Interpolation[B]
final case class Product[A, B](
left: Interpolation[A],
right: Interpolation[B]
) extends Interpolation[(A, B)]
final case class Constant[A](value: A) extends Interpolation[A]
implicit val interpolationInstance
: Functor[Interpolation] with Semigroupal[Interpolation] =
new Functor[Interpolation] with Semigroupal[Interpolation] {
def product[A, B](
fa: Interpolation[A],
fb: Interpolation[B]
): Interpolation[(A, B)] =
fa.product(fb)
def map[A, B](fa: Interpolation[A])(f: A => B): Interpolation[B] =
fa.map(f)
}
/** Construct a half-open interpolation, which starts at the given start value
* and ends at (but does not generate) the given stop value.
*/
def halfOpen[A](start: A, stop: A)(implicit
i: Interpolator[A]
): Interpolation[A] =
HalfOpen(start, stop, i)
/** Construct a closed interpolation, which starts at the given start value
* and ends at the given stop value.
*/
def closed[A](start: A, stop: A)(implicit
i: Interpolator[A]
): Interpolation[A] =
Closed(start, stop, i)
/** Construct an interpolation that has a constant value. */
def constant[A](value: A): Interpolation[A] =
Constant(value)
}