<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>
<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>