<!-- https://dribbble.com/shots/7032036-Blowfire -->
<a class="source-link" target="_blank" href="https://dribbble.com/shots/7032036-Blowfire">from dribbble.com</a>
<div class="puzzle">
<svg class="puzzle__svg" width="0" height="0" style="position: absolute">
<defs>
<clipPath id="puzzle-clip-path" clipPathUnits="objectBoundingBox">
<rect class="puzzle__clip-rect" x="0" y="0" width="1" height="1" />
<rect class="puzzle__clip-rect" x="0" y="0" width="1" height="1" />
<rect class="puzzle__clip-rect" x="0" y="0" width="1" height="1" />
</clipPath>
</defs>
</svg>
<div class="puzzle__inner">
<div class="puzzle__images">
<img class="puzzle__image" src="https://images.unsplash.com/photo-1491833485966-73cfb9ccea53?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1200&q=80" alt="">
<img class="puzzle__image" src="https://images.unsplash.com/photo-1566765790386-c43812572bc2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1200&q=80" alt="">
<img class="puzzle__image" src="https://images.unsplash.com/photo-1495360010541-f48722b34f7d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1200&q=80" alt="">
<img class="puzzle__image" src="https://images.unsplash.com/photo-1511275539165-cc46b1ee89bf?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NDF8fGNhdHxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=1200&q=80" alt="">
</div>
</div>
<div class="puzzle_controls">
<button class="puzzle__prev">< prev</button>
<button class="puzzle__next">next ></button>
</div>
</div>
body {
margin: 0;
overflow: hidden;
font-family: sans-serif;
background-color: #011;
}
.source-link {
position: absolute;
top: 20px;
left: 20px;
text-decoration: none;
padding: 8px 12px;
background-color: #fffa;
color: #011;
z-index: 10;
}
.puzzle {
position: relative;
height: 100vh;
overflow: hidden;
}
.puzzle__inner {
height: 100%;
}
.puzzle__images {
position: relative;
display: grid;
height: 100%;
clip-path: url("#puzzle-clip-path");
}
.puzzle__image {
position: absolute;
grid-area: 1 / -1;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
.puzzle_controls {
position: absolute;
left: 20px;
bottom: 20px;
z-index: 10;
}
.puzzle__prev,
.puzzle__next {
font: inherit;
background-color: #fffa;
color: #011;
padding: 8px 12px;
margin-right: 8px;
border: none;
cursor: pointer;
}
import anime from "https://cdn.skypack.dev/animejs@3.2.1";
const lerp = (t, a, b) => (1 - t) * a + t * b;
const everyFrame = (fn) => {
return anime({
duration: Infinity,
easing: 'linear',
update: (anim) => fn(anim.currentTime),
});
};
const puzzle = {
clipBox: { x: 0, y: 0, w: 996, h: 758 },
scale: 0.75,
origin: { x: 1, y: 0.5 },
size: { w: 1, h: 1 },
mouse: {
x: 0,
y: 0,
sx: 0,
sy: 0,
isInside: false
},
pieces: [
{ x: 0, y: 0, width: 1, height: 1 },
{ x: 0, y: 0, width: 1, height: 1 },
{ x: 0, y: 0, width: 1, height: 1 },
],
};
const pazzlesParts = [
[
{ x: 46, y: 71, w: 470, h: 687 },
{ x: 556, y: 0, w: 272, h: 398 },
{ x: 556, y: 456, w: 440, h: 262 }
],
[
{ x: 197, y: 24, w: 272, h: 396 },
{ x: 517, y: 60, w: 479, h: 687 },
{ x: 0, y: 480, w: 469, h: 267 }
],
[
{ x: 82, y: 25, w: 283, h: 718 },
{ x: 405, y: 35, w: 378, h: 147 },
{ x: 405, y: 243, w: 520, h: 472 }
],
[
{ x: 220, y: 21, w: 429, h: 123 },
{ x: 689, y: 44, w: 202, h: 687 },
{ x: 61, y: 205, w: 588, h: 553 }
]
];
function getPieceProps(piece) {
const x = ((piece.x / puzzle.clipBox.w) - puzzle.origin.x) * puzzle.scale + puzzle.origin.x;
const y = ((piece.y / puzzle.clipBox.h) - puzzle.origin.y) * puzzle.scale + puzzle.origin.y;
const width = (piece.w / puzzle.clipBox.w) * puzzle.scale;
const height = (piece.h / puzzle.clipBox.h) * puzzle.scale;
return { x, y, width, height };
}
const puzzleEl = document.querySelector('.puzzle');
const prevBtn = document.querySelector('.puzzle__prev');
const nextBtn = document.querySelector('.puzzle__next');
const puzzleImages = puzzleEl.querySelectorAll('.puzzle__image');
const puzzleInner = puzzleEl.querySelector('.puzzle__inner');
const puzzleClips = puzzleEl.querySelectorAll('.puzzle__clip-rect');
function onResize() {
puzzle.size.w = puzzleEl.clientWidth;
puzzle.size.h = puzzleEl.clientHeight;
}
window.addEventListener('resize', onResize);
onResize();
let currentIdx = 0;
let pieceSpin = 0;
function setCurrentSlide(currentIdx, nextIdx, dir) {
const prevImage = puzzleImages[currentIdx];
const nextImage = puzzleImages[nextIdx];
anime.set(prevImage, { zIndex: -1 });
anime.set(nextImage, { zIndex: 1, opacity: 1 });
const parts = pazzlesParts[nextIdx];
pieceSpin -= 1;
const tl = anime.timeline();
puzzle.pieces.forEach((targetPiece, i) => {
const shift = pieceSpin % parts.length;
const piece = parts[(i + shift + parts.length) % parts.length];
tl.add({
targets: targetPiece,
duration: 500,
easing: 'easeInOutQuad',
getPieceProps(piece),
}, 0);
});
const translateFrom = dir === 1 ? '-100%' : '100%';
tl.add({
targets: nextImage,
duration: 500,
easing: 'easeOutQuad',
translateX: [translateFrom, 0],
complete() {
anime.set(prevImage, { zIndex: -2, opacity: 0 });
}
});
}
puzzleImages.forEach((img, i) => {
anime.set(img, {
opacity: i === currentIdx ? 1 : 0,
zIndex: i === currentIdx ? 1 : -2
});
});
pazzlesParts[currentIdx].forEach((piece, i) => {
const props = getPieceProps(piece);
Object.assign(puzzle.pieces[i], props);
});
puzzleClips.forEach((rectEl, i) => {
anime.set(rectEl, puzzle.pieces[i]);
});
puzzle.mouse.x = puzzle.size.w * ((0.5 - puzzle.origin.x) * puzzle.scale + puzzle.origin.x);
puzzle.mouse.y = puzzle.size.h * ((0.5 - puzzle.origin.y) * puzzle.scale + puzzle.origin.y);
puzzle.mouse.sx = puzzle.mouse.x;
puzzle.mouse.sy = puzzle.mouse.y;
prevBtn.addEventListener('click', () => {
const nextIdx = (currentIdx - 1 + puzzleImages.length) % puzzleImages.length;
setCurrentSlide(currentIdx, nextIdx, -1);
currentIdx = nextIdx;
});
nextBtn.addEventListener('click', () => {
const nextIdx = (currentIdx + 1) % puzzleImages.length;
setCurrentSlide(currentIdx, nextIdx, 1);
currentIdx = nextIdx;
});
puzzleInner.addEventListener('mousemove', (event) => {
puzzle.mouse.x = event.clientX;
puzzle.mouse.y = event.clientY;
puzzle.mouse.isInside = true;
});
puzzleInner.addEventListener('mouseleave', (event) => {
puzzle.mouse.isInside = false;
});
everyFrame((time) => {
const x = puzzle.mouse.isInside
? puzzle.mouse.x
: puzzle.size.w * ((0.5 - puzzle.origin.x) * puzzle.scale + puzzle.origin.x);
const y = puzzle.mouse.isInside
? puzzle.mouse.y
: puzzle.size.h * ((0.5 - puzzle.origin.y) * puzzle.scale + puzzle.origin.y);
puzzle.mouse.sx = lerp(0.05, puzzle.mouse.sx, x);
puzzle.mouse.sy = lerp(0.05, puzzle.mouse.sy, y);
const mx = puzzle.mouse.sx / puzzle.size.w - (0.5 - puzzle.origin.x) * puzzle.scale - puzzle.origin.x;
const my = puzzle.mouse.sy / puzzle.size.h - (0.5 - puzzle.origin.y) * puzzle.scale - puzzle.origin.y;
puzzleClips.forEach((rectEl, i) => {
const piece = puzzle.pieces[i];
anime.set(rectEl, {
x: piece.x + mx,
y: piece.y + my,
width: piece.width,
height: piece.height
});
});
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.