We're
Here To
Stick By
You
HTML
<section class="ball-sec">
<!-- TEXT -->
<div class="text-wrap">
<div class="text-row"><span class="text-line text-left">We're</span></div>
<div class="text-row"><span class="text-line text-left">Here To</span></div>
<div class="text-row"><span class="text-line text-right">Stick By</span></div>
<div class="text-row"><span class="text-line text-right">You</span></div>
</div>
<!-- BOTTOM CURVE — single concave-up curve, thin on left → tall on right, bowed downward through the middle -->
<svg class="curve-bg" viewBox="0 0 1600 400" preserveAspectRatio="none">
<path d="M0,400 L0,330 C600,395 1100,290 1600,25 L1600,400 Z" fill="#003900"/>
</svg>
<!-- BALLS (swap src with your tape roll images) -->
<img class="ball ball-1" src="https://sweet-tape.com/wp-content/uploads/2026/05/Group-511.svg" alt="Cloth Tape">
<img class="ball ball-2" src="https://sweet-tape.com/wp-content/uploads/2026/06/Group-645.svg" alt="Masking Tape">
<img class="ball ball-3" src="https://sweet-tape.com/wp-content/uploads/2026/05/Group-403.svg" alt="OPP Tape Normal Brown">
<img class="ball ball-4" src="https://sweet-tape.com/wp-content/uploads/2026/05/Group-424.svg" alt="OPP Tape Low Noise">
<img class="ball ball-5" src="https://sweet-tape.com/wp-content/uploads/2026/06/Group-575.svg" alt="Stationery Tape">
<img class="ball ball-6" src="https://sweet-tape.com/wp-content/uploads/2026/06/Group-643-copy.svg" alt="Double-Sided Normal Tape">
</section>
CSS
.ball-sec {
position: relative;
width: 100%;
height: 150vh;
min-height: 700px;
background: #B6FE00;
overflow: hidden;
font-family: 'Arial Black', Impact, sans-serif;
}
/* ---------- TEXT ---------- */
.text-wrap {
position: relative;
z-index: 1;
padding: 100px 0 0;
pointer-events: none; /* let clicks pass through to balls underneath */
}
.text-row {
width: 100%;
text-align: center; /* centers the inline-block text-line span */
}
.text-line {
display: inline-block; /* sized to text content → measurable bounds */
/* font-size: clamp(3rem, 9vw, 9rem); */
font-weight: 900;
color: #003900;
/* line-height: 0.95; */
text-transform: uppercase;
letter-spacing: -2px;
white-space: nowrap;
font-size: 13.889vw;
line-height: 0.75;
}
/* ---------- CURVE AT BOTTOM ---------- */
.curve-bg {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 42%;
z-index: 0;
pointer-events: none;
display: block;
}
/* ---------- BALLS ---------- */
.ball {
position: absolute;
/* width: clamp(140px, 15vw, 230px);
height: clamp(140px, 15vw, 230px); */
width: clamp(182px, 19.5vw, 300px);
height: clamp(182px, 19.5vw, 300px);
border-radius: 50%;
top: -300px; /* start off-screen above */
z-index: 2;
object-fit: contain;
/* Fallback look in case the img src is missing — remove if you want */
background: rgba(255,255,255,0.15);
}
/* horizontal positions + fallback tints so you can preview without images */
.ball-1 { left: 3%; } /* Cloth Tape */
.ball-2 { left: 19%; } /* Masking Tape */
.ball-3 { left: 34%; } /* OPP Tape Green */
.ball-4 { left: 50%; } /* OPP Tape Low Noise */
.ball-5 { left: 66%;} /* Foam Tape */
.ball-6 { left: 82%; } /* Tissue Tape */
/* Drag cursor only appears once .draggable is added (after balls settle) */
.ball.draggable {
cursor: grab;
-webkit-user-drag: none;
user-select: none;
}
.ball.draggable:active { cursor: grabbing; }
JS
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
<script>
jQuery(document).ready(function( $ ){
/* =============================================================
TEXT ANIMATION (GSAP)
============================================================= */
gsap.set('.text-left', { x: (i, el) => -el.getBoundingClientRect().left });
gsap.set('.text-right', { x: (i, el) => window.innerWidth - el.getBoundingClientRect().right });
const tl = gsap.timeline({ delay: 4 });
tl.to('.text-left', { x: 0, duration: 1.5, ease: 'power2.out' })
.to('.text-right', { x: 0, duration: 1.5, ease: 'power2.out' }, '<')
.call(initPhysics, [], '+=0.25');
/* =============================================================
RESPONSIVE BOTTOM CURVE — one source of truth
- drives BOTH the visual SVG path and the physics floor
- mobile (<=768px) gets a flatter, subtler curve
============================================================= */
const isMobile = window.matchMedia('(max-width: 768px)').matches;
const CURVE = isMobile
// flatter: starts lower, gentle bow, much smaller rise on the right
? { heightPct: 0.28, pts: [ {x:0,y:360}, {x:600,y:375}, {x:1100,y:340}, {x:1600,y:200} ] }
// original desktop curve
: { heightPct: 0.42, pts: [ {x:0,y:330}, {x:600,y:395}, {x:1100,y:290}, {x:1600,y:25 } ] };
// Paint the visual SVG right away from the same points (no flash during intro)
(function applyCurve(){
const p = CURVE.pts;
const d = `M0,400 L0,${p[0].y} C${p[1].x},${p[1].y} ${p[2].x},${p[2].y} ${p[3].x},${p[3].y} L1600,400 Z`;
document.querySelector('.curve-bg path').setAttribute('d', d);
document.querySelector('.curve-bg').style.height = (CURVE.heightPct * 100) + '%';
})();
/* =============================================================
BALL PHYSICS (Matter.js)
============================================================= */
function initPhysics() {
const { Engine, World, Bodies, Body, Runner, Mouse, MouseConstraint } = Matter;
const section = document.querySelector('.ball-sec');
const W = section.clientWidth;
const H = section.clientHeight;
// 30% bigger than original, smaller on mobile
const ballSize = Math.min(230, Math.max(140, W * 0.15)) * (isMobile ? 0.9 : 1.3);
const ballRadius = ballSize / 2;
/* ---------- SVG curve sampling (reads the shared CURVE) ---------- */
const CURVE_H_PCT = CURVE.heightPct;
const CURVE_TOP_Y = 1 - CURVE_H_PCT;
const BEZ = CURVE.pts;
function curvePointAt(t) {
const u = 1 - t;
const b0 = u * u * u, b1 = 3 * u * u * t, b2 = 3 * u * t * t, b3 = t * t * t;
const sx = b0 * BEZ[0].x + b1 * BEZ[1].x + b2 * BEZ[2].x + b3 * BEZ[3].x;
const sy = b0 * BEZ[0].y + b1 * BEZ[1].y + b2 * BEZ[2].y + b3 * BEZ[3].y;
return {
x: (sx / 1600) * W,
y: H * CURVE_TOP_Y + (sy / 400) * (H * CURVE_H_PCT)
};
}
/* ---------- engine ---------- */
const engine = Engine.create();
engine.gravity.y = 1.8;
const world = engine.world;
/* ---------- curve as a chain of static rectangles ---------- */
const SEGS = 60;
for (let i = 0; i < SEGS; i++) {
const p1 = curvePointAt(i / SEGS);
const p2 = curvePointAt((i + 1) / SEGS);
const cx = (p1.x + p2.x) / 2;
const cy = (p1.y + p2.y) / 2;
const len = Math.hypot(p2.x - p1.x, p2.y - p1.y) + 4;
const ang = Math.atan2(p2.y - p1.y, p2.x - p1.x);
World.add(world, Bodies.rectangle(cx, cy, len, 20, {
isStatic: true, angle: ang, friction: 0.5, restitution: 0.6
}));
}
/* ---------- side walls ----------
Right wall pushed further out so balls have room to start
OFF-SCREEN on the right; it still catches any overshoot. */
World.add(world, [
Bodies.rectangle(-100, H / 2, 200, H * 4, { isStatic: true }),
Bodies.rectangle(W + 500, H / 2, 200, H * 4, { isStatic: true })
]);
/* ---------- ball bodies — ENTER FROM THE RIGHT ---------- */
const ballEls = document.querySelectorAll('.ball');
const balls = [];
ballEls.forEach((el, i) => {
// Start just past the right edge, at varied heights
const startX = W + ballRadius + 50;
const startY = H * 0.18 + (Math.random() - 0.5) * 90;
el.style.width = ballSize + 'px';
el.style.height = ballSize + 'px';
el.style.top = '0px';
el.style.left = '0px';
el.style.transform = `translate(${startX - ballRadius}px, ${startY - ballRadius}px)`;
const body = Bodies.circle(startX, startY, ballRadius * 0.96, {
restitution: 0.7,
friction: 0.35,
frictionAir: 0.008,
density: 0.0015,
inertia: Infinity
});
balls.push({ el, body });
// Stream them in one-by-one with a leftward shove.
// Bodies not yet added stay parked off-screen right (tick reads their
// initial position), so they wait their turn without colliding.
setTimeout(() => {
World.add(world, body);
Body.setVelocity(body, { x: isMobile ? -10 : -20, y: 0 }); // slower entry on mobile
}, i * 380); // stagger between balls
});
/* ---------- run the engine ---------- */
Runner.run(Runner.create(), engine);
/* ---------- drag interaction ---------- */
const mouse = Mouse.create(section);
const mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: { stiffness: 0.2, damping: 0.1, render: { visible: false } }
});
mouse.element.removeEventListener('wheel', mouse.mousewheel);
mouse.element.removeEventListener('DOMMouseScroll', mouse.mousewheel);
// Slightly later so the last (staggered) ball has time to settle
setTimeout(() => {
World.add(world, mouseConstraint);
ballEls.forEach(el => el.classList.add('draggable'));
}, 4500);
/* ---------- sync DOM each frame — translate ONLY, never rotate ---------- */
(function tick() {
balls.forEach(({ el, body }) => {
Body.setAngle(body, 0);
Body.setAngularVelocity(body, 0);
el.style.transform =
`translate(${body.position.x - ballRadius}px, ${body.position.y - ballRadius}px)`;
});
requestAnimationFrame(tick);
})();
}
});
</script>




