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; -}