JIT
Grid - Untitled
Create a box on the screen filled with 50 different, random fruit emojis. The emojis must have physics (matter.js style logic, but written from scratch): they fall, bounce off each other, and stack up. The user can pick them up and throw them with the mouse. Add a 'Mix' button that spins the container like a washing machine.
<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%;
  }
}
</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 &amp; 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>*
Updated files
v1