Skip to content

Instantly share code, notes, and snippets.

@superlopuh
Created March 31, 2017 09:55
Show Gist options
  • Save superlopuh/9a119422e54de5ec240ec8993ed0d054 to your computer and use it in GitHub Desktop.
Save superlopuh/9a119422e54de5ec240ec8993ed0d054 to your computer and use it in GitHub Desktop.
Animation Curves Python
# MIT License
#
# Copyright (c) 2017 Snips
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# https://en.wikipedia.org/wiki/Cubic_Hermite_spline
class CubicHermite(object):
def __init__(self, m_0, m_1):
self.m_0 = m_0
self.m_1 = m_1
def interpolate(self, t):
t_squared = t * t
t_cubed = t * t_squared
start_factor = (t_cubed - 2 * t_squared + t) * self.m_0
end_factor = (t_cubed - t_squared) * self.m_1
return start_factor + end_factor - 2 * t_cubed + 3 * t_squared
def interpolate_derivative(self, t):
t_squared = t * t
start_factor = (3 * t_squared - 4 * t + 1) * self.m_0
end_factor = (3 * t_squared - 2 * t) * self.m_1
return start_factor + end_factor - 6 * t_squared + 6 * t
# Approximates time for given s in range 0...1
# delta is acceptable error in the time coordinate
# uses Newton-Raphson approximation with a maximum of 100 passes
def interpolate_inverse(self, proportion, delta=0.01):
if proportion < self.interpolate(delta):
return 0
elif proportion > self.interpolate(1 - delta):
return 1
def approximate_next(current):
f = self.interpolate(current) - proportion
f_prime = self.interpolate_derivative(current)
return current - f / f_prime
# start with same proportion in time
current = proportion
# max 100 iterations
count = 100
while (abs(self.interpolate(current) - proportion) > delta
and 0 < count):
current = approximate_next(current)
count += 1
return current
class AnimationCurve(object):
def __init__(self, distance, duration, frequency_start, frequency_end):
assert(0 < distance)
self.distance = distance
self.duration = duration
self.frequency_start = frequency_start
self.frequency_end = frequency_end
self._cubic_hermite = CubicHermite(
frequency_start / distance * duration,
frequency_end / distance * duration
)
def distance_at_time(self, time):
unscaled = self._cubic_hermite.interpolate(
time / self.duration
)
return unscaled * self.distance
def frequency_at_time(self, time):
unscaled = self._cubic_hermite.interpolate_derivative(
time / self.duration
)
return unscaled / self.duration * self.distance
def time_to_distance(self, distance):
unscaled = self._cubic_hermite.interpolate_inverse(
distance / self.distance
)
return unscaled * self.duration
class Animation(object):
def __init__(self, frames, duration, frequency_start, frequency_end):
assert(0 < len(frames))
self.frames = frames
self.duration = duration
self.frequency_start = frequency_start
self.frequeny_end = frequency_end
self._animation_curve = AnimationCurve(
float(len(frames)),
duration,
frequency_start,
frequency_end
)
def get_frames(self):
return zip(self.frames, self.get_durations())
def get_durations(self):
times = map(
lambda frame: self._animation_curve.time_to_distance(frame + 1),
range(1, len(self.frames) + 1)
)
durations = [times[0]]
for index in range(1, len(times)):
durations.append(times[index] - times[index - 1])
durations = map(lambda duration: duration * self.duration, durations)
return durations
@staticmethod
def ease_in(frames, duration=None, end_frequency=None):
if end_frequency is None:
end_frequency = DEFAULT_FREQUENCY
if duration is None:
duration = 2 * len(frames) / end_frequency
return Animation(frames, duration, 0, end_frequency)
@staticmethod
def ease_out(frames, duration=None, start_frequency=None):
if start_frequency is None:
start_frequency = DEFAULT_FREQUENCY
if duration is None:
duration = 2 * len(frames) / start_frequency
return Animation(frames, duration, start_frequency, 0)
@staticmethod
def ease_in_out(frames, duration=None):
if duration is None:
duration = len(frames) * DEFAULT_PERIOD
return Animation(frames, duration, 0, 0)
@staticmethod
def linear(frames, duration=None):
if duration is None:
duration = len(frames) * DEFAULT_PERIOD
return AnimationCurve(
frames,
duration,
DEFAULT_FREQUENCY,
DEFAULT_FREQUENCY
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment