diff --git a/package.json b/package.json
index 10e63e0..30bfcba 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
{
- "name": "nyc-frontend-library-template",
+ "name": "colib-js",
"version": "0.0.0-development",
- "description": "A template for open source projects.",
+ "description": "Tweening and logic sequencing for typescript",
"repository": {
"type": "git",
- "url": "https://github.com/twobulls/nyc-frontend-library-template.git"
+ "url": "https://github.com/twobulls/colib-js.git"
},
"license": "Apache-2.0",
"main": "dist/index.min.js",
diff --git a/src/core/command-queue.ts b/src/core/command-queue.ts
new file mode 100644
index 0000000..8a11ff2
--- /dev/null
+++ b/src/core/command-queue.ts
@@ -0,0 +1,119 @@
+import { Command, CommandOperation, CommandState } from './commands';
+
+/**
+ * The CommandQueue class is one of core primitives for running commands.
+ * It operates, as its name suggests, as a FIFO queue. All Commands Enqueued
+ * to the queue run in sequential order. When it is fed time via Update, it
+ * will remove Commands from the queue as they complete.
+ */
+export class CommandQueue {
+ /**
+ * Gets or sets a value indicating whether this `CommandQueue` is paused.
+ */
+ paused = false;
+
+ /**
+ * Gets the elapsed time since the current executing CommandDelegate started.
+ */
+ get deltaTimeAccumulation() {
+ return this._deltaTimeAccumulation;
+ }
+
+ /**
+ * Indicates whether the CommandQueue is currently in an update loop. Update should
+ * never be again while this is true.
+ */
+ get updating() {
+ return this._updating;
+ }
+
+ private commands: Command[] = [];
+ private currentCommand?: Command;
+ private _deltaTimeAccumulation = 0.0;
+ private _updating = false;
+
+ /**
+ * Enqueue the specified command. Commands are queued up in the order specified.
+ * Multiple calls to `enqueue` result is the same sequential ordering ie.
+ * @example
+ * CommandQueue queue = new CommandQueue();
+ * queue.Enqueue(commandOne);
+ * queue.Enqueue(commandTwo);
+ * // Is equivalent to
+ * queue.Enqueue(commandOne, commandTwo);
+ * @param commands The `Command`s to be enqueued. The `CommandQueue` will dequeue the commands over succesive calls to
+ * update.
+ */
+ enqueue(...commands: Command[]): CommandQueue {
+ this.commands.push(...commands);
+ return this;
+ }
+
+ /**
+ * Updates the queue with a zero time update. This will make sure the first available command is started.
+ */
+ process() {
+ // If we are already in an update loop, then just let the queue continue running.
+ if (!this.updating) {
+ this.update(0.0);
+ }
+ }
+
+ /**
+ * Tries to update a queue until it has complete. Note, this can result in an infinite loop if
+ * commands in the queue rely on external state changes.
+ */
+ runToEnd() {
+ this.update(Number.MAX_VALUE, CommandOperation.FastForward);
+ }
+
+ /**
+ * Updates the CommandQueue. This causes CommandDelegates to be executed
+ * in the order than are enqueued. Update will return after a `Command` elects to pause. This method can't be called
+ * recursively.
+ * @param deltaTime The time, in seconds, since the last update. Must be >= 0.
+ * @param operation The update operation to use. Fastforward will try to force commands to reach the end of the queue.
+ * @returns If the queue is finished as no `Command`s remain, returns `true`, `false` otherwise.
+ */
+ update(deltaTime: number, operation = CommandOperation.Normal): boolean {
+ if (deltaTime < 0.0) {
+ throw RangeError('deltaTime is expected to be positive.');
+ }
+ if (this._updating) {
+ // Guard against recursive calls.
+ throw new Error("update can't be called recursively.");
+ }
+ this._updating = true;
+
+ try {
+ if (!this.paused) {
+ this._deltaTimeAccumulation += deltaTime;
+ let shouldRun = this.commands.length !== 0 || this.currentCommand !== undefined;
+ while (shouldRun) {
+ if (this.currentCommand === undefined) {
+ const [firstCommand, ...remainder] = this.commands;
+ this.currentCommand = firstCommand;
+ this.commands = remainder;
+ }
+
+ const result = this.currentCommand(this.deltaTimeAccumulation, operation);
+ if (result.complete) {
+ this.currentCommand = undefined;
+ }
+
+ // Only run again if an action just finished,
+ // (indicated by currentCommand == null), and we have more actions.
+ shouldRun = result.complete && this.commands.length !== 0 && !this.paused;
+ }
+ }
+ const done = this.commands.length === 0 && this.currentCommand === undefined;
+ return done;
+ } finally {
+ this._updating = false;
+ deltaTime = this.deltaTimeAccumulation;
+ if (this.currentCommand === undefined) {
+ this._deltaTimeAccumulation = 0.0;
+ }
+ }
+ }
+}
diff --git a/src/core/commands.ts b/src/core/commands.ts
new file mode 100644
index 0000000..ed256f9
--- /dev/null
+++ b/src/core/commands.ts
@@ -0,0 +1,465 @@
+import { Ease } from './ease';
+
+export enum CommandOperation {
+ Normal,
+ FastForward
+}
+
+export interface CommandState {
+ deltaTime: number;
+ complete: boolean;
+}
+
+/**
+ * The base building block for all commands.
+ * @description
+ * This is what the CommandQueue and CommandScheduler update. Commands typically capture state, and are only safe to be
+ * invoked by a single queue/scheduler at once.
+ * Inside options, deltaTime is the time to update the command by. The command should modify deltaTime, subtracting the
+ * time it has consumed. The command sets the completed flag to true when it has completed, or false otherwise. Once the
+ * delegate has completed, the next call should restart it. If the operation is set to fast forward, the command should
+ * try to immediately complete.
+ */
+export type Command = (deltaTime: number, operation: CommandOperation) => CommandState;
+
+/**
+ * A one shot command. It doesn't take up any time, and completes immediately.
+ */
+export type CommandAct = () => void;
+
+/**
+ * A condition returns true or false, which can change the flow control for some commands.
+ */
+export type CommandCondition = () => boolean;
+
+/**
+ * A duration command is executed over a period of time. The value t is normalized from 0 to 1.
+ */
+export type CommandDuration = (t: number) => void;
+
+/**
+ * A command factory creates a command.
+ */
+export type CommandFactory = () => Command;
+
+export type CommandIterator = IterableIterator;
+/**
+ * A coroutine command uses generators to produce a sequence of commands over time.
+ * @example
+ * function *aCoroutine(): CommandIterator {
+ * yield wait(5); // Log t for 5 seconds
+ * console.log("Now this is called");
+ * yield duration(t => console.log(t), 10); // Log t for 10 seconds
+ * console.log("This is also called");
+ * }
+ */
+export type CommandCoroutine = () => CommandIterator;
+
+/**
+ * Runs a `CommandAct`, which takes up no time and immediately finishes.
+ * @param command The command to execute.
+ */
+export function act(command: CommandAct): Command {
+ return deltaTime => {
+ command();
+ return { deltaTime, complete: true };
+ };
+}
+
+/**
+ * Interruptable commands are useful for situations where a command is waiting for an external action to finish,
+ * but a queue running the command wants to fast foward. For example, consider a command to play an audio source.
+ * The command starts the Audio, then polls waiting for it to finish. Suddenly a queue running the command
+ * is told to runToEnd. In this case, the onInterrupt action is called, which stops the audio source and performs
+ * cleanup. The command then finishes, and the queue continues.
+ * @param command The command to make interruptable
+ * @param onInterrupt The action to perform if the
+ */
+export function interruptable(command: Command, onInterrupt: () => void): Command {
+ let started = false;
+ return (deltaTime, operation) => {
+ if (operation === CommandOperation.FastForward) {
+ if (started) {
+ onInterrupt();
+ started = false;
+ }
+ return { deltaTime, complete: true };
+ }
+ started = true;
+ const result = command(deltaTime, operation);
+ if (result.complete) {
+ started = false;
+ }
+ return result;
+ };
+}
+
+/**
+ * A command which does nothing. Can be useful as a return value.
+ */
+export function none(): Command {
+ return (deltaTime, operation) => ({ deltaTime, complete: true });
+}
+
+/**
+ * CommandDuration runs a command over a duration of time.
+ * @param command The command to execute.
+ * @param commandDuration The duration of time, in seconds, to apply the command over. Must be greater than or equal to 0.
+ * @param ease An easing function to apply to the t parameter of a CommandDuration. If undefined, linear easing is used.
+ */
+export function duration(command: CommandDuration, commandDuration: number, ease?: Ease): Command {
+ checkDurationGreaterThanOrEqualToZero(commandDuration);
+ if (commandDuration === 0.0) {
+ // Sometimes it is convenient to create duration commands with
+ // a time of zero, so we have a special case.
+ return (deltaTime, operation) => {
+ let t = 1.0;
+ if (ease !== undefined) {
+ t = ease(t);
+ }
+ command(t);
+ return { deltaTime, complete: true };
+ };
+ }
+
+ let elapsedTime = 0.0;
+
+ return (deltaTime, operation) => {
+ elapsedTime += deltaTime;
+
+ let t = elapsedTime / commandDuration;
+ t = t < 0.0 ? 0.0 : t > 1.0 ? 1.0 : t;
+
+ if (operation === CommandOperation.FastForward) {
+ t = 1;
+ }
+
+ if (ease != null) {
+ t = ease(t);
+ }
+ command(t);
+
+ const complete = elapsedTime >= commandDuration;
+ if (operation === CommandOperation.FastForward) {
+ elapsedTime = 0.0;
+ } else if (complete) {
+ deltaTime = elapsedTime - commandDuration;
+ elapsedTime = 0.0;
+ } else {
+ deltaTime = 0.0;
+ }
+ return { deltaTime, complete };
+ };
+}
+
+/**
+ * A Wait command does nothing until duration has elapsed
+ * @property commandDuration The duration of time, in seconds, to wait. Must be greater than 0.
+ */
+export function waitForSeconds(commandDuration: number): Command {
+ checkDurationGreaterThanZero(commandDuration);
+ let elapsedTime = 0.0;
+ return (deltaTime, operation) => {
+ if (operation === CommandOperation.FastForward) {
+ elapsedTime = 0.0;
+ return { deltaTime, complete: true };
+ }
+ elapsedTime += deltaTime;
+ deltaTime = 0.0;
+ const complete = elapsedTime >= commandDuration;
+ if (complete) {
+ deltaTime = elapsedTime - commandDuration;
+ elapsedTime = 0.0;
+ }
+ return { deltaTime, complete };
+ };
+}
+
+/**
+ * Waits a specified number of calls to update. This ignores time althogether.
+ * @param frameCount The number of frames to wait. Must be > 0.
+ */
+export function waitForFrames(frameCount: number): Command {
+ frameCount = Math.ceil(frameCount);
+ if (frameCount <= 0) {
+ throw RangeError('frameCount must be > 0.');
+ }
+ let counter = frameCount;
+ return (deltaTime, operation) => {
+ if (operation === CommandOperation.FastForward) {
+ return { deltaTime, complete: true };
+ }
+ if (counter > 0) {
+ --counter;
+ deltaTime = 0;
+ return { deltaTime, complete: false };
+ }
+ counter = frameCount;
+ return { deltaTime, complete: true };
+ };
+}
+
+/**
+ * A parallel command executes several commands in parallel. It finishes
+ * when the last command has finished.
+ * @param commands The commands to execute.
+ */
+export function parallel(...commands: Command[]): Command {
+ // Optimization.
+ if (commands.length === 0) {
+ return none();
+ }
+ if (commands.length === 1) {
+ return commands[0];
+ }
+
+ const finishedCommands = [...Array(commands.length)].fill(false);
+
+ return (deltaTime, operation) => {
+ let complete = true;
+ let smallestDeltaTime = deltaTime;
+ for (let i = 0; i < commands.length; ++i) {
+ if (finishedCommands[i]) {
+ continue;
+ }
+ const result = commands[i](deltaTime, operation);
+ finishedCommands[i] = result.complete;
+ complete = commands && result.complete;
+ smallestDeltaTime = Math.min(result.deltaTime, smallestDeltaTime);
+ }
+
+ if (complete) {
+ finishedCommands.fill(false);
+ }
+ deltaTime = smallestDeltaTime;
+ return { deltaTime, complete };
+ };
+}
+
+/**
+ * A sequence command executes several commands sequentially.
+ * @param commands A parameter list of commands to execute sequentially.
+ */
+export function sequence(...commands: Command[]): Command {
+ // Optimization.
+ if (commands.length === 0) {
+ return none();
+ }
+ if (commands.length === 1) {
+ return commands[0];
+ }
+
+ let index = 0;
+ return (deltaTime, operation) => {
+ let complete = true;
+ while (complete) {
+ const result = commands[index](deltaTime, operation);
+ deltaTime = result.deltaTime;
+ complete = result.complete;
+ if (complete) {
+ index += 1;
+ }
+ if (index === commands.length) {
+ index = 0;
+ return result;
+ }
+ }
+ return { complete, deltaTime };
+ };
+}
+
+/**
+ * The repeat command repeats a delegate a given number of times.
+ * @param repeatCount The number of times to repeat the given command. Must be > 0.
+ * @param commands The commands to repeat. All of the basic commands are repeatable without side-effects.
+ */
+export function repeat(repeatCount: number, ...commands: Command[]): Command {
+ if (repeatCount <= 0) {
+ throw new RangeError('repeatCount must be > 0.');
+ }
+ const seq = sequence(...commands);
+ let count = 0;
+ return (deltaTime, operation) => {
+ let complete = true;
+ while (complete && count < repeatCount) {
+ const result = seq(deltaTime, operation);
+ deltaTime = result.deltaTime;
+ complete = result.complete;
+ if (complete) {
+ count++;
+ }
+ }
+ count %= repeatCount;
+ return { complete, deltaTime };
+ };
+}
+
+/**
+ * Repeats a command forever. Make sure that the commands you are repeating will consume some time, otherwise this will
+ * create an infinite loop.
+ * @param commands The commands to execute.
+ */
+export function repeatForever(...commands: Command[]): Command {
+ const seq = sequence(...commands);
+ return (deltaTime, operation) => {
+ let complete = true;
+ while (complete) {
+ const result = seq(deltaTime, operation);
+ complete = result.complete;
+ deltaTime = result.deltaTime;
+ }
+ return { complete, deltaTime };
+ };
+}
+
+/**
+ * Creates a command which runs a coroutine.
+ * @param command The command to generate the coroutine.
+ * @description
+ * Coroutines, (also known as generators in ES6), are methods which can be paused/resumed using the `yield` operator.
+ * @example
+ *
+ * const queue = new CommandQueue();
+ *
+ * function *coroutineWithNoArguments() {
+ * yield return waitForSeconds(2.0);
+ * }
+ *
+ * function *coroutineWithArguments(firstVal: number, secondVal: number, thirdVal: number) {
+ * console.log(firstVal);
+ * yield waitForSeconds(1.0); // You can return any Command here.
+ * console.log(secondValue);
+ * yield; // Wait a single frame.
+ * console.log(thirdVal);
+ * }
+ *
+ * queue.enqueue(
+ * coroutine(coroutineWithNoArguments),
+ * coroutine(() => coroutineWithArguments(1, 2, 3))
+ * );
+ */
+export function coroutine(command: CommandCoroutine): Command {
+ let iterator: CommandIterator | undefined;
+ let currentCommand: Command | undefined;
+
+ return (deltaTime, operation) => {
+ // Create our coroutine, if we don't have one.
+ if (iterator === undefined) {
+ iterator = command();
+ // Finish if we couldn't create a coroutine.
+ if (iterator === undefined) {
+ return { complete: true, deltaTime };
+ }
+ }
+
+ let complete = true;
+ while (complete) {
+ // Set the current command.
+ if (currentCommand === undefined) {
+ const { done, value } = iterator.next();
+ if (done) {
+ iterator = undefined;
+ return { complete: true, deltaTime };
+ }
+ currentCommand = value;
+ if (currentCommand === undefined) {
+ // Yield return null will wait a frame, like with
+ // Unity coroutines.
+ currentCommand = waitForFrames(1);
+ }
+ }
+ const result = currentCommand(deltaTime, operation);
+ complete = result.complete;
+ deltaTime = result.deltaTime;
+ if (complete) {
+ currentCommand = undefined;
+ }
+ }
+ return { complete, deltaTime };
+ };
+}
+
+/**
+ * Chooses a random child command to perform. Re-evaluated on repeat.
+ * @param commands
+ * A list of commands to choose from at random. Only one command will be performed.
+ * Undefined commands can be passed. At least one command must be specified.
+ */
+export function chooseRandom(...commands: (Command | undefined)[]): Command {
+ if (commands.length === 0) {
+ throw RangeError('Must have at least one command parameter.');
+ }
+ return defer(() => {
+ const index = Math.floor(Math.random() * commands.length) % commands.length;
+ const result = commands[index];
+ return result === undefined ? none() : result;
+ });
+}
+
+///
+/// Defers the creation of the Command until just before the point of execution.
+///
+///
+/// The action which will create the CommandDelegate.
+/// This must not be null, but it can return a null CommandDelegate.
+///
+export function defer(commandDeferred: CommandFactory): Command {
+ let command: Command | undefined;
+ return sequence(
+ act(() => {
+ command = commandDeferred();
+ }),
+ (deltaTime, operation) => {
+ if (command !== undefined) {
+ return command(deltaTime, operation);
+ }
+ return { complete: true, deltaTime };
+ }
+ );
+}
+
+/**
+ * Consumes all the time from the current update, but let's execution continue.
+ * Useful for compensating for loading bumps.
+ */
+export function consumeTime(): Command {
+ return (deltaTime, operation) => {
+ if (operation === CommandOperation.FastForward) {
+ return { complete: true, deltaTime };
+ }
+ deltaTime = Number.EPSILON < deltaTime ? Number.EPSILON : deltaTime;
+ return { complete: true, deltaTime };
+ };
+}
+
+/**
+ * Slows down, or increases the rate at which time flows through the given subcommands.
+ * @param dilationAmount
+ * The scale of the dilation to perform. For instance, a dilationAmount
+ * of 2 will make time flow twice as quickly. This number must be greater than 0.
+ * @param commands A list of commands to choose to dilate time for.
+ */
+export function dilateTime(dilationAmount: number, ...commands: Command[]): Command {
+ if (dilationAmount <= 0.0) {
+ throw RangeError('dilationAmount must be greater than 0');
+ }
+ const command = sequence(...commands);
+ return (deltaTime, operation) => {
+ const newDelta = deltaTime * dilationAmount;
+ const result = command(newDelta, operation);
+ deltaTime = result.deltaTime / dilationAmount;
+ return { ...result, deltaTime };
+ };
+}
+
+function checkDurationGreaterThanZero(durationAmount: number) {
+ if (durationAmount <= 0.0) {
+ throw RangeError('duration must be > 0');
+ }
+}
+
+function checkDurationGreaterThanOrEqualToZero(durationAmount: number) {
+ if (durationAmount < 0.0) {
+ throw RangeError('duration must be >= 0');
+ }
+}
diff --git a/src/core/ease.ts b/src/core/ease.ts
new file mode 100644
index 0000000..5159a22
--- /dev/null
+++ b/src/core/ease.ts
@@ -0,0 +1,349 @@
+/**
+ * This class contains helper methods for creating common easing functions.
+ * An easing function takes an input value t where an uneased t
+ * ranges from 0 <= t <= 1 . Some easing functions, (such as BackEase returns
+ * values outside the range 0 <= t <= 1). For a given valid easing function, f(t),
+ * f(0) = 0 and f(1) = 1.
+ **/
+export type Ease = (t: number) => number;
+
+/**
+ * The default ease. It doesn't modify the value
+ * of t.
+ **/
+export function linear(): Ease {
+ return t => t;
+}
+
+/**
+ * Quantises t into numSteps + 1 levels, using the round operation.
+ * @param numSteps Must be >= 1
+ */
+export function roundStep(numSteps: number = 1): Ease {
+ checkNumStepsGreaterThanZero(numSteps);
+ const roundedSteps = Math.round(numSteps);
+
+ return t => Math.round(t * roundedSteps) / roundedSteps;
+}
+
+/**
+ * Quantises t into numSteps + 1 levels, using the ceil operation.
+ * This increases the average value of t over the duration
+ * of the ease.
+ * @param numSteps Must be >= 1
+ */
+export function ceilStep(numSteps: number = 1): Ease {
+ checkNumStepsGreaterThanZero(numSteps);
+ const roundedSteps = Math.round(numSteps);
+ return t => Math.ceil(t * roundedSteps) / roundedSteps;
+}
+
+/**
+ * Quantises t into numSteps + 1 levels, using floor operation.
+ * This decreases the average value of t over the duration of the ease.
+ * @param numSteps Must be >= 1
+ */
+export function floorStep(numSteps = 1): Ease {
+ checkNumStepsGreaterThanZero(numSteps);
+ const roundedSteps = Math.round(numSteps);
+ return t => Math.floor(t * roundedSteps) / roundedSteps;
+}
+
+function checkNumStepsGreaterThanZero(numSteps: number) {
+ if (numSteps <= 0) {
+ throw new RangeError('numSteps must be > 0');
+ }
+}
+
+/**
+ * Averages the output from several easing functions.
+ * @param eases The list of eases to average together.
+ */
+export function averageComposite(...eases: Ease[]): Ease {
+ return t => {
+ const average = eases.reduce((total, ease) => total + ease(t), 0);
+ return average / eases.length;
+ };
+}
+
+/**
+ * Sequentially triggers easing functions. For instance, if we have
+ * 3 easing functions, 0 <= t < 0.33 is handled by first easing function
+ * 0.33 <= t < 0.66 by second, and 0.66 <= t <= 1.0 by third.
+ * @param eases The list of eases to chain together.
+ */
+export function sequentialComposite(...eases: Ease[]): Ease {
+ return t => {
+ const index = Math.floor(t * eases.length);
+ if (index >= eases.length) {
+ return 1.0;
+ }
+ if (index < 0) {
+ return 0.0;
+ } else {
+ const sequenceLength = 1.0 / eases.length;
+ const sequenceT = (t - index * sequenceLength) / sequenceLength;
+ return (eases[index](sequenceT) + index) * sequenceLength;
+ }
+ };
+}
+
+export interface WeightedEaseConfig {
+ weight: number;
+ ease: Ease;
+}
+/**
+ * Averages the output of several easing function using a weighting for each.
+ * @param eases The list of eases to average together.
+ */
+export function weightedComposite(...eases: WeightedEaseConfig[]): Ease {
+ const totalWeight = eases.reduce((total, ease) => total + ease.weight, 0);
+
+ return t => {
+ const weightedTotal = eases.reduce((total, ease) => total + ease.ease(t) * ease.weight, 0);
+ return weightedTotal / totalWeight;
+ };
+}
+
+/**
+ * Eases a value, by pipelining it throguh several easing functions.
+ * The output of the first ease is used as input for the next.
+ */
+export function chainComposite(...eases: Ease[]): Ease {
+ return t => eases.reduce((lastT, ease) => ease(lastT), t);
+}
+
+/**
+ * Combines two easing functions. The inEase parameter maps to the range
+ * 0.0 <= t < 0.5, outEase maps to the range 0.5 <= t < 1.0
+ * @param inEase The ease in function
+ * @param outEase The ease out function
+ */
+export function inOutEase(inEase: Ease, outEase: Ease): Ease {
+ return t => {
+ if (t < 0.5) {
+ return 0.5 * inEase(t / 0.5);
+ }
+ return 0.5 * outEase((t - 0.5) / 0.5) + 0.5;
+ };
+}
+
+/**
+ * Flips an ease about the x/y axis, so ease ins become ease outs etcs.
+ * @param inEase The ease to flip
+ */
+export function flip(inEase: Ease): Ease {
+ return t => 1.0 - inEase(1.0 - t);
+}
+
+/**
+ * Creates a polynomial easing function, (quadratic, cubic etc).
+ * @param power The power of the easing function. Must be > 0 .
+ */
+export function inPolynomial(power: number): Ease {
+ if (power <= 0) {
+ throw new RangeError('power must be > 0');
+ }
+ return t => Math.pow(t, power);
+}
+
+export function outPolynomial(power: number): Ease {
+ return flip(inPolynomial(power));
+}
+export function inOutPolynomial(power: number): Ease {
+ return inOutEase(inPolynomial(power), outPolynomial(power));
+}
+
+export function inQuad(): Ease {
+ return inPolynomial(2.0);
+}
+export function outQuad(): Ease {
+ return outPolynomial(2.0);
+}
+export function inOutQuad(): Ease {
+ return inOutPolynomial(2.0);
+}
+
+export function inCubic(): Ease {
+ return inPolynomial(3.0);
+}
+export function outCubic(): Ease {
+ return outPolynomial(3.0);
+}
+export function inOutCubic(): Ease {
+ return inOutPolynomial(3.0);
+}
+
+export function inQuart(): Ease {
+ return inPolynomial(4.0);
+}
+export function outQuart(): Ease {
+ return outPolynomial(4.0);
+}
+export function inOutQuart(): Ease {
+ return inOutPolynomial(4.0);
+}
+
+export function inQuint(): Ease {
+ return inPolynomial(5.0);
+}
+export function outQuint(): Ease {
+ return outPolynomial(5.0);
+}
+export function inOutQuint(): Ease {
+ return inOutPolynomial(5.0);
+}
+
+/**
+ * Eases using a trigonometric functions.
+ **/
+export function inSin(): Ease {
+ return t => 1.0 - Math.cos((t * Math.PI) / 2.0);
+}
+export function outSin(): Ease {
+ return flip(inSin());
+}
+export function inOutSin(): Ease {
+ return inOutEase(inSin(), outSin());
+}
+
+/**
+ * An ease with an elastic effect.
+ * @param amplitude The maximum amount of displacement caused by the elastic effect
+ * @param period How springy the elastic effect is.
+ */
+export function elastic(amplitude = 1.0, period = 0.3): Ease {
+ return t => {
+ let tempAmplitude = amplitude;
+ let s = 0.0;
+
+ if (t === 0) {
+ return 0.0;
+ } else if (t === 1.0) {
+ return 1.0;
+ }
+
+ if (tempAmplitude < 1.0) {
+ tempAmplitude = 1.0;
+ s = period / 4.0;
+ } else {
+ s = (period / (2.0 * Math.PI)) * Math.asin(1.0 / tempAmplitude);
+ }
+ t -= 1.0;
+ return -(tempAmplitude * Math.pow(2.0, 10.0 * t) * Math.sin(((t - s) * 2.0 * Math.PI) / period));
+ };
+}
+
+export function inElastic(): Ease {
+ return elastic();
+}
+export function outElastic(): Ease {
+ return flip(elastic());
+}
+export function intOutElastic(): Ease {
+ return inOutEase(inElastic(), outElastic());
+}
+
+export function inExpo(): Ease {
+ return t => (t === 1.0 ? 1.0 : Math.pow(2.0, 10.0 * (t - 1.0)));
+}
+export function outExpo(): Ease {
+ return flip(inExpo());
+}
+export function inOutExpo(): Ease {
+ return inOutEase(inExpo(), outExpo());
+}
+
+export function inCirc(): Ease {
+ return t => 1.0 - Math.sqrt(1.0 - t * t);
+}
+export function outCirc(): Ease {
+ return flip(inCirc());
+}
+export function inOutCirc(): Ease {
+ return inOutEase(inCirc(), outCirc());
+}
+
+/**
+ * The in back ease is used to reverse a little, before shooting towards a target.
+
+ * @param overshoot The amount to overshoot the goal by.
+ */
+export function inBack(overshoot = 0.2): Ease {
+ return t => t * t * t - t * overshoot * Math.sin(t * Math.PI);
+}
+
+/**
+ * The in back ease is used to overshoot a target.
+ * @param overshoot The amount to overshoot the goal by.
+ */
+export function outBack(overshoot = 0.2): Ease {
+ return flip(inBack(overshoot));
+}
+
+/**
+ * The in back ease is used to overshoot a target.
+ * @param overshoot The amount to overshoot the goal by.
+ */
+export function inOutBack(overshoot = 0.2): Ease {
+ return inOutEase(inBack(overshoot * 2.0), outBack(overshoot * 2.0));
+}
+
+export function inBounce(): Ease {
+ return t => {
+ t = 1.0 - t;
+ if (t < 1.0 / 2.75) {
+ return 1.0 - 7.5625 * t * t;
+ } else if (t < 2.0 / 2.75) {
+ t -= 1.5 / 2.75;
+ return 1.0 - (7.5625 * t * t + 0.75);
+ } else if (t < 2.5 / 2.75) {
+ t -= 2.25 / 2.75;
+ return 1.0 - (7.5625 * t * t + 0.9375);
+ } else {
+ t -= 2.625 / 2.75;
+ return 1.0 - (7.5625 * t * t + 0.984375);
+ }
+ };
+}
+export function outBounce(): Ease {
+ return flip(inBounce());
+}
+export function inOutBounce(): Ease {
+ return inOutEase(inBounce(), outBounce());
+}
+
+/**
+ * A Hermite curve easing function. The Hermite curve is a cheap easing function, with adjustable gradients at it's endpoints.
+ * @param startGradient The gradient, (x/y), at the start of the ease. The closer this is to zero, the smoother the ease.
+ * @param endGradient The gradient (x/y), at the end of the ease. The closer this is to zero, the smoother the ease.
+ */
+export function hermite(startGradient = 0.0, endGradient = 0.0): Ease {
+ return t => {
+ // Hermite curve over normalised t interval:
+ // p(t) = (-2t^2 - 3t^2 + 1) * p0 + (t^3 - 2t^2 + t) * m0 + (-2t^3+ 3t^2) *p1 + (t^3- t^2) * m1
+ // Where p0 = p at time 0, p1 = p at time 1, m0 = tangent at time 0, m1 = tangent at time 1.
+ // Note that in our case p0 = 0, and p1 = 1, while m0 = startGradient, and m1 = endGradient.
+ // This gives :
+ // p(t) = (t^3 - 2t^2 + t) * m0 - 2t^3 + 3t^2 + (t^3 - t^2) * m1
+ const tSqr = t * t;
+ const tCbd = t * t * t;
+ return (tCbd - 2 * tSqr + t) * startGradient - 2 * tCbd + 3 * tSqr + (tCbd - tSqr) * endGradient;
+ };
+}
+
+export function inHermite(): Ease {
+ return hermite(0.0, 1.0);
+}
+
+export function outHermite(): Ease {
+ return hermite(1.0, 0.0);
+}
+
+export function inOutHermite(): Ease {
+ return hermite();
+}
+
+export function Smooth(): Ease {
+ return hermite();
+}
diff --git a/src/core/index.ts b/src/core/index.ts
new file mode 100644
index 0000000..ef1a6df
--- /dev/null
+++ b/src/core/index.ts
@@ -0,0 +1,3 @@
+export * from './commands';
+export * from './ease';
+export * from './command-queue';
diff --git a/src/index.ts b/src/index.ts
index 3531606..f7c462a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1 +1 @@
-export * from './math';
+export * from 'core';
diff --git a/src/math/add.spec.ts b/src/math/add.spec.ts
deleted file mode 100644
index d4b3d47..0000000
--- a/src/math/add.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { add } from './add';
-
-describe('add', () => {
- it('adds two positive numbers', () => {
- const output = add(1, 2);
- expect(output).toBe(3);
- });
-});
diff --git a/src/math/add.ts b/src/math/add.ts
deleted file mode 100644
index e5b1307..0000000
--- a/src/math/add.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function add(first: number, second: number) {
- return first + second;
-}
diff --git a/src/math/index.ts b/src/math/index.ts
deleted file mode 100644
index 7901162..0000000
--- a/src/math/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { add } from './add';
-export { sub } from './sub';
diff --git a/src/math/sub.spec.ts b/src/math/sub.spec.ts
deleted file mode 100644
index 691b6b4..0000000
--- a/src/math/sub.spec.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { sub } from './sub';
-
-describe('sub', () => {
- it('subs two positive numbers', () => {
- const output = sub(1, 2);
- expect(output).toBe(-1);
- });
-});
diff --git a/src/math/sub.ts b/src/math/sub.ts
deleted file mode 100644
index e974b20..0000000
--- a/src/math/sub.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function sub(first: number, second: number) {
- return first - second;
-}