<div class='center'>
<div id='app'></div>
</div>
// For codepen display only:
html, body { width: 100%; height: 100%; }
.center {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
// Parallax Hover Styles
.ph-wrapper {
position: relative;
transition: all 0.3s ease-out;
margin: 0;
.ph-shadow,
.ph-layers,
.ph-layer,
.ph-lighting {
transition: all 0.3s ease-out;
}
.ph-shadow,
.ph-layers,
.ph-layer,
.ph-lighting {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
img {
border-radius: 6px;
}
.ph-text > * {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-size: 4rem;
color: #fff;
font-weight: 300;
}
.ph-shadow {
height: 90%;
width: 90%;
left: 5%;
top: 5%;
background: none;
}
.ph-lighting {
background-image: linear-gradient(135deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0) 33%);
}
.ph-heading {
margin: 0;
padding: 0;
color: #fff;
font-size: 3rem;
text-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5);
}
}
View Compiled
const config = {
scale: 1.03, // How large to scale the item: 1.00 -> 1.10~
rotation: 0.3, // Rotation modifier: 0.1 (more) -> 0.5 (less)
alpha: 0.4, // Alpha channel modifer: 1.01 -> 1.1~
shadow: 8 // How much the shadow moves
};
class ParallaxHover extends React.Component {static propTypes() {
return {
children: React.Proptypes.node.isRequired,
width: React.Proptypes.string.isRequired,
height: React.Proptypes.string.isRequired
};
}
constructor(props) {
super(props);
this.state = {
rotateX: 0,
rotateY: 0,
shadowMovement: 20,
shadowSize: 50,
scale: 1,
angle: 0,
alpha: 0
};
}
__buildState(rotateX, rotateY, shadowMovement, shadowSize, scale, angle, alpha) {
this.setState({
rotateX: rotateX,
rotateY: rotateY,
shadowMovement: shadowMovement,
shadowSize: shadowSize,
scale: scale,
angle: angle,
alpha: alpha
});
}
__buildTransformStrings(rotateX, rotateY, scale) {
return {
WebkitTransform: `perspective(1000px) scale(${scale}) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
MozTransform: `perspective(1000px) scale(${scale}) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
transform: `perspective(1000px) scale(${scale}) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`
};
}
__calculateDistance(bounds, offsetX, offsetY) {
const distanceX = Math.pow(offsetX - (bounds.width / 2), 2);
const distanceY = Math.pow(offsetY - (bounds.height / 2), 2);
return Math.floor(Math.sqrt(distanceX + distanceY));
}
__calculateAlphaFromCenter(current) {
const max = Math.max(this.props.width, this.props.height);
return current / max * config.alpha;
}
__handleMouseMove({ pageX, pageY, nativeEvent}) {
const width = this.props.width;
const height = this.props.height;
const { scrollTop: scrollTop, scrollLeft: scrollLeft } = document.body;
const bounds = this.refs.wrapper.getBoundingClientRect();
const centerX = this.props.width / 2;
const centerY = this.props.height / 2;
const widthMultiplier = 320 / this.props.width;
const offsetX = 0.52 - (pageX - bounds.left - scrollLeft) / width;
const offsetY = 0.52 - (pageY - bounds.top - scrollTop) / height;
const deltaX = (pageX - bounds.left - scrollLeft) - centerX;
const deltaY = (pageY - bounds.top - scrollTop) - centerY;
const rotateX = (deltaY - offsetY) * (0.08 * widthMultiplier);
const rotateY = (offsetX - deltaX) * (0.04 * widthMultiplier);
const angleRad = Math.atan2(deltaY, deltaX);
const angleRaw = angleRad * 180 / Math.PI - 90;
const angleDeg = angleRaw < 0 ? angleRaw + 360 : angleRaw;
const distanceFromCenter = this.__calculateDistance(bounds, nativeEvent.offsetX, nativeEvent.offsetY);
const shadowMovement = centerY * 0.25;
const shadowSize = 120;
const alpha = this.__calculateAlphaFromCenter(distanceFromCenter);
this.__buildState(rotateX, rotateY, shadowMovement, shadowSize, config.scale, angleDeg, alpha);
}
__handleMouseLeave() {
this.__buildState(0, 0, 20, 50, 1, 0, 0);
}
__renderChildren(children) {
const st = this.state;
if (!Array.isArray(children)) {
const styles = this.__buildTransformStrings(st.rotateX, st.rotateY, st.scale);
console.log(this.__buildTransformStrings(st.rotateX, st.rotateY, st.scale));
return <div style={styles} className='ph-layer'>{children}</div>;
}
return children.map((layer, key) => {
const num = key + 1;
const rotateX = Math.floor(st.rotateX / num);
const rotateY = Math.floor(st.rotateY / num);
let styles = this.__buildTransformStrings(rotateX, rotateY, st.scale);
let textClass;
if (layer.ref === 'text') {
textClass = 'ph-text';
const shadow = {
textShadow: `${rotateY * 0.5}px ${rotateX * 0.5}px 10px rgba(0, 0, 0, 0.5)`
};
styles = Object.assign({}, shadow, styles);
}
return <div style={styles} className={`ph-layer ${textClass}`} key={key}>{layer}</div>;
});
}
render() {
const st = this.state;
const baseTransforms = this.__buildTransformStrings(st.rotateX, st.rotateY, st.scale);
const stylesWrapper = Object.assign({}, baseTransforms, {
width: this.props.width,
height: this.props.height
});
const stylesShadow = Object.assign({}, baseTransforms, {
boxShadow: `0px ${st.shadowMovement}px ${st.shadowSize}px rgba(0, 0, 0, 0.6)`
});
const stylesLighting = Object.assign({}, baseTransforms, {
backgroundImage: `linear-gradient(${st.angle}deg, rgba(255,255,255, ${st.alpha}) 0%, rgba(255,255,255,0) 40%)`
});
return (
<div style={{ transformStyle: 'preserve-3d'}}>
<figure ref='wrapper' className='ph-wrapper' style={stylesWrapper} onMouseMove={this.__handleMouseMove.bind(this)} onMouseLeave={this.__handleMouseLeave.bind(this)}>
<div className='ph-shadow' style={stylesShadow} />
<div className='ph-layers'>
{this.__renderChildren(this.props.children)}
</div>
<div className='ph-lighting' style={stylesLighting} />
</figure>
</div>
);
}
}
const App = React.createClass({
render() {
return (
<div>
<ParallaxHover width='500' height='300'>
<img ref='image' src='http://lorempixel.com/500/300/abstract/' />
<h1 ref='text'>Parallax Hover</h1>
</ParallaxHover>
</div>
);
}
});
const Node = document.getElementById('app');
ReactDOM.render(React.createElement(App), Node);
View Compiled
This Pen doesn't use any external CSS resources.