diff --git a/packages/loaders/src/Dots.example.md b/packages/loaders/src/Dots.example.md index b51c8e99848..16a82047dce 100644 --- a/packages/loaders/src/Dots.example.md +++ b/packages/loaders/src/Dots.example.md @@ -71,7 +71,7 @@ const Color = ({ name, color, includeSample }) => @@ -94,13 +94,13 @@ const Color = ({ name, color, includeSample }) => - + setState({ velocity: parseFloat(event.target.value) })} - min={-0.5} - max={1} - step={0.05} + value={state.duration} + onChange={event => setState({ duration: parseFloat(event.target.value) })} + min={625} + max={2500} + step={625} /> @@ -125,7 +125,7 @@ const Color = ({ name, color, includeSample }) => - + diff --git a/packages/loaders/src/Dots.js b/packages/loaders/src/Dots.js index 56cada58653..a1bb42cdb36 100644 --- a/packages/loaders/src/Dots.js +++ b/packages/loaders/src/Dots.js @@ -8,7 +8,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { retrieveXCoordinate, retrieveYCoordinate, KEYFRAME_MAX } from './utils/dot-coordinates'; +import { DOT_ONE_FRAMES, DOT_TWO_FRAMES, DOT_THREE_FRAMES } from './utils/dot-coordinates'; import { DotsCircle, StyledSVG } from './styled-elements'; import ScheduleContainer from './containers/ScheduleContainer'; @@ -21,10 +21,9 @@ export default class Dots extends React.Component { **/ size: PropTypes.any, /** - * Velocity (speed) of the animation. Between -1 and 1. - * This should only be maniuplated at extreme sizes. + * Duration (ms) of the animation. Default is 1250ms. **/ - velocity: PropTypes.number, + duration: PropTypes.number, /** * Color of the loader. Can inherit from `color` styling. **/ @@ -39,46 +38,40 @@ export default class Dots extends React.Component { static defaultProps = { size: 'inherit', color: 'inherit', - velocity: 0.05, - delayMS: 750 + delayMS: 750, + duration: 1250 }; - state = { - frame: 0, - timestamp: 0 - }; - - performAnimationFrame = (timestamp = 0) => { - const { velocity } = this.props; - - let pinnedVelocity = velocity; - - if (velocity < -1) { - pinnedVelocity = -0.9; - } else if (velocity > 1) { - pinnedVelocity = 1; - } + constructor(props) { + super(props); - this.setState(prevState => { - const factor = 1000 + 1000 * pinnedVelocity; - const elapsed = (timestamp - prevState.timestamp) / factor; - const frame = prevState.frame + (elapsed % KEYFRAME_MAX); - - return { frame, timestamp }; - }); - }; + this.state = { + frame: 0, + timestamp: 0, + rawFrame: 0, + totalFrames: 100 + }; + } - retrieveFrame = offset => { - const loop = KEYFRAME_MAX * 2; + performAnimationFrame = (timestamp = 0) => { + const { duration } = this.props; + const { totalFrames, rawFrame } = this.state; + const elapsedTime = timestamp - this.state.timestamp; + const frameMultiplier = (totalFrames + 1) / duration; + const nextValue = rawFrame + elapsedTime * frameMultiplier; + const actualFrame = Math.floor(nextValue); + const frame = actualFrame % totalFrames; + const currentRawFrame = nextValue % totalFrames; - return (this.state.frame + offset * loop) % loop; + this.setState({ timestamp, frame, rawFrame: currentRawFrame }); }; render() { const { size, color, delayMS, ...other } = this.props; - const dotOneFrame = this.retrieveFrame(0); - const dotTwoFrame = this.retrieveFrame(1 / 3); - const dotThreeFrame = this.retrieveFrame(2 / 3); + const { frame } = this.state; + const [dotOneX, dotOneY] = DOT_ONE_FRAMES[frame]; + const [dotTwoX, dotTwoY] = DOT_TWO_FRAMES[frame]; + const [dotThreeX, dotThreeY] = DOT_THREE_FRAMES[frame]; return ( @@ -92,21 +85,9 @@ export default class Dots extends React.Component { {...other} > - - - + + + )} diff --git a/packages/loaders/src/Spinner.js b/packages/loaders/src/Spinner.js index 87a49e5d0c1..37be31901f2 100644 --- a/packages/loaders/src/Spinner.js +++ b/packages/loaders/src/Spinner.js @@ -19,14 +19,6 @@ import ScheduleContainer from './containers/ScheduleContainer'; const COMPONENT_ID = 'loaders.spinner'; export default class Spinner extends React.Component { - constructor(props) { - super(props); - - this.strokeWidthValues = this.computeFrames(STROKE_WIDTH_FRAMES); - this.rotationValues = this.computeFrames(ROTATION_FRAMES); - this.dasharrayValues = this.computeFrames(DASHARRAY_FRAMES); - } - static propTypes = { /** * Size of the loader. Can inherit from `font-size` styling. @@ -62,54 +54,25 @@ export default class Spinner extends React.Component { timestamp: 0 }; - computeFrames = frames => { - const { duration } = this.props; - const { totalFrames } = this.state; - - return Object.entries(frames).reduce((acc, item, index, arr) => { - const [frame, value] = item; - const [nextFrame, nextValue] = arr[index + 1] || [totalFrames, arr[0][1]]; - const diff = nextFrame - frame; - const frameHz = 1000 / 60; - - let subDuration = (duration / (totalFrames - 1)) * diff; - let lastValue = value; - - acc[frame] = value; - for (let idx = 0; idx < diff; idx++) { - lastValue = lastValue + (nextValue - lastValue) * (frameHz / subDuration); - subDuration = (duration / (totalFrames - 1)) * (diff - idx); - - acc[parseInt(frame, 10) + idx + 1] = lastValue; - } - acc[nextFrame] = nextValue; - - return acc; - }, {}); - }; - performAnimationFrame = (nowTime = 0) => { const { totalFrames, rawFrame, timestamp } = this.state; const { duration } = this.props; const elapsedTime = nowTime - timestamp; + const frameMultiplier = (totalFrames + 1) / duration; + const nextValue = rawFrame + elapsedTime * frameMultiplier; + const actualFrame = Math.floor(nextValue); + const frame = actualFrame % totalFrames; + const currentRawFrame = nextValue % totalFrames; - this.setState(() => { - const frameMultiplier = (totalFrames + 1) / duration; - const nextValue = rawFrame + elapsedTime * frameMultiplier; - const actualFrame = Math.floor(nextValue); - const frame = actualFrame % totalFrames; - const currentRawFrame = nextValue % totalFrames; - - return { frame, rawFrame: currentRawFrame, timestamp: nowTime }; - }); + this.setState({ frame, rawFrame: currentRawFrame, timestamp: nowTime }); }; render() { const { size, color, delayMS, ...other } = this.props; const { frame } = this.state; - const strokeWidthValue = this.strokeWidthValues[frame]; - const rotationValue = this.rotationValues[frame]; - const dasharrayValue = this.dasharrayValues[frame]; + const strokeWidthValue = STROKE_WIDTH_FRAMES[frame]; + const rotationValue = ROTATION_FRAMES[frame]; + const dasharrayValue = DASHARRAY_FRAMES[frame]; const WIDTH = 80; const HEIGHT = 80; diff --git a/packages/loaders/src/utils/animations.js b/packages/loaders/src/utils/animations.js index 31bd615eb22..f6374146bd5 100644 --- a/packages/loaders/src/utils/animations.js +++ b/packages/loaders/src/utils/animations.js @@ -5,28 +5,59 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -/** - * Accelerating from zero velocity - * @param {Number} time Time - */ -export function easeInCubic(time) { - return time * time * time; -} +const X_DISTANCE = 31; +const Y_DISTANCE = 27; +const isArray = arr => Array.isArray(arr); /** - * Decelerating to zero velocity - * @param {Number} time Time + * Linear interpolation between sample frames provided + * @param {Object} frames - The frame samples to compute from + * @param {Object} opts - Options to change computed values + * @param {Number} opts.duration - Duration of animation in ms + * @param {Number} opts.totalFrames - Frames to generate from sample + * @param {Number} opts.factor - Factor to increase amout of x coordinate */ -export function easeOutCubic(time) { - const value = time - 1; +export function computeFrames(frames, opts = {}) { + const { duration = 1250, totalFrames = 100, factor = 0 } = opts; + const movex = factor * X_DISTANCE; + const movey = Y_DISTANCE; - return value * value * value + 1; -} + return Object.entries(frames).reduce((acc, item, index, arr) => { + const [frame, value] = item; + const [x, y] = isArray(value) ? value : []; + const [nextFrame, nextValue] = arr[index + 1] || [totalFrames, arr[0][1]]; + const [xNext, yNext] = isArray(nextValue) ? nextValue : []; + const diff = nextFrame - frame; + const frameHz = 1000 / 60; -/** - * Acceleration until halfway, then deceleration - * @param {Number} time Time - */ -export function easeInOutCubic(time) { - return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; + let subDuration = (duration / totalFrames) * diff; + let lastValue = value; + const [xLast, yLast] = isArray(lastValue) ? lastValue : []; + + acc[frame] = isArray(value) ? [x + movex, y + movey] : value + movex; + // Compute the linear interpolation between current and next frame + for (let idx = 0; idx < diff; idx++) { + if (isArray(lastValue)) { + lastValue = [ + xLast + (xNext - xLast) * (frameHz / subDuration), + yLast + (yNext - yLast) * (frameHz / subDuration) + ]; + } else { + lastValue = + lastValue + + (isArray(nextValue) ? xNext - lastValue : nextValue - lastValue) * + (frameHz / subDuration); + } + + subDuration = (duration / totalFrames) * (diff - idx); + + acc[parseInt(frame, 10) + idx + 1] = isArray(lastValue) + ? [xLast + movex, yLast + movey] + : lastValue + movex; + } + + acc[nextFrame] = isArray(nextValue) ? [xNext + movex, yNext + movey] : nextValue + movex; + + return acc; + }, {}); } diff --git a/packages/loaders/src/utils/animations.spec.js b/packages/loaders/src/utils/animations.spec.js index 0dd3622c78f..81e1cda3845 100644 --- a/packages/loaders/src/utils/animations.spec.js +++ b/packages/loaders/src/utils/animations.spec.js @@ -5,28 +5,19 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -import { easeInCubic, easeOutCubic, easeInOutCubic } from './animations'; +import { computeFrames } from './animations'; describe('animations', () => { - describe('easeInCubic()', () => { - it('returns correct value based on time', () => { - expect(easeInCubic(2)).toBe(8); - }); - }); - - describe('easeOutCubic()', () => { - it('returns correct value based on time', () => { - expect(easeOutCubic(2)).toBe(2); - }); - }); - - describe('easeInOutCubic()', () => { - it('returns correct value when time is less than 0.5', () => { - expect(easeInOutCubic(0.45)).toBeCloseTo(0.3645); - }); + describe('computeFrames', () => { + it('linear interpolation between frames', () => { + const { 0: first, 50: middle, 100: last } = computeFrames( + { 0: 1, 99: 100 }, + { duration: 1000, totalFrames: 100 } + ); - it('returns correct value when time is greater than 0.5', () => { - expect(easeInOutCubic(3)).toBe(33); + expect(first).toBe(1); + expect(last).toBe(1); + expect(middle).toBe(68.99705398202416); }); }); }); diff --git a/packages/loaders/src/utils/dot-coordinates.js b/packages/loaders/src/utils/dot-coordinates.js index 554cb5d7e95..a7d82f704c3 100644 --- a/packages/loaders/src/utils/dot-coordinates.js +++ b/packages/loaders/src/utils/dot-coordinates.js @@ -5,81 +5,102 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -import { easeInCubic, easeInOutCubic, easeOutCubic } from './animations'; +import { computeFrames } from './animations'; -export const KEYFRAME_1 = 0.166666667; -export const KEYFRAME_2 = 0.55; -export const KEYFRAME_3 = 1.166666667; -export const KEYFRAME_4 = 1.333333333; -export const KEYFRAME_5 = 1.533333333; -export const KEYFRAME_MAX = 1.766666667; +export const DOT_ONE_FRAMES = computeFrames({ + 0: [0, 5], + 3: [1, -5], + 6: [3, -15], + 8: [5, -18], + 9: [7, -21], + 11: [8, -22], + 13: [9, -23], + 16: [12, -25], + 18: [13, -26], + 23: [18, -26], + 24: [19, -25], + 28: [22, -23], + 31: [24, -21], + 33: [26, -18], + 34: [28, -14], + 36: [29, -12], + 38: [30, -5], + 39: [31, 5], + 54: [31, 3], + 59: [33, 0], + 61: [43, 0], + 63: [48, 0], + 64: [51, 0], + 66: [53, 0], + 68: [55, 0], + 69: [57, 0], + 76: [60, 0], + 81: [61, 0], + 83: [62, 0], + 100: [62, 0] +}); -const WIDTH = 80; -const HEIGHT = WIDTH * 0.9; -const CIRCLE_RADIUS = WIDTH * 0.1125; -const MID_X = WIDTH / 2 - CIRCLE_RADIUS; -const MID_Y = HEIGHT / 2 - CIRCLE_RADIUS; -const BOTTOM = MID_Y + 5; +export const DOT_TWO_FRAMES = computeFrames( + { + 0: [0, 0], + 4: [0, 0], + 6: [-1, 0], + 8: [-2, 0], + 9: [-5, 0], + 11: [-7, 0], + 13: [-12, 0], + 14: [-17, 0], + 16: [-19, 0], + 18: [-22, 0], + 19: [-25, 0], + 21: [-26, 0], + 23: [-27, 0], + 24: [-28, 0], + 26: [-29, 0], + 29: [-30, 0], + 33: [-31, 0], + 89: [-31, 0], + 91: [-31, 1], + 94: [-31, 2], + 98: [-31, 3], + 99: [-31, 4], + 100: [-31, 5] + }, + { factor: 1 } +); -/** - * Retrieve the X coordinate value - * @param {Number} frame The current frame - */ -export function retrieveXCoordinate(frame) { - let retVal; - - const _frame = frame % KEYFRAME_MAX; - - if (_frame < KEYFRAME_1) { - return MID_X; - } else if (_frame < KEYFRAME_2) { - const frameValue = _frame - KEYFRAME_1; - const frameMaximum = KEYFRAME_2 - KEYFRAME_1; - const easeValue = easeInOutCubic(frameValue / frameMaximum); - - retVal = MID_X - easeValue * MID_X; - } else if (_frame < KEYFRAME_4) { - retVal = 0; - } else { - const frameValue = _frame - KEYFRAME_4; - const frameMaximum = KEYFRAME_MAX - KEYFRAME_4; - - retVal = MID_X * (frameValue / frameMaximum); - } - - if (frame >= KEYFRAME_MAX) { - retVal = MID_X * 2 - retVal; - } - - return retVal; -} - -/** - * Retrieve the Y coordinate value - * @param {Number} frame The current frame - */ -export function retrieveYCoordinate(frame) { - const _frame = frame % KEYFRAME_MAX; - - if (_frame < KEYFRAME_1) { - return (_frame / KEYFRAME_1) * -1 * (BOTTOM - MID_Y) + BOTTOM; - } else if (_frame < KEYFRAME_3) { - return MID_Y; - } else if (_frame < KEYFRAME_4) { - const frameValue = _frame - KEYFRAME_3; - const frameMaximum = KEYFRAME_4 - KEYFRAME_3; - - return (frameValue / frameMaximum) * (BOTTOM - MID_Y) + MID_Y; - } else if (_frame < KEYFRAME_5) { - const frameValue = _frame - KEYFRAME_4; - const frameMaximum = KEYFRAME_5 - KEYFRAME_4; - const easeValue = easeOutCubic(frameValue / frameMaximum); - - return BOTTOM - easeValue * BOTTOM; - } - const frameValue = _frame - KEYFRAME_5; - const frameMaximum = KEYFRAME_MAX - KEYFRAME_5; - const easeValue = easeInCubic(frameValue / frameMaximum); - - return easeValue * BOTTOM; -} +export const DOT_THREE_FRAMES = computeFrames( + { + 0: [0, 0], + 39: [0, 0], + 44: [0, 1], + 46: [0, 2], + 48: [0, 3], + 49: [0, 4], + 51: [0, 5], + 53: [-1, -6], + 54: [-2, -13], + 56: [-3, -15], + 58: [-5, -19], + 59: [-7, -21], + 61: [-8, -22], + 63: [-9, -24], + 64: [-11, -25], + 66: [-12, -26], + 74: [-19, -26], + 76: [-20, -25], + 78: [-22, -24], + 81: [-24, -21], + 83: [-26, -19], + 84: [-28, -15], + 86: [-29, -13], + 88: [-30, -6], + 89: [-31, 5], + 91: [-31, 4], + 93: [-31, 3], + 94: [-31, 2], + 98: [-31, 1], + 100: [-31, 0] + }, + { factor: 2 } +); diff --git a/packages/loaders/src/utils/dot-coordinates.spec.js b/packages/loaders/src/utils/dot-coordinates.spec.js deleted file mode 100644 index 5d386095680..00000000000 --- a/packages/loaders/src/utils/dot-coordinates.spec.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright Zendesk, Inc. - * - * Use of this source code is governed under the Apache License, Version 2.0 - * found at http://www.apache.org/licenses/LICENSE-2.0. - */ - -import { retrieveXCoordinate, retrieveYCoordinate } from './dot-coordinates'; - -describe('Dot Coordinates', () => { - describe('retrieveXCoordinate()', () => { - it('returns MID_X if sub-frame is within KEYFRAME_1', () => { - expect(retrieveXCoordinate(0.111)).toBe(31); - }); - - it('returns eased MID_X if sub-frame is within KEYFRAME_2', () => { - expect(retrieveXCoordinate(0.5)).toBeCloseTo(0.27517); - }); - - it('returns 0 if sub-frame is within KEYFRAME_4', () => { - expect(retrieveXCoordinate(1.3)).toBe(0); - }); - - it('returns MID_X upper transform if sub-frame is within KEYFRAME_MAX', () => { - expect(retrieveXCoordinate(1.8)).toBe(31); - }); - - it('returns MID_X lower transform if sub-frame otherwise', () => { - expect(retrieveXCoordinate(1.6)).toBeCloseTo(19.0769); - }); - - it('returns correct value if sub-frame is greater than KEYFRAME_MAX', () => { - expect(retrieveXCoordinate(3)).toBe(62); - }); - }); - - describe('retrieveYCoordinate()', () => { - it('returns correct value if sub-frame is within KEYFRAME_1', () => { - expect(retrieveYCoordinate(0.111)).toBeCloseTo(28.67); - }); - - it('returns correct value if sub-frame is within KEYFRAME_3', () => { - expect(retrieveYCoordinate(0.5)).toBe(27); - }); - - it('returns correct value if sub-frame is within KEYFRAME_4', () => { - expect(retrieveYCoordinate(1.3)).toBeCloseTo(31.0); - }); - - it('returns correct value if sub-frame is within KEYFRAME_5', () => { - expect(retrieveYCoordinate(1.5)).toBeCloseTo(0.148); - }); - - it('returns correct value otherwise', () => { - expect(retrieveYCoordinate(1.7)).toBeCloseTo(11.6618); - }); - }); -}); diff --git a/packages/loaders/src/utils/spinner-coordinates.js b/packages/loaders/src/utils/spinner-coordinates.js index d62bf03b212..0ad6bec77d7 100644 --- a/packages/loaders/src/utils/spinner-coordinates.js +++ b/packages/loaders/src/utils/spinner-coordinates.js @@ -5,7 +5,9 @@ * found at http://www.apache.org/licenses/LICENSE-2.0. */ -export const STROKE_WIDTH_FRAMES = { +import { computeFrames } from './animations'; + +export const STROKE_WIDTH_FRAMES = computeFrames({ 0: 6, 14: 5, 26: 4, @@ -15,9 +17,9 @@ export const STROKE_WIDTH_FRAMES = { 70: 4, 80: 5, 91: 6 -}; +}); -export const ROTATION_FRAMES = { +export const ROTATION_FRAMES = computeFrames({ 0: -90, 8: -81, 36: -30, @@ -43,9 +45,9 @@ export const ROTATION_FRAMES = { 88: 235, 90: 243, 99: 270 -}; +}); -export const DASHARRAY_FRAMES = { +export const DASHARRAY_FRAMES = computeFrames({ 0: 0, 13: 2, 26: 13, @@ -69,4 +71,4 @@ export const DASHARRAY_FRAMES = { 89: 7, 98: 1, 99: 0 -}; +});