Last active
June 13, 2019 03:30
-
-
Save BinaryMuse/9431d5cecc3d57c4c317 to your computer and use it in GitHub Desktop.
DND: Blog Post - Staggered Animation with React Motion
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 })); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)`, | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; | |
} | |
}); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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