Virtual Playground
Click the ball to make it jump! Select a planet to change gravity.
const clipPaths = {
circle: '',
square: '',
triangle: 'polygon(50% 0%, 0% 100%, 100% 100%)',
star: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
};
/* ββ Gravity calculation ββββββββββββββββββββββββββββββββββββββββ
BASE_GRAVITY is the raw pixel-per-frameΒ² acceleration for Earth.
Each planet's actual gravity = BASE_GRAVITY Γ planet.multiplier.
This matches the original Earth gravity value of 0.5 exactly.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const BASE_GRAVITY = 0.5;
/* Active physics state β starts on Earth */
let currentPlanet = PLANETS.earth;
let gravity = BASE_GRAVITY * currentPlanet.multiplier; // 0.5
const ball = document.getElementById('ball');
const area = document.getElementById('playArea');
const colorPicker = document.getElementById('ballColor');
const shapeSelect = document.getElementById('ballShape');
let x = 180, y = 180, vx = 0, vy = 0;
let bouncing = true;
/* ββ Physics loop βββββββββββββββββββββββββββββββββββββββββββββββ
gravity is re-read from the live variable each frame, so
switching planets takes effect immediately with no restart.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
function update() {
/* Apply gravity to vertical velocity */
vy += gravity;
x += vx;
y += vy;
if (y + 40 > 400) {
y = 400 - 40;
vy *= -0.7;
}
if (y < 0) {
y = 0;
// Bounce away from ceiling
vy = Math.abs(vy) * 0.7;
// Prevent repeated ceiling collisions when gravity is reversed
if (gravity < 0 && vy < 1.5) {
vy = 1.5;
}
}
if (x + 40 > 400) {
x = 400 - 40;
vx *= -0.7;
}
if (x < 0) {
x = 0;
vx *= -0.7;
}
ball.style.left = x + 'px';
ball.style.top = y + 'px';
if (bouncing) requestAnimationFrame(update);
}
/* Click the ball to launch it upward with a random horizontal kick */
ball.addEventListener('click', () => {
vy = -10;
vx = (Math.random() - 0.5) * 10;
});
/* Reset restores position and velocity; planet/color/shape unchanged */
function resetBall() {
x = 180; y = 180; vx = 0; vy = 0;
}
/* ββ Planet selector ββββββββββββββββββββββββββββββββββββββββββββ
Clicking a planet button updates gravity and bounce immediately.
Active button styling reflects the current selection.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
document.querySelectorAll('.planet-btn').forEach(btn => {
btn.addEventListener('click', () => {
const key = btn.dataset.planet;
currentPlanet = PLANETS[key];
/* Recalculate live gravity from multiplier */
gravity = BASE_GRAVITY * currentPlanet.multiplier;
/* Update active highlight */
document.querySelectorAll('.planet-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
/* ββ Ball colour ββββββββββββββββββββββββββββββββββββββββββββββββ
Color picker input fires on every drag β instant update.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
colorPicker.addEventListener('input', () => {
ball.style.background = colorPicker.value;
});
ball.style.background = colorPicker.value;
/* ββ Ball shape βββββββββββββββββββββββββββββββββββββββββββββββββ
CSS clip-path handles triangle and star; border-radius for
circle and square. No canvas needed.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
const CLIP_PATHS = {
circle: '',
square: '',
triangle: 'polygon(50% 0%, 0% 100%, 100% 100%)',
star: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
};
function applyShape(shape) {
ball.style.borderRadius = shape === 'circle' ? '50%' : '0';
ball.style.clipPath = CLIP_PATHS[shape];
}
shapeSelect.addEventListener('change', () => applyShape(shapeSelect.value));
/* Kick off the animation loop */
update();