UNPKG

xstream

Version:

An extremely intuitive, small, and fast functional reactive stream library for JavaScript

225 lines (205 loc) 5.95 kB
import {Stream} from '../core'; import concat from './concat'; export type Ease = (x: number, from: number, to: number) => number; export type Easings = { easeIn: Ease; easeOut: Ease; easeInOut: Ease; } export type NumericFunction = (input: number) => number; export interface TweenConfig { from: number; to: number; duration: number; ease?: Ease; interval?: number; } function interpolate(y: number, from: number, to: number): number { return (from * (1 - y) + to * y); } function flip(fn: NumericFunction): NumericFunction { return x => 1 - fn(1 - x); } function createEasing(fn: NumericFunction): Easings { let fnFlipped = flip(fn); return { easeIn(x, from, to) { return interpolate(fn(x), from, to); }, easeOut(x, from, to) { return interpolate(fnFlipped(x), from, to); }, easeInOut(x, from, to) { const y = (x < 0.5) ? (fn(2 * x) * 0.5) : (0.5 + fnFlipped(2 * (x - 0.5)) * 0.5); return interpolate(y, from, to); } }; }; let easingPower2 = createEasing(x => x * x); let easingPower3 = createEasing(x => x * x * x); let easingPower4 = createEasing(x => { const xx = x * x; return xx * xx; }); const EXP_WEIGHT = 6; const EXP_MAX = Math.exp(EXP_WEIGHT) - 1; function expFn(x: number): number { return (Math.exp(x * EXP_WEIGHT) - 1) / EXP_MAX; } let easingExponential = createEasing(expFn); const OVERSHOOT = 1.70158; let easingBack = createEasing(x => x * x * ((OVERSHOOT + 1) * x - OVERSHOOT)); const PARAM1 = 7.5625; const PARAM2 = 2.75; function easeOutFn(x: number): number { let z = x; if (z < 1 / PARAM2) { return (PARAM1 * z * z); } else if (z < 2 / PARAM2) { return (PARAM1 * (z -= 1.5 / PARAM2) * z + 0.75); } else if (z < 2.5 / PARAM2) { return (PARAM1 * (z -= 2.25 / PARAM2) * z + 0.9375); } else { return (PARAM1 * (z -= 2.625 / PARAM2) * z + 0.984375); } } let easingBounce = createEasing(x => 1 - easeOutFn(1 - x)); let easingCirc = createEasing(x => -(Math.sqrt(1 - x * x) - 1)); const PERIOD = 0.3; const OVERSHOOT_ELASTIC = PERIOD / 4; const AMPLITUDE = 1; function elasticIn(x: number): number { let z = x; if (z <= 0) { return 0; } else if (z >= 1) { return 1; } else { z -= 1; return -(AMPLITUDE * Math.pow(2, 10 * z)) * Math.sin((z - OVERSHOOT_ELASTIC) * (2 * Math.PI) / PERIOD); } } let easingElastic = createEasing(elasticIn); const HALF_PI = Math.PI * 0.5; let easingSine = createEasing(x => 1 - Math.cos(x * HALF_PI)); const DEFAULT_INTERVAL: number = 15; export interface TweenFactory { (config: TweenConfig): Stream<number>; linear: { ease: Ease }; power2: Easings; power3: Easings; power4: Easings; exponential: Easings; back: Easings; bounce: Easings; circular: Easings; elastic: Easings; sine: Easings; } /** * Creates a stream of numbers emitted in a quick burst, following a numeric * function like sine or elastic or quadratic. tween() is meant for creating * streams for animations. * * Example: * * ```js * import tween from 'xstream/extra/tween' * * const stream = tween({ * from: 20, * to: 100, * ease: tween.exponential.easeIn, * duration: 1000, // milliseconds * }) * * stream.addListener({ * next: (x) => console.log(x), * error: (err) => console.error(err), * complete: () => console.log('concat completed'), * }) * ``` * * The stream would behave like the plot below: * * ```text * 100 # * | * | * | * | * 80 # * | * | * | * | # * 60 * | * | # * | * | # * 40 * | # * | # * | ## * | ### * 20######## * +---------------------> time * ``` * * Provide a configuration object with **from**, **to**, **duration**, **ease**, * **interval** (optional), and this factory function will return a stream of * numbers following that pattern. The first number emitted will be `from`, and * the last number will be `to`. The numbers in between follow the easing * function you specify in `ease`, and the stream emission will last in total * `duration` milliseconds. * * The easing functions are attached to `tween` too, such as * `tween.linear.ease`, `tween.power2.easeIn`, `tween.exponential.easeOut`, etc. * Here is a list of all the available easing options: * * - `tween.linear` with ease * - `tween.power2` with easeIn, easeOut, easeInOut * - `tween.power3` with easeIn, easeOut, easeInOut * - `tween.power4` with easeIn, easeOut, easeInOut * - `tween.exponential` with easeIn, easeOut, easeInOut * - `tween.back` with easeIn, easeOut, easeInOut * - `tween.bounce` with easeIn, easeOut, easeInOut * - `tween.circular` with easeIn, easeOut, easeInOut * - `tween.elastic` with easeIn, easeOut, easeInOut * - `tween.sine` with easeIn, easeOut, easeInOut * * @factory true * @param {TweenConfig} config An object with properties `from: number`, * `to: number`, `duration: number`, `ease: function` (optional, defaults to * linear), `interval: number` (optional, defaults to 15). * @return {Stream} */ function tween({ from, to, duration, ease = tweenFactory.linear.ease, interval = DEFAULT_INTERVAL }): Stream<number> { const totalTicks = Math.round(duration / interval); return Stream.periodic(interval) .take(totalTicks) .map(tick => ease(tick / totalTicks, from, to)) .compose(s => concat<number>(s, Stream.of(to))); } const tweenFactory: TweenFactory = <TweenFactory> tween; tweenFactory.linear = { ease: interpolate }; tweenFactory.power2 = easingPower2; tweenFactory.power3 = easingPower3; tweenFactory.power4 = easingPower4; tweenFactory.exponential = easingExponential; tweenFactory.back = easingBack; tweenFactory.bounce = easingBounce; tweenFactory.circular = easingCirc; tweenFactory.elastic = easingElastic; tweenFactory.sine = easingSine; export default tweenFactory;