<section class="demo">
<header class="demo__head">
<span>Drag to explore</span>
<span class="demo__hint">← →</span>
</header>
<div class="demo__viewport">
<div class="demo__track" id="track">
<article class="demo__slide" data-tone="a"><b>01</b><span>Aurora</span></article>
<article class="demo__slide" data-tone="b"><b>02</b><span>Basalt</span></article>
<article class="demo__slide" data-tone="c"><b>03</b><span>Cobalt</span></article>
<article class="demo__slide" data-tone="d"><b>04</b><span>Dune</span></article>
<article class="demo__slide" data-tone="e"><b>05</b><span>Ember</span></article>
<article class="demo__slide" data-tone="f"><b>06</b><span>Frost</span></article>
</div>
</div>
</section>.demo {
min-height: 100dvh;
display: flex;
flex-direction: column;
justify-content: center;
gap: 22px;
background: #08080a;
color: #f4f4f2;
font-family: "Helvetica Neue", Inter, system-ui, sans-serif;
overflow: hidden;
}
.demo__head {
display: flex;
justify-content: space-between;
padding: 0 24px;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #6b6b70;
}
.demo__hint { color: #3f3f46; }
.demo__viewport {
overflow: hidden;
cursor: grab;
}
.demo__viewport:active { cursor: grabbing; }
.demo__track {
display: flex;
gap: 14px;
padding: 0 24px;
width: max-content;
will-change: transform;
}
.demo__slide {
flex: 0 0 168px;
height: 232px;
border-radius: 18px;
padding: 18px;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid rgba(255, 255, 255, 0.07);
transform-origin: center center;
will-change: transform;
user-select: none;
}
.demo__slide b {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2em;
color: rgba(255, 255, 255, 0.5);
}
.demo__slide span {
font-size: 19px;
letter-spacing: -0.02em;
}
.demo__slide[data-tone="a"] { background: linear-gradient(160deg, #2b2f4a, #14161f); }
.demo__slide[data-tone="b"] { background: linear-gradient(160deg, #23262a, #101113); }
.demo__slide[data-tone="c"] { background: linear-gradient(160deg, #1b3350, #0c1420); }
.demo__slide[data-tone="d"] { background: linear-gradient(160deg, #3a3123, #17130d); }
.demo__slide[data-tone="e"] { background: linear-gradient(160deg, #4a2422, #1d0f0e); }
.demo__slide[data-tone="f"] { background: linear-gradient(160deg, #26404a, #0f181d); }gsap.registerPlugin(Draggable);
const track = document.querySelector("#track");
const slides = gsap.utils.toArray(".demo__slide");
const maxDrag = () => Math.min(0, window.innerWidth - track.scrollWidth - 24);
// 속도 → 기울기. quickTo로 프레임마다 새 트윈을 만들지 않습니다.
const skewTo = slides.map((slide) => gsap.quickTo(slide, "skewX", { duration: 0.5, ease: "power3" }));
const clamp = gsap.utils.clamp(-12, 12);
let lastX = 0;
let lastTime = 0;
function applyVelocitySkew(currentX) {
const now = performance.now();
const dt = now - lastTime || 16;
const velocity = ((currentX - lastX) / dt) * 16;
lastX = currentX;
lastTime = now;
const skew = clamp(velocity * -0.35);
skewTo.forEach((set) => set(skew));
}
Draggable.create(track, {
type: "x",
edgeResistance: 0.9,
dragResistance: 0.05,
bounds: { minX: maxDrag(), maxX: 0 },
onDrag() { applyVelocitySkew(this.x); },
onDragEnd() {
// InertiaPlugin 없이 만드는 관성: 마지막 속도만큼 더 흘려보냅니다.
const throwX = gsap.utils.clamp(maxDrag(), 0, this.x + (this.x - lastX) * 14);
gsap.to(track, { x: throwX, duration: 1.1, ease: "power3.out" });
skewTo.forEach((set) => set(0));
},
});
window.addEventListener("resize", () => {
Draggable.get(track).applyBounds({ minX: maxDrag(), maxX: 0 });
});
gsap.from(slides, { y: 26, opacity: 0, duration: 0.9, ease: "expo.out", stagger: 0.06 });