DESIGN IS THE
MECHANISM...
THAT ELEVATES OR BURIES
THE MESSAGE.
An interactive, scroll-synchronized motion experiment featuring vector path wipes, kinetic typography transitions, and real-time camera tracking that follows a drawing path through 3D-like zoom space.
This experiment explores a scroll-synchronized kinetic storytelling technique where user scrolling controls both vector path trimming and virtual camera movement:
<div class="dan-scroll-follow-line_html">
<div class="dan-scroll-follow-line_body">
<div class="viewport-wrapper" id="scroll-wrapper">
<div class="stage" id="main-stage">
<!-- LAYER 1 (Bottom): Text Group 1 Div -->
<div id="Text_Group_1" class="text-group group-1 layer-text-1">
<p class="anton-line-1 color-dark">DESIGN IS THE</p>
<p class="anton-line-2 color-blue">MECHANISM...</p>
</div>
<!-- LAYER 2: Vector 1 (Black SVG Wipe Line - Default Desktop stroke-width=400) -->
<svg class="svg-layer layer-vector-1" viewBox="0 0 1440 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector_1" transform="translate(-110.239 73.9586)" d="M0 128.955C5.81006 126.747 11.6201 124.539 37.4925 116.276C63.365 108.014 109.124 93.7637 217.471 76.0479C325.819 58.3321 495.368 37.5824 696.481 23.0591C897.595 8.53579 1125.13 0.867647 1270.03 0.0689775C1414.93 -0.729692 1470.28 5.57348 1504.78 10.3731C1568.8 19.2812 1584.27 31.3975 1596.9 42.1092C1621.72 63.1459 1623.14 86.3407 1625.45 104.886C1627.44 120.856 1617.86 132.187 1588.43 167.643C1578.01 180.197 1560.53 190.745 1530.42 206.232C1513.19 215.094 1490.01 221.814 1446.91 230.409C1403.81 239.004 1341 248.202 1204.69 262.388C1068.38 276.573 860.46 295.469 698.195 319.445C535.929 343.422 425.612 371.908 341.287 397.884C256.961 423.861 201.969 446.465 168.54 461.106C100.07 491.092 74.011 525.754 65.1443 540.036C57.3235 552.634 60.4516 568.302 64.2272 583.176C68.5776 600.314 85.9027 611.657 103.576 619.117C108.17 621.056 113.716 621.22 127.287 621.66C140.858 622.1 162.534 622.075 382.487 592.794C602.44 563.513 1020.01 504.976 1247.31 475.143C1474.61 445.309 1498.97 445.952 1520.59 448.083C1560.56 452.022 1592.9 462.997 1619.35 474.766C1660.6 493.118 1671.5 513.434 1679.54 527.461C1687.12 540.661 1684.49 556.042 1678.76 573.373C1666.08 611.764 1620.57 627.362 1571.56 644.494C1530.7 658.773 1455.38 678.234 1360.11 698.575C1264.84 718.915 1151.38 737.971 1035.42 753.298C919.466 768.625 804.451 779.644 675.796 799.736C547.142 819.827 408.333 848.656 311.702 873.562C215.071 898.468 164.824 918.578 135.405 931.1C81.9987 953.832 64.8441 977.407 55.6602 992.2C51.3879 999.081 51.6838 1006.97 51.7916 1014.01C51.91 1021.74 55.6756 1029.35 60.9794 1036.86C67.4915 1046.07 79.9994 1052.03 102.772 1059.35C119.075 1064.59 146.163 1068.96 200.401 1072.66C254.64 1076.36 335.502 1078.25 631.688 1028.65C927.874 979.037 1436.93 877.872 1702.91 825.383C1968.88 772.894 1976.34 772.147 1984.03 771.378" stroke="#1E1E1E" stroke-width="400" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<!-- LAYER 3: zoom and rotate -> position -> follow line (Blue SVG Layer) -->
<svg class="svg-layer layer-blue-line" viewBox="0 0 1440 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="zoom_and_rotate" transform="translate(66.5772 15)">
<g id="position">
<path id="follow_line" d="M0 0C52.5506 44.3702 130.954 87.1636 139.729 162.22C152.662 272.846 64.4045 362.441 100.73 483.495C152.884 657.3 291.427 721.24 466.028 770.664C595.135 807.211 684.652 816.347 809.603 767.443C926.297 721.771 1032.59 681.571 1052.59 557.863C1075.3 417.37 961.363 294.665 829.464 269.732C673.8 240.306 596.423 410.348 609.648 519.924C622.874 629.501 703.664 674.287 718.114 770.456C731.881 862.083 628.109 881.535 628.109 1064.35" stroke="#347DE6" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>
<!-- LAYER 4 (Top): Text Group 2 Div -->
<div id="Text_Group_2" class="text-group group-2 layer-text-2">
<p id="that_elevates_or_buries" class="anton-line-1 color-white">THAT ELEVATES OR BURIES</p>
<p id="the_message_" class="anton-line-2 color-blue">THE MESSAGE.</p>
</div>
</div>
</div><style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.dan-scroll-follow-line_html, .dan-scroll-follow-line_body {
width: 100%;
min-height: 100vh;
background-color: #ffffff;
font-family: 'Anton', sans-serif;
}
/* Outer Pinned Viewport Container */
.viewport-wrapper {
width: 100vw;
height: 100vh;
position: relative;
background-color: #ffffff;
overflow: hidden;
}
/* 1440x1024 Stage */
.stage {
width: 1440px;
height: 1024px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: center center;
}
/* SVG Layer Overlay */
.svg-layer {
position: absolute;
top: 0;
left: 0;
width: 1440px;
height: 1024px;
display: block;
pointer-events: none;
overflow: visible;
}
/* LAYER ORDER MATCHING FIGMA SCREENSHOT */
.layer-text-1 { z-index: 1; }
.layer-vector-1 { z-index: 2; }
.layer-blue-line { z-index: 3; }
.layer-text-2 { z-index: 4; }
/* HTML Text Group Divs */
.text-group {
position: absolute;
left: 95px;
width: 1300px;
user-select: none;
}
/* Text Group 1 Position */
.text-group.group-1 {
top: 344px;
}
/* Text Group 2 Position (Positioned at 450px so blue line tip ends 20px above text) */
.text-group.group-2 {
top: 450px;
}
.anton-line-1 {
font-size: 110px;
line-height: 0.92;
letter-spacing: 0px;
margin: 0;
text-transform: uppercase;
}
.anton-line-2 {
font-size: 180px;
line-height: 0.92;
letter-spacing: 0px;
margin: 10px 0 0 0;
text-transform: uppercase;
}
.color-dark { color: #1E1E1E; }
.color-blue { color: #347DE6; }
.color-white { color: #FFFFFF; }
</style><!-- GSAP 3.15.0 & ScrollTrigger CDN -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script>
// Responsive Stage Scaler: Perfect desktop landscape fit by default, portrait adjustment on mobile
function resizeStage() {
const stage = document.getElementById('main-stage');
const vector1 = document.querySelector('#Vector_1');
const scaleX = window.innerWidth / 1440;
const scaleY = window.innerHeight / 1024;
const isPortrait = window.innerHeight > window.innerWidth;
if (isPortrait) {
// Mobile / Portrait orientation mode
const scale = Math.min(scaleX, scaleY);
stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
if (vector1) vector1.setAttribute('stroke-width', '1200');
} else {
// Desktop / Landscape orientation mode (Original Perfect Fit)
const scale = Math.max(scaleX, scaleY);
stage.style.transform = `translate(-50%, -50%) scale(${scale})`;
if (vector1) vector1.setAttribute('stroke-width', '400');
}
}
window.addEventListener('resize', resizeStage);
resizeStage();
window.addEventListener("load", () => {
gsap.registerPlugin(ScrollTrigger);
const vector1 = document.querySelector("#Vector_1");
const followLine = document.querySelector("#follow_line");
const len1 = vector1.getTotalLength();
const len2 = followLine.getTotalLength();
const isPortrait = window.innerHeight > window.innerWidth;
const scaleX = window.innerWidth / 1440;
const scaleY = window.innerHeight / 1024;
const stageScale = isPortrait ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
// Camera Start Offsets (Calculated so blue line start touches 15px from top edge on mobile screen)
const initialScale = isPortrait ? 1.8 : 1.0;
const startX = isPortrait ? 301 : 0;
const startY = isPortrait ? (497 + (15 - window.innerHeight / 2) / stageScale) : 0;
// Initial Stroke Setup
gsap.set(vector1, {
strokeDasharray: len1,
strokeDashoffset: len1,
opacity: 0,
visibility: "visible"
});
gsap.set(followLine, {
strokeDasharray: len2,
strokeDashoffset: len2,
visibility: "visible"
});
// Camera Pivot Setup
gsap.set("#zoom_and_rotate", {
svgOrigin: "527.748 532.174",
x: 66.577,
y: 15,
scale: initialScale
});
gsap.set("#position", { x: startX, y: startY });
// Text Group 2 Setup
gsap.set("#that_elevates_or_buries", { x: 3, y: -50, opacity: 0 });
gsap.set("#the_message_", { y: -130, opacity: 0 });
const followProgress = { value: 0 };
// Master Timeline
const tl = gsap.timeline({
scrollTrigger: {
trigger: "#scroll-wrapper",
start: "top top",
end: "+=3500", // Responsive scroll length
scrub: 0.8, // Quick scrubbing response
pin: true,
anticipatePin: 1
}
});
// 1. Black Line Wipe (Starts immediately on scroll at t = 0.05s)
tl.to(vector1, { opacity: 1, duration: 0.05 }, 0.05)
.to(vector1, {
strokeDashoffset: 0,
duration: 1.8,
ease: "cubic-bezier(0.877, 0.071, 0.509, 1.001)"
}, 0.05)
.to(".viewport-wrapper", {
backgroundColor: "#1E1E1E",
duration: 1.2,
ease: "power1.inOut"
}, 0.05);
// 2. Blue Line Draw WITH Camera Tip-Tracking
tl.to(followProgress, {
value: 1,
duration: 3.0,
ease: "cubic-bezier(0.5, 0, 0.5, 1)",
onUpdate: () => {
const p = followProgress.value;
const currentLen = p * len2;
// Trim/draw stroke
gsap.set(followLine, { strokeDashoffset: len2 - currentLen });
// Tip Tracking
const tip = followLine.getPointAtLength(currentLen);
const tipX = 527.748 - tip.x;
const tipY = 532.174 - tip.y;
const finalX = -0.495;
const finalY = -637.971;
if (p < 0.22) {
// Smooth transition from top border start position into centered tip tracking
const rawBlend = Math.min(1, Math.max(0, p / 0.22));
const easeBlend = rawBlend * rawBlend * (3 - 2 * rawBlend); // Smoothstep
const blendedX = gsap.utils.interpolate(startX, tipX, easeBlend);
const blendedY = gsap.utils.interpolate(startY, tipY, easeBlend);
gsap.set("#position", { x: blendedX, y: blendedY });
} else if (p < 0.82) {
// Centered tip tracking during main drawing phase
gsap.set("#position", { x: tipX, y: tipY });
} else {
// Smooth lerp blend to final text position
const rawBlend = Math.min(1, Math.max(0, (p - 0.82) / 0.18));
const easeBlend = rawBlend * rawBlend * (3 - 2 * rawBlend); // Smoothstep
const blendedX = gsap.utils.interpolate(tipX, finalX, easeBlend);
const blendedY = gsap.utils.interpolate(tipY, finalY, easeBlend);
gsap.set("#position", { x: blendedX, y: blendedY });
}
}
}, 1.8);
// 3. Zoom Camera Scale
if (isPortrait) {
tl.to("#zoom_and_rotate", { scale: 1.8, duration: 0.4 }, 1.8)
.to("#zoom_and_rotate", { scale: 3.9, duration: 0.8, ease: "power2.inOut" }, 2.2)
.to("#zoom_and_rotate", { scale: 1.9, duration: 1.6, ease: "power2.inOut" }, 3.0);
} else {
tl.to("#zoom_and_rotate", { scale: 1.0, duration: 0.4 }, 1.8)
.to("#zoom_and_rotate", { scale: 1.9, duration: 0.6, ease: "power2.inOut" }, 2.2)
.to("#zoom_and_rotate", { scale: 3.9, duration: 0.8, ease: "power2.inOut" }, 2.8)
.to("#zoom_and_rotate", { scale: 1.9, duration: 1.2, ease: "power2.inOut" }, 3.6);
}
// 4. Text Group 2 Reveal
tl.to("#that_elevates_or_buries", {
y: 0,
opacity: 1,
duration: 0.8,
ease: "cubic-bezier(0.42, 0, 0.004, 0.993)"
}, 4.2);
tl.to("#the_message_", {
y: 0,
opacity: 1,
duration: 0.8,
ease: "cubic-bezier(0.42, 0, 0.004, 0.993)"
}, 4.4);
ScrollTrigger.refresh();
});
</script>