Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active March 20, 2023 01:31
Show Gist options
  • Save roipeker/456b1c5f597e6628b1da002dec24cac8 to your computer and use it in GitHub Desktop.
Save roipeker/456b1c5f597e6628b1da002dec24cac8 to your computer and use it in GitHub Desktop.
linear interpolation utils (+ easings)
/// Copyright 2022 roipeker
///
/// 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.
///
/// Author: roipeker
///
///
/// `Lerper` gist is a collection of useful functions to make your own
/// tweens based on time/ratio/fraction.
///
/// Almost all methods are exposed as extension methods on `double`. So
/// you basically modify 1 number into a target value, making super simple
/// to chain transformations.
///
/// It also contains a basic CurvePath class to define a path of segments
/// of different types (linear, quadratic, cubic) and to get the value
/// at a given ratio (0-1). This can be used to create a motion path or
/// as a complex easing function to interpolate a single value over time.
///
/// Example:
///
/// ```dart
/// // counter as a frame counter...
/// var frames = 0;
/// void onTick(){
/// // ratio of 120 ticks (2 seconds if 60fps).
/// // [wrapLerp()] is a method that will return a value between 0 and 1,
/// // looping the value every 120 frames.
/// // It also takes a "start" value, that will work as a "delay" to
/// // the wrap interpolation, clamping `t` to 0.
/// var t = ++frames.wrapLerp(120)
/// // `x` will interpolate during 120 frames between 10 and 400 using
/// // sineOut curve.
/// var x = t.sineOut().lerp(10, 400);
/// // draw something with `x`
/// }
/// ```
///
/// ====
import 'dart:developer' as dev;
import 'dart:math' as math;
/// A class to define a path with different types of curves.
/// The path is defined by a start value and a list of segments.
/// Each segment is a linear or a curve.
/// Each segment starts at the end of the previous segment.
/// You can add segments to the path using the
/// line, quadraticBezier, and cubicBezier methods.
/// Use [transform( ratio )] to get the value at a given ratio (0-1).
class CurvePath {
/// The starting point of the path.
late double start;
/// The list of curve segments in the path.
final _segments = <Segment>[];
/// The length of the path.
int _pathLength = 0;
/// The total strength of all curve segments in the path.
double _totalStrength = 0.0;
/// Creates a [CurvePath] object with the given [start] position.
CurvePath(this.start);
/// Get the path data as a list of numbers, to be stored
/// or serialized.
List<num> get path {
final out = <num>[start];
for (var seg in _segments) {
if (seg is LinearSegment) {
out.addAll([1, seg.end, seg.strength]);
} else if (seg is QuadraticBezierSegment) {
out.addAll([2, seg.end, seg.strength, seg.control]);
} else if (seg is CubicBezierSegment) {
out.addAll([3, seg.end, seg.strength, seg.control1, seg.control2]);
} else {
dev.log("Segment not implemented.");
}
}
return out;
}
/// Sets the path data from a list of numbers.
set path(List<num> value) {
clear();
start = value[0].toDouble();
var i = 1;
while (i < value.length) {
switch (value[i]) {
case 1:
_addSegment(LinearSegment(value[i + 1] + .0, value[i + 2] + .0));
i += 3;
break;
case 2:
_addSegment(
QuadraticBezierSegment(
value[i + 1] + .0,
value[i + 2] + .0,
value[i + 3] + .0,
),
);
i += 4;
break;
case 3:
_addSegment(
CubicBezierSegment(value[i + 1] + .0, value[i + 2] + .0,
value[i + 3] + .0, value[i + 4] + .0),
);
i += 5;
break;
default:
dev.log("Segment not implemented.");
}
}
}
/// Checks if the path is empty or not.
bool isConstant() => _pathLength == 0;
/// Adds a segment to the path.
void _addSegment(Segment segment) {
_segments.add(segment);
_pathLength++;
_totalStrength += segment.strength;
}
/// Starts a path at the given position creating a line to [end].
/// [strength] defines a multiplier for the segment weight, used
/// in transform() when the segments are not equally distributed.
/// Returns a [CurvePath] object.
static CurvePath createLine(double end, [double strength = 1]) =>
CurvePath(0).line(end, strength);
/// Adds a linear segment to the path.
///
/// [end] is the end position of the segment.
/// [strength] defines a multiplier for the segment weight, used
/// in transform() when the segments are not equally distributed.
///
/// Returns a [CurvePath] object.
CurvePath line(double end, [double strength = 1]) {
_addSegment(LinearSegment(end, strength));
return this;
}
/// Adds a quadratic bezier segment to the path.
///
/// [end] is the end position of the segment.
/// [control] is the control point
CurvePath quadraticBezier(double end, double control, [double strength = 1]) {
_addSegment(QuadraticBezierSegment(end, strength, control));
return this;
}
/// Adds a cubic bezier segment to the path.
CurvePath cubicBezier(double end, double control1, double control2,
[double strength = 1]) {
_addSegment(CubicBezierSegment(end, strength, control1, control2));
return this;
}
/// Get the last value of the path
double getEnd() =>
(_pathLength > 0) ? _segments[_pathLength - 1].end : double.nan;
/// Get the value of the path at the given [rate] ratio.
double transform(double rate) {
double r = start;
if (_pathLength == 1) {
r = _segments[0].transform(start, rate);
} else if (_pathLength > 1) {
double ratio = rate * _totalStrength;
double lastEnd = start;
for (final path in _segments) {
if (ratio > path.strength) {
ratio -= path.strength;
lastEnd = path.end;
} else {
r = path.transform(lastEnd, ratio / path.strength);
break;
}
}
}
return r;
}
/// clears the segments in the path.
void clear() {
_pathLength = 0;
_totalStrength = 0;
_segments.clear();
}
}
/// Represents a segment with an end value and strength.
class Segment {
/// The end value of the segment.
final double end;
/// The strength of the segment.
final double strength;
/// Creates a new [Segment] object with the specified [end] and [strength].
const Segment(this.end, this.strength);
/// Transforms the [start] value by adding the [delta] value to it.
///
/// Returns the transformed value or NaN if the transformation is not possible.
/// Throws an [ArgumentError] if [start] is NaN.
double transform(double start, double delta){
if (start.isNaN) {
throw ArgumentError('Start value cannot be NaN');
}
return double.nan;
}
}
/// A [LinearSegment] is a [Segment] with a linear transformation function.
class LinearSegment extends Segment {
/// Creates a new [LinearSegment] object with the specified [end] and [strength].
///
/// The [end] and [strength] values are inherited from the [Segment] class.
const LinearSegment(double end, double strength) : super(end, strength);
/// Transforms the [start] value by adding the [delta] value to it and applying a linear function.
///
/// The transformation function for a [LinearSegment] is (start + delta * (end - start)).
///
/// Returns the transformed value.
///
/// Throws an [ArgumentError] if [start] is NaN.
@override
double transform(double start, double delta) {
if (start.isNaN) {
throw ArgumentError('Start value cannot be NaN');
}
return start + delta * (end - start);
}
}
/// A [QuadraticBezierSegment] is a [Segment] with a quadratic Bézier curve transformation function.
class QuadraticBezierSegment extends Segment {
/// The control value of the quadratic Bézier curve.
final double control;
/// Creates a new [QuadraticBezierSegment] object with the specified [end], [strength], and [control] values.
///
/// The [end] and [strength] values are inherited from the [Segment] class.
const QuadraticBezierSegment(double end, double strength, this.control) : super(end, strength);
/// Transforms the [start] value by adding the [delta] value to it and applying a quadratic Bézier curve transformation function.
///
/// The transformation function for a [QuadraticBezierSegment] is (1 - t)² * start + 2 * (1 - t) * t * control + t² * end, where t is [delta].
///
/// Returns the transformed value.
///
/// Throws an [ArgumentError] if [start] is NaN.
@override
double transform(double start, double delta) {
if (start.isNaN) {
throw ArgumentError('Start value cannot be NaN');
}
final inv = 1 - delta;
return inv * inv * start + 2 * inv * delta * control + delta * delta * end;
}
}
/// A [CubicBezierSegment] is a [Segment] with a cubic Bézier curve transformation function.
class CubicBezierSegment extends Segment {
/// The first control value of the cubic Bézier curve.
final double control1;
/// The second control value of the cubic Bézier curve.
final double control2;
/// Creates a new [CubicBezierSegment] object with the specified [end], [strength], [control1], and [control2] values.
///
/// The [end] and [strength] values are inherited from the [Segment] class.
const CubicBezierSegment(
super.end,
super.strength,
this.control1,
this.control2,
);
/// Transforms the [start] value by adding the [delta] value to it and applying a cubic Bézier curve transformation function.
///
/// The transformation function for a [CubicBezierSegment] is (1 - t)³ * start + 3 * (1 - t)² * t * control1 + 3 * (1 - t) * t² * control2 + t³ * end, where t is [delta].
///
/// Returns the transformed value.
@override
double transform(double start, double delta) {
final inv = 1 - delta;
final inv2 = inv * inv;
final d2 = delta * delta;
return inv2 * inv * start +
3 * inv2 * delta * control1 +
3 * inv * d2 * control2 +
d2 * delta * end;
}
}
/// A utility class for various easing functions.
abstract class EasingTools {
/// The value of pi.
@pragma('vm:prefer-inline')
static const pi = 3.1415926535897932384626433832795;
/// Half of the value of pi.
@pragma('vm:prefer-inline')
static const piHalf = pi / 2;
/// Two times the value of pi.
static const pi2 = pi * 2;
/// The natural logarithm of 2.
@pragma('vm:prefer-inline')
static const ln2 = 0.6931471805599453;
/// The natural logarithm of 10.
@pragma('vm:prefer-inline')
static const ln2_10 = 6.931471805599453;
// === LINEAR ===
/// Linear easing function.
/// Returns the input value [t] as is.
@pragma('vm:prefer-inline')
static double linear(double t) => t;
// === SINE ===
/// Sine easing function with "in" direction.
/// Returns the sine value of [t] multiplied by pi/2.
@pragma('vm:prefer-inline')
static double sineIn(double t) {
if (t == 0) return 0;
if (t == 1) return 1;
return 1 - math.cos(t * pi / 2);
}
/// Sine easing function with "out" direction.
/// Returns the sine value of [t] multiplied by pi/2.
@pragma('vm:prefer-inline')
static double sineOut(double t) {
if (t == 0) return 0;
if (t == 1) return 1;
return math.sin(t * piHalf);
}
@pragma('vm:prefer-inline')
static double sineInOut(double t) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else {
return -0.5 * (math.cos(pi * t) - 1);
}
}
@pragma('vm:prefer-inline')
static double sineOutIn(double t) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else if (t < 0.5) {
return 0.5 * math.sin((t * 2) * piHalf);
} else {
return -0.5 * math.cos((t * 2 - 1) * piHalf) + 1;
}
}
/// === QUAD ===
@pragma('vm:prefer-inline')
static double quadIn(double t) => t * t;
@pragma('vm:prefer-inline')
static double quadOut(double t) => -t * (t - 2);
@pragma('vm:prefer-inline')
static double quadInOut(double t) =>
(t < .5) ? t * t * 2 : (-2 * t * (t - 2) - 1);
@pragma('vm:prefer-inline')
static double quadOutIn(double t) {
return (t < 0.5)
? -0.5 * (t = (t * 2)) * (t - 2)
: 0.5 * (t = (t * 2 - 1)) * t + 0.5;
}
/// === CUBIC ===
@pragma('vm:prefer-inline')
static double cubicIn(double t) => t * t * t;
@pragma('vm:prefer-inline')
static double cubicOut(double t) {
return (--t) * t * t + 1;
// t--;
// return t * t * t + 1;
}
@pragma('vm:prefer-inline')
static double cubicInOut(double t) {
// t *= 2;
// if (t < 1) {
// return 0.5 * t * t * t;
// }
// t -= 2;
// return 0.5 * (t * t * t + 2);
return ((t *= 2) < 1) ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2);
}
@pragma('vm:prefer-inline')
static double cubicOutIn(double t) {
return 0.5 * ((t = t * 2 - 1) * t * t + 1);
}
/// === QUART ===
@pragma('vm:prefer-inline')
static double quartIn(double t) => (t *= t) * t;
@pragma('vm:prefer-inline')
static double quartOut(double t) => 1 - (t = (t = t - 1) * t) * t;
@pragma('vm:prefer-inline')
static double quartInOut(double t) {
return ((t *= 2) < 1)
? 0.5 * (t *= t) * t
: -0.5 * ((t = (t -= 2) * t) * t - 2);
}
@pragma('vm:prefer-inline')
static double quartOutIn(double t) {
return (t < 0.5)
? -0.5 * (t = (t = t * 2 - 1) * t) * t + 0.5
: 0.5 * (t = (t = t * 2 - 1) * t) * t + 0.5;
}
/// === QUINT ===
@pragma('vm:prefer-inline')
static double quintIn(double t) {
return t * (t *= t) * t;
// return t * t * t * t * t ;
}
@pragma('vm:prefer-inline')
static double quintOut(double t) => (t = t - 1) * (t *= t) * t + 1;
@pragma('vm:prefer-inline')
static double quintInOut(double t) {
return ((t *= 2) < 1)
? 0.5 * t * (t *= t) * t
: 0.5 * (t -= 2) * (t *= t) * t + 1;
}
@pragma('vm:prefer-inline')
static double quintOutIn(double t) =>
0.5 * ((t = t * 2 - 1) * (t *= t) * t + 1);
/// === EXPONENTIAL ===
/// Uses pow() or Logarithmic Curve.
@pragma('vm:prefer-inline')
static double expoIn(double t) {
// return math.pow(2, 10 * (t - 1)).toDouble();
return t == 0 ? 0 : math.exp(ln2_10 * (t - 1));
}
@pragma('vm:prefer-inline')
static double expoOut(double t) {
return t == 1 ? 1 : (1 - math.exp(-ln2_10 * t));
}
@pragma('vm:prefer-inline')
static double expoInOut(double t) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else if ((t *= 2) < 1) {
return 0.5 * math.exp(ln2_10 * (t - 1));
} else {
return 0.5 * (2 - math.exp(-ln2_10 * (t - 1)));
}
}
@pragma('vm:prefer-inline')
static double expoOutIn(double t) {
if (t < 0.5) {
return 0.5 * (1 - math.exp(-20 * ln2 * t));
} else if (t == 0.5) {
return 0.5;
} else {
return 0.5 * (math.exp(20 * ln2 * (t - 1)) + 1);
}
}
/// === CIRCULAR ===
// static double circIn(double t) => 1 - math.sqrt(1 - t * t);
// public static inline function circIn(t:Float):Float {
@pragma('vm:prefer-inline')
static double circIn(double t) {
if (t < -1 || 1 < t) {
return 0;
} else {
return 1 - math.sqrt(1 - t * t);
}
}
/// Returns the result of applying the "circOut" easing function to the given time value [t].
///
/// The "circOut" function starts motion slowly and then accelerates motion as it executes.
/// It ends abruptly at its destination.
///
/// If [t] is less than 0 or greater than 2, this function returns 0.
@pragma('vm:prefer-inline')
static double circOut(double t) {
if (t < 0 || 2 < t) {
return 0;
} else {
return math.sqrt(t * (2 - t));
}
}
/// An easing function that applies a circular easing effect in both the
/// beginning and ending of the animation.
///
/// The easing equation for this method is based on a circular quarter wave,
/// and it returns the rate of change of the animation according to the
/// input [t].
///
/// The input [t] represents the elapsed fraction of the animation time,
/// with values outside the range [0, 1] also supported.
///
/// If [t] is outside the range of [-0.5, 1.5], this function returns 0.5.
/// Otherwise, if [t] is less than 1, it returns a negative half of the
/// square root of 1 minus t squared, minus 1. Otherwise, it returns half
/// of the square root of 1 minus the quantity (t minus 2) squared, plus 1.
@pragma('vm:prefer-inline')
static double circInOut(double t) {
if (t < -0.5 || 1.5 < t) {
return 0.5;
} else if ((t *= 2) < 1) {
return -0.5 * (math.sqrt(1 - t * t) - 1);
} else {
return 0.5 * (math.sqrt(1 - (t -= 2) * t) + 1);
}
}
@pragma('vm:prefer-inline')
static double circOutIn(double t) {
if (t < 0) {
return 0;
} else if (1 < t) {
return 1;
} else if (t < 0.5) {
return 0.5 * math.sqrt(1 - (t = t * 2 - 1) * t);
} else {
return -0.5 * ((math.sqrt(1 - (t = t * 2 - 1) * t) - 1) - 1);
}
}
/// === bounce ===
/// Computes the bounce-in easing function for a given progress value [t].
///
/// The bounce-in function starts slow and accelerates as it progresses, before
/// bouncing back down to its final value.
///
/// [t] is the progress value of the animation, which should be in the range [0, 1].
///
/// Returns the value of the animation at the given progress value, which is also in the range [0, 1].
@pragma('vm:prefer-inline')
static double bounceIn(double t) {
if ((t = 1 - t) < (1 / 2.75)) {
return 1 - ((7.5625 * t * t));
} else if (t < (2 / 2.75)) {
return 1 - ((7.5625 * (t -= (1.5 / 2.75)) * t + 0.75));
} else if (t < (2.5 / 2.75)) {
return 1 - ((7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375));
} else {
return 1 - ((7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375));
}
}
@pragma('vm:prefer-inline')
static double bounceOut(double t) {
const n1 = 7.5625;
const d1 = 2.75;
if (t < 1 / d1) {
return n1 * t * t;
} else if (t < 2 / d1) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if (t < 2.5 / d1) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
// if (t < (1 / 2.75)) {
// return (7.5625 * t * t);
// } else if (t < (2 / 2.75)) {
// return (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
// } else if (t < (2.5 / 2.75)) {
// return (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
// } else {
// return (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
// }
}
@pragma('vm:prefer-inline')
static double bounceInOut(double t) {
if (t < 0.5) {
if ((t = (1 - t * 2)) < (1 / 2.75)) {
return (1 - ((7.5625 * t * t))) * 0.5;
} else if (t < (2 / 2.75)) {
return (1 - ((7.5625 * (t -= (1.5 / 2.75)) * t + 0.75))) * 0.5;
} else if (t < (2.5 / 2.75)) {
return (1 - ((7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375))) * 0.5;
} else {
return (1 - ((7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375))) * 0.5;
}
} else {
if ((t = (t * 2 - 1)) < (1 / 2.75)) {
return ((7.5625 * t * t)) * 0.5 + 0.5;
} else if (t < (2 / 2.75)) {
return ((7.5625 * (t -= (1.5 / 2.75)) * t + 0.75)) * 0.5 + 0.5;
} else if (t < (2.5 / 2.75)) {
return ((7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375)) * 0.5 + 0.5;
} else {
return ((7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375)) * 0.5 + 0.5;
}
}
}
@pragma('vm:prefer-inline')
static double bounceOutIn(double t) {
if (t < 0.5) {
if ((t = (t * 2)) < (1 / 2.75)) {
return 0.5 * (7.5625 * t * t);
} else if (t < (2 / 2.75)) {
return 0.5 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
} else if (t < (2.5 / 2.75)) {
return 0.5 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
} else {
return 0.5 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
}
} else {
if ((t = (1 - (t * 2 - 1))) < (1 / 2.75)) {
return 0.5 - (0.5 * (7.5625 * t * t)) + 0.5;
} else if (t < (2 / 2.75)) {
return 0.5 - (0.5 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75)) + 0.5;
} else if (t < (2.5 / 2.75)) {
return 0.5 - (0.5 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375)) + 0.5;
} else {
return 0.5 -
(0.5 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375)) +
0.5;
}
}
}
/// === BACK easing ===
static const double defaultOvershoot = 1.70158;
@pragma('vm:prefer-inline')
static double backIn(double t, [double c1 = EasingTools.defaultOvershoot]) {
// if (t == 0) {
// return 0;
// } else if (t == 1) {
// return 1;
// } else {
// return t * t * ((overshoot + 1) * t - overshoot);
// }
final c3 = c1 + 1;
return c3 * t * t * t - c1 * t * t;
}
@pragma('vm:prefer-inline')
static double backOut(double t, [double c1 = EasingTools.defaultOvershoot]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else {
return ((t = t - 1) * t * ((c1 + 1) * t + c1) + 1);
}
}
@pragma('vm:prefer-inline')
static double backInOut(double t,
[double c1 = EasingTools.defaultOvershoot]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else if ((t *= 2) < 1) {
return 0.5 * (t * t * (((c1 * 1.525) + 1) * t - c1 * 1.525));
} else {
return 0.5 * ((t -= 2) * t * (((c1 * 1.525) + 1) * t + c1 * 1.525) + 2);
}
}
@pragma('vm:prefer-inline')
static double backOutIn(double t,
[double c1 = EasingTools.defaultOvershoot]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else if (t < 0.5) {
return 0.5 * ((t = t * 2 - 1) * t * ((c1 + 1) * t + c1) + 1);
} else {
return 0.5 * (t = t * 2 - 1) * t * ((c1 + 1) * t - c1) + 0.5;
}
}
/// === ELASTIC easing ===
static const double defaultAmplitude = 1;
static const double defaultPeriod = 0.0003;
@pragma('vm:prefer-inline')
static double elasticIn(
double t, [
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else {
var s = period / 4;
return -(amplitude *
math.exp(ln2_10 * (t -= 1)) *
math.sin((t * 0.001 - s) * (2 * pi) / period));
}
}
@pragma('vm:prefer-inline')
static double elasticOut(
double t, [
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else {
var s = period / 4;
return amplitude *
math.exp(-ln2_10 * t) *
math.sin((t * 0.001 - s) * (2 * pi) / period) +
1;
}
}
@pragma('vm:prefer-inline')
static double elasticInOut(double t,
[double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude]) {
if (t == 0) {
return 0;
} else if (t == 1) {
return 1;
} else {
var s = period / 4;
if ((t *= 2) < 1) {
return -0.5 *
(amplitude *
math.exp(ln2_10 * (t -= 1)) *
math.sin((t * 0.001 - s) * pi2 / period));
} else {
return amplitude *
math.exp(-ln2_10 * (t -= 1)) *
math.sin((t * 0.001 - s) * pi2 / period) *
0.5 +
1;
}
}
}
@pragma('vm:prefer-inline')
static double elasticOutIn(double t,
[double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude]) {
if (t < 0.5) {
if ((t *= 2) == 0) {
return 0;
} else {
var s = period / 4;
return (amplitude / 2) *
math.exp(-ln2_10 * t) *
math.sin((t * 0.001 - s) * pi2 / period) +
0.5;
}
} else {
if (t == 0.5) {
return 0.5;
} else if (t == 1) {
return 1;
} else {
t = t * 2 - 1;
var s = period / 4;
return -((amplitude / 2) *
math.exp(ln2_10 * (t -= 1)) *
math.sin((t * 0.001 - s) * pi2 / period)) +
0.5;
}
}
}
@pragma('vm:prefer-inline')
static double warpOut(double t) => t <= 0 ? 0 : 1;
@pragma('vm:prefer-inline')
static double warpIn(double t) => t < 1 ? 0 : 1;
@pragma('vm:prefer-inline')
static double warpInOut(double t) => t < .5 ? 0 : 1;
@pragma('vm:prefer-inline')
static double warpOutIn(double t) {
if (t <= 0) {
return 0;
} else if (t < 1) {
return .5;
}
return 1;
}
}
abstract class LerpTools {
static final _rnd = math.Random();
/// Calculates the spread given a [rate] and [scale].
/// The [scale] determines the maximum distance from the [rate] at which the spread can occur.
/// Returns a value between -[scale] and [scale].
@pragma('vm:prefer-inline')
static double spread(double rate, double scale) => lerp(rate, -scale, scale);
// This method generates a random value within a range of [-rate, rate], centered around a given value (center).
// If no randomCallback is provided, it uses a default random number generator.
@pragma('vm:prefer-inline')
static double shake(
double rate, [
double center = 0,
double Function()? randomCallback,
]) =>
center + spread((randomCallback ?? _rnd.nextDouble)(), rate);
@pragma('vm:prefer-inline')
static double sin(double rate) => math.sin(rate * EasingTools.pi2);
@pragma('vm:prefer-inline')
static double cos(double rate) => math.cos(rate * EasingTools.pi2);
/// Reverts the progress of an animation.
///
/// Given an animation's rate (a value between 0 and 1), this function
/// returns a new rate value that represents the opposite progress,
/// going from 1 to 0.
@pragma('vm:prefer-inline')
static double revert(double rate) => 1 - rate;
/// Clamps a [value] between a [min] and a [max] value.
///
/// If [value] is less than [min], returns [min]. If [value] is greater than
/// [max], returns [max]. Otherwise, returns [value].
///
/// * [value]: The value to clamp.
/// * [min]: The minimum value to clamp [value] to. Defaults to 0.0.
/// * [max]: The maximum value to clamp [value] to. Defaults to 1.0.
///
/// Returns a double value that is clamped between [min] and [max].
static double clamp(double value, [double min = 0.0, double max = 1.0]) {
if (value <= min) {
return min;
} else if (value >= max) {
return max;
} else {
return value;
}
}
/// Linear interpolation between [from] and [to] by a [rate] value.
///
/// [rate] should be a value between 0.0 and 1.0.
///
/// Returns the interpolated value.
@pragma('vm:prefer-inline')
static double lerp(num rate, num from, num to) =>
from * (1.0 - rate) + to * rate.toDouble();
/// Calculates the inverse linear interpolation of a given value between a given range.
///
/// Given a value between [from] and [to], this function returns the ratio of how far the
/// given value is from from to to. For example, if value is halfway between from
/// and to, this function will return 0.5.
///
/// Arguments:
/// - value: the value to find the ratio of.
/// - from: the start of the range.
/// - to: the end of the range.
///
/// Returns:
/// - A double value representing the ratio of how far value is between from and to.
@pragma('vm:prefer-inline')
static double inverseLerp(num value, num from, num to) =>
(value - from) / (to - from);
@pragma('vm:prefer-inline')
static double mixEasing(
double rate,
EaseFun easing1,
EaseFun easing2, [
double easing2Strength = 0.5,
]) =>
easing2Strength.lerp(easing1(rate), easing2(rate));
/// Mixes two easing functions based on a given strength value.
///
/// [rate]: A value between 0.0 and 1.0 indicating the current position of the easing.
/// [easing1]: The first easing function to mix.
/// [easing2]: The second easing function to mix.
/// [easing2Strength]: A value between 0.0 and 1.0 indicating the strength of the second easing function.
///
/// Returns the resulting value of mixing both easing functions.
@pragma('vm:prefer-inline')
static double crossfadeEasing(
double rate,
EaseFun easing1,
EaseFun easing2,
EaseFun easing2StrengthEasing, [
double easing2StrengthStart = 0,
double easing2StrengthEnd = 1,
]) =>
easing2StrengthEasing(rate)
.lerp(easing2StrengthStart, easing2StrengthEnd)
.lerp(easing1(rate), easing2(rate));
/// Connects two easing functions together with a smooth transition between them.
///
/// This method returns a new easing function that starts with `easing1` and then
/// gradually transitions into `easing2` over the duration of `switchTime`.
///
/// - `time`: the current time in the range of 0 to 1.
/// - `easing1`: the first easing function to use before `switchTime`.
/// - `easing2`: the second easing function to use after `switchTime`.
/// - `switchTime`: the time at which to switch from `easing1` to `easing2` (default: 0.5).
/// - `switchValue`: the value at which to switch from `easing1` to `easing2` (default: 0.5).
///
/// Returns: The eased value at the current time.
@pragma('vm:prefer-inline')
static double connectEasing(
double time,
EaseFun easing1,
EaseFun easing2, [
double switchTime = 0.5,
double switchValue = 0.5,
]) =>
(time < switchTime)
? //
easing1(time.invLerp(0, switchTime)).lerp(0, switchValue)
: //
easing2(time.invLerp(switchTime, 1)).lerp(switchValue, 1);
/// The yoyo method takes in a rate and an easing function ease,
/// and returns a value based on the input rate and the ease function.
/// The returned value goes back and forth between the beginning and end values,
/// similar to a yo-yo moving up and down.
/// If the input rate is less than 0.5, the ease function is applied to rate
/// multiplied by 2. Otherwise, the ease function is applied to (1 - rate)
/// multiplied by 2. This creates a mirrored easing effect where the value will gradually change from 0 to 1 and then back to 0 again.
@pragma('vm:prefer-inline')
static double yoyo(double rate, EaseFun ease) =>
(rate < 0.5) ? ease(rate * 2) : ease((1 - rate) * 2);
/// Reverses the easing function after the halfway point.
///
/// Given an [EaseFun] function and a [rate] value between 0.0 and 1.0,
/// applies the easing function to the value if [rate] is less than 0.5.
/// Otherwise, applies the reverse of the easing function to the value
/// after the halfway point (rate of 0.5). Returns the eased value.
///
/// Example usage:
///
/// ```dart
/// double reverseEase = EasingTools.reverse(rate, EasingTools.easeInOutQuad);
/// ```
///
/// In the above example, the [rate] value is first passed through the `easeInOutQuad`
/// easing function, and then the `reverse` function is applied to it.
/// If [rate] is less than 0.5, the result of `easeInOutQuad` on `rate` is returned.
/// If [rate] is greater than or equal to 0.5, the reverse of `easeInOutQuad`
/// is applied to the result of `(rate - .5) * 2`.
///
/// The returned value is always between 0.0 and 1.0, inclusive.
@pragma('vm:prefer-inline')
static double reverse(double rate, EaseFun ease) =>
(rate < 0.5) ? ease(rate * 2) : (1 - ease((rate - .5) * 2));
/// Applies a 2nd degree bezier curve easing function to [rate] between [from]
/// and [to], using the [control] point as the curve's anchor.
///
/// Returns a value between [from] and [to] based on the [rate] value provided.
@pragma('vm:prefer-inline')
static double bezier2(double rate, double from, double control, double to) =>
lerp(rate, lerp(rate, from, control), lerp(rate, control, to));
@pragma('vm:prefer-inline')
static double _evaluateCubic(double a, double b, double m) =>
3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m;
/// Flutter's `Cubic`.
/// Evaluates a cubic easing function at a given rate using an iterative
/// binary search to estimate the `x` value that corresponds to the `y` value
/// of the input `rate`. The easing function is defined by four control points
/// `a`, `b`, `c`, and `d`, which define the start, end, and two control
/// points of a cubic Bezier curve.
///
/// The function returns the interpolated `y` value of the cubic Bezier curve
/// at the calculated `x` value.
///
/// [rate] The input rate at which to evaluate the easing function, typically
/// a value between 0.0 and 1.0.
///
/// [a] The start value of the cubic Bezier curve.
///
/// [b] The end value of the cubic Bezier curve.
///
/// [c] The first control point of the cubic Bezier curve.
///
/// [d] The second control point of the cubic Bezier curve.
///
/// [resolution] The maximum difference allowed between the estimated and
/// target `y` value of the easing function before returning the estimate.
///
/// Returns the interpolated `y` value of the cubic Bezier curve at the
/// calculated `x` value.
@pragma('vm:prefer-inline')
static double cubic(double rate, double a, double b, double c, double d,
[double resolution = .001]) {
double start = 0.0;
double end = 1.0;
while (true) {
final double midpoint = (start + end) / 2;
final double estimate = _evaluateCubic(a, c, midpoint);
if ((rate - estimate).abs() < resolution) {
return _evaluateCubic(b, d, midpoint);
}
if (estimate < rate) {
start = midpoint;
} else {
end = midpoint;
}
}
}
/// cubic
/// Interpolates a value on a cubic bezier curve defined by 4 control points.
///
/// The `rate` parameter determines how far along the curve to interpolate,
/// with a value of 0 resulting in the `from` point and a value of 1 resulting
/// in the `to` point.
///
/// The `from`, `control1`, `control2`, and `to` parameters define the 4
/// control points of the cubic bezier curve.
///
/// This method is a convenience wrapper around the `bezier2` method, which
/// computes the value of a quadratic bezier curve. The cubic bezier curve is
/// approximated by dividing it into two quadratic curves using the control
/// points `from`, `control1`, and `control2`.
@pragma('vm:prefer-inline')
static double bezier3(
double rate,
double from,
double control1,
double control2,
double to,
) =>
bezier2(
rate,
lerp(rate, from, control1),
lerp(rate, control1, control2),
lerp(rate, control2, to),
);
@pragma('vm:prefer-inline')
static double bezier(double rate, Iterable<double> values) {
if (values.length < 2) {
throw "points length must be more than 2";
} else if (values.length == 2) {
return lerp(rate, values.first, values.last);
} else if (values.length == 3) {
return bezier2(rate, values.first, values.elementAt(1), values.last);
} else {
return _bezier(rate, values);
}
}
static double _bezier(double rate, Iterable<double> values) {
if (values.length == 4) {
return bezier3(rate, values.first, values.elementAt(1),
values.elementAt(2), values.last);
}
final iterValues = values.toList(growable: false);
final newLen = values.length - 1;
final output = List.filled(values.length - 1, 0.0);
for (var i = 0; i < newLen; ++i) {
output[i] = lerp(rate, iterValues[i], iterValues[i + 1]);
}
return _bezier(rate, output);
}
/// Evaluates a uniform quadratic B-spline with given `rate` and `values`.
///
/// The `values` parameter must have at least two elements, otherwise a
/// `String` error message is thrown.
///
/// If `values` has exactly two elements, then the result is the linear
/// interpolation of the two values based on `rate`.
///
/// Otherwise, the result is computed based on a quadratic B-spline function.
/// The values are divided into a set of three adjacent points (triads), each
/// of which is used to calculate a parabolic curve. The final curve is
/// constructed by smoothly joining these curves together.
///
/// The `rate` parameter is expected to be between 0.0 and 1.0 inclusive. The
/// function returns a value that corresponds to the position along the curve
/// specified by `rate`.
static double uniformQuadBSpline(double rate, Iterable<double> values) {
if (values.length < 2) {
throw "points length must be more than 2";
}
if (values.length == 2) {
return lerp(rate, values.first, values.last);
}
final max = values.length - 2;
final scaledRate = rate * max;
final index = scaledRate.floor().clamp(0, max - 1);
final innerRate = scaledRate - index;
final p0 = values.elementAt(index);
final p1 = values.elementAt(index + 1);
final p2 = values.elementAt(index + 2);
return innerRate * innerRate * (p0 / 2 - p1 + p2 / 2) +
innerRate * (-p0 + p1) +
p0 / 2;
}
/// Interpolates a value along a polyline defined by a list of control points.
///
/// The rate parameter specifies the position along the polyline to interpolate,
/// with 0.0 indicating the beginning of the polyline and 1.0 indicating the end.
///
/// The values parameter is a list of doubles that define the control points of the polyline.
/// The length of the list must be at least 2, and each value in the list represents a point on the polyline.
///
/// This function returns the interpolated value at the specified rate position along the polyline.
///
/// Throws an error if the length of values is less than 2.
@pragma('vm:prefer-inline')
static double polyline(double rate, List<double> values) {
if (values.length < 2) {
throw "points length must be more than 2";
} else {
final max = values.length - 1;
final scaledRate = rate * max;
final index = scaledRate.clamp(0, max - 1).floor();
return lerp(scaledRate - index, values[index], values[index + 1]);
}
}
}
typedef EaseFun = double Function(double rate);
/// An extension on the double type providing a set of easing functions and math operations.
extension DoubleTween on double {
/// Applies sine function on the current value.
double sinRate() => LerpTools.sin(this);
/// Applies cosine function on the current value.
double cosRate() => LerpTools.cos(this);
/// Reverts the current value between 0 and 1.
double revert() => LerpTools.revert(clamped(0, 1));
/// Applies sine-in easing function on the current value.
double sineIn() => EasingTools.sineIn(this);
/// Applies sine-out easing function on the current value.
double sineOut() => EasingTools.sineOut(this);
double sineInOut() => EasingTools.sineInOut(this);
double sineOutIn() => EasingTools.sineOutIn(this);
double quadIn() => EasingTools.quadIn(this);
double quadOut() => EasingTools.quadOut(this);
double quadInOut() => EasingTools.quadInOut(this);
double quadOutIn() => EasingTools.quadOutIn(this);
double cubicIn() => EasingTools.cubicIn(this);
double cubicOut() => EasingTools.cubicOut(this);
double cubicInOut() => EasingTools.cubicInOut(this);
double cubicOutIn() => EasingTools.cubicOutIn(this);
double quintIn() => EasingTools.quintIn(this);
double quintOut() => EasingTools.quintOut(this);
double quintInOut() => EasingTools.quintInOut(this);
double quintOutIn() => EasingTools.quintOutIn(this);
double expoIn() => EasingTools.expoIn(this);
double expoOut() => EasingTools.expoOut(this);
double expoInOut() => EasingTools.expoInOut(this);
double expoOutIn() => EasingTools.expoOutIn(this);
double circIn() => EasingTools.circIn(this);
double circOut() => EasingTools.circOut(this);
double circInOut() => EasingTools.circInOut(this);
double circOutIn() => EasingTools.circOutIn(this);
double bounceIn() => EasingTools.bounceIn(this);
double bounceOut() => EasingTools.bounceOut(this);
double bounceInOut() => EasingTools.bounceInOut(this);
double bounceOutIn() => EasingTools.bounceOutIn(this);
double backIn() => EasingTools.backIn(this);
double backOut() => EasingTools.backOut(this);
double backInOut() => EasingTools.backInOut(this);
double backOutIn() => EasingTools.backOutIn(this);
double elasticIn([
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) =>
EasingTools.elasticIn(
this,
period,
amplitude,
);
double elasticOut([
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) =>
EasingTools.elasticOut(
this,
period,
amplitude,
);
double elasticInOut([
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) =>
EasingTools.elasticInOut(
this,
period,
amplitude,
);
double elasticOutIn([
double period = EasingTools.defaultPeriod,
double amplitude = EasingTools.defaultAmplitude,
]) =>
EasingTools.elasticOutIn(
this,
period,
amplitude,
);
double warpIn() => EasingTools.warpIn(this);
double warpOut() => EasingTools.warpOut(this);
double warpInOut() => EasingTools.warpInOut(this);
double warpOutIn() => EasingTools.warpOutIn(this);
double shake([
double center = 0,
double Function()? randomCallback,
]) =>
LerpTools.shake(this, center, randomCallback);
double lerp(num from, num to) {
return LerpTools.lerp(this, from, to);
}
double invLerp(num from, num to) => LerpTools.inverseLerp(
this,
from,
to,
);
/// An extension on [double] providing a method for wrapping a value
/// around a range and returning the linear interpolation within that range.
/// [end] is the upper limit of the range, while [start] is the lower limit,
/// and [clamped] determines whether the returned value is clamped to the range [0,1].
///
/// If [clamped] is set to true, the returned value will always be within the range [0,1].
/// If not, the returned value can be greater than 1 or less than 0.
/// To wrap a normalized value [lerp()] to a range.
/// Useful to keep a "loop" during a time rate.
/// For example...
/// ```dart
/// var frame = 0;
/// onTick(){
/// // it will wait 20 frames, then run til frame 80.
/// // and wrap back (modulo) to 0, and wait 20 frames again...
/// var t = ++frame.wrapLerp(80, 20);
/// print(t);
/// }
///
double wrapLerp(num end, [num start = 0, bool clamped = true]) {
var value = LerpTools.inverseLerp(this % end, start, end);
if (clamped) {
value = value.clamped(0, 1);
}
return value;
}
// used to set bounds to wrapped normalized values.
// for example, set a delay (start) and duration (end).
// based on a rate (wrappedRate) previously
// wrapped by [wrapLerp()].
// use [validLerp()] to check if the result is in valid range
// (0-1).
@pragma('vm:prefer-inline')
double invWrapLerp(num wrappedRate, num start, num end, [bool clamp = true]) {
final result = invLerp(start / wrappedRate, end / wrappedRate);
return clamp ? result.clamped(0, 1) : result;
}
/// To repeat x [times] a normalized value [lerp()].
/// Can be chained with [reverse()] and [yoyo()].
double repeatLerp(double times) {
final invRepeat = 1 / times;
return (this % invRepeat).invLerp(0, invRepeat);
}
@pragma('vm:prefer-inline')
double clamped(double min, double max) => LerpTools.clamp(this, min, max);
/// Returns true if the value is a valid lerp value, i.e., between 0 and 1 inclusive
/// to use in if() statement
@pragma('vm:prefer-inline')
bool validLerp() => this >= 0 && this <= 1;
/// This method returns a value between 0 and 1, calculated by interpolating two easing functions.
/// The first parameter is the current value between 0 and 1, the second and third parameters are the two easing functions to interpolate,
/// and the fourth parameter is the strength of the interpolation, which defaults to 0.5.
/// It returns the interpolated value.
@pragma('vm:prefer-inline')
double mixEase(EaseFun ease1, EaseFun ease2, [double strength = 0.5]) {
return LerpTools.mixEasing(this, ease1, ease2, strength);
}
@pragma('vm:prefer-inline')
double yoyo([EaseFun ease = EasingTools.linear]) =>
LerpTools.yoyo(this, ease);
@pragma('vm:prefer-inline')
double reverse([EaseFun ease = EasingTools.linear]) =>
LerpTools.reverse(this, ease);
@pragma('vm:prefer-inline')
double connectEasing(
EaseFun easing1,
EaseFun easing2, [
double switchTime = 0.5,
double switchValue = 0.5,
]) {
return LerpTools.connectEasing(
this, easing1, easing2, switchTime, switchValue);
}
@pragma('vm:prefer-inline')
double crossfadeEasing(
EaseFun easing1,
EaseFun easing2,
EaseFun easing2StrengthEasing, [
double easing2StrengthStart = 0,
double easing2StrengthEnd = 1,
]) =>
LerpTools.crossfadeEasing(
this,
easing1,
easing2,
easing2StrengthEasing,
easing2StrengthStart,
easing2StrengthEnd,
);
// quadratic
@pragma('vm:prefer-inline')
double bezier2(double from, double control, double to) =>
LerpTools.bezier2(this, from, control, to);
/// cubic easing.
@pragma('vm:prefer-inline')
double cubic(
double from,
double control1,
double control2,
double to, [
double resolution = .001,
]) =>
LerpTools.cubic(this, from, control1, control2, to, resolution);
/// cubic
@pragma('vm:prefer-inline')
double bezier3(
double from,
double control1,
double control2,
double to,
) =>
LerpTools.bezier3(this, from, control1, control2, to);
double bezier(Iterable<double> points) => LerpTools.bezier(this, points);
double bspline(Iterable<double> points) =>
LerpTools.uniformQuadBSpline(this, points);
double polyline(Iterable<double> points) =>
LerpTools.polyline(this, points.toList(growable: false));
}
extension EaseInt on int {
double lerp(num from, num to) => LerpTools.lerp(
toDouble(),
from.toDouble(),
to.toDouble(),
);
double wrapLerp(num end, [num start = 0]) {
return LerpTools.inverseLerp(this % end, start, end);
}
double shake([
double center = 0,
double Function()? randomCallback,
]) =>
LerpTools.shake(toDouble(), center, randomCallback);
double lerpInverse(num from, num to) => LerpTools.inverseLerp(
toDouble(),
from.toDouble(),
to.toDouble(),
);
}
// class GPointTools {
// static GPoint polyline(double rate, List<GPoint> points, [GPoint? output]) {
// output ??= GPoint(0,0);
// var x = <double>[];
// var y = <double>[];
// for (var p in points) {
// x.add(p.x);
// y.add(p.y);
// }
// output.x = rate.polyline(x);
// output.y = rate.polyline(y);
// return output;
// }
// }
/// expose constants easing functions for LerpTools functions like
/// [LerpTools.yoyo] and [LerpTools.reverse].
const linear = EasingTools.linear;
const sineIn = EasingTools.sineIn;
const sineOut = EasingTools.sineOut;
const sineInOut = EasingTools.sineInOut;
const sineOutIn = EasingTools.sineOutIn;
const quadIn = EasingTools.quadIn;
const quadOut = EasingTools.quadOut;
const quadInOut = EasingTools.quadInOut;
const quadOutIn = EasingTools.quadOutIn;
const cubicIn = EasingTools.cubicIn;
const cubicOut = EasingTools.cubicOut;
const cubicInOut = EasingTools.cubicInOut;
const cubicOutIn = EasingTools.cubicOutIn;
const quintIn = EasingTools.quintIn;
const quintOut = EasingTools.quintOut;
const quintInOut = EasingTools.quintInOut;
const quintOutIn = EasingTools.quintOutIn;
const expoIn = EasingTools.expoIn;
const expoOut = EasingTools.expoOut;
const expoInOut = EasingTools.expoInOut;
const expoOutIn = EasingTools.expoOutIn;
const circIn = EasingTools.circIn;
const circOut = EasingTools.circOut;
const circInOut = EasingTools.circInOut;
const circOutIn = EasingTools.circOutIn;
const bounceIn = EasingTools.bounceIn;
const bounceOut = EasingTools.bounceOut;
const bounceInOut = EasingTools.bounceInOut;
const bounceOutIn = EasingTools.bounceOutIn;
const backIn = EasingTools.backIn;
const backOut = EasingTools.backOut;
const backInOut = EasingTools.backInOut;
const backOutIn = EasingTools.backOutIn;
const elasticIn = EasingTools.elasticIn;
const elasticOut = EasingTools.elasticOut;
const elasticInOut = EasingTools.elasticInOut;
const elasticOutIn = EasingTools.elasticOutIn;
const warpIn = EasingTools.warpIn;
const warpOut = EasingTools.warpOut;
const warpInOut = EasingTools.warpInOut;
const warpOutIn = EasingTools.warpOutIn;
// Gives back an `EaseFun` callback transform, useful for [LerpTools.yoyo]
EaseFun easeCubic(double a, b, c, d) => (double rate) => rate.cubic(a, b, c, d);
// Make an instance with the configuration for a Cubic Bezier.
// useful for [LerpTools.yoyo].
// ```
// double time = (getTimer() * .004);
// final easeCubic = CubicParams(0.25, 0.1, 0.25, 1.0);
// box.x = time.yoyo(easeCubic).lerp(0, 100);
// ```
class CubicParams {
final double a, b, c, d;
const CubicParams(this.a, this.b, this.c, this.d);
double call(double rate) => rate.cubic(a, b, c, d);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment