<!-- 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">&lt; prev</button>
    <button class="puzzle__next">next &gt;</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
    });
  });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.