-
  const COVERS = [
    "https://static.tildacdn.com/tild3165-6337-4634-b431-383534393735/photo.jpg",
    "https://static.tildacdn.com/tild6531-3436-4638-b664-383361633333/photo.jpg",
    "https://static.tildacdn.com/tild6266-3730-4332-b136-333634616432/029.jpg",
    "https://static.tildacdn.com/tild3231-6138-4032-b332-636338333261/26.png",
    "https://static.tildacdn.com/tild3466-3731-4732-a632-663035393565/Cover_1.jpg",
    "https://static.tildacdn.com/tild3430-3662-4839-a634-393838303135/cover-4_1.jpg",
    "https://static.tildacdn.com/tild6330-3734-4135-b031-623133356634/photo_2021-02-10_15-.jpg",
    "https://static.tildacdn.com/tild3833-3363-4739-a366-633662316163/_1.jpg",
    "https://static.tildacdn.com/tild3263-3136-4238-a637-306366653230/photo.jpg",
    "https://static.tildacdn.com/tild6239-3338-4539-a235-303362663961/2.jpg",
    
    "https://static.tildacdn.com/tild3532-6633-4330-a232-333037346564/-4.jpg",
    
    "https://static.tildacdn.com/tild3339-3262-4165-b839-346432363437/DBir7uqx-kI.jpg",
    
    "https://static.tildacdn.com/tild6462-3463-4665-b432-623966323964/DxsWpPmqt8w.jpg",
    
    "https://static.tildacdn.com/tild3138-3462-4562-b030-353437326165/Velvet.jpg",
    
    "https://static.tildacdn.com/tild6263-6237-4031-a338-323736326636/cover.jpg",
    
    "https://static.tildacdn.com/tild3937-6663-4231-b533-633936633832/zvonkiy-6.jpg",
    
    "https://static.tildacdn.com/tild3261-3030-4134-b838-613336323233/photo.jpg",
    
    "https://static.tildacdn.com/tild3364-6634-4239-a336-646138616162/-.jpg",
    
    "https://static.tildacdn.com/tild6435-3433-4166-b664-343966323630/Collar_White_-_Big_D.png",
    
    "https://static.tildacdn.com/tild3864-6635-4764-b330-623531346534/IMG_8528-2-min.jpg",
    
    "https://static.tildacdn.com/tild6636-3364-4637-a466-396464616365/Vonka-min.png",
    
    "https://static.tildacdn.com/tild3665-6361-4530-b139-613066303738/-min.jpg",
  ]

