'use strict'; (function () { var rebound = {}; var util = rebound.util = {}; var concat = Array.prototype.concat; var slice = Array.prototype.slice; // Bind a function to a context object. util.bind = function bind(func, context) { var args = slice.call(arguments, 2); return function () { func.apply(context, concat.call(args, slice.call(arguments))); }; }; // Add all the properties in the source to the target. util.extend = function extend(target, source) { for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } }; // SpringSystem // ------------ // **SpringSystem** is a set of Springs that all run on the same physics // timing loop. To get started with a Rebound animation you first // create a new SpringSystem and then add springs to it. var SpringSystem = rebound.SpringSystem = function SpringSystem(looper) { this._springRegistry = {}; this._activeSprings = []; this.listeners = []; this._idleSpringIndices = []; this.looper = looper || new AnimationLooper(); this.looper.springSystem = this; }; util.extend(SpringSystem.prototype, { _springRegistry: null, _isIdle: true, _lastTimeMillis: -1, _activeSprings: null, listeners: null, _idleSpringIndices: null, // A SpringSystem is iterated by a looper. The looper is responsible // for executing each frame as the SpringSystem is resolved to idle. // There are three types of Loopers described below AnimationLooper, // SimulationLooper, and SteppingSimulationLooper. AnimationLooper is // the default as it is the most useful for common UI animations. setLooper: function setLooper(looper) { this.looper = looper; looper.springSystem = this; }, // Add a new spring to this SpringSystem. This Spring will now be solved for // during the physics iteration loop. By default the spring will use the // default Origami spring config with 40 tension and 7 friction, but you can // also provide your own values here. createSpring: function createSpring(tension, friction) { var springConfig; if (tension === undefined || friction === undefined) { springConfig = SpringConfig.DEFAULT_ORIGAMI_SPRING_CONFIG; } else { springConfig = SpringConfig.fromOrigamiTensionAndFriction(tension, friction); } return this.createSpringWithConfig(springConfig); }, // Add a spring with a specified bounciness and speed. To replicate Origami // compositions based on PopAnimation patches, use this factory method to // create matching springs. createSpringWithBouncinessAndSpeed: function createSpringWithBouncinessAndSpeed(bounciness, speed) { var springConfig; if (bounciness === undefined || speed === undefined) { springConfig = SpringConfig.DEFAULT_ORIGAMI_SPRING_CONFIG; } else { springConfig = SpringConfig.fromBouncinessAndSpeed(bounciness, speed); } return this.createSpringWithConfig(springConfig); }, // Add a spring with the provided SpringConfig. createSpringWithConfig: function createSpringWithConfig(springConfig) { var spring = new Spring(this); this.registerSpring(spring); spring.setSpringConfig(springConfig); return spring; }, // You can check if a SpringSystem is idle or active by calling // getIsIdle. If all of the Springs in the SpringSystem are at rest, // i.e. the physics forces have reached equilibrium, then this // method will return true. getIsIdle: function getIsIdle() { return this._isIdle; }, // Retrieve a specific Spring from the SpringSystem by id. This // can be useful for inspecting the state of a spring before // or after an integration loop in the SpringSystem executes. getSpringById: function getSpringById(id) { return this._springRegistry[id]; }, // Get a listing of all the springs registered with this // SpringSystem. getAllSprings: function getAllSprings() { var vals = []; for (var id in this._springRegistry) { if (this._springRegistry.hasOwnProperty(id)) { vals.push(this._springRegistry[id]); } } return vals; }, // registerSpring is called automatically as soon as you create // a Spring with SpringSystem#createSpring. This method sets the // spring up in the registry so that it can be solved in the // solver loop. registerSpring: function registerSpring(spring) { this._springRegistry[spring.getId()] = spring; }, // Deregister a spring with this SpringSystem. The SpringSystem will // no longer consider this Spring during its integration loop once // this is called. This is normally done automatically for you when // you call Spring#destroy. deregisterSpring: function deregisterSpring(spring) { removeFirst(this._activeSprings, spring); delete this._springRegistry[spring.getId()]; }, advance: function advance(time, deltaTime) { while (this._idleSpringIndices.length > 0) { this._idleSpringIndices.pop(); }for (var i = 0, len = this._activeSprings.length; i < len; i++) { var spring = this._activeSprings[i]; if (spring.systemShouldAdvance()) { spring.advance(time / 1000.0, deltaTime / 1000.0); } else { this._idleSpringIndices.push(this._activeSprings.indexOf(spring)); } } while (this._idleSpringIndices.length > 0) { var idx = this._idleSpringIndices.pop(); idx >= 0 && this._activeSprings.splice(idx, 1); } }, // This is our main solver loop called to move the simulation // forward through time. Before each pass in the solver loop // onBeforeIntegrate is called on an any listeners that have // registered themeselves with the SpringSystem. This gives you // an opportunity to apply any constraints or adjustments to // the springs that should be enforced before each iteration // loop. Next the advance method is called to move each Spring in // the systemShouldAdvance forward to the current time. After the // integration step runs in advance, onAfterIntegrate is called // on any listeners that have registered themselves with the // SpringSystem. This gives you an opportunity to run any post // integration constraints or adjustments on the Springs in the // SpringSystem. loop: function loop(currentTimeMillis) { var listener; if (this._lastTimeMillis === -1) { this._lastTimeMillis = currentTimeMillis - 1; } var ellapsedMillis = currentTimeMillis - this._lastTimeMillis; this._lastTimeMillis = currentTimeMillis; var i = 0, len = this.listeners.length; for (i = 0; i < len; i++) { listener = this.listeners[i]; listener.onBeforeIntegrate && listener.onBeforeIntegrate(this); } this.advance(currentTimeMillis, ellapsedMillis); if (this._activeSprings.length === 0) { this._isIdle = true; this._lastTimeMillis = -1; } for (i = 0; i < len; i++) { listener = this.listeners[i]; listener.onAfterIntegrate && listener.onAfterIntegrate(this); } if (!this._isIdle) { this.looper.run(); } }, // activateSpring is used to notify the SpringSystem that a Spring // has become displaced. The system responds by starting its solver // loop up if it is currently idle. activateSpring: function activateSpring(springId) { var spring = this._springRegistry[springId]; if (this._activeSprings.indexOf(spring) == -1) { this._activeSprings.push(spring); } if (this.getIsIdle()) { this._isIdle = false; this.looper.run(); } }, // Add a listener to the SpringSystem so that you can receive // before/after integration notifications allowing Springs to be // constrained or adjusted. addListener: function addListener(listener) { this.listeners.push(listener); }, // Remove a previously added listener on the SpringSystem. removeListener: function removeListener(listener) { removeFirst(this.listeners, listener); }, // Remove all previously added listeners on the SpringSystem. removeAllListeners: function removeAllListeners() { this.listeners = []; } }); // Spring // ------ // **Spring** provides a model of a classical spring acting to // resolve a body to equilibrium. Springs have configurable // tension which is a force multipler on the displacement of the // spring from its rest point or `endValue` as defined by [Hooke's // law](http://en.wikipedia.org/wiki/Hooke's_law). Springs also have // configurable friction, which ensures that they do not oscillate // infinitely. When a Spring is displaced by updating it's resting // or `currentValue`, the SpringSystems that contain that Spring // will automatically start looping to solve for equilibrium. As each // timestep passes, `SpringListener` objects attached to the Spring // will be notified of the updates providing a way to drive an // animation off of the spring's resolution curve. var Spring = rebound.Spring = function Spring(springSystem) { this._id = 's' + Spring._ID++; this._springSystem = springSystem; this.listeners = []; this._currentState = new PhysicsState(); this._previousState = new PhysicsState(); this._tempState = new PhysicsState(); }; util.extend(Spring, { _ID: 0, MAX_DELTA_TIME_SEC: 0.064, SOLVER_TIMESTEP_SEC: 0.001 }); util.extend(Spring.prototype, { _id: 0, _springConfig: null, _overshootClampingEnabled: false, _currentState: null, _previousState: null, _tempState: null, _startValue: 0, _endValue: 0, _wasAtRest: true, _restSpeedThreshold: 0.001, _displacementFromRestThreshold: 0.001, listeners: null, _timeAccumulator: 0, _springSystem: null, // Remove a Spring from simulation and clear its listeners. destroy: function destroy() { this.listeners = []; this.frames = []; this._springSystem.deregisterSpring(this); }, // Get the id of the spring, which can be used to retrieve it from // the SpringSystems it participates in later. getId: function getId() { return this._id; }, // Set the configuration values for this Spring. A SpringConfig // contains the tension and friction values used to solve for the // equilibrium of the Spring in the physics loop. setSpringConfig: function setSpringConfig(springConfig) { this._springConfig = springConfig; return this; }, // Retrieve the SpringConfig used by this Spring. getSpringConfig: function getSpringConfig() { return this._springConfig; }, // Set the current position of this Spring. Listeners will be updated // with this value immediately. If the rest or `endValue` is not // updated to match this value, then the spring will be dispalced and // the SpringSystem will start to loop to restore the spring to the // `endValue`. // // A common pattern is to move a Spring around without animation by // calling. // // ``` // spring.setCurrentValue(n).setAtRest(); // ``` // // This moves the Spring to a new position `n`, sets the endValue // to `n`, and removes any velocity from the `Spring`. By doing // this you can allow the `SpringListener` to manage the position // of UI elements attached to the spring even when moving without // animation. For example, when dragging an element you can // update the position of an attached view through a spring // by calling `spring.setCurrentValue(x)`. When // the gesture ends you can update the Springs // velocity and endValue // `spring.setVelocity(gestureEndVelocity).setEndValue(flingTarget)` // to cause it to naturally animate the UI element to the resting // position taking into account existing velocity. The codepaths for // synchronous movement and spring driven animation can // be unified using this technique. setCurrentValue: function setCurrentValue(currentValue, skipSetAtRest) { this._startValue = currentValue; this._currentState.position = currentValue; if (!skipSetAtRest) { this.setAtRest(); } this.notifyPositionUpdated(false, false); return this; }, // Get the position that the most recent animation started at. This // can be useful for determining the number off oscillations that // have occurred. getStartValue: function getStartValue() { return this._startValue; }, // Retrieve the current value of the Spring. getCurrentValue: function getCurrentValue() { return this._currentState.position; }, // Get the absolute distance of the Spring from it's resting endValue // position. getCurrentDisplacementDistance: function getCurrentDisplacementDistance() { return this.getDisplacementDistanceForState(this._currentState); }, getDisplacementDistanceForState: function getDisplacementDistanceForState(state) { return Math.abs(this._endValue - state.position); }, // Set the endValue or resting position of the spring. If this // value is different than the current value, the SpringSystem will // be notified and will begin running its solver loop to resolve // the Spring to equilibrium. Any listeners that are registered // for onSpringEndStateChange will also be notified of this update // immediately. setEndValue: function setEndValue(endValue) { if (this._endValue == endValue && this.isAtRest()) { return this; } this._startValue = this.getCurrentValue(); this._endValue = endValue; this._springSystem.activateSpring(this.getId()); for (var i = 0, len = this.listeners.length; i < len; i++) { var listener = this.listeners[i]; var onChange = listener.onSpringEndStateChange; onChange && onChange(this); } return this; }, // Retrieve the endValue or resting position of this spring. getEndValue: function getEndValue() { return this._endValue; }, // Set the current velocity of the Spring. As previously mentioned, // this can be useful when you are performing a direct manipulation // gesture. When a UI element is released you may call setVelocity // on its animation Spring so that the Spring continues with the // same velocity as the gesture ended with. The friction, tension, // and displacement of the Spring will then govern its motion to // return to rest on a natural feeling curve. setVelocity: function setVelocity(velocity) { if (velocity === this._currentState.velocity) { return this; } this._currentState.velocity = velocity; this._springSystem.activateSpring(this.getId()); return this; }, // Get the current velocity of the Spring. getVelocity: function getVelocity() { return this._currentState.velocity; }, // Set a threshold value for the movement speed of the Spring below // which it will be considered to be not moving or resting. setRestSpeedThreshold: function setRestSpeedThreshold(restSpeedThreshold) { this._restSpeedThreshold = restSpeedThreshold; return this; }, // Retrieve the rest speed threshold for this Spring. getRestSpeedThreshold: function getRestSpeedThreshold() { return this._restSpeedThreshold; }, // Set a threshold value for displacement below which the Spring // will be considered to be not displaced i.e. at its resting // `endValue`. setRestDisplacementThreshold: function setRestDisplacementThreshold(displacementFromRestThreshold) { this._displacementFromRestThreshold = displacementFromRestThreshold; }, // Retrieve the rest displacement threshold for this spring. getRestDisplacementThreshold: function getRestDisplacementThreshold() { return this._displacementFromRestThreshold; }, // Enable overshoot clamping. This means that the Spring will stop // immediately when it reaches its resting position regardless of // any existing momentum it may have. This can be useful for certain // types of animations that should not oscillate such as a scale // down to 0 or alpha fade. setOvershootClampingEnabled: function setOvershootClampingEnabled(enabled) { this._overshootClampingEnabled = enabled; return this; }, // Check if overshoot clamping is enabled for this spring. isOvershootClampingEnabled: function isOvershootClampingEnabled() { return this._overshootClampingEnabled; }, // Check if the Spring has gone past its end point by comparing // the direction it was moving in when it started to the current // position and end value. isOvershooting: function isOvershooting() { var start = this._startValue; var end = this._endValue; return this._springConfig.tension > 0 && (start < end && this.getCurrentValue() > end || start > end && this.getCurrentValue() < end); }, // Spring.advance is the main solver method for the Spring. It takes // the current time and delta since the last time step and performs // an RK4 integration to get the new position and velocity state // for the Spring based on the tension, friction, velocity, and // displacement of the Spring. advance: function advance(time, realDeltaTime) { var isAtRest = this.isAtRest(); if (isAtRest && this._wasAtRest) { return; } var adjustedDeltaTime = realDeltaTime; if (realDeltaTime > Spring.MAX_DELTA_TIME_SEC) { adjustedDeltaTime = Spring.MAX_DELTA_TIME_SEC; } this._timeAccumulator += adjustedDeltaTime; var tension = this._springConfig.tension, friction = this._springConfig.friction, position = this._currentState.position, velocity = this._currentState.velocity, tempPosition = this._tempState.position, tempVelocity = this._tempState.velocity, aVelocity, aAcceleration, bVelocity, bAcceleration, cVelocity, cAcceleration, dVelocity, dAcceleration, dxdt, dvdt; while (this._timeAccumulator >= Spring.SOLVER_TIMESTEP_SEC) { this._timeAccumulator -= Spring.SOLVER_TIMESTEP_SEC; if (this._timeAccumulator < Spring.SOLVER_TIMESTEP_SEC) { this._previousState.position = position; this._previousState.velocity = velocity; } aVelocity = velocity; aAcceleration = tension * (this._endValue - tempPosition) - friction * velocity; tempPosition = position + aVelocity * Spring.SOLVER_TIMESTEP_SEC * 0.5; tempVelocity = velocity + aAcceleration * Spring.SOLVER_TIMESTEP_SEC * 0.5; bVelocity = tempVelocity; bAcceleration = tension * (this._endValue - tempPosition) - friction * tempVelocity; tempPosition = position + bVelocity * Spring.SOLVER_TIMESTEP_SEC * 0.5; tempVelocity = velocity + bAcceleration * Spring.SOLVER_TIMESTEP_SEC * 0.5; cVelocity = tempVelocity; cAcceleration = tension * (this._endValue - tempPosition) - friction * tempVelocity; tempPosition = position + cVelocity * Spring.SOLVER_TIMESTEP_SEC * 0.5; tempVelocity = velocity + cAcceleration * Spring.SOLVER_TIMESTEP_SEC * 0.5; dVelocity = tempVelocity; dAcceleration = tension * (this._endValue - tempPosition) - friction * tempVelocity; dxdt = 1.0 / 6.0 * (aVelocity + 2.0 * (bVelocity + cVelocity) + dVelocity); dvdt = 1.0 / 6.0 * (aAcceleration + 2.0 * (bAcceleration + cAcceleration) + dAcceleration); position += dxdt * Spring.SOLVER_TIMESTEP_SEC; velocity += dvdt * Spring.SOLVER_TIMESTEP_SEC; } this._tempState.position = tempPosition; this._tempState.velocity = tempVelocity; this._currentState.position = position; this._currentState.velocity = velocity; if (this._timeAccumulator > 0) { this._interpolate(this._timeAccumulator / Spring.SOLVER_TIMESTEP_SEC); } if (this.isAtRest() || this._overshootClampingEnabled && this.isOvershooting()) { if (this._springConfig.tension > 0) { this._startValue = this._endValue; this._currentState.position = this._endValue; } else { this._endValue = this._currentState.position; this._startValue = this._endValue; } this.setVelocity(0); isAtRest = true; } var notifyActivate = false; if (this._wasAtRest) { this._wasAtRest = false; notifyActivate = true; } var notifyAtRest = false; if (isAtRest) { this._wasAtRest = true; notifyAtRest = true; } this.notifyPositionUpdated(notifyActivate, notifyAtRest); }, notifyPositionUpdated: function notifyPositionUpdated(notifyActivate, notifyAtRest) { for (var i = 0, len = this.listeners.length; i < len; i++) { var listener = this.listeners[i]; if (notifyActivate && listener.onSpringActivate) { listener.onSpringActivate(this); } if (listener.onSpringUpdate) { listener.onSpringUpdate(this); } if (notifyAtRest && listener.onSpringAtRest) { listener.onSpringAtRest(this); } } }, // Check if the SpringSystem should advance. Springs are advanced // a final frame after they reach equilibrium to ensure that the // currentValue is exactly the requested endValue regardless of the // displacement threshold. systemShouldAdvance: function systemShouldAdvance() { return !this.isAtRest() || !this.wasAtRest(); }, wasAtRest: function wasAtRest() { return this._wasAtRest; }, // Check if the Spring is atRest meaning that it's currentValue and // endValue are the same and that it has no velocity. The previously // described thresholds for speed and displacement define the bounds // of this equivalence check. If the Spring has 0 tension, then it will // be considered at rest whenever its absolute velocity drops below the // restSpeedThreshold. isAtRest: function isAtRest() { return Math.abs(this._currentState.velocity) < this._restSpeedThreshold && (this.getDisplacementDistanceForState(this._currentState) <= this._displacementFromRestThreshold || this._springConfig.tension === 0); }, // Force the spring to be at rest at its current position. As // described in the documentation for setCurrentValue, this method // makes it easy to do synchronous non-animated updates to ui // elements that are attached to springs via SpringListeners. setAtRest: function setAtRest() { this._endValue = this._currentState.position; this._tempState.position = this._currentState.position; this._currentState.velocity = 0; return this; }, _interpolate: function _interpolate(alpha) { this._currentState.position = this._currentState.position * alpha + this._previousState.position * (1 - alpha); this._currentState.velocity = this._currentState.velocity * alpha + this._previousState.velocity * (1 - alpha); }, getListeners: function getListeners() { return this.listeners; }, addListener: function addListener(newListener) { this.listeners.push(newListener); return this; }, removeListener: function removeListener(listenerToRemove) { removeFirst(this.listeners, listenerToRemove); return this; }, removeAllListeners: function removeAllListeners() { this.listeners = []; return this; }, currentValueIsApproximately: function currentValueIsApproximately(value) { return Math.abs(this.getCurrentValue() - value) <= this.getRestDisplacementThreshold(); } }); // PhysicsState // ------------ // **PhysicsState** consists of a position and velocity. A Spring uses // this internally to keep track of its current and prior position and // velocity values. var PhysicsState = function PhysicsState() {}; util.extend(PhysicsState.prototype, { position: 0, velocity: 0 }); // SpringConfig // ------------ // **SpringConfig** maintains a set of tension and friction constants // for a Spring. You can use fromOrigamiTensionAndFriction to convert // values from the [Origami](http://facebook.github.io/origami/) // design tool directly to Rebound spring constants. var SpringConfig = rebound.SpringConfig = function SpringConfig(tension, friction) { this.tension = tension; this.friction = friction; }; // Loopers // ------- // **AnimationLooper** plays each frame of the SpringSystem on animation // timing loop. This is the default type of looper for a new spring system // as it is the most common when developing UI. var AnimationLooper = rebound.AnimationLooper = function AnimationLooper() { this.springSystem = null; var _this = this; var _run = function _run() { _this.springSystem.loop(Date.now()); }; this.run = function () { util.onFrame(_run); }; }; // **SimulationLooper** resolves the SpringSystem to a resting state in a // tight and blocking loop. This is useful for synchronously generating // pre-recorded animations that can then be played on a timing loop later. // Sometimes this lead to better performance to pre-record a single spring // curve and use it to drive many animations; however, it can make dynamic // response to user input a bit trickier to implement. rebound.SimulationLooper = function SimulationLooper(timestep) { this.springSystem = null; var time = 0; var running = false; timestep = timestep || 16.667; this.run = function () { if (running) { return; } running = true; while (!this.springSystem.getIsIdle()) { this.springSystem.loop(time += timestep); } running = false; }; }; // **SteppingSimulationLooper** resolves the SpringSystem one step at a // time controlled by an outside loop. This is useful for testing and // verifying the behavior of a SpringSystem or if you want to control your own // timing loop for some reason e.g. slowing down or speeding up the // simulation. rebound.SteppingSimulationLooper = function (timestep) { this.springSystem = null; var time = 0; // this.run is NOOP'd here to allow control from the outside using // this.step. this.run = function () {}; // Perform one step toward resolving the SpringSystem. this.step = function (timestep) { this.springSystem.loop(time += timestep); }; }; // Math for converting from // [Origami](http://facebook.github.io/origami/) to // [Rebound](http://facebook.github.io/rebound). // You mostly don't need to worry about this, just use // SpringConfig.fromOrigamiTensionAndFriction(v, v); var OrigamiValueConverter = rebound.OrigamiValueConverter = { tensionFromOrigamiValue: function tensionFromOrigamiValue(oValue) { return (oValue - 30.0) * 3.62 + 194.0; }, origamiValueFromTension: function origamiValueFromTension(tension) { return (tension - 194.0) / 3.62 + 30.0; }, frictionFromOrigamiValue: function frictionFromOrigamiValue(oValue) { return (oValue - 8.0) * 3.0 + 25.0; }, origamiFromFriction: function origamiFromFriction(friction) { return (friction - 25.0) / 3.0 + 8.0; } }; // BouncyConversion provides math for converting from Origami PopAnimation // config values to regular Origami tension and friction values. If you are // trying to replicate prototypes made with PopAnimation patches in Origami, // then you should create your springs with // SpringSystem.createSpringWithBouncinessAndSpeed, which uses this Math // internally to create a spring to match the provided PopAnimation // configuration from Origami. var BouncyConversion = rebound.BouncyConversion = function (bounciness, speed) { this.bounciness = bounciness; this.speed = speed; var b = this.normalize(bounciness / 1.7, 0, 20.0); b = this.projectNormal(b, 0.0, 0.8); var s = this.normalize(speed / 1.7, 0, 20.0); this.bouncyTension = this.projectNormal(s, 0.5, 200); this.bouncyFriction = this.quadraticOutInterpolation(b, this.b3Nobounce(this.bouncyTension), 0.01); }; util.extend(BouncyConversion.prototype, { normalize: function normalize(value, startValue, endValue) { return (value - startValue) / (endValue - startValue); }, projectNormal: function projectNormal(n, start, end) { return start + n * (end - start); }, linearInterpolation: function linearInterpolation(t, start, end) { return t * end + (1.0 - t) * start; }, quadraticOutInterpolation: function quadraticOutInterpolation(t, start, end) { return this.linearInterpolation(2 * t - t * t, start, end); }, b3Friction1: function b3Friction1(x) { return 0.0007 * Math.pow(x, 3) - 0.031 * Math.pow(x, 2) + 0.64 * x + 1.28; }, b3Friction2: function b3Friction2(x) { return 0.000044 * Math.pow(x, 3) - 0.006 * Math.pow(x, 2) + 0.36 * x + 2.; }, b3Friction3: function b3Friction3(x) { return 0.00000045 * Math.pow(x, 3) - 0.000332 * Math.pow(x, 2) + 0.1078 * x + 5.84; }, b3Nobounce: function b3Nobounce(tension) { var friction = 0; if (tension <= 18) { friction = this.b3Friction1(tension); } else if (tension > 18 && tension <= 44) { friction = this.b3Friction2(tension); } else { friction = this.b3Friction3(tension); } return friction; } }); util.extend(SpringConfig, { // Convert an origami Spring tension and friction to Rebound spring // constants. If you are prototyping a design with Origami, this // makes it easy to make your springs behave exactly the same in // Rebound. fromOrigamiTensionAndFriction: function fromOrigamiTensionAndFriction(tension, friction) { return new SpringConfig(OrigamiValueConverter.tensionFromOrigamiValue(tension), OrigamiValueConverter.frictionFromOrigamiValue(friction)); }, // Convert an origami PopAnimation Spring bounciness and speed to Rebound // spring constants. If you are using PopAnimation patches in Origami, this // utility will provide springs that match your prototype. fromBouncinessAndSpeed: function fromBouncinessAndSpeed(bounciness, speed) { var bouncyConversion = new rebound.BouncyConversion(bounciness, speed); return this.fromOrigamiTensionAndFriction(bouncyConversion.bouncyTension, bouncyConversion.bouncyFriction); }, // Create a SpringConfig with no tension or a coasting spring with some // amount of Friction so that it does not coast infininitely. coastingConfigWithOrigamiFriction: function coastingConfigWithOrigamiFriction(friction) { return new SpringConfig(0, OrigamiValueConverter.frictionFromOrigamiValue(friction)); } }); SpringConfig.DEFAULT_ORIGAMI_SPRING_CONFIG = SpringConfig.fromOrigamiTensionAndFriction(40, 7); util.extend(SpringConfig.prototype, { friction: 0, tension: 0 }); // Here are a couple of function to convert colors between hex codes and RGB // component values. These are handy when performing color // tweening animations. var colorCache = {}; util.hexToRGB = function (color) { if (colorCache[color]) { return colorCache[color]; } color = color.replace('#', ''); if (color.length === 3) { color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2]; } var parts = color.match(/.{2}/g); var ret = { r: parseInt(parts[0], 16), g: parseInt(parts[1], 16), b: parseInt(parts[2], 16) }; colorCache[color] = ret; return ret; }; util.rgbToHex = function (r, g, b) { r = r.toString(16); g = g.toString(16); b = b.toString(16); r = r.length < 2 ? '0' + r : r; g = g.length < 2 ? '0' + g : g; b = b.length < 2 ? '0' + b : b; return '#' + r + g + b; }; var MathUtil = rebound.MathUtil = { // This helper function does a linear interpolation of a value from // one range to another. This can be very useful for converting the // motion of a Spring to a range of UI property values. For example a // spring moving from position 0 to 1 could be interpolated to move a // view from pixel 300 to 350 and scale it from 0.5 to 1. The current // position of the `Spring` just needs to be run through this method // taking its input range in the _from_ parameters with the property // animation range in the _to_ parameters. mapValueInRange: function mapValueInRange(value, fromLow, fromHigh, toLow, toHigh) { var fromRangeSize = fromHigh - fromLow; var toRangeSize = toHigh - toLow; var valueScale = (value - fromLow) / fromRangeSize; return toLow + valueScale * toRangeSize; }, // Interpolate two hex colors in a 0 - 1 range or optionally provide a // custom range with fromLow,fromHight. The output will be in hex by default // unless asRGB is true in which case it will be returned as an rgb string. interpolateColor: function interpolateColor(val, startColor, endColor, fromLow, fromHigh, asRGB) { fromLow = fromLow === undefined ? 0 : fromLow; fromHigh = fromHigh === undefined ? 1 : fromHigh; startColor = util.hexToRGB(startColor); endColor = util.hexToRGB(endColor); var r = Math.floor(util.mapValueInRange(val, fromLow, fromHigh, startColor.r, endColor.r)); var g = Math.floor(util.mapValueInRange(val, fromLow, fromHigh, startColor.g, endColor.g)); var b = Math.floor(util.mapValueInRange(val, fromLow, fromHigh, startColor.b, endColor.b)); if (asRGB) { return 'rgb(' + r + ',' + g + ',' + b + ')'; } else { return util.rgbToHex(r, g, b); } }, degreesToRadians: function degreesToRadians(deg) { return deg * Math.PI / 180; }, radiansToDegrees: function radiansToDegrees(rad) { return rad * 180 / Math.PI; } }; util.extend(util, MathUtil); // Utilities // --------- // Here are a few useful JavaScript utilities. // Lop off the first occurence of the reference in the Array. function removeFirst(array, item) { var idx = array.indexOf(item); idx != -1 && array.splice(idx, 1); } var _onFrame; if (typeof window !== 'undefined') { _onFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; } if (!_onFrame && typeof process !== 'undefined' && process.title === 'node') { _onFrame = setImmediate; } // Cross browser/node timer functions. util.onFrame = function onFrame(func) { return _onFrame(func); }; // Export the public api using exports for common js or the window for // normal browser inclusion. if (typeof exports != 'undefined') { util.extend(exports, rebound); } else if (typeof window != 'undefined') { window.rebound = rebound; } })(); 'use strict'; /** * Polygon. * Create a regular polygon and provide api to compute inscribed child. */ var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Polygon = function () { function Polygon() { var radius = arguments.length <= 0 || arguments[0] === undefined ? 100 : arguments[0]; var sides = arguments.length <= 1 || arguments[1] === undefined ? 3 : arguments[1]; var depth = arguments.length <= 2 || arguments[2] === undefined ? 0 : arguments[2]; var colors = arguments[3]; _classCallCheck(this, Polygon); this._radius = radius; this._sides = sides; this._depth = depth; this._colors = colors; this._x = 0; this._y = 0; this.rotation = 0; this.scale = 1; // Get basePolygon points straight away. this.points = this._getRegularPolygonPoints(); } /** * Get the points of any regular polygon based on * the number of sides and radius. */ _createClass(Polygon, [{ key: '_getRegularPolygonPoints', value: function _getRegularPolygonPoints() { var points = []; var i = 0; while (i < this._sides) { // Note that sin and cos are inverted in order to draw // polygon pointing down like: ∇ var x = -this._radius * Math.sin(i * 2 * Math.PI / this._sides); var y = this._radius * Math.cos(i * 2 * Math.PI / this._sides); points.push({ x: x, y: y }); i++; } return points; } /** * Get the inscribed polygon points by calling `getInterpolatedPoint` * for the points (start, end) of each side. */ }, { key: '_getInscribedPoints', value: function _getInscribedPoints(points, progress) { var _this = this; var inscribedPoints = []; points.forEach(function (item, i) { var start = item; var end = points[i + 1]; if (!end) { end = points[0]; } var point = _this._getInterpolatedPoint(start, end, progress); inscribedPoints.push(point); }); return inscribedPoints; } /** * Get interpolated point using linear interpolation * on x and y axis. */ }, { key: '_getInterpolatedPoint', value: function _getInterpolatedPoint(start, end, progress) { var Ax = start.x; var Ay = start.y; var Bx = end.x; var By = end.y; // Linear interpolation formula: // point = start + (end - start) * progress; var Cx = Ax + (Bx - Ax) * progress; var Cy = Ay + (By - Ay) * progress; return { x: Cx, y: Cy }; } /** * Update children points array. */ }, { key: '_getUpdatedChildren', value: function _getUpdatedChildren(progress) { var children = []; for (var i = 0; i < this._depth; i++) { // Get basePolygon points on first lap // then get previous child points. var points = children[i - 1] || this.points; var inscribedPoints = this._getInscribedPoints(points, progress); children.push(inscribedPoints); } return children; } /** * Render children, first update children array, * then loop and draw each child. */ }, { key: 'renderChildren', value: function renderChildren(context, progress) { var _this2 = this; var children = this._getUpdatedChildren(progress); // child = array of points at a certain progress over the parent sides. children.forEach(function (points, i) { // Draw child. context.beginPath(); points.forEach(function (point) { return context.lineTo(point.x, point.y); }); context.closePath(); // Set colors. var strokeColor = _this2._colors.stroke; var childColor = _this2._colors.child; if (strokeColor) { context.strokeStyle = strokeColor; context.stroke(); } if (childColor) { var rgb = rebound.util.hexToRGB(childColor); var alphaUnit = 1 / children.length; var alpha = alphaUnit + alphaUnit * i; var rgba = 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + alpha + ')'; context.fillStyle = rgba; // Set Shadow. context.shadowColor = 'rgba(0,0,0, 0.1)'; context.shadowBlur = 10; context.shadowOffsetX = 0; context.shadowOffsetY = 0; context.fill(); } }); } /** * Render. */ }, { key: 'render', value: function render(context) { context.save(); context.translate(this._x, this._y); if (this.rotation !== 0) { context.rotate(rebound.MathUtil.degreesToRadians(this.rotation)); } if (this.scale !== 1) { context.scale(this.scale, this.scale); } // Draw basePolygon. context.beginPath(); this.points.forEach(function (point) { return context.lineTo(point.x, point.y); }); context.closePath(); // Set colors. var strokeColor = this._colors.stroke; var childColor = this._colors.base; if (strokeColor) { context.strokeStyle = strokeColor; context.stroke(); } if (childColor) { context.fillStyle = childColor; context.fill(); } context.restore(); } }]); return Polygon; }(); 'use strict'; /** * Spinner. * Create a canvas element, append it to the body, render polygon with * inscribed children, provide init and complete methods to control spinner. */ var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Spinner = function () { function Spinner(params) { _classCallCheck(this, Spinner); var id = params.id, radius = params.radius, sides = params.sides, depth = params.depth, colors = params.colors, alwaysForward = params.alwaysForward, restAt = params.restAt, renderBase = params.renderBase; if (sides < 3) { console.warn('At least 3 sides required.'); sides = 3; } this._canvas = document.createElement('canvas'); this._canvas.style.backgroundColor = colors.background; this._canvasW = null; this._canvasH = null; this._canvasOpacity = 1; this._centerX = null; this._centerY = null; this._alwaysForward = alwaysForward; this._restThreshold = restAt; this._renderBase = renderBase; this._springRangeLow = 0; this._springRangeHigh = this._restThreshold || 1; // Instantiate basePolygon. this._basePolygon = new Polygon(radius, sides, depth, colors); this._progress = 0; this._isAutoSpin = null; this._isCompleting = null; } /** * Init spinner. */ _createClass(Spinner, [{ key: 'init', value: function init(spring, autoSpin) { this._addCanvas(); this._spring = spring; this._addSpringListener(); this._isAutoSpin = autoSpin; if (autoSpin) { // Start auto spin. this._spin(); } else { // Render first frame only. this._spring.setEndValue(0); this.render(); } } }, { key: '_addCanvas', value: function _addCanvas() { document.body.appendChild(this._canvas); this._context = this._canvas.getContext('2d'); this._setCanvasSize(); } }, { key: '_setCanvasSize', value: function _setCanvasSize() { this._canvasW = this._canvas.width = window.innerWidth; this._canvasH = this._canvas.height = window.innerHeight; this._canvas.style.position = 'fixed'; this._canvas.style.top = 0; this._canvas.style.left = 0; this._centerX = this._canvasW / 2; this._centerY = this._canvasH / 2; } }, { key: '_addSpringListener', value: function _addSpringListener() { var ctx = this; // Add a listener to the spring. Every time the physics // solver updates the Spring's value onSpringUpdate will // be called. this._spring.addListener({ onSpringUpdate: function onSpringUpdate(spring) { var val = spring.getCurrentValue(); // Input range in the `from` parameters. var fromLow = 0, fromHigh = 1, // Property animation range in the `to` parameters. toLow = ctx._springRangeLow, toHigh = ctx._springRangeHigh; val = rebound.MathUtil.mapValueInRange(val, fromLow, fromHigh, toLow, toHigh); // Note that the render method is // called with the spring motion value. ctx.render(val); } }); } /** * Start complete animation. */ }, { key: 'setComplete', value: function setComplete() { this._isCompleting = true; } }, { key: '_completeAnimation', value: function _completeAnimation() { // Fade out the canvas. this._canvasOpacity -= 0.1; this._canvas.style.opacity = this._canvasOpacity; // Stop animation and remove canvas. if (this._canvasOpacity <= 0) { this._isAutoSpin = false; this._spring.setAtRest(); this._canvas.remove(); } } /** * Spin animation. */ }, { key: '_spin', value: function _spin() { if (this._alwaysForward) { var currentValue = this._spring.getCurrentValue(); // Switch the animation range used to compute the value // in the `onSpringUpdate`, so to change the reverse animation // of the spring at a certain threshold. if (this._restThreshold && currentValue === 1) { this._switchSpringRange(); } // In order to keep the motion going forward // when spring reach 1 reset to 0 at rest. if (currentValue === 1) { this._spring.setCurrentValue(0).setAtRest(); } } // Restart the spinner. this._spring.setEndValue(this._spring.getCurrentValue() === 1 ? 0 : 1); } }, { key: '_switchSpringRange', value: function _switchSpringRange() { var threshold = this._restThreshold; this._springRangeLow = this._springRangeLow === threshold ? 0 : threshold; this._springRangeHigh = this._springRangeHigh === threshold ? 1 : threshold; } /** * Render. */ }, { key: 'render', value: function render(progress) { // Update progess if present and round to 4th decimal. if (progress) { this._progress = Math.round(progress * 10000) / 10000; } // Restart the spin. if (this._isAutoSpin && this._spring.isAtRest()) { this._spin(); } // Complete the animation. if (this._isCompleting) { this._completeAnimation(); } // Clear canvas and save context. this._context.clearRect(0, 0, this._canvasW, this._canvasH); this._context.save(); // Move to center. this._context.translate(this._centerX, this._centerY); this._context.lineWidth = 1.5; // Render basePolygon. if (this._renderBase) { this._basePolygon.render(this._context); } // Render inscribed polygons. this._basePolygon.renderChildren(this._context, this._progress); this._context.restore(); } }]); return Spinner; }(); 'use strict'; // Custom SETTINGS for each demo in related index.html var settings = SETTINGS || { rebound: { tension: 2, friction: 5 }, spinner: { radius: 80, sides: 3, depth: 4, colors: { background: '#000000', stroke: '#000000', base: '#222222', child: '#FFFFFF' }, alwaysForward: true, // When false the spring will reverse normally. restAt: 0.5, // A number from 0.1 to 0.9 || null for full rotation renderBase: true // Optionally render basePolygon } }; /** * Demo. */ var demo = { settings: settings, spring: null, spinner: null, /** * Initialize Rebound.js with settings. * Rebound is used to generate a spring which * is then used to animate the spinner. * See more: http://facebook.github.io/rebound-js/docs/rebound.html */ initRebound: function initRebound() { var settings = demo.settings.rebound; // Create a SpringSystem. var springSystem = new rebound.SpringSystem(); // Add a spring to the system. demo.spring = springSystem.createSpring(settings.tension, settings.friction); }, /** * Initialize Spinner with settings. */ initSpinner: function initSpinner() { var settings = demo.settings.spinner; // Instantiate Spinner. demo.spinner = new Spinner(settings); }, /** * Initialize demo. */ init: function init() { var spinnerTypeAutoSpin = true; // Instantiate animation engine and spinner system. demo.initRebound(); demo.initSpinner(); // Init animation with Rebound Spring System. demo.spinner.init(demo.spring, spinnerTypeAutoSpin); if (spinnerTypeAutoSpin) { // Fake loading time, in a real world just call demo.spinner.setComplete(); // whenever the preload will be completed. //change the load time setTimeout(function () { demo.spinner.setComplete(); }, 12000); } else { // Perform real ajax request. demo.loadSomething(); } }, /** * Ajax Request. */ loadSomething: function loadSomething() { var oReq = new XMLHttpRequest(); oReq.addEventListener('progress', function (oEvent) { if (oEvent.lengthComputable) { var percent = Math.ceil(oEvent.loaded / oEvent.total * 100); console.log('ajax loding percent', percent); // By setting the end value with the actual loading percentage // the spinner will progress based on the actual ajax loading time. demo.spring.setEndValue(percent * 0.01); } }); oReq.addEventListener('load', function (e) { // Complete the loading animation. demo.spinner.setComplete(); }); oReq.open('GET', '/img/something.jpg'); oReq.send(); } };