Skip to content

Instantly share code, notes, and snippets.

@BinaryMuse
Last active June 13, 2019 03:30
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BinaryMuse/9431d5cecc3d57c4c317 to your computer and use it in GitHub Desktop.
Save BinaryMuse/9431d5cecc3d57c4c317 to your computer and use it in GitHub Desktop.
DND: Blog Post - Staggered Animation with React Motion
import React from "react";
import { Motion, StaggeredMotion, spring } from "react-motion";
import { constant, range } from "lodash";
const DEG_TO_RAD = Math.PI / 180;
const MAIN_BUTTON_DIAM = 100;
const CHILD_BUTTON_DIAM = 50;
const CHILDREN_ICONS = [
"at", "linkedin", "facebook", "github", "twitter"
];
const M_X = 500;
const M_Y = 450;
const FLYOUT_RADIUS = 120;
const SEPARATION_ANGLE = 40;
const FAN_ANGLE = (CHILDREN_ICONS.length - 1) * SEPARATION_ANGLE;
const BASE_ANGLE = (180 - FAN_ANGLE) / 2;
const toRadians = (deg) => deg * DEG_TO_RAD;
const deltaPosition = (idx, percent) => {
const angle = BASE_ANGLE + idx * SEPARATION_ANGLE;
const dX = FLYOUT_RADIUS * Math.cos(toRadians(angle)) * percent;
const dY = FLYOUT_RADIUS * Math.sin(toRadians(angle)) * percent;
return {
dX: dX + CHILD_BUTTON_DIAM / 2,
dY: dY + CHILD_BUTTON_DIAM / 2
};
};
class Application extends React.Component {
constructor(...args) {
super(...args);
this.state = {
isOpen: false
};
this.toggleMenu = this.toggleMenu.bind(this);
}
mainButtonStyles(percent) {
const deg = 135 * percent;
return {
width: MAIN_BUTTON_DIAM,
height: MAIN_BUTTON_DIAM,
top: M_Y - MAIN_BUTTON_DIAM / 2,
left: M_X - MAIN_BUTTON_DIAM / 2,
transform: `rotate(${deg}deg)`,
};
}
childButtonStyle(idx, percent) {
const { dX, dY } = deltaPosition(idx, percent);
const deg = 360 * percent;
return {
width: CHILD_BUTTON_DIAM,
height: CHILD_BUTTON_DIAM,
top: M_Y - dY,
left: M_X - dX,
transform: `rotate(${deg}deg)`,
};
}
render() {
const { isOpen } = this.state;
const goalPercent = isOpen ? 1.0 : 0.0;
const springParams = [210, 20];
const defaultStyles = range(CHILDREN_ICONS.length).map(constant({ percent: 0.0 }));
const nextStyles = (previousStyles) => {
return previousStyles.map((prev, i) => {
if (i === 0) {
return { percent: spring(goalPercent, springParams) };
} else {
const lastButtonPreviousPercent = previousStyles[i - 1].percent;
const thisButtonPreviousPercent = previousStyles[i].percent;
const shouldThisAnimate = isOpen ?
lastButtonPreviousPercent > 0.2 :
lastButtonPreviousPercent < 0.8;
return { percent: shouldThisAnimate ? spring(goalPercent, springParams) : thisButtonPreviousPercent };
}
});
};
return (
<div>
<StaggeredMotion defaultStyles={defaultStyles} styles={nextStyles}>
{(interpolatedStyles) => {
const leaderPercent = interpolatedStyles[0].percent;
return <div>
{interpolatedStyles.map(({ percent }, idx) => {
const style = this.childButtonStyle(idx, percent);
return (
<div className="child-button" style={style} key={idx}>
<i className={`fa fa-${CHILDREN_ICONS[idx]}`} />
</div>
);
})}
<div className="main-button" style={this.mainButtonStyles(leaderPercent)} onClick={this.toggleMenu}>
<i className="fa fa-plus" />
</div>
</div>
}}
</StaggeredMotion>
</div>
);
}
toggleMenu(e) {
e.preventDefault();
this.setState(s => ({ isOpen: !s.isOpen }));
}
}
const deltaPosition = (idx, percent) => {
const angle = BASE_ANGLE + idx * SEPARATION_ANGLE;
const dX = FLYOUT_RADIUS * Math.cos(toRadians(angle)) * percent;
const dY = FLYOUT_RADIUS * Math.sin(toRadians(angle)) * percent;
return {
dX: dX + CHILD_BUTTON_DIAM / 2,
dY: dY + CHILD_BUTTON_DIAM / 2
};
};
mainButtonStyles(percent) {
const deg = 135 * percent;
return {
width: MAIN_BUTTON_DIAM,
height: MAIN_BUTTON_DIAM,
top: M_Y - MAIN_BUTTON_DIAM / 2,
left: M_X - MAIN_BUTTON_DIAM / 2,
transform: `rotate(${deg}deg)`,
};
}
childButtonStyle(idx, percent) {
const { dX, dY } = deltaPosition(idx, percent);
const deg = 360 * percent;
return {
width: CHILD_BUTTON_DIAM,
height: CHILD_BUTTON_DIAM,
top: M_Y - dY,
left: M_X - dX,
transform: `rotate(${deg}deg)`,
};
}
const { isOpen } = this.state;
const goalPercent = isOpen ? 1.0 : 0.0;
const springParams = [210, 20];
const defaultStyles = range(CHILDREN_ICONS.length).map(constant({ percent: 0.0 }));
const nextStyles = (previousStyles) => {
return previousStyles.map((prev, i) => {
if (i === 0) {
return { percent: spring(goalPercent, springParams) };
} else {
const lastButtonPreviousPercent = previousStyles[i - 1].percent;
const thisButtonPreviousPercent = previousStyles[i].percent;
const shouldThisAnimate = isOpen ?
lastButtonPreviousPercent > 0.2 :
lastButtonPreviousPercent < 0.8;
return { percent: shouldThisAnimate ? spring(goalPercent, springParams) : thisButtonPreviousPercent };
}
});
};
return (
<div>
<StaggeredMotion defaultStyles={defaultStyles} styles={nextStyles}>
{(interpolatedStyles) => {
const leaderPercent = interpolatedStyles[0].percent;
return <div>
{interpolatedStyles.map(({ percent }, idx) => {
const style = this.childButtonStyle(idx, percent);
return (
<div className="child-button" style={style} key={idx}>
<i className={`fa fa-${CHILDREN_ICONS[idx]}`} />
</div>
);
})}
<div className="main-button" style={this.mainButtonStyles(leaderPercent)} onClick={this.toggleMenu}>
<i className="fa fa-plus" />
</div>
</div>
}}
</StaggeredMotion>
</div>
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment