
고정된 상태로 스크롤할 때 안에서 스탭만 바뀌는 구조
(스크롤 내리면 고정된 상태로 다음 sea&U&I active)
<div class="history-container">
<div class="history-visuals">
<div class="history-visual-item active">
<img src="." alt="">
</div>
<div class="history-visual-item">
<img src="." alt="">
</div>
<div class="history-visual-item">
<img src="." alt="">
</div>
<div class="history-visual-item">
<img src="." alt="">
</div>
<div class="history-visual-item">
<img src="." alt="">
</div>
<div class="history-visual-item">
<img src="." alt="">
</div>
</div>
<div class="history-contents">
<div class="history-steps">
<div class="step active">
<div>vintage love</div>
</div>
<div class="step">
<div>highway love</div>
</div>
<div class="step">
<div>hate my home</div>
</div>
<div class="step">
<div>Still On My Brain</div>
</div>
<div class="step">
<div>miko!</div>
</div>
<div class="step">
<div>Sea&U&I</div>
</div>
</div>
<div class="history-details">
<div class="step active">
<div class="title">vintage<br /> love</div>
</div>
<div class="step">
<div class="title">highway<br /> love</div>
</div>
<div class="step">
<div class="title">hate my<br /> home</div>
</div>
<div class="step">
<div class="title">Still On<br /> My Brain</div>
</div>
<div class="step">
<div class="title">miko!</div>
</div>
<div class="step">
<div class="title">Sea&U&I</div>
</div>
</div>
</div>
</div>
js
class Section {
constructor() {
this.section = document.querySelector('.section');
this.spheres = this.section.querySelectorAll('.history-visuals .history-visual-item');
this.indicators = this.section.querySelectorAll('.history-steps .step');
this.descriptions = this.section.querySelectorAll('.history-details .step');
this.total = this.spheres.length;
this.activeIndex = -1;
this._pending = null; // 예약된 delayedCall 저장
this.init();
}
endPoint(num) {
return () => `+=${window.innerHeight * num}`;
}
clearAllActive() {
this.spheres.forEach(el => el.classList.remove('active'));
this.indicators.forEach(el => el.classList.remove('active'));
this.descriptions.forEach(el => el.classList.remove('active'));
}
setActive(index) {
index = Math.max(0, Math.min(this.total - 1, index));
if (index === this.activeIndex && !this._pending) return;
if (this._pending) this._pending.kill();
this.clearAllActive();
this._pending = gsap.delayedCall(0.25, () => {
this.spheres[index]?.classList.add('active');
this.indicators[index]?.classList.add('active');
this.descriptions[index]?.classList.add('active');
this.activeIndex = index;
this._pending = null;
});
}
init() {
ScrollTrigger.create({
trigger: this.section,
start: 'top top',
end: this.endPoint(3.5),
pin: this.section,
scrub: 1,
invalidateOnRefresh: true,
onUpdate: (self) => {
const index = Math.round(self.progress * (this.total - 1));
this.setActive(index);
}
});
this.setActive(0);
}
}
.history-container {
.history-visuals {
position: absolute;
.history-visual-item {
position: absolute;
opacity: 0;
}
}
.history-contents {
.history-steps {
.step {
opacity: 0.3;
z-index: 0;
transition: opacity 0.3s cubic-bezier(0.45, 0, 0.55, 1);
}
}
.history-details {
position: relative;
.step {
transform: translateX(-30px);
opacity: 0;
position: absolute;
right: 0;
bottom: 0;
z-index: 0;
transition: opacity .25s ease-in-out, transform .25s ease-out;
&.active {
transform: translateX(0);
}
}
}
}
.active{
z-index: 1;
opacity: 1;
pointer-events: auto;
}
}