.boxes
- const COUNT = 10
- let b = 0
while b < COUNT
.box= b + 1
- b++
.controls
button.next Prev
button.prev Next
View Compiled
*
box-sizing border-box
body
display grid
place-items center
min-height 100vh
padding 0
margin 0
overflow-y hidden
background hsl(0, 0%, 10%)
.controls
position absolute
top 50%
left 50%
transform translate(-50%, -50%)
z-index 200
display flex
justify-content space-between
width 500px
max-width 90vw
button
height 48px
width 48px
border-radius 50%
.boxes
height 100vh
width 100%
overflow hidden
position absolute
.box
position absolute
top 50%
left 50%
height 25vmin
width 25vmin
display grid
place-items center
font-size 5vmin
font-family sans-serif
transform translate(-50%, -50%)
&:nth-of-type(odd)
background hsl(90, 80%, 70%)
&:nth-of-type(even)
background hsl(90, 80%, 40%)
View Compiled
import gsap from 'https://cdn.skypack.dev/gsap'
import ScrollTrigger from 'https://cdn.skypack.dev/gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const STAGGER = 0.2
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()
.fromTo(
BOX,
{
xPercent: 100,
},
{
xPercent: -200,
duration: 1,
ease: 'none',
immediateRender: false,
}, 0
)
.fromTo(
BOX, {
opacity: 0,
}, {
opacity: 1,
duration: 0.25,
repeat: 1,
repeatDelay: 0.5,
immediateRender: false,
ease: 'none',
yoyo: true,
}, 0)
.fromTo(
BOX,
{
scale: 0,
},
{
scale: 1,
repeat: 1,
zIndex: BOXES.length,
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 END_TIME = START_TIME + CYCLE_DURATION
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({
start: 0,
end: '+=2000',
horizontal: false,
pin: '.boxes',
onUpdate: self => {
const SCROLL = self.scroll()
if (SCROLL > self.end - 1) {
// Go forwards in time
WRAP(1, 1)
} else if (SCROLL < 1 && self.direction < 0) {
// Go backwards in time
WRAP(-1, self.end - 1)
} else {
const NEW_POS = (iteration + self.progress) * LOOP_HEAD.duration()
SCRUB.vars.position = NEW_POS
SCRUB.invalidate().restart()
}
}
})
const WRAP = (iterationDelta, scrollTo) => {
iteration += iterationDelta
TRIGGER.scroll(scrollTo)
TRIGGER.update()
}
const SNAP = gsap.utils.snap(1 / BOXES.length)
const progressToScroll = progress => gsap.utils.clamp(1, TRIGGER.end - 1, gsap.utils.wrap(0, 1, progress) * TRIGGER.end)
const scrollToPosition = position => {
const SNAP_POS = SNAP(position)
const PROGRESS = (SNAP_POS - LOOP_HEAD.duration() * iteration) / LOOP_HEAD.duration()
const SCROLL = progressToScroll(PROGRESS)
if (PROGRESS >= 1 || PROGRESS < 0) return WRAP(Math.floor(PROGRESS), SCROLL)
TRIGGER.scroll(SCROLL)
}
ScrollTrigger.addEventListener('scrollEnd', () => scrollToPosition(SCRUB.vars.position))
const NEXT = () => scrollToPosition(SCRUB.vars.position - (1 / BOXES.length))
const PREV = () => scrollToPosition(SCRUB.vars.position + (1 / BOXES.length))
document.addEventListener('keydown', event => {
if (event.keyCode === 37 || event.keyCode === 65) NEXT()
if (event.keyCode === 39 || event.keyCode === 68) PREV()
})
document.querySelector('.next').addEventListener('click', NEXT)
document.querySelector('.prev').addEventListener('click', PREV)
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.