<div id="app"></div>
<a href="https://www.craftedbygc.com/" target="_blank" class="footer-link">
Inspired By
</a>
<a href="https://twitter.com/NikolayTalanov" target="_blank" class="footer-link footer-link--twitter">
<img src="https://cdn1.iconfinder.com/data/icons/logotypes/32/twitter-128.png">
</a>
*, *:before, *:after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Roboto', Helvetica, Arial, sans-serif;
background: #000;
}
$baseClass: '.distorted-gallery';
$imgHeight: 70vh;
$imgRatio: 1.5;
$transATMS: 800;
$transAT: $transATMS * 1ms;
$hoverAT: 0.3s;
$easing: cubic-bezier(.97,.13,.34,1.15);
// initial settings for inactive images before one of them becomes active
$inactiveX: 170%;
$inactiveY: 0;
$inactiveDepth: -30px; // based on 1000px perspective
$inactiveRotateX: 8deg;
$inactiveRotateY: 35deg;
$inactiveOriginY: 50%;
$inactiveScale: scale(2, 0.7);
#{$baseClass} {
--transition-time: #{$transATMS};
overflow: hidden;
position: relative;
height: 100vh;
perspective: 1000px;
transform-style: preserve-3d;
&__image {
$width: $imgHeight / $imgRatio;
position: absolute;
left: 50%;
top: 50%;
width: $width;
height: $imgHeight;
margin-left: $width / -2;
margin-top: $imgHeight / -2;
object-fit: cover;
object-position: 50% 50%;
opacity: 0;
transition-timing-function: ease-in-out;
will-change: transform, opacity;
&.s--left {
transform-origin: 0 $inactiveOriginY;
transform: rotateX($inactiveRotateX * -1) rotateY($inactiveRotateY * -1) translate3d($inactiveX * -1, $inactiveY * -1, $inactiveDepth) $inactiveScale;
}
&.s--right {
transform-origin: 100% $inactiveOriginY;
transform: rotateX($inactiveRotateX) rotateY($inactiveRotateY) translate3d($inactiveX, $inactiveY, $inactiveDepth) $inactiveScale;
}
&.s--prev {
opacity: 0;
transition: all $transAT * 0.7;
}
&.s--prev-left {
transform-origin: 0 100%;
transform: rotate(-5deg) translate3d(-50%, 30%, 0);
}
&.s--prev-right {
transform-origin: 100% 100%;
transform: rotate(5deg) translate3d(50%, 30%, 0);
}
&.s--active {
opacity: 1;
transform: translate3d(0, 0, 0);
transition: transform $transAT * 0.8 $transAT * 0.2 $easing, opacity $transAT * 0.4 $transAT * 0.2 ease-in;
}
#{$baseClass}.s--no-transition & {
transition: all 0s 0s;
}
}
&__control {
$size: 50px;
z-index: 100;
position: absolute;
left: 50px;
top: 50%;
width: $size;
height: $size;
margin-top: $size/-2;
border-radius: 50%;
background: rgba(255,255,255,0.4);
cursor: pointer;
&:before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 20px;
height: 20px;
margin-left: -10px;
margin-top: -10px;
border: 2px solid #000;
border-bottom: none;
border-right: none;
transform: translateX(5px) rotate(-45deg);
}
&--right {
left: auto;
right: 50px;
&:before {
transform: translateX(-5px) rotate(135deg);
}
}
}
}
.footer-link {
z-index: 100;
position: absolute;
left: 5px;
bottom: 5px;
font-size: 16px;
color: #fff;
img {
width: 32px;
vertical-align: top;
}
&--twitter {
left: auto;
right: 5px;
}
}
View Compiled
class DistortedGallery extends React.PureComponent {
constructor(props) {
super(props);
this.changeTO = null;
this.animatingTO = null;
this.AUTOCHANGE_TIME = 4000;
this.transitionTime = 1000; // default value, updated in DidMount
this.state = {
isAnimating: false,
activeImg: 0,
leftImg: this.getLimitIndex(),
rightImg: 1,
prevImgLeft: -1,
prevImgRight: -1,
noTransition: false,
};
}
componentWillUnmount() {
window.clearTimeout(this.changeTO);
window.clearTimeout(this.animatingTO);
}
componentDidMount() {
this.transitionTime = Number(getComputedStyle(this.galleryRef).getPropertyValue('--transition-time'));
//this.runAutochangeTO();
}
runAutochangeTO = () => {
this.changeTO = setTimeout(() => {
this.changeImages();
this.runAutochangeTO();
}, this.AUTOCHANGE_TIME);
}
runAnimatingTO = () => {
this.animatingTO = setTimeout(() => (
this.setState({ noTransition: true, prevImgLeft: -1, prevImgRight: -1 }, () => {
const layoutTrigger = this.galleryRef.offsetTop;
this.setState({ noTransition: false, isAnimating: false });
})
), this.transitionTime);
}
getLimitIndex = () => this.props.images.length - 1
getImgIndex = (targetIndex) => {
const limitIndex = this.getLimitIndex();
if (targetIndex < 0) return limitIndex;
if (targetIndex > limitIndex) return 0;
return targetIndex;
}
changeImages = (back = false) => {
window.clearTimeout(this.changeTO);
if (this.state.isAnimating) return;
const change = back ? -1 : 1;
this.setState((st) => {
const activeImg = this.getImgIndex(st.activeImg + change);
return {
isAnimating: true,
activeImg: activeImg,
leftImg: this.getImgIndex(activeImg - 1),
rightImg: this.getImgIndex(activeImg + 1),
...(back ? { prevImgRight: st.activeImg } : { prevImgLeft: st.activeImg }),
};
}, this.runAnimatingTO);
}
render() {
return (
<div
className={classNames(
'distorted-gallery',
{ 's--no-transition': this.state.noTransition },
)}
ref={(_gal) => { this.galleryRef = _gal; }}
>
<div className="distorted-gallery__images">
{this.props.images.map((src, i) => (
<img
className={classNames(
'distorted-gallery__image',
{
's--active': i === this.state.activeImg,
's--left': i === this.state.leftImg,
's--right': i === this.state.rightImg,
's--prev': i === this.state.prevImgLeft || i === this.state.prevImgRight,
's--prev-left': i === this.state.prevImgLeft,
's--prev-right': i === this.state.prevImgRight,
},
)}
src={src}
alt={`Cute Cat №${i + 1}`}
/>
))}
</div>
<div
className="distorted-gallery__control"
onClick={() => this.changeImages(true)}
/>
<div
className="distorted-gallery__control distorted-gallery__control--right"
onClick={() => this.changeImages()}
/>
</div>
);
}
}
const NUM_OF_IMAGES = 7;
const images = Array.from(Array(NUM_OF_IMAGES).keys()).map(i => (
`https://s3-us-west-2.amazonaws.com/s.cdpn.io/142996/dis-slider-cat${i + 1}.jpg`
));
ReactDOM.render(<DistortedGallery images={images} />, document.querySelector('#app'));
View Compiled
This Pen doesn't use any external CSS resources.