.boxes
  - const COUNT = 22
  - let b = 0
  while b < COUNT
    .box(style=`--src: url(${COVERS[b]})`)
      span= b + 1
      img(src=COVERS[b])
    - b++
  .controls
    //button.next
      span Previous album
      svg(viewBox="0 0 448 512" width="100" title="Previous Album")
        path(d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z")
    //button.prev
      span Next album
      svg(viewBox="0 0 448 512" width="100" title="Next Album")
        path(d="M424.4 214.7L72.4 6.6C43.8-10.3 0 6.1 0 47.9V464c0 37.5 40.7 60.1 72.4 41.3l352-208c31.4-18.5 31.5-64.1 0-82.6z")
//svg.scroll-icon(viewBox="0 0 24 24")
  path(fill="currentColor" d="M20 6H23L19 2L15 6H18V18H15L19 22L23 18H20V6M9 3.09C11.83 3.57 14 6.04 14 9H9V3.09M14 11V15C14 18.3 11.3 21 8 21S2 18.3 2 15V11H14M7 9H2C2 6.04 4.17 3.57 7 3.09V9Z")
.drag-proxy
View Compiled
*
  box-sizing border-box

:root
  --bg hsl(0, 0%, 10%)
  --min-size 200px

body
  display grid
  place-items center
  min-height 100vh
  padding 0
  margin 0
  overflow-y hidden
  background black

.drag-proxy
  visibility hidden
  position absolute

.controls
  position absolute
  top calc(50% + clamp(var(--min-size), 20vmin, 20vmin))
  left 50%
  transform translate(-50%, -50%) scale(1.5)
  display flex
  justify-content space-between
  min-width var(--min-size)
  height 44px
  width 20vmin
  z-index 300

button
  height 48px
  width 48px
  border-radius 50%
  position absolute
  top 0%
  outline transparent
  cursor pointer
  background none
  appearance none
  border 0
  transition transform 0.1s
  transform translate(0, calc(var(--y, 0)))


  &:before
    border 2px solid hsl(0, 0%, 90%)
    background linear-gradient(hsla(0, 0%, 80%, 0.65), hsl(0, 0%, 0%)) hsl(0, 0%, 0%)
    content ''
    box-sizing border-box
    position absolute
    top 50%
    left 50%
    transform translate(-50%, -50%)
    height 80%
    width 80%
    border-radius 50%

  &:active:before
    background linear-gradient(hsl(0, 0%, 0%), hsla(0, 0%, 0%, 0.0)) hsl(0, 0%, 0%)

  &:nth-of-type(1)
    right 100%

  &:nth-of-type(2)
    left 100%

button span
  position absolute
  width 1px
  height 1px
  padding 0
  margin -1px
  overflow hidden
  clip rect(0, 0, 0, 0)
  white-space nowrap
  border-width 0

button:hover
  --y -5%


button svg
  position absolute
  top 50%
  left 50%
  transform translate(-50%, -50%) rotate(0deg) translate(2%, 0)
  height 30%
  fill hsl(0, 0%, 90%)

button:nth-of-type(1) svg
  transform translate(-50%, -50%) rotate(180deg) translate(2%, 0)

.scroll-icon
  height 30px
  position fixed
  top 1rem
  right 1rem
  color hsl(0, 0%, 90%)
  animation action 4s infinite

@keyframes action
  0%, 25%, 50%, 100%
    transform translate(0, 0)
  12.5%, 37.5%
    transform translate(0, 25%)

.boxes
  height 100vh
  width 100%
  overflow hidden
  position absolute
  transform-style preserve-3d
  perspective 800px
  touch-action none

.box
  transform-style preserve-3d
  position absolute
  top 50%
  left 50%
  height 20vmin
  width 20vmin
  min-height var(--min-size)
  min-width var(--min-size)
  display none

  &:after
    content ''
    position absolute
    top 50%
    left 50%
    height 100%
    width 100%
    background-image var(--src)
    background-size cover
    transform translate(-50%, -50%) rotate(180deg) translate(0, -100%) translate(0, -0.5vmin)
    opacity 0.75

  &:before
    content ''
    position absolute
    top 50%
    left 50%
    height 100%
    width 100%
    background linear-gradient(var(--bg) 50%, transparent)
    transform translate(-50%, -50%) rotate(180deg) translate(0, -100%) translate(0, -0.5vmin) scale(1.01)
    z-index 2


  img
    position absolute
    height 100%
    width 100%
    top 0
    left 0
    object-fit cover


  &:nth-of-type(odd)
    background hsl(90, 80%, 70%)

  &:nth-of-type(even)
    background hsl(90, 80%, 40%)

@supports(-webkit-box-reflect: below)
  .box
    -webkit-box-reflect below 0.5vmin linear-gradient(transparent 0 50%, white 100%)

    &:after
    &:before
      display none
View Compiled
import gsap from 'https://cdn.skypack.dev/gsap'
import ScrollTrigger from 'https://cdn.skypack.dev/gsap/ScrollTrigger'
import Draggable from 'https://cdn.skypack.dev/gsap/Draggable'

gsap.registerPlugin(ScrollTrigger)
gsap.registerPlugin(Draggable)

gsap.set('.box', {
  yPercent: -50,
})

const STAGGER = 0.1
const DURATION = 1
const OFFSET = 0
const BOXES = gsap.utils.toArray('.box')

const LOOP = gsap.timeline({
  paused: true,
  repeat: -1,
  ease: 'none',
})

const SHIFTS = [...BOXES, ...BOXES, ...BOXES]

SHIFTS.forEach((BOX, index) => {
  const BOX_TL = gsap
    .timeline()
    .set(BOX, {
      xPercent: 250,
      rotateY: -50,
      opacity: 0,
      scale: 0.5,
    })
    // Opacity && Scale
    .to(
      BOX,
      {
        opacity: 1,
        scale: 1,
        duration: 0.1,
      },
      0
    )
    .to(
      BOX,
      {
        opacity: 0,
        scale: 0.5,
        duration: 0.1,
      },
      0.9
    )
    // Panning
    .fromTo(
      BOX,
      {
        xPercent: 250,
      },
      {
        xPercent: -350,
        duration: 1,
        immediateRender: false,
        ease: 'power1.inOut',
      },
      0
    )
    // Rotations
    .fromTo(
      BOX,
      {
        rotateY: -50,
      },
      {
        rotateY: 50,
        immediateRender: false,
        duration: 1,
        ease: 'power4.inOut',
      },
      0
    )
    // Scale && Z
    .to(
      BOX,
      {
        z: 100,
        scale: 1.25,
        duration: 0.1,
        repeat: 1,
        yoyo: true,
      },
      0.4
    )
    .fromTo(
      BOX,
      {
        zIndex: 1,
      },
      {
        zIndex: BOXES.length,
        repeat: 1,
        yoyo: true,
        ease: 'none',
        duration: 0.5,
        immediateRender: false,
      },
      0
    )
  LOOP.add(BOX_TL, index * STAGGER)
})

const CYCLE_DURATION = STAGGER * BOXES.length
const START_TIME = CYCLE_DURATION + DURATION * 0.5 + OFFSET

const LOOP_HEAD = gsap.fromTo(
  LOOP,
  {
    totalTime: START_TIME,
  },
  {
    totalTime: `+=${CYCLE_DURATION}`,
    duration: 1,
    ease: 'none',
    repeat: -1,
    paused: true,
  }
)

const PLAYHEAD = {
  position: 0,
}

const POSITION_WRAP = gsap.utils.wrap(0, LOOP_HEAD.duration())

const SCRUB = gsap.to(PLAYHEAD, {
  position: 0,
  onUpdate: () => {
    LOOP_HEAD.totalTime(POSITION_WRAP(PLAYHEAD.position))
  },
  paused: true,
  duration: 0.25,
  ease: 'power3',
})

//let iteration = 0
//const TRIGGER = ScrollTrigger.create({
  
//document.querySelector('.next').addEventListener('click', NEXT)
//document.querySelector('.prev').addEventListener('click', PREV)

// Dragging
// let startX = 0
// let startOffset = 0

// const onPointerMove = e => {
//   e.preventDefault()
//   SCRUB.vars.position = startOffset + (startX - e.pageX) * 0.001
//   SCRUB.invalidate().restart() // same thing as we do in the ScrollTrigger's onUpdate
// }

// const onPointerUp = e => {
//   document.removeEventListener('pointermove', onPointerMove)
//   document.removeEventListener('pointerup', onPointerUp)
//   document.removeEventListener('pointercancel', onPointerUp)
//   scrollToPosition(SCRUB.vars.position)
// }

// // when the user presses on anything except buttons, start a drag...
// document.addEventListener('pointerdown', e => {
//   if (e.target.tagName.toLowerCase() !== 'button') {
//     document.addEventListener('pointermove', onPointerMove)
//     document.addEventListener('pointerup', onPointerUp)
//     document.addEventListener('pointercancel', onPointerUp)
//     startX = e.pageX
//     startOffset = SCRUB.vars.position
//   }
// })

gsap.set('.box', { display: 'block' })

gsap.set('button', {
  z: 200,
})

Draggable.create('.drag-proxy', {
  type: 'x',
  trigger: '.box',
  onPress() {
    this.startOffset = SCRUB.vars.position
  },
  onDrag() {
    SCRUB.vars.position = this.startOffset + (this.startX - this.x) * 0.001
    SCRUB.invalidate().restart() // same thing as we do in the ScrollTrigger's onUpdate
  },
  onDragEnd() {
    //scrollToPosition(SCRUB.vars.position)
  },
})
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.