Skip to content

Commit

Permalink
fix(Spinner): remake fallback animation using SVG
Browse files Browse the repository at this point in the history
  • Loading branch information
zhzz committed Nov 9, 2020
1 parent 021b772 commit a6375d9
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 174 deletions.
32 changes: 18 additions & 14 deletions packages/react-ui/components/Spinner/Spinner.styles.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { css, memoizeStyle } from '../../lib/theming/Emotion';
import { Theme } from '../../lib/theming/Theme';
import { AnimationKeyframes } from '../../lib/theming/AnimationKeyframes';
import { isIE11 } from '../../lib/utils';

const styles = {
circle(t: Theme) {
return css`
stroke: ${t.spinnerColor};
animation: ${AnimationKeyframes.spinnerCircleOffset(t)} 1s cubic-bezier(0.5, 0.2, 0.5, 0.8) infinite,
${AnimationKeyframes.spinnerCircleLength(t)} 2s cubic-bezier(0.36, 0.14, 0.38, 0.69) infinite,
${AnimationKeyframes.spinnerCircleRotate(t)} 2s linear infinite,
${AnimationKeyframes.spinnerColor(t)} 6s ease-in-out infinite;
${!isIE11
? `
animation: ${AnimationKeyframes.spinnerCircleOffset(t)} 1s cubic-bezier(0.5, 0.2, 0.5, 0.8) infinite,
${AnimationKeyframes.spinnerCircleLength(t)} 2s cubic-bezier(0.36, 0.14, 0.38, 0.69) infinite,
${AnimationKeyframes.spinnerCircleRotate(t)} 2s linear infinite,
${AnimationKeyframes.spinnerColor(t)} 6s ease-in-out infinite;
`
: ``}
`;
},
circleDimmed(t: Theme) {
return css`
stroke: ${t.spinnerDimmedColor};
animation: ${AnimationKeyframes.spinnerCircleOffset(t)} 1s cubic-bezier(0.5, 0.2, 0.5, 0.8) infinite,
${AnimationKeyframes.spinnerCircleLength(t)} 2s cubic-bezier(0.36, 0.14, 0.38, 0.69) infinite,
${AnimationKeyframes.spinnerCircleRotate(t)} 2s linear infinite;
${!isIE11
? `
animation: ${AnimationKeyframes.spinnerCircleOffset(t)} 1s cubic-bezier(0.5, 0.2, 0.5, 0.8) infinite,
${AnimationKeyframes.spinnerCircleLength(t)} 2s cubic-bezier(0.36, 0.14, 0.38, 0.69) infinite,
${AnimationKeyframes.spinnerCircleRotate(t)} 2s linear infinite;
`
: ``}
`;
},

Expand Down Expand Up @@ -65,13 +76,6 @@ const styles = {
display: inline-block;
`;
},

fallback() {
return css`
display: inline-block;
position: relative;
`;
},
};

export const jsStyles = memoizeStyle(styles);
17 changes: 10 additions & 7 deletions packages/react-ui/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ import cn from 'classnames';
import { locale } from '../../lib/locale/decorators';
import { Theme } from '../../lib/theming/Theme';
import { ThemeContext } from '../../lib/theming/ThemeContext';
import { hasSvgAnimationSupport } from '../../lib/utils';
import { SpinnerIcon } from '../../internal/icons/SpinnerIcon';
import { SpinnerOld } from '../../internal/SpinnerOld';

import { jsStyles } from './Spinner.styles';
import { SpinnerFallback, types } from './SpinnerFallback';
import { SpinnerLocale, SpinnerLocaleHelper } from './locale';

export const types: {
[key: string]: SpinnerType;
} = {
big: 'big',
mini: 'mini',
normal: 'normal',
};

export type SpinnerType = 'mini' | 'normal' | 'big';

export interface SpinnerProps {
Expand Down Expand Up @@ -99,10 +105,7 @@ export class Spinner extends React.Component<SpinnerProps> {

return (
<div className={jsStyles.spinner()}>
<span className={jsStyles.inner()}>
{hasSvgAnimationSupport && this.renderSpinner(type, dimmed)}
{!hasSvgAnimationSupport && <SpinnerFallback type={type} dimmed={dimmed} />}
</span>
<span className={jsStyles.inner()}>{this.renderSpinner(type, dimmed)}</span>
{caption && this.renderCaption(type, caption)}
</div>
);
Expand All @@ -111,7 +114,7 @@ export class Spinner extends React.Component<SpinnerProps> {
private renderSpinner = (type: SpinnerType, dimmed?: boolean) => {
const circleClassName = dimmed ? jsStyles.circleDimmed(this.theme) : jsStyles.circle(this.theme);

return <SpinnerIcon size={type} className={circleClassName} />;
return <SpinnerIcon size={type} className={circleClassName} dimmed={dimmed} />;
};

private renderCaption = (type: SpinnerType, caption: React.ReactNode) => (
Expand Down
107 changes: 0 additions & 107 deletions packages/react-ui/components/Spinner/SpinnerFallback.tsx

This file was deleted.

179 changes: 179 additions & 0 deletions packages/react-ui/components/Spinner/SpinnerFallbackAnimation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { ColorFactory } from '../../lib/styles/ColorFactory';

export class SpinnerFallbackAnimationRunner {
private interval: ReturnType<typeof rafInterval> | null = null;

constructor(private animations: Animation[], private FPS: number) {
this.start();
}

public start = () => {
this.animations.forEach(animation => animation.start());
this.interval = rafInterval(() => {
this.animations.forEach(animation => animation.step());
}, this.FPS);
};

public stop = () => {
if (this.interval) {
this.interval.clear();
this.animations.forEach(animation => animation.finish());
}
};
}

const rafInterval = (fn: () => void, delay: number) => {
let lastcall = 0;
let cleared = false;
let rafId = 0;

const interval = () => {
if (cleared) return;
const timestamp = new Date().getTime();
if (!lastcall) lastcall = timestamp;
if (timestamp - lastcall > delay) {
fn();
lastcall = timestamp;
}
rafId = requestAnimationFrame(interval);
};
interval();

return {
clear: () => {
cleared = true;
cancelAnimationFrame(rafId);
},
};
};

class Animation {
private startTime = 0;
private isFinished = false;

constructor(
private duration: number,
private onProgress: (progress: number) => void,
private onFinish?: (animation: Animation) => void,
) {}

public step = () => {
if (this.isFinished) return;
const timestamp = new Date().getTime();
if (!this.startTime) this.startTime = timestamp;
const progress = (timestamp - this.startTime) / this.duration;

this.onProgress(progress);

if (progress >= 1) {
this.finish();
}
};

public reset = () => {
this.startTime = 0;
this.isFinished = false;
};

public finish = () => {
this.isFinished = true;
this.onFinish && this.onFinish(this);
};

public start = () => {
if (!this.isFinished) {
this.step();
}
};
}

export const createOffsetAnimation = (
from: number,
to: number,
duration: number,
setStyleProperty: CSSStyleDeclaration['setProperty'],
units = '',
) => {
return new Animation(
duration,
progress => {
const current = from + (to - from) * progress;
setStyleProperty('stroke-dashoffset', `${current}${units}`);
},
animation => {
animation.reset();
},
);
};

export const createLengthAnimation = (
from: number[],
to: number[],
duration: number,
setStyleProperty: CSSStyleDeclaration['setProperty'],
units = '',
) => {
let reverse = false;
return new Animation(
duration,
progress => {
const p = reverse ? 1 - progress : progress;
const current = [from[0] + (to[0] - from[0]) * p, from[1] + (to[1] - from[1]) * p];
setStyleProperty('stroke-dasharray', `${current[0]}${units} ${current[1]}${units}`);
},
animation => {
reverse = !reverse;
animation.reset();
},
);
};

export const createColorAnimation = (
colors: string[],
duration: number,
setStyleProperty: CSSStyleDeclaration['setProperty'],
) => {
const rgbColors = colors.map(color => ColorFactory.create(color).rgb);
let currentIndex = 0;
let nextIndex = 1;

return new Animation(
duration,
progress => {
const from = rgbColors[currentIndex];
const to = rgbColors[nextIndex];
if (from && to) {
const current = [
Math.round(from[0] + (to[0] - from[0]) * progress),
Math.round(from[1] + (to[1] - from[1]) * progress),
Math.round(from[2] + (to[2] - from[2]) * progress),
];
setStyleProperty('stroke', `rgb(${current})`);
}
},
animation => {
animation.reset();
currentIndex = nextIndex;
nextIndex = (nextIndex + 1) % colors.length;
},
);
};

export const createRotationAnimation = (
from: number,
to: number,
duration: number,
setStyleProperty: CSSStyleDeclaration['setProperty'],
units = 'deg',
) => {
return new Animation(
duration,
progress => {
const current = Math.round(from + (to - from) * progress);
setStyleProperty('transform', `rotate(${current}${units})`);
},
animation => {
animation.reset();
},
);
};
Loading

0 comments on commit a6375d9

Please sign in to comment.