Section 1
Logo
Slogan
Section 3
<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js"></script>
<div class="section" id="section-1">Section 1</div>
<div class="section" id="section-2">
<div class="box-image-animated">
<div class="triangle triangle-1"></div>
<div class="triangle triangle-2"></div>
<div class="triangle triangle-3"></div>
<div class="red-dot"></div>
</div>
<div class="logo">Logo</div>
<div class="slogan">Slogan</div>
</div>
<div class="section" id="section-3">Section 3</div>.section {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background: #1e1e2f;
position: relative;
overflow: hidden;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.triangle {
background: linear-gradient(to top, #a1c4fd, #c2e9fb);
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
opacity: 0;
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.triangle-1 {
width: 250px;
height: 250px;
z-index: 4;
opacity: 1;
}
.triangle-2 {
width: 350px;
height: 350px;
z-index: 2;
background: #999
}
.triangle-3 {
width: 450px;
height: 450px;
background: yellow;
z-index: 1;
}
.red-dot {
width: 20px;
height: 20px;
background: red;
border-radius: 50%;
position: absolute;
bottom: 240px;
left: 50%;
transform: translateX(-50%);
opacity: 0;
z-index: 3;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
.logo {
font-size: 24px;
color: #fff;
font-weight: bold;
position: absolute;
top: 50px;
left: 50px;
opacity: 0;
z-index: 5;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.slogan {
font-size: 18px;
color: #fff;
font-style: italic;
position: absolute;
top: 200px;
left: 50px;
opacity: 0;
z-index: 6;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
#section-1 {
background-color: #000;
}
#section-3 {
background-color: #333;
}
gsap.registerPlugin(ScrollTrigger);
const section = document.querySelector("#section-2");
const elements = [
'.triangle-2',
'.triangle-3',
'.red-dot',
'.logo',
'.slogan'
];
let scrollStep = 0;
let isScrolling = false;
let isLocked = false;
let touchStartY = 0;
// Ẩn tất cả phần tử ban đầu
gsap.set(elements, { opacity: 0, y: 50 });
// ScrollTrigger để pin section-2
ScrollTrigger.create({
trigger: section,
start: "top top",
end: () => "+=" + window.innerHeight,
pin: true,
pinSpacing: true,
scrub: false,
onEnter: () => {
if (scrollStep < elements.length) lockScroll();
},
onLeave: () => {
if (scrollStep === elements.length) unlockScroll();
},
onLeaveBack: () => {
unlockScroll(); // Rời khỏi section-2 về section-1 thì mở lại scroll
},
onEnterBack: () => {
if (scrollStep > 0) lockScroll();
}
});
// ✅ Xử lý trạng thái scrollStep khi reload
window.addEventListener("load", () => {
const sectionRect = section.getBoundingClientRect();
const isSectionVisible = sectionRect.top <= 0 && sectionRect.bottom >= 0;
if (isSectionVisible) {
const sectionHeight = section.offsetHeight;
const visibleRatio = (window.innerHeight - sectionRect.top) / sectionHeight;
// Ước tính số phần tử nên hiển thị dựa trên % section được nhìn thấy
scrollStep = Math.min(elements.length, Math.floor(visibleRatio * elements.length));
// Hiện đúng số phần tử theo step
for (let i = 0; i < scrollStep; i++) {
gsap.set(elements[i], { opacity: 1, y: 0 });
}
if (scrollStep < elements.length) {
lockScroll(); // nếu chưa hết bước, vẫn khóa
}
}
});
// Cuộn chuột (desktop)
window.addEventListener('wheel', (e) => {
if (!isLocked || isScrolling) return;
const direction = e.deltaY > 0 ? 1 : -1;
handleScroll(direction, e);
}, { passive: false });
// Vuốt tay (mobile)
window.addEventListener('touchstart', (e) => {
touchStartY = e.touches[0].clientY;
}, { passive: true });
window.addEventListener('touchend', (e) => {
if (!isLocked || isScrolling) return;
const deltaY = touchStartY - (e.changedTouches?.[0]?.clientY || 0);
const direction = deltaY > 20 ? 1 : deltaY < -20 ? -1 : 0;
if (direction !== 0) {
handleScroll(direction, e);
}
}, { passive: false });
// Xử lý cuộn từng bước
function handleScroll(direction, e) {
e.preventDefault();
if (isScrolling) return;
const sectionRect = section.getBoundingClientRect();
const isSectionVisible = sectionRect.top <= 0 && sectionRect.bottom >= 0;
if (!isSectionVisible) return;
// Cuộn xuống để hiện từng phần tử
if (direction === 1 && scrollStep < elements.length) {
isScrolling = true;
playStep(scrollStep, () => {
scrollStep++;
isScrolling = false;
if (scrollStep === elements.length) {
unlockScroll();
ScrollTrigger.refresh();
}
});
return;
}
// Cuộn lên để ẩn từng phần tử
if (direction === -1 && scrollStep > 0) {
isScrolling = true;
scrollStep--;
reverseStep(scrollStep, () => {
isScrolling = false;
if (scrollStep === 0) unlockScroll();
});
}
}
// Hiển thị phần tử theo bước
function playStep(index, onComplete) {
const el = elements[index];
if (!el) return onComplete?.();
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.5,
ease: "power2.out",
onComplete
});
}
// Ẩn phần tử theo bước
function reverseStep(index, onComplete) {
const el = elements[index];
if (!el) return onComplete?.();
gsap.to(el, {
opacity: 0,
y: 50,
duration: 0.5,
ease: "power2.in",
onComplete
});
}
// Khóa cuộn
function lockScroll() {
isLocked = true;
document.body.style.overflow = 'hidden';
}
// Mở khóa cuộn
function unlockScroll() {
isLocked = false;
document.body.style.overflow = '';
}
// Đảm bảo không bị kẹt scroll khi reload ngoài section-2
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
const rect = section.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
if (!isVisible) unlockScroll();
}, 100);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/smooth-scrollbar/8.8.4/smooth-scrollbar.min.js"></script>
<div class="scroller">
<section class="orange">
<div class="text">This is some text inside of a div block.</div>
</section>
<section class="black">
<div class="text-wrap">
<div class="panel-text blue-text">Blue</div>
<div class="panel-text red-text">Red</div>
<div class="panel-text orange-text">Orange</div>
<div class="panel-text purple-text">Purple</div>
</div>
<div class="p-wrap">
<div class="panel blue"></div>
<div class="panel red"></div>
<div class="panel orange"></div>
<div class="panel purple"></div>
</div>
</section>
<section class="blue"></section>
</div>.scroller {
height: 100vh;
}
.orange {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
height: 100vh;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
background-color: #753500;
}
.text {
color: #fff;
}
.black {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
height: 100vh;
-webkit-justify-content: space-around;
-ms-flex-pack: distribute;
justify-content: space-around;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
background-color: #070707;
}
.blue {
height: 100vh;
background-color: #00026d;
}
.text-wrap {
position: relative;
overflow: hidden;
width: 450px;
height: 80vh;
}
.panel-text {
position: absolute;
left: 0%;
top: 0%;
right: 0%;
bottom: 0%;
z-index: 1;
width: 100%;
height: 100%;
font-size: 40px;
text-transform: uppercase;
font-weight: 900;
text-align:center;
background-color: #070707;
transform:translateY(100%);
opacity: 0;
}
.panel-text.blue-text {
color: blue;
}
.panel-text.red-text {
color: red;
}
.panel-text.purple-text {
color: purple;
}
.panel-text.orange-text {
color: orange;
}
.p-wrap {
position: relative;
overflow: hidden;
width: 450px;
height: 80vh;
}
.panel {
position: absolute;
left: 0%;
top: 0%;
right: 0%;
bottom: 0%;
z-index: 1;
width: 100%;
height: 100%;
background-image: url("../images/5ed12171d9d512cb2feead83_5.jpg");
background-position: 50% 50%;
background-size: cover;
background-repeat: no-repeat;
}
.panel._2 {
z-index: 1;
background-image: url("../images/5f5a5b3515c4dd0c2c455925_110642301_938622823267359_7859124022958180678_n201.jpg");
}
.panel.blue {
z-index: auto;
}
.panel.red {
z-index: auto;
background-color: red;
background-image: none;
}
.panel.orange {
z-index: auto;
background-color: #cf5d00;
background-image: none;
}
.panel.purple {
z-index: auto;
background-color: #808;
background-image: none;
}
gsap.registerPlugin(ScrollTrigger);
let bodyScrollBar = Scrollbar.init(document.body, {
damping: 0.1,
delegateTo: document,
});
ScrollTrigger.scrollerProxy(".scroller", {
scrollTop(value) {
if (arguments.length) {
bodyScrollBar.scrollTop = value;
}
return bodyScrollBar.scrollTop;
},
});
bodyScrollBar.addListener(ScrollTrigger.update);
gsap.set(".panel", { zIndex: (i, target, targets) => targets.length - i });
var images = gsap.utils.toArray('.panel:not(.purple)');
images.forEach((image, i) => {
var tl = gsap.timeline({
scrollTrigger: {
trigger: "section.black",
scroller: ".scroller",
start: () => "top -" + (window.innerHeight*(i+0.5)),
end: () => "+=" + window.innerHeight,
scrub: true,
toggleActions: "play none reverse none",
invalidateOnRefresh: true,
}
})
tl
.to(image, { height: 0 })
;
});
gsap.set(".panel-text", { zIndex: (i, target, targets) => targets.length - i });
var texts = gsap.utils.toArray('.panel-text');
texts.forEach((text, i) => {
var tl = gsap.timeline({
scrollTrigger: {
trigger: "section.black",
scroller: ".scroller",
start: () => "top -" + (window.innerHeight*i),
end: () => "+=" + window.innerHeight,
scrub: true,
toggleActions: "play none reverse none",
invalidateOnRefresh: true,
}
})
tl
.to(text, { duration: 0.33, opacity: 1, y:"50%" })
.to(text, { duration: 0.33, opacity: 0, y:"0%" }, 0.66)
;
});
ScrollTrigger.create({
trigger: "section.black",
scroller: ".scroller",
scrub: true,
markers: true,
pin: true,
start: () => "top top",
end: () => "+=" + ((images.length + 1) * window.innerHeight),
invalidateOnRefresh: true,
});