<html>
<head>
<meta charset="UTF-8" >
<title>Fruit Physics Box</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #fef6ff 0, #ffe9f0 40%, #ffe7c4 100%);
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.app {
width: min(90vw, 1100px);
height: min(80vh, 720px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 1.5vh;
}
.header {
text-align: center;
color: #442236;
text-shadow: 0 1px 0 #fff3, 0 3px 8px #00000022;
}
.header h1 {
font-size: clamp(2rem, 3vw, 2.7rem);
font-weight: 800;
letter-spacing: 0.03em;
margin-bottom: 0.2em;
}
.header p {
font-size: clamp(0.9rem, 1.2vw, 1rem);
opacity: 0.75;
}
.controls {
display: flex;
justify-content: center;
gap: 1rem;
width: 100%;
}
button {
border: none;
border-radius: 999px;
padding: 0.6rem 1.4rem;
font-size: clamp(0.9rem, 1.1vw, 1rem);
font-weight: 600;
letter-spacing: 0.02em;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, #ff6b6b, #ff9f1a);
box-shadow: 0 8px 18px rgba(255, 111, 60, 0.45);
transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(255, 111, 60, 0.55);
filter: brightness(1.05);
}
button:active {
transform: translateY(1px) scale(0.97);
box-shadow: 0 4px 10px rgba(255, 111, 60, 0.35);
filter: brightness(0.98);
}
.world-wrapper {
flex: 1;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem 0;
}
.world {
position: relative;
width: 100%;
height: 100%;
max-height: 600px;
border-radius: 24px;
background: radial-gradient(circle at 20% 0, #ffffff 0, #ffeef7 45%, #ffd8bf 100%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.16);
overflow: hidden;
}
.world-inner {
position: absolute;
inset: 5% 7%;
border-radius: 20px;
background: linear-gradient(180deg, #fffdf8 0, #ffe8bf 90%);
box-shadow:
inset 0 0 0 2px rgba(255, 255, 255, 0.7),
inset 0 12px 25px rgba(255, 255, 255, 0.9),
0 10px 25px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.fruit {
position: absolute;
font-size: min(4vw, 42px);
cursor: grab;
user-select: none;
touch-action: none;
transform-origin: 50% 50%;
pointer-events: auto;
will-change: transform;
}
.fruit.dragging {
cursor: grabbing;
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.22));
z-index: 10;
}
.overlay-label {
position: absolute;
top: 8px;
left: 12px;
font-size: 0.8rem;
color: #c68060;
background: rgba(255, 255, 255, 0.7);
padding: 4px 10px;
border-radius: 999px;
backdrop-filter: blur(4px);
}
@media (max-width: 800px) {
.app {
width: 94vw;
height: 86vh;
}
.world-inner {
inset: 6% 5%;
}
}
.app {
width: min(90vw, 1100px);
height: min(80vh, 720px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 1.5vh;
}
.header {
text-align: center;
color: #442236;
text-shadow: 0 1px 0 #fff3, 0 3px 8px #00000022;
}
.header h1 {
font-size: clamp(2rem, 3vw, 2.7rem);
font-weight: 800;
letter-spacing: 0.03em;
margin-bottom: 0.2em;
}
.header p {
font-size: clamp(0.9rem, 1.2vw, 1rem);
opacity: 0.75;
}
.controls {
display: flex;
justify-content: center;
gap: 1rem;
width: 100%;
}
button {
border: none;
border-radius: 999px;
padding: 0.6rem 1.4rem;
font-size: clamp(0.9rem, 1.1vw, 1rem);
font-weight: 600;
letter-spacing: 0.02em;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, #ff6b6b, #ff9f1a);
box-shadow: 0 8px 18px rgba(255, 111, 60, 0.45);
transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(255, 111, 60, 0.55);
filter: brightness(1.05);
}
button:active {
transform: translateY(1px) scale(0.97);
box-shadow: 0 4px 10px rgba(255, 111, 60, 0.35);
filter: brightness(0.98);
}
.world-wrapper {
flex: 1;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem 0;
}
.world {
position: relative;
width: 100%;
height: 100%;
max-height: 600px;
border-radius: 24px;
background: radial-gradient(circle at 20% 0, #ffffff 0, #ffeef7 45%, #ffd8bf 100%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.16);
overflow: hidden;
}
.world-inner {
position: absolute;
inset: 5% 7%;
border-radius: 20px;
background: linear-gradient(180deg, #fffdf8 0, #ffe8bf 90%);
box-shadow:
inset 0 0 0 2px rgba(255, 255, 255, 0.7),
inset 0 12px 25px rgba(255, 255, 255, 0.9),
0 10px 25px rgba(0, 0, 0, 0.18);
overflow: hidden;
}
.fruit {
position: absolute;
font-size: min(4vw, 42px);
cursor: grab;
user-select: none;
touch-action: none;
transform-origin: 50% 50%;
pointer-events: auto;
will-change: transform;
}
.fruit.dragging {
cursor: grabbing;
filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.22));
z-index: 10;
}
.overlay-label {
position: absolute;
top: 8px;
left: 12px;
font-size: 0.8rem;
color: #c68060;
background: rgba(255, 255, 255, 0.7);
padding: 4px 10px;
border-radius: 999px;
backdrop-filter: blur(4px);
}
@media (max-width: 800px) {
.app {
width: 94vw;
height: 86vh;
}
.world-inner {
inset: 6% 5%;
}
}
</style>
</head>
<body>
<div class="app">
<div class="header">
<h1>Fruit Blender Physics Box</h1>
<p>Pick up fruit, toss it around, then hit Mix to spin the whole box.</p>
</div>
<div class="controls">
<button id="mixButton">Mix</button>
</div>
<div class="world-wrapper">
<div class="world">
<div class="world-inner" id="world">
<div class="overlay-label">50 fruits · Click & drag to throw · Mix to spin</div>
</div>
</div>
</div>
</div>
<script>
const FRUITS = [
"🍎","🍏","🍐","🍊","🍋","🍌","🍉","🍇","🍓","🫐",
"🍈","🍒","🍑","🥭","🍍","🥝","🍅","🥥","🍆","🌽",
"🥕","🌶️","🫑","🥒","🥦","🧄","🧅","🥔","🍄","🥬",
"🍋","🍎","🍏","🍉","🍇","🍓","🍑","🥭","🍍","🍒",
"🍐","🍌","🍊","🥝","🍅","🥥","🍈","🫐","🍇","🍓"
];
const worldEl = document.getElementById("world");
const mixButton = document.getElementById("mixButton");
let bodies = [];
let worldBounds = { x:0, y:0, w:0, h:0 };
let lastTime = null;
const GRAVITY = 1800;
const RESTITUTION = 0.45;
const FRICTION = 0.025;
const AIR_FRICTION = 0.0008;
const MAX_VELOCITY = 3000;
const MIX_DURATION = 1500;
let mixStartTime = null;
let mixDirection = 1;
function updateWorldBounds() {
const rect = worldEl.getBoundingClientRect();
worldBounds = { x:0, y:0, w:rect.width, h:rect.height };
}
function createBodies() {
worldEl.querySelectorAll(".fruit").forEach(n=>n.remove());
bodies = [];
updateWorldBounds();
const count = 50;
for (let i=0;i<count;i++) {
const el = document.createElement("div");
el.className = "fruit";
el.textContent = FRUITS[i % FRUITS.length];
worldEl.appendChild(el);
const rect = el.getBoundingClientRect();
const r = Math.max(rect.width, rect.height) * 0.5;
const x = worldBounds.w * (0.15 + 0.7*Math.random());
const y = worldBounds.h * (0.05 + 0.15*Math.random());
bodies.push({
el,
x, y,
vx: (Math.random()-0.5)*200,
vy: (Math.random()-0.5)*200,
r,
mass: r*r,
angle: 0,
angVel: (Math.random()-0.5)*2,
isDragging: false,
dragOffsetX:0,
dragOffsetY:0,
lastMouseX:0,
lastMouseY:0,
lastMouseVx:0,
lastMouseVy:0
});
}
}
function applyPhysics(dt) {
for (const b of bodies) {
if (b.isDragging) {
b.vx = b.lastMouseVx * 1.2;
b.vy = b.lastMouseVy * 1.2;
b.angVel *= 0.4;
continue;
}
b.vy += GRAVITY * dt;
if (!mixStartTime) {
b.vx -= b.vx * FRICTION;
}
b.vx -= b.vx * AIR_FRICTION;
b.vy -= b.vy * AIR_FRICTION;
b.vx = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, b.vx));
b.vy = Math.max(-MAX_VELOCITY, Math.min(MAX_VELOCITY, b.vy));
b.x += b.vx * dt;
b.y += b.vy * dt;
b.angle += b.angVel * dt;
const left = b.x - b.r;
const right = b.x + b.r;
const top = b.y - b.r;
const bottom = b.y + b.r;
if (left < 0) {
b.x = b.r;
b.vx = -b.vx * RESTITUTION;
b.angVel += b.vy * 0.0005;
}
if (right > worldBounds.w) {
b.x = worldBounds.w - b.r;
b.vx = -b.vx * RESTITUTION;
b.angVel -= b.vy * 0.0005;
}
if (top < 0) {
b.y = b.r;
b.vy = -b.vy * RESTITUTION;
}
if (bottom > worldBounds.h) {
b.y = worldBounds.h - b.r;
if (Math.abs(b.vy) < 25) {
b.vy = 0;
} else {
b.vy = -b.vy * RESTITUTION;
}
b.vx -= b.vx * 0.12;
b.angVel -= b.vx * 0.0006;
}
}
for (let i=0;i<bodies.length;i++) {
for (let j=i+1;j<bodies.length;j++) {
const a = bodies[i];
const b = bodies[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const distSq = dx*dx + dy*dy;
const minDist = a.r + b.r;
if (distSq === 0 || distSq > minDist*minDist) continue;
const dist = Math.sqrt(distSq);
const nx = dx / dist;
const ny = dy / dist;
const overlap = minDist - dist;
const totalMass = a.mass + b.mass;
const ratioA = b.mass / totalMass;
const ratioB = a.mass / totalMass;
a.x -= nx * overlap * ratioA;
a.y -= ny * overlap * ratioA;
b.x += nx * overlap * ratioB;
b.y += ny * overlap * ratioB;
const rvx = b.vx - a.vx;
const rvy = b.vy - a.vy;
const velAlongNormal = rvx*nx + rvy*ny;
if (velAlongNormal > 0) continue;
const e = RESTITUTION;
const jImpulse = -(1+e) * velAlongNormal / (1/a.mass + 1/b.mass);
const impulseX = jImpulse * nx;
const impulseY = jImpulse * ny;
a.vx -= impulseX / a.mass;
a.vy -= impulseY / a.mass;
b.vx += impulseX / b.mass;
b.vy += impulseY / b.mass;
const tangentX = -ny;
const tangentY = nx;
const jt = -(rvx*tangentX + rvy*tangentY) / (1/a.mass + 1/b.mass);
const mu = 0.7;
const jtClamped = Math.max(-mu*jImpulse, Math.min(mu*jImpulse, jt));
a.vx -= (jtClamped * tangentX) / a.mass;
a.vy -= (jtClamped * tangentY) / a.mass;
b.vx += (jtClamped * tangentX) / b.mass;
b.vy += (jtClamped * tangentY) / b.mass;
const angFactor = 0.0009;
a.angVel -= jImpulse * angFactor;
b.angVel += jImpulse * angFactor;
}
}
}
function applyMix(time) {
if (!mixStartTime) return;
const t = (time - mixStartTime) / MIX_DURATION;
if (t >= 1) {
mixStartTime = null;
worldEl.style.transition = "";
worldEl.style.transform = "rotate(0deg)";
return;
}
const intensity = Math.sin(t * Math.PI) * 1.2 + 0.2;
const freq = 18;
const angle = mixDirection * Math.sin(t * Math.PI * freq) * (10 + 15*intensity);
worldEl.style.transform = `rotate(${angle}deg)`;
const centerX = worldBounds.w/2;
const centerY = worldBounds.h/2;
for (const b of bodies) {
const dx = b.x - centerX;
const dy = b.y - centerY;
const dist = Math.sqrt(dx*dx + dy*dy) || 1;
const tangentialX = -dy / dist;
const tangentialY = dx / dist;
const mixForce = 2600 * intensity;
b.vx += tangentialX * mixForce * (1/60);
b.vy += tangentialY * mixForce * (1/60);
}
}
function render() {
for (const b of bodies) {
b.el.style.transform = `translate(${b.x - b.r}px, ${b.y - b.r}px) rotate(${b.angle}rad)`;
}
}
function loop(timestamp) {
if (!lastTime) lastTime = timestamp;
const dt = Math.min((timestamp - lastTime)/1000, 0.033);
lastTime = timestamp;
applyMix(timestamp);
applyPhysics(dt);
render();
requestAnimationFrame(loop);
}
function setupDragging() {
let activeBody = null;
function findBodyFromEvent(e) {
const target = e.target;
if (!target.classList.contains("fruit")) return null;
return bodies.find(b => b.el === target);
}
function pointerDown(e) {
const b = findBodyFromEvent(e);
if (!b) return;
e.preventDefault();
activeBody = b;
b.isDragging = true;
b.el.classList.add("dragging");
const rect = worldEl.getBoundingClientRect();
const clientX = e.clientX ?? (e.touches && e.touches[0].clientX);
const clientY = e.clientY ?? (e.touches && e.touches[0].clientY);
const x = clientX - rect.left;
const y = clientY - rect.top;
b.dragOffsetX = x - b.x;
b.dragOffsetY = y - b.y;
b.lastMouseX = x;
b.lastMouseY = y;
b.lastMouseVx = 0;
b.lastMouseVy = 0;
}
function pointerMove(e) {
if (!activeBody) return;
const rect = worldEl.getBoundingClientRect();
const clientX = e.clientX ?? (e.touches && e.touches[0].clientX);
const clientY = e.clientY ?? (e.touches && e.touches[0].clientY);
const x = clientX - rect.left;
const y = clientY - rect.top;
const dx = x - activeBody.lastMouseX;
const dy = y - activeBody.lastMouseY;
activeBody.lastMouseVx = dx * 60;
activeBody.lastMouseVy = dy * 60;
activeBody.lastMouseX = x;
activeBody.lastMouseY = y;
activeBody.x = x - activeBody.dragOffsetX;
activeBody.y = y - activeBody.dragOffsetY;
}
function pointerUp() {
if (!activeBody) return;
activeBody.isDragging = false;
activeBody.el.classList.remove("dragging");
activeBody = null;
}
worldEl.addEventListener("mousedown", pointerDown);
worldEl.addEventListener("touchstart", pointerDown, {passive:false});
window.addEventListener("mousemove", pointerMove);
window.addEventListener("touchmove", pointerMove, {passive:false});
window.addEventListener("mouseup", pointerUp);
window.addEventListener("touchend", pointerUp);
window.addEventListener("touchcancel", pointerUp);
}
function setupMixButton() {
mixButton.addEventListener("click", () => {
mixDirection *= -1;
mixStartTime = performance.now();
});
}
window.addEventListener("resize", () => {
const oldBounds = {...worldBounds};
updateWorldBounds();
const sx = worldBounds.w / (oldBounds.w || worldBounds.w);
const sy = worldBounds.h / (oldBounds.h || worldBounds.h);
for (const b of bodies) {
b.x *= sx;
b.y *= sy;
}
});
function init() {
createBodies();
setupDragging();
setupMixButton();
requestAnimationFrame(loop);
}
window.addEventListener("load", init);
</script>
</body>
</html>*