Interactive Stress Ball in JS

🛠️ What You’ll Need

  • Basic HTML/CSS/JS chops. If you don’t know how to write <div>s or call document.querySelector, pause here and come back later.
  • GSAP and its plugins:
  • Draggable for the drag‑and‑drop
  • InertiaPlugin for that sweet, natural deceleration
  • A modern browser (because we rely on the CSS :has() selector)

CodePen Demo: https://codepen.io/emilandersson/pen/bNNOYyK

📐 HTML Structure

<div class="radio-input" id="theme">
  <label class="label">
    <input type="radio" checked name="value-radio" value="1" />
    <p class="text">Stress Ball</p>
  </label>
  <label class="label">
    <input type="radio" name="value-radio" value="2" />
    <p class="text">Tennis Ball</p>
  </label>
</div>

<div class="presentation">
  <h1>STRESS BALL</h1>
  <p>Feel all your stress fade away</p>
</div>

<div class="ball"></div>

<div class="hint backdrop">
  Drag and release the ball to make it spin and bounce
</div>
  1. .radio-input: Two options (stress ball vs. tennis ball)
  2. .presentation: Headline & subtext that update via CSS variables
  3. .ball: The draggable object
  4. .hint: A subtle instruction overlay

🎨 CSS – Variables & Themes

:root {
  --ball: url("stress-ball.png");
  --color-1: #854ade;
  --color-2: #62596f;
  --filter: brightness(1.3);

  /* When tennis ball is checked… */
  &:has(#theme [value="2"]:checked) {
    --ball: url("tennis-ball.png");
    --color-1: #5ea132;
    --color-2: #465040;
    --filter: brightness(1);
  }
}
  1. CSS Variables let us swap images and colors on the fly
  2. :has() selector watches the radio buttons—no JS needed to change themes

The rest of the CSS handles layout, mobile responsiveness, the “glassmorphism” for the radio buttons, and a quick squash‑&‑stretch keyframe for impact feedback.

🏃‍♂️ JavaScript – Physics on Demand

1. Initialization

const ball = document.querySelector(".ball");
const tracker = InertiaPlugin.track(ball, "x,y")[0];
let vw = window.innerWidth, vh = window.innerHeight;

gsap.set(ball, {
  xPercent: -50, yPercent: -50,
  x: vw/2, y: vh/2,
  rotation: 0
});
  • Center the ball
  • Start tracking its velocity

2. Make It Draggable

const draggable = new Draggable(ball, {
  bounds: window,
  onPress() {
    gsap.killTweensOf(ball);
    this.update();
    isCurrentlyDragging = true;
  },
  onRelease() { isCurrentlyDragging = false; },
  onDragEnd: animateBounce
});
  • bounds: window keeps the ball onscreen
  • On drag end, we hand off to our bounce routine

3. Bounce Logic

function animateBounce(x="+=0", y="+=0", vx="auto", vy="auto") {
  const vx0 = tracker.get("x"),
        vy0 = tracker.get("y"),
        speed = Math.hypot(vx0, vy0),
        direction = vx0 >= 0 ? 1 : -1,
        angularVel = direction * speed * 0.25;

  // Spin it based on throw speed
  gsap.to(ball, {
    rotation: "+=" + angularVel,
    duration: 2,
    ease: "power2.out",
    overwrite: false
  });

  // Let InertiaPlugin handle the glide‑and‑stop  
  gsap.fromTo(ball, { x, y }, {
    inertia: { x: vx, y: vy },
    onUpdate: checkBounds,
    overwrite: false
  });
}
  • Rotation: Faster throws spin more
  • Inertia: Off we go—friction included in the plugin

4. Edge Detection & Squash

function checkBounds() {
  const r = ball.getBoundingClientRect().width/2;
  let x = gsap.getProperty(ball)("x"),
      y = gsap.getProperty(ball)("y"),
      vx = tracker.get("x"),
      vy = tracker.get("y"),
      hit = false;

  // If we hit vertical or horizontal walls…
  if (x + r > vw || x - r < 0) { vx *= -0.5; hit = true; squash("x", tracker.get("x")); }
  if (y + r > vh || y - r < 0) { vy *= -0.5; hit = true; squash("y", tracker.get("y")); }

  if (hit) {
    // Re‑bounce from corrected position
    animateBounce(
      Math.min(Math.max(x, r), vw - r),
      Math.min(Math.max(y, r), vh - r),
      vx, vy
    );
  }
}

And the squash(axis, velocity) function briefly flattens the ball on impact, then returns it to normal. It’s quick and dirty, but it looks awesome.

5. Bonus: Fling When Cursor Leaves

document.addEventListener("mouseout", (e) => {
  if (e.relatedTarget===null && isCurrentlyDragging) {
    const x=…, y=…, vx=…, vy=…; // grab current state
    draggable.endDrag(e);
    animateBounce(x, y, vx*2, vy*2);
  }
});

If you drag and then drag off the browser window, it treats that like a super‑charged toss. Because why not?

✅ Wrapping Up

  1. HTML sets the stage: balls, labels, hints
  2. CSS handles all the looks and theme swapping with variables + :has()
  3. JS + GSAP brings the ball to life: drag, throw, spin, squash, bounce, repeat

Search

Start typing to see results...