|
import streamlit as st |
|
|
|
def create_animation_app(): |
|
st.title("Bouncing Yellow Balls & Jellyfish in a Rotating Sphere") |
|
|
|
|
|
|
|
|
|
|
|
|
|
html_code = r""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<!-- Include p5.js from a CDN --> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.min.js"></script> |
|
<style> |
|
/* Remove default margins & center the canvas */ |
|
body { |
|
margin: 0; |
|
padding: 0; |
|
overflow: hidden; |
|
background: black; |
|
} |
|
#p5-container { |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="p5-container"></div> |
|
<script> |
|
// --- Global Variables --- |
|
let balls = []; |
|
let jellyfish; |
|
const sphereRadius = 300; // radius of the bounding sphere |
|
let sphereCenter; |
|
let rotationAngle = 0; |
|
const numBalls = 100; |
|
|
|
// --- p5.js Setup --- |
|
function setup() { |
|
// Create an 800x800 canvas and attach it to our container |
|
let canvas = createCanvas(800, 800); |
|
canvas.parent('p5-container'); |
|
sphereCenter = createVector(width/2, height/2); |
|
|
|
// Set color mode to HSB for our jellyfish drawing. |
|
colorMode(HSB, 360, 100, 100, 1); |
|
|
|
// Create 100 balls. Each ball is given a random position (inside the sphere) |
|
// and a random velocity. |
|
for (let i = 0; i < numBalls; i++) { |
|
balls.push(new Ball()); |
|
} |
|
|
|
// Create one jellyfish object. (Its drawn “shape” is produced by the jellyA() and getJellyColor() functions below.) |
|
jellyfish = new Jellyfish(); |
|
} |
|
|
|
// --- p5.js Draw Loop --- |
|
function draw() { |
|
// Create a fade/trail effect by drawing a semi-transparent black background. |
|
// (Note: using HSB mode so black is 0 saturation and 0 brightness.) |
|
background(0, 0, 0, 0.1); |
|
|
|
// Increment our rotation angle very slowly. |
|
rotationAngle += 0.005; |
|
|
|
// All drawing and physics is done in a coordinate system with origin at sphereCenter. |
|
push(); |
|
translate(sphereCenter.x, sphereCenter.y); |
|
// Slowly rotate the entire coordinate system. |
|
rotate(rotationAngle); |
|
|
|
// Draw the bounding sphere (a circle) |
|
noFill(); |
|
stroke(255); |
|
strokeWeight(2); |
|
ellipse(0, 0, sphereRadius * 2, sphereRadius * 2); |
|
|
|
// --- Update Physics --- |
|
// 1. Update positions for all balls and the jellyfish. |
|
for (let ball of balls) { |
|
ball.update(); |
|
} |
|
jellyfish.update(); |
|
|
|
// 2. Check and resolve ball-ball collisions. |
|
for (let i = 0; i < balls.length; i++) { |
|
for (let j = i + 1; j < balls.length; j++) { |
|
balls[i].collide(balls[j]); |
|
} |
|
} |
|
|
|
// 3. Check each ball for collisions with the sphere boundary and display them. |
|
for (let ball of balls) { |
|
ball.checkBoundaryCollision(); |
|
ball.display(); |
|
} |
|
|
|
// 4. Check the jellyfish for sphere collisions and display it. |
|
jellyfish.checkBoundaryCollision(); |
|
jellyfish.display(); |
|
|
|
pop(); |
|
} |
|
|
|
// --- Ball Class --- |
|
class Ball { |
|
constructor() { |
|
this.r = 5; // radius of the ball |
|
// Random position inside the sphere (by picking a random angle and radius) |
|
let angle = random(TWO_PI); |
|
let rad = random(sphereRadius - this.r); |
|
this.pos = createVector(rad * cos(angle), rad * sin(angle)); |
|
// Random speed and direction |
|
let speed = random(1, 3); |
|
let vAngle = random(TWO_PI); |
|
this.vel = createVector(speed * cos(vAngle), speed * sin(vAngle)); |
|
// Yellow color in HSB (hue=60, saturation=100, brightness=100) |
|
this.col = color(60, 100, 100); |
|
} |
|
|
|
update() { |
|
this.pos.add(this.vel); |
|
} |
|
|
|
// Check for collision with the circular (spherical) boundary. |
|
checkBoundaryCollision() { |
|
let d = this.pos.mag(); |
|
if (d + this.r > sphereRadius) { |
|
// Compute the normal (pointing outward) |
|
let normal = this.pos.copy().normalize(); |
|
// Reflect the velocity: v = v - 2*(v·normal)*normal |
|
let dot = this.vel.dot(normal); |
|
this.vel.sub(p5.Vector.mult(normal, 2 * dot)); |
|
// Ensure the ball is repositioned just inside the boundary. |
|
this.pos = normal.mult(sphereRadius - this.r); |
|
} |
|
} |
|
|
|
// Check for collisions with another ball (elastic collision for equal masses) |
|
collide(other) { |
|
let diff = p5.Vector.sub(other.pos, this.pos); |
|
let distBetween = diff.mag(); |
|
if (distBetween < this.r + other.r) { |
|
// Separate the overlapping balls by moving them apart equally. |
|
let overlap = (this.r + other.r) - distBetween; |
|
let displacement = diff.copy().normalize().mult(overlap / 2); |
|
this.pos.sub(displacement); |
|
other.pos.add(displacement); |
|
|
|
// Compute collision response |
|
let normal = diff.copy().normalize(); |
|
let relativeVelocity = p5.Vector.sub(this.vel, other.vel); |
|
let dotProd = relativeVelocity.dot(normal); |
|
// Only resolve if balls are moving toward each other. |
|
if (dotProd > 0) { |
|
// For equal masses, exchange the velocity components along the collision normal. |
|
let impulse = normal.copy().mult(dotProd); |
|
this.vel.sub(impulse); |
|
other.vel.add(impulse); |
|
} |
|
} |
|
} |
|
|
|
display() { |
|
noStroke(); |
|
fill(this.col); |
|
ellipse(this.pos.x, this.pos.y, this.r * 2, this.r * 2); |
|
} |
|
} |
|
|
|
// --- Jellyfish Class --- |
|
class Jellyfish { |
|
constructor() { |
|
// For the jellyfish we choose a larger effective “radius” (for collision) of about 40. |
|
this.size = 40; |
|
// Start at a random position inside the sphere (with a margin) |
|
this.pos = createVector(random(-sphereRadius + this.size, sphereRadius - this.size), |
|
random(-sphereRadius + this.size, sphereRadius - this.size)); |
|
// Give it a random velocity. |
|
let speed = random(1, 2); |
|
let angle = random(TWO_PI); |
|
this.vel = createVector(speed * cos(angle), speed * sin(angle)); |
|
// A time parameter used in the jellyfish drawing. |
|
this.t = 0; |
|
} |
|
|
|
update() { |
|
this.pos.add(this.vel); |
|
this.t += 0.05; |
|
} |
|
|
|
// Bounce off the sphere boundary using a simple circular collision. |
|
checkBoundaryCollision() { |
|
if (this.pos.mag() + this.size > sphereRadius) { |
|
let normal = this.pos.copy().normalize(); |
|
let dot = this.vel.dot(normal); |
|
this.vel.sub(p5.Vector.mult(normal, 2 * dot)); |
|
this.pos = normal.mult(sphereRadius - this.size); |
|
} |
|
} |
|
|
|
display() { |
|
push(); |
|
translate(this.pos.x, this.pos.y); |
|
// Draw the jellyfish using a grid of points computed by the jellyA() function. |
|
// (We subtract 200 from the computed positions so that the drawing is centered.) |
|
strokeWeight(1.5); |
|
for (let y = 99; y < 300; y += 4) { |
|
for (let x = 99; x < 300; x += 2) { |
|
let res = jellyA(x, y, this.t); |
|
let px = res[0] - 200; |
|
let py = res[1] - 200; |
|
stroke(getJellyColor(x, y, this.t)); |
|
point(px, py); |
|
} |
|
} |
|
pop(); |
|
} |
|
} |
|
|
|
// --- Jellyfish Drawing Functions --- |
|
// Replicate the provided jellyfish “a(x,y)” function. |
|
function jellyA(x, y, t) { |
|
let k = x / 8 - 25; |
|
let e = y / 8 - 25; |
|
// d is computed as (k^2+e^2)/99 |
|
let d = (k * k + e * e) / 99; |
|
let q = x / 3 + k * 0.5 / cos(y * 5) * sin(d * d - t); |
|
let c = d / 2 - t / 8; |
|
let xPos = q * sin(c) + e * sin(d + k - t) + 200; |
|
let yPos = (q + y / 8 + d * 9) * cos(c) + 200; |
|
return [xPos, yPos]; |
|
} |
|
|
|
// Replicate the provided getColor function for the jellyfish. |
|
function getJellyColor(x, y, t) { |
|
let hue = (sin(t / 2) * 360 + x / 3 + y / 3) % 360; |
|
let saturation = 70 + sin(t) * 30; |
|
let brightness = 50 + cos(t / 2) * 20; |
|
return color(hue, saturation, brightness, 0.5); |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
st.components.v1.html(html_code, height=820, scrolling=False) |
|
|
|
if __name__ == "__main__": |
|
create_animation_app() |
|
|