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 calldocument.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>
.radio-input
: Two options (stress ball vs. tennis ball).presentation
: Headline & subtext that update via CSS variables.ball
: The draggable object.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);
}
}
- CSS Variables let us swap images and colors on the fly
: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
- HTML sets the stage: balls, labels, hints
- CSS handles all the looks and theme swapping with variables +
:has()
- JS + GSAP brings the ball to life: drag, throw, spin, squash, bounce, repeat