-
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
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.