|
| 1 | +// legacy code forked from amator, will be refactored and replaced |
| 2 | + |
| 3 | +/** |
| 4 | + * https://github.com/gre/bezier-easing |
| 5 | + * BezierEasing - use bezier curve for transition easing function |
| 6 | + * by Gaëtan Renaudeau 2014 - 2015 – MIT License |
| 7 | + */ |
| 8 | + |
| 9 | +// These values are established by empiricism with tests (tradeoff: performance VS precision) |
| 10 | +var NEWTON_ITERATIONS = 4 |
| 11 | +var NEWTON_MIN_SLOPE = 0.001 |
| 12 | +var SUBDIVISION_PRECISION = 0.0000001 |
| 13 | +var SUBDIVISION_MAX_ITERATIONS = 10 |
| 14 | + |
| 15 | +var kSplineTableSize = 11 |
| 16 | +var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0) |
| 17 | + |
| 18 | +var float32ArraySupported = typeof Float32Array === 'function' |
| 19 | + |
| 20 | +function A(aA1, aA2) { |
| 21 | + return 1.0 - 3.0 * aA2 + 3.0 * aA1 |
| 22 | +} |
| 23 | +function B(aA1, aA2) { |
| 24 | + return 3.0 * aA2 - 6.0 * aA1 |
| 25 | +} |
| 26 | +function C(aA1) { |
| 27 | + return 3.0 * aA1 |
| 28 | +} |
| 29 | + |
| 30 | +// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. |
| 31 | +function calcBezier(aT, aA1, aA2) { |
| 32 | + return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT |
| 33 | +} |
| 34 | + |
| 35 | +// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. |
| 36 | +function getSlope(aT, aA1, aA2) { |
| 37 | + return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1) |
| 38 | +} |
| 39 | + |
| 40 | +function binarySubdivide(aX, aA, aB, mX1, mX2) { |
| 41 | + var currentX, |
| 42 | + currentT, |
| 43 | + i = 0 |
| 44 | + do { |
| 45 | + currentT = aA + (aB - aA) / 2.0 |
| 46 | + currentX = calcBezier(currentT, mX1, mX2) - aX |
| 47 | + if (currentX > 0.0) { |
| 48 | + aB = currentT |
| 49 | + } else { |
| 50 | + aA = currentT |
| 51 | + } |
| 52 | + } while ( |
| 53 | + Math.abs(currentX) > SUBDIVISION_PRECISION && |
| 54 | + ++i < SUBDIVISION_MAX_ITERATIONS |
| 55 | + ) |
| 56 | + return currentT |
| 57 | +} |
| 58 | + |
| 59 | +function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { |
| 60 | + for (var i = 0; i < NEWTON_ITERATIONS; ++i) { |
| 61 | + var currentSlope = getSlope(aGuessT, mX1, mX2) |
| 62 | + if (currentSlope === 0.0) { |
| 63 | + return aGuessT |
| 64 | + } |
| 65 | + var currentX = calcBezier(aGuessT, mX1, mX2) - aX |
| 66 | + aGuessT -= currentX / currentSlope |
| 67 | + } |
| 68 | + return aGuessT |
| 69 | +} |
| 70 | + |
| 71 | +function bezier(mX1, mY1, mX2, mY2) { |
| 72 | + if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { |
| 73 | + throw new Error('bezier x values must be in [0, 1] range') |
| 74 | + } |
| 75 | + |
| 76 | + // Precompute samples table |
| 77 | + var sampleValues = float32ArraySupported |
| 78 | + ? new Float32Array(kSplineTableSize) |
| 79 | + : new Array(kSplineTableSize) |
| 80 | + if (mX1 !== mY1 || mX2 !== mY2) { |
| 81 | + for (var i = 0; i < kSplineTableSize; ++i) { |
| 82 | + sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2) |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + function getTForX(aX) { |
| 87 | + var intervalStart = 0.0 |
| 88 | + var currentSample = 1 |
| 89 | + var lastSample = kSplineTableSize - 1 |
| 90 | + |
| 91 | + for ( |
| 92 | + ; |
| 93 | + currentSample !== lastSample && sampleValues[currentSample] <= aX; |
| 94 | + ++currentSample |
| 95 | + ) { |
| 96 | + intervalStart += kSampleStepSize |
| 97 | + } |
| 98 | + --currentSample |
| 99 | + |
| 100 | + // Interpolate to provide an initial guess for t |
| 101 | + var dist = |
| 102 | + (aX - sampleValues[currentSample]) / |
| 103 | + (sampleValues[currentSample + 1] - sampleValues[currentSample]) |
| 104 | + var guessForT = intervalStart + dist * kSampleStepSize |
| 105 | + |
| 106 | + var initialSlope = getSlope(guessForT, mX1, mX2) |
| 107 | + if (initialSlope >= NEWTON_MIN_SLOPE) { |
| 108 | + return newtonRaphsonIterate(aX, guessForT, mX1, mX2) |
| 109 | + } else if (initialSlope === 0.0) { |
| 110 | + return guessForT |
| 111 | + } else { |
| 112 | + return binarySubdivide( |
| 113 | + aX, |
| 114 | + intervalStart, |
| 115 | + intervalStart + kSampleStepSize, |
| 116 | + mX1, |
| 117 | + mX2 |
| 118 | + ) |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + return function BezierEasing(x) { |
| 123 | + if (mX1 === mY1 && mX2 === mY2) { |
| 124 | + return x // linear |
| 125 | + } |
| 126 | + // Because JavaScript number are imprecise, we should guarantee the extremes are right. |
| 127 | + if (x === 0) { |
| 128 | + return 0 |
| 129 | + } |
| 130 | + if (x === 1) { |
| 131 | + return 1 |
| 132 | + } |
| 133 | + return calcBezier(getTForX(x), mY1, mY2) |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +// Predefined set of animations. Similar to CSS easing functions |
| 138 | +var animations = { |
| 139 | + ease: bezier(0.25, 0.1, 0.25, 1), |
| 140 | + easeIn: bezier(0.42, 0, 1, 1), |
| 141 | + easeOut: bezier(0, 0, 0.58, 1), |
| 142 | + easeInOut: bezier(0.42, 0, 0.58, 1), |
| 143 | + linear: bezier(0, 0, 1, 1), |
| 144 | +} |
| 145 | + |
| 146 | +export function animate(source, target, options) { |
| 147 | + var start = Object.create(null) |
| 148 | + var diff = Object.create(null) |
| 149 | + options = options || {} |
| 150 | + // We let clients specify their own easing function |
| 151 | + var easing = |
| 152 | + typeof options.easing === 'function' |
| 153 | + ? options.easing |
| 154 | + : animations[options.easing] |
| 155 | + |
| 156 | + // if nothing is specified, default to ease (similar to CSS animations) |
| 157 | + if (!easing) { |
| 158 | + if (options.easing) { |
| 159 | + console.warn('Unknown easing function in amator: ' + options.easing) |
| 160 | + } |
| 161 | + easing = animations.ease |
| 162 | + } |
| 163 | + |
| 164 | + var step = typeof options.step === 'function' ? options.step : noop |
| 165 | + var done = typeof options.done === 'function' ? options.done : noop |
| 166 | + |
| 167 | + var scheduler = getScheduler(options.scheduler) |
| 168 | + |
| 169 | + var keys = Object.keys(target) |
| 170 | + keys.forEach(function(key) { |
| 171 | + start[key] = source[key] |
| 172 | + diff[key] = target[key] - source[key] |
| 173 | + }) |
| 174 | + |
| 175 | + var durationInMs = options.duration || 400 |
| 176 | + var durationInFrames = Math.max(1, durationInMs * 0.06) // 0.06 because 60 frames pers 1,000 ms |
| 177 | + var previousAnimationId |
| 178 | + var frame = 0 |
| 179 | + |
| 180 | + previousAnimationId = scheduler.next(loop) |
| 181 | + |
| 182 | + return { |
| 183 | + cancel: cancel, |
| 184 | + } |
| 185 | + |
| 186 | + function cancel() { |
| 187 | + scheduler.cancel(previousAnimationId) |
| 188 | + previousAnimationId = 0 |
| 189 | + } |
| 190 | + |
| 191 | + function loop() { |
| 192 | + var t = easing(frame / durationInFrames) |
| 193 | + frame += 1 |
| 194 | + setValues(t) |
| 195 | + if (frame <= durationInFrames) { |
| 196 | + previousAnimationId = scheduler.next(loop) |
| 197 | + step(source) |
| 198 | + } else { |
| 199 | + previousAnimationId = 0 |
| 200 | + setTimeout(function() { |
| 201 | + done(source) |
| 202 | + }, 0) |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + function setValues(t) { |
| 207 | + keys.forEach(function(key) { |
| 208 | + source[key] = diff[key] * t + start[key] |
| 209 | + }) |
| 210 | + } |
| 211 | +} |
| 212 | + |
| 213 | +function noop() {} |
| 214 | + |
| 215 | +function getScheduler(scheduler) { |
| 216 | + if (!scheduler) { |
| 217 | + var canRaf = typeof window !== 'undefined' && window.requestAnimationFrame |
| 218 | + return canRaf ? rafScheduler() : timeoutScheduler() |
| 219 | + } |
| 220 | + if (typeof scheduler.next !== 'function') |
| 221 | + throw new Error('Scheduler is supposed to have next(cb) function') |
| 222 | + if (typeof scheduler.cancel !== 'function') |
| 223 | + throw new Error('Scheduler is supposed to have cancel(handle) function') |
| 224 | + |
| 225 | + return scheduler |
| 226 | +} |
| 227 | + |
| 228 | +function rafScheduler() { |
| 229 | + return { |
| 230 | + next: window.requestAnimationFrame.bind(window), |
| 231 | + cancel: window.cancelAnimationFrame.bind(window), |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +function timeoutScheduler() { |
| 236 | + return { |
| 237 | + next: function(cb) { |
| 238 | + return setTimeout(cb, 1000 / 60) |
| 239 | + }, |
| 240 | + cancel: function(id) { |
| 241 | + return clearTimeout(id) |
| 242 | + }, |
| 243 | + } |
| 244 | +} |
0 commit comments