Last active
January 2, 2019 07:17
-
-
Save bruskowski/9ed3dd4a3e5a2173fb0e7fc85df98f01 to your computer and use it in GitHub Desktop.
Adds hover trigger with reverse animation to Henrique Gusso's Magic Move Framer X component.
This file contains hidden or 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 * as React from 'react' | |
import { PropertyControls, ControlType, Animatable, animate } from 'framer' | |
interface Props { | |
width: number | |
height: number | |
animate: string | |
delay: number | |
target: React.ReactChild | |
source: React.ReactChild | |
easing: string | |
tension: number | |
friction: number | |
duration: number | |
curve: string | |
} | |
export class MagicMoveHover extends React.Component<Props> { | |
static defaultProps = { | |
width: 250, | |
height: 250, | |
animate: 'onTap', | |
delay: 1, | |
easing: 'spring', | |
tension: 550, | |
friction: 25, | |
curve: '0.25, 0.1, 0.25, 1', | |
duration: 0.4, | |
} | |
static propertyControls: PropertyControls = { | |
children: { | |
type: ControlType.ComponentInstance, | |
title: 'Source', | |
}, | |
target: { | |
type: ControlType.ComponentInstance, | |
title: 'Target', | |
}, | |
animate: { | |
type: ControlType.Enum, | |
title: 'Animate', | |
options: ['auto', 'delay', 'onTap', 'onHover'], | |
optionTitles: ['Auto', 'Delay', 'onTap', 'onHover'], | |
}, | |
delay: { | |
type: ControlType.Number, | |
title: 'Delay', | |
max: 10, | |
step: 0.1, | |
hidden(props) { | |
return props.animate != 'delay' | |
}, | |
}, | |
easing: { | |
type: ControlType.Enum, | |
title: 'Easing', | |
options: [ | |
'spring', | |
'bezier', | |
'linear', | |
'ease', | |
'easeIn', | |
'easeOut', | |
'easeInOut', | |
], | |
optionTitles: [ | |
'Spring', | |
'Bézier curve', | |
'Linear', | |
'Ease', | |
'Ease-in', | |
'Ease-out', | |
'Ease-in-out', | |
], | |
}, | |
tension: { | |
type: ControlType.Number, | |
title: 'Tension', | |
max: 1000, | |
hidden(props) { | |
return props.easing != 'spring' | |
}, | |
}, | |
friction: { | |
type: ControlType.Number, | |
title: 'Friction', | |
hidden(props) { | |
return props.easing != 'spring' | |
}, | |
}, | |
duration: { | |
type: ControlType.Number, | |
title: 'Duration', | |
max: 10, | |
step: 0.1, | |
hidden(props) { | |
return props.easing == 'spring' | |
}, | |
}, | |
curve: { | |
type: ControlType.String, | |
title: 'Curve', | |
hidden(props) { | |
return props.easing != 'bezier' | |
}, | |
}, | |
} | |
state = { | |
foundIDs: [], | |
} | |
magicList = [] | |
magicListReverse = [] | |
elements = {} | |
i = 0 | |
runMagic = () => { | |
this.magicList.forEach(exec => { | |
exec() | |
}) | |
} | |
runMagicReverse = () => { | |
this.magicListReverse.forEach(exec => { | |
exec() | |
}) | |
} | |
magic = (start, end) => { | |
const { props } = this | |
const options = {} | |
const animated = Animatable(start) | |
if (props.easing == 'spring') { | |
options['tension'] = props.tension | |
options['friction'] = props.friction | |
} else { | |
options['duration'] = props.duration | |
} | |
if (props.easing == 'bezier') { | |
options['curve'] = JSON.parse(`[${props.curve}]`) | |
} | |
this.magicList.push(() => animate[props.easing](animated, end, options)) | |
this.magicListReverse.push(() => animate[props.easing](animated, start, options)) | |
return animated | |
} | |
cleanSide = (props, side, parentSize) => { | |
if (typeof props[side] == 'string') { | |
if (props[side].includes('fr')) { | |
return parseFloat(props[side]) * parentSize[side] | |
} | |
return (parseFloat(props[side]) / 100) * parentSize[side] | |
} | |
return props[side] | |
} | |
getSize = props => { | |
const { left, right, top, bottom, parentSize } = props | |
const size = { | |
width: [left, right], | |
height: [top, bottom], | |
} | |
const returnSize = { | |
width: null, | |
height: null, | |
} | |
for (const side in size) { | |
returnSize[side] = size[side].every(i => i != null) | |
? parentSize[side] - size[side][0] - size[side][1] | |
: this.cleanSide(props, side, parentSize) | |
} | |
return returnSize | |
} | |
getConstraints = props => { | |
const orientation = { top: 'Y', left: 'X' } | |
const constraints = ['top', 'left'] | |
const returnConstraints = { | |
top: null, | |
left: null, | |
} | |
constraints.forEach(side => { | |
const sizeSide = side == 'left' ? 'width' : 'height' | |
const oppositeSide = side == 'left' ? 'right' : 'bottom' | |
const cleanSide = this.cleanSide(props, sizeSide, props.parentSize) | |
if (constraints.every(i => props[i] != null)) { | |
returnConstraints[side] = props[side] | |
} else { | |
if (![side, oppositeSide].every(i => props[i] == null)) { | |
if (props[side] != null) { | |
returnConstraints[side] = props[side] | |
} else { | |
returnConstraints[side] = | |
props.parentSize[sizeSide] - props[oppositeSide] - cleanSide | |
} | |
} else { | |
returnConstraints[side] = | |
(props.parentSize[sizeSide] * | |
parseFloat(props['center' + orientation[side]])) / | |
100 - | |
cleanSide / 2 | |
} | |
} | |
}) | |
return returnConstraints | |
} | |
handleProps = (element, render, isParent, parentSize, stopPropagation) => { | |
const props = {} | |
const { elements, i } = this | |
if (!isParent && !render) { | |
if (elements[i] == null) elements[i] = [] | |
elements[i] = [...elements[i], { ...element.props, parentSize }] | |
} | |
if (render) { | |
const propsTransform = this.state.foundIDs.find(e => { | |
return e.start.id == element.props.id | |
}) | |
if (propsTransform) { | |
const { start, end } = propsTransform | |
if (element.type.name == 'WithEventsHOC') { | |
const { getConstraints, getSize, magic } = this | |
const constraints = [getConstraints(start), getConstraints(end)] | |
const size = [getSize(start), getSize(end)] | |
const found = Object.keys(start).filter(key => { | |
if (typeof start[key] == 'string' && start[key]) { | |
if (start[key].includes('fr')) { | |
return true | |
} | |
} | |
}) | |
if (!found.length && !stopPropagation) { | |
props['bottom'] = null | |
props['right'] = null | |
props['top'] = magic(constraints[0].top, constraints[1].top) | |
props['left'] = magic(constraints[0].left, constraints[1].left) | |
props['width'] = magic(size[0].width, size[1].width) | |
props['height'] = magic(size[0].height, size[1].height) | |
props['background'] = magic(start.background, end.background) | |
props['opacity'] = magic(start.opacity, end.opacity) | |
props['rotation'] = magic(start.rotation, end.rotation) | |
} | |
} | |
} | |
} | |
return props | |
} | |
clone = ( | |
element, | |
render = false, | |
isParent = false, | |
parentSize = null, | |
stopPropagation = false, | |
) => { | |
if (isParent) { | |
this.i = 0 | |
} | |
this.i++ | |
if (element.type.name == 'Unwrap') { | |
stopPropagation = true | |
} | |
return React.cloneElement( | |
element, | |
this.handleProps( | |
element, | |
render, | |
isParent, | |
parentSize, | |
stopPropagation, | |
), | |
React.Children.map(element.props.children, child => { | |
const { width, height } = | |
element.type.name == 'Unwrap' | |
? parentSize | |
: this.getSize({ ...element.props, parentSize }) | |
return this.clone( | |
child, | |
render, | |
false, | |
{ width, height }, | |
stopPropagation, | |
) | |
}), | |
) | |
} | |
processProps = () => { | |
const { children, target } = this.props | |
const { elements } = this | |
if (children.length && target.length) { | |
this.clone(children[0], false, true) | |
this.clone(target[0], false, true) | |
const foundElements = [] | |
Object.keys(elements).forEach(key => { | |
foundElements.push({ | |
start: elements[key][0], | |
end: elements[key][1], | |
}) | |
}) | |
this.setState({ | |
foundIDs: [...this.state.foundIDs, ...foundElements], | |
}) | |
} | |
} | |
componentDidMount() { | |
this.processProps() | |
} | |
componentDidUpdate(prevProps) { | |
const { children, target, animate, delay } = this.props | |
if (children !== prevProps.children || target !== prevProps.target) { | |
this.processProps() | |
} | |
if (animate == 'auto') { | |
this.runMagic() | |
} | |
if (animate == 'delay') { | |
setTimeout(() => { | |
this.runMagic() | |
}, delay * 1000) | |
} | |
} | |
render() { | |
const { width, height, children, target, animate } = this.props | |
return children[0] && target[0] ? ( | |
<div onClick={animate == 'onTap' ? this.runMagic : undefined} onMouseEnter={animate == 'onHover' ? this.runMagic : undefined} onMouseLeave={animate == 'onHover' ? this.runMagicReverse : undefined}> | |
{this.clone(children[0], true, true)} | |
</div> | |
) : ( | |
<div style={{ width: width, height: height }}> | |
<div style={{ ...messageBoxStyle, width: width, height: height }}> | |
<div style={{ display: 'flex' }}> | |
<div | |
style={{ | |
...(children[0] ? numberStyleOff : numberStyle), | |
marginRight: 15, | |
}} | |
> | |
Source | |
</div> | |
<div style={target[0] ? numberStyleOff : numberStyle}> | |
Target | |
</div> | |
</div> | |
<div style={textStyle}>Connect to source and target</div> | |
</div> | |
</div> | |
) | |
} | |
} | |
const messageBoxStyle: React.CSSProperties = { | |
display: 'flex', | |
alignItems: 'center', | |
justifyContent: 'center', | |
flexDirection: 'column', | |
minWidth: 175, | |
height: 58, | |
padding: 16, | |
borderRadius: 3, | |
background: 'rgba(136, 85, 255, 0.1)', | |
boxShadow: 'inset 0 0 0 1px rgba(137, 87, 255, 0.5)', | |
} | |
const numberStyle: React.CSSProperties = { | |
display: 'flex', | |
alignItems: 'center', | |
justifyContent: 'center', | |
width: 66, | |
height: 34, | |
borderRadius: 3, | |
fontSize: 14, | |
fontWeight: 800, | |
background: '#8855FF', | |
color: 'white', | |
} | |
const numberStyleOff: React.CSSProperties = { | |
display: 'flex', | |
alignItems: 'center', | |
justifyContent: 'center', | |
width: 66, | |
height: 34, | |
borderRadius: 3, | |
fontSize: 14, | |
fontWeight: 800, | |
background: 'rgba(135, 85, 255, .3)', | |
color: 'white', | |
} | |
const textStyle: React.CSSProperties = { | |
fontSize: 16, | |
lineHeight: 1.3, | |
color: '#8855FF', | |
marginTop: 10, | |
textAlign: 'center', | |
} | |
const subTextStyle: React.CSSProperties = { | |
display: 'block', | |
fontSize: 11, | |
fontWeight: 500, | |
opacity: 0.8, | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment