bike-bear-logobike-bear-logobike-bear-logobike-bear-logo
✕
Published by bikebear on June 30, 2026
Categories
  • GSAP
  • DateJune 30, 2026
We're
Here To
Stick By
You
Cloth Tape Masking Tape OPP Tape Normal Brown OPP Tape Low Noise Stationery Tape Double-Sided Normal Tape

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>

Related posts

June 30, 2026

Flilp Bento Hover Effect


Read more
June 30, 2026

Inertia trail burst


Read more
March 31, 2026

Text Marquee GSAP


Read more
© 2026 Betheme by Muffin group | All Rights Reserved | Powered by WordPress