JIT
Grid - Untitled
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Fruit Physics Mixer</title> <style> body { margin: 0; padding: 0; width: 100vw; height: 100vh; background-color: #2c3e50; display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; overflow: hidden; }
    #game-container {
        position: relative;
        width: 90%;
        height: 80%;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    canvas {
        box-shadow: 0 10px 30px rgba(0,0,0,0.3);
        background: #ecf0f1;
        border-radius: 8px;
        cursor: grab;
    }

    canvas:active {
        cursor: grabbing;
    }

    .controls {
        margin-top: 20px;
        height: 10%;
        display: flex;
        align-items: center;
    }

    button {
        background-color: #e74c3c;
        color: white;
        border: none;
        padding: 15px 40px;
        font-size: 1.5rem;
        font-weight: bold;
        border-radius: 50px;
        cursor: pointer;
        box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4);
        transition: transform 0.1s, background-color 0.2s;
        text-transform: uppercase;
        letter-spacing: 1px;
    }

    button:hover {
        background-color: #c0392b;
        transform: scale(1.05);
    }

    button:active {
        transform: scale(0.95);
    }
</style>
</head> <body>
<div id="game-container">
    <canvas id="simulation"></canvas>
</div>

<div class="controls">
    <button id="mix-btn">Mix It!</button>
</div>

<script>
    // --- Configuration ---
    const GRAVITY = 0.4;
    const FRICTION = 0.99;
    const WALL_BOUNCE = 0.5; // Low bounce for stacking
    const OBJ_BOUNCE = 0.3;
    const SUB_STEPS = 8; // Physics iterations per frame for stability
    const FRUIT_SIZE = 24; // Radius
    const FRUITS = [
        '🍎','🍐','🍊','🍋','🍌','🍉','🍇','🍓','🫐','🍈',
        '🍒','🍑','🥭','🍍','🥥','🥝','🍅','🥑','🍏','🍆'
    ];

    // --- Setup Canvas ---
    const canvas = document.getElementById('simulation');
    const ctx = canvas.getContext('2d');
    let width, height;

    // Box dimensions
    let boxSize = { w: 0, h: 0 };
    let boxAngle = 0;
    let targetBoxAngle = 0;
    let boxAngularVel = 0;
    let isMixing = false;

    function resize() {
        const container = document.getElementById('game-container');
        canvas.width = container.clientWidth;
        canvas.height = container.clientHeight;
        width = canvas.width;
        height = canvas.height;
        
        // Box size relative to canvas
        boxSize.w = Math.min(width, height) * 0.8;
        boxSize.h = Math.min(width, height) * 0.8;
    }
    window.addEventListener('resize', resize);
    resize();

    // --- Vector Math Helpers ---
    const Vec2 = {
        add: (v1, v2) => ({ x: v1.x + v2.x, y: v1.y + v2.y }),
        sub: (v1, v2) => ({ x: v1.x - v2.x, y: v1.y - v2.y }),
        mul: (v, s) => ({ x: v.x * s, y: v.y * s }),
        dot: (v1, v2) => v1.x * v2.x + v1.y * v2.y,
        len: (v) => Math.sqrt(v.x * v.x + v.y * v.y),
        norm: (v) => {
            const l = Math.sqrt(v.x * v.x + v.y * v.y);
            return l === 0 ? { x: 0, y: 0 } : { x: v.x / l, y: v.y / l };
        },
        rotate: (v, angle) => {
            const c = Math.cos(angle);
            const s = Math.sin(angle);
            return { x: v.x * c - v.y * s, y: v.x * s + v.y * c };
        },
        cross: (v1, v2) => v1.x * v2.y - v1.y * v2.x // 2D cross product returns scalar
    };

    // --- Physics Object ---
    class Ball {
        constructor(x, y, emoji) {
            this.pos = { x: x, y: y };
            this.oldPos = { x: x, y: y }; // For Verlet-ish throwing calculation
            this.vel = { x: (Math.random() - 0.5) * 10, y: (Math.random() - 0.5) * 10 };
            this.radius = FRUIT_SIZE;
            this.emoji = emoji;
            this.mass = 1;
            this.angle = Math.random() * Math.PI * 2;
            this.angularVel = 0;
        }

        update() {
            // Apply Gravity
            this.vel.y += GRAVITY;
            
            // Apply Friction
            this.vel.x *= FRICTION;
            this.vel.y *= FRICTION;

            // Update Position
            this.pos = Vec2.add(this.pos, this.vel);
            
            // Update Rotation
            this.angle += this.angularVel;
            this.angularVel *= 0.95; // Angular friction
        }
    }

    // --- State ---
    const balls = [];
    
    // Create Balls
    for (let i = 0; i < 50; i++) {
        balls.push(new Ball(
            width/2 + (Math.random() - 0.5) * 100, 
            height/2 + (Math.random() - 0.5) * 100, 
            FRUITS[Math.floor(Math.random() * FRUITS.length)]
        ));
    }

    // Mouse Interaction
    const mouse = { x: 0, y: 0, isDown: false, selectedBall: null };

    canvas.addEventListener('mousedown', (e) => {
        const rect = canvas.getBoundingClientRect();
        mouse.x = e.clientX - rect.left;
        mouse.y = e.clientY - rect.top;
        mouse.isDown = true;

        // Find clicked ball
        for (let b of balls) {
            const d = Vec2.len(Vec2.sub(mouse, b.pos));
            if (d < b.radius * 1.5) {
                mouse.selectedBall = b;
                break;
            }
        }
    });

    window.addEventListener('mousemove', (e) => {
        const rect = canvas.getBoundingClientRect();
        mouse.x = e.clientX - rect.left;
        mouse.y = e.clientY - rect.top;
    });

    window.addEventListener('mouseup', () => {
        mouse.isDown = false;
        mouse.selectedBall = null;
    });

    // --- Physics Engine ---

    function resolveCollisions() {
        // 1. Ball to Ball
        for (let i = 0; i < balls.length; i++) {
            for (let j = i + 1; j < balls.length; j++) {
                const b1 = balls[i];
                const b2 = balls[j];
                
                const distVec = Vec2.sub(b1.pos, b2.pos);
                const dist = Vec2.len(distVec);
                const minDist = b1.radius + b2.radius;

                if (dist < minDist && dist > 0) {
                    const overlap = minDist - dist;
                    const n = Vec2.norm(distVec);

                    // Separate (Positional Correction)
                    const correction = Vec2.mul(n, overlap * 0.5);
                    b1.pos = Vec2.add(b1.pos, correction);
                    b2.pos = Vec2.sub(b2.pos, correction);

                    // Bounce (Velocity Resolution)
                    const relVel = Vec2.sub(b1.vel, b2.vel);
                    const velAlongNormal = Vec2.dot(relVel, n);

                    if (velAlongNormal < 0) {
                        const j = -(1 + OBJ_BOUNCE) * velAlongNormal;
                        const impulse = Vec2.mul(n, j * 0.5); // Assume equal mass
                        b1.vel = Vec2.add(b1.vel, impulse);
                        b2.vel = Vec2.sub(b2.vel, impulse);
                        
                        // Add some random spin on collision
                        const tangent = { x: -n.y, y: n.x };
                        const vDotT = Vec2.dot(relVel, tangent);
                        b1.angularVel += vDotT * 0.01;
                        b2.angularVel -= vDotT * 0.01;
                    }
                }
            }
        }

        // 2. Ball to Rotating Box
        const cx = width / 2;
        const cy = height / 2;
        const hw = boxSize.w / 2;
        const hh = boxSize.h / 2;

        // Precalculate box rotation trig
        // We rotate the WORLD into the BOX's local space (inverse rotation)
        const cos = Math.cos(-boxAngle);
        const sin = Math.sin(-boxAngle);

        for (let b of balls) {
            // Translate to center relative
            const dx = b.pos.x - cx;
            const dy = b.pos.y - cy;

            // Rotate to local space
            const localX = dx * cos - dy * sin;
            const localY = dx * sin + dy * cos;

            // Check bounds in local space
            let collission = false;
            let normalLocal = { x: 0, y: 0 };
            let pen = 0;

            // Check Right Wall
            if (localX > hw - b.radius) {
                normalLocal = { x: -1, y: 0 };
                pen = localX - (hw - b.radius);
                collission = true;
            }
            // Check Left Wall
            else if (localX < -hw + b.radius) {
                normalLocal = { x: 1, y: 0 };
                pen = (-hw + b.radius) - localX;
                collission = true;
            }
            // Check Bottom Wall
            else if (localY > hh - b.radius) {
                normalLocal = { x: 0, y: -1 };
                pen = localY - (hh - b.radius);
                collission = true;
            }
            // Check Top Wall
            else if (localY < -hh + b.radius) {
                normalLocal = { x: 0, y: 1 };
                pen = (-hh + b.radius) - localY;
                collission = true;
            }

            if (collission) {
                // Transform normal back to world space
                // Rotation of box is +boxAngle
                const worldNormal = Vec2.rotate(normalLocal, boxAngle);

                // 1. Positional Correction
                const correction = Vec2.mul(worldNormal, pen);
                b.pos = Vec2.add(b.pos, correction);

                // 2. Velocity Resolution with Moving Wall
                // Wall Velocity at impact point: V = omega x r
                // r is vector from center to ball
                const r = { x: b.pos.x - cx, y: b.pos.y - cy };
                const wallVel = {
                    x: -boxAngularVel * r.y,
                    y: boxAngularVel * r.x
                };

                const relVel = Vec2.sub(b.vel, wallVel);
                const velAlongNormal = Vec2.dot(relVel, worldNormal);

                if (velAlongNormal < 0) {
                    const j = -(1 + WALL_BOUNCE) * velAlongNormal;
                    const impulse = Vec2.mul(worldNormal, j);
                    b.vel = Vec2.add(b.vel, impulse);
                    
                    // Apply friction from wall spin
                    // Tangent vector
                    let tangent = { x: -worldNormal.y, y: worldNormal.x };
                    // Ensure tangent opposes velocity
                    if (Vec2.dot(relVel, tangent) > 0) tangent = Vec2.mul(tangent, -1);
                    
                    // Friction impulse
                    const jt = -Vec2.dot(relVel, tangent) * 0.1; // 0.1 friction coef
                    b.vel = Vec2.add(b.vel, Vec2.mul(tangent, jt));
                }
            }
        }
    }

    // --- Main Loop ---

    function update() {
        // 1. Update Box Logic
        if (isMixing) {
            boxAngularVel += 0.002;
            if (boxAngularVel > 0.15) boxAngularVel = 0.15;
        } else {
            // Dampen to stop
            boxAngularVel *= 0.95;
            // Spring back to 0 angle
            if (Math.abs(boxAngularVel) < 0.001) {
                // Simple P-controller to snap back
                let diff = -boxAngle;
                // normalize diff to -PI to PI
                while (diff > Math.PI) diff -= Math.PI*2;
                while (diff < -Math.PI) diff += Math.PI*2;
                
                boxAngularVel += diff * 0.01;
                boxAngularVel *= 0.9; // damping
            }
        }
        boxAngle += boxAngularVel;

        // 2. Sub-step Physics
        const dt = 1 / SUB_STEPS;
        for (let step = 0; step < SUB_STEPS; step++) {
            
            balls.forEach(b => {
                // Dragging overrides physics
                if (b === mouse.selectedBall) {
                    // Spring force to mouse
                    const diff = Vec2.sub(mouse, b.pos);
                    b.vel = Vec2.mul(diff, 0.2); // Strong spring
                    // We don't update position via velocity here, we let the velocity integration handle it naturally 
                    // or just hard set it. For better collisions while dragging, let's use force.
                }
                
                // Standard update (Gravity + Move)
                // We scale gravity by dt for sub-stepping correctness? 
                // Simple approach: Just apply partial gravity
                b.vel.y += GRAVITY * dt; 
                b.vel.x *= Math.pow(FRICTION, dt);
                b.vel.y *= Math.pow(FRICTION, dt);
                
                b.pos.x += b.vel.x * dt;
                b.pos.y += b.vel.y * dt;
                
                b.angle += b.angularVel * dt;
                b.angularVel *= 0.99;
            });

            resolveCollisions();
        }

        // 3. Keep objects vaguely inside if they glitch out
        balls.forEach(b => {
             const dist = Vec2.len(Vec2.sub(b.pos, {x: width/2, y: height/2}));
             if (dist > Math.max(width, height)) {
                 b.pos = {x: width/2, y: height/2};
                 b.vel = {x: 0, y: 0};
             }
        });
    }

    function draw() {
        // Clear
        ctx.fillStyle = '#ecf0f1';
        ctx.fillRect(0, 0, width, height);

        const cx = width / 2;
        const cy = height / 2;

        // Draw Box
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(boxAngle);
        ctx.lineWidth = 10;
        ctx.strokeStyle = '#34495e';
        ctx.strokeRect(-boxSize.w/2, -boxSize.h/2, boxSize.w, boxSize.h);
        
        // Draw "glass" effect
        ctx.fillStyle = 'rgba(52, 152, 219, 0.1)';
        ctx.fillRect(-boxSize.w/2, -boxSize.h/2, boxSize.w, boxSize.h);
        ctx.restore();

        // Draw Balls
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.font = `${FRUIT_SIZE * 1.8}px serif`; // Size of emoji

        balls.forEach(b => {
            ctx.save();
            ctx.translate(b.pos.x, b.pos.y);
            ctx.rotate(b.angle);
            ctx.fillText(b.emoji, 0, 5); // slight offset for emoji baseline
            
            // Debug Circle
            // ctx.beginPath();
            // ctx.arc(0,0, b.radius, 0, Math.PI*2);
            // ctx.strokeStyle = 'rgba(0,0,0,0.1)';
            // ctx.stroke();
            
            ctx.restore();
        });

        // Draw Drag Line
        if (mouse.isDown && mouse.selectedBall) {
            ctx.beginPath();
            ctx.moveTo(mouse.selectedBall.pos.x, mouse.selectedBall.pos.y);
            ctx.lineTo(mouse.x, mouse.y);
            ctx.strokeStyle = 'rgba(231, 76, 60, 0.5)';
            ctx.lineWidth = 2;
            ctx.stroke();
        }
    }

    function loop() {
        update();
        draw();
        requestAnimationFrame(loop);
    }

    // --- Controls ---
    const mixBtn = document.getElementById('mix-btn');
    mixBtn.addEventListener('mousedown', () => {
        isMixing = true;
        mixBtn.style.backgroundColor = '#c0392b';
        mixBtn.innerText = "Spinning!";
    });

    window.addEventListener('mouseup', () => {
        if(isMixing) {
            isMixing = false;
            mixBtn.style.backgroundColor = '#e74c3c';
            mixBtn.innerText = "Mix It!";
        }
    });

    // Start
    loop();

</script>
</body> </html>
Updated files
v1
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.