sphere-electrons/sketch.js
2025-04-26 12:37:53 +00:00

319 lines
7.9 KiB
JavaScript

let camera;
let red;
let charges = [];
let faces = [];
let sphere_radius;
const SURFACE_NONE = 0;
const SURFACE_CIRCLES = 1;
const SURFACE_EARTH = 2;
let surface = SURFACE_CIRCLES;
let physics = false;
let skeleton = false;
let polytope = false;
let buttons_surface;
let checkbox_physics;
let checkbox_skeleton;
let checkbox_polytope;
let aside_size;
function preload() {
earth_image = loadImage("atlas1.jpg");
}
function setup() {
createCanvas(0, 0, WEBGL);
aside_size = int(getComputedStyle(document.documentElement).getPropertyValue('--aside-size').replace('px', ''));
windowResized();
camera = createCamera();
red = color(0xbf, 0x00, 0x00);
sphere_radius = min(250, min(width, height) / 2 * 0.8);
checkbox_physics = document.getElementById("checkbox-physics");
checkbox_physics.checked = physics;
checkbox_skeleton = document.getElementById("checkbox-skeleton");
checkbox_skeleton.checked = skeleton;
checkbox_polytope = document.getElementById("checkbox-polytope");
checkbox_polytope.checked = polytope;
buttons_surface = [
document.getElementById("button-surface-none"),
document.getElementById("button-surface-circles"),
document.getElementById("button-surface-earth"),
]
buttons_surface[surface].disabled = true;
}
function windowResized() {
if (windowWidth >= windowHeight) {
resizeCanvas(windowWidth - aside_size, windowHeight);
} else {
resizeCanvas(windowWidth, windowHeight - aside_size);
}
}
function draw() {
background(50);
orbitControl();
camera.centerX = 0;
camera.centerY = 0;
camera.centerZ = 0;
make_lights();
if (physics) move_charges(charges);
draw_charges(sphere_radius);
if (skeleton) draw_skeleton(sphere_radius);
if (polytope) {
if (physics || faces.length === 0) find_faces();
draw_faces(sphere_radius);
}
draw_sphere(sphere_radius, 25);
}
function face_dist_sq([v1, v2, v3]) {
const center = p5.Vector.add(v1, v2).add(v3).mult(1 / 3);
return createVector(camera.eyeX, camera.eyeY, camera.eyeZ).sub(center).magSq();
};
function find_faces() {
faces = [];
for (let i = 2; i < charges.length; i += 1) {
for (let j = 1; j < i; j += 1) {
for (let k = 0; k < j; k += 1) {
// Check if p1 p2 p3 form a face of the convex polytope
// enclosing all vertices ...
const p1 = charges[i].position;
const p2 = charges[j].position;
const p3 = charges[k].position;
const normal = p5.Vector.sub(p2, p1).cross(p5.Vector.sub(p3, p1));
// ... by checking if the other vertices are on the same
// side of the plane generated by p1 p2 p3
let plane_separates_vertices = false;
let euler_formula = false;
for (let r = 1; r < charges.length; r += 1) {
for (let s = 0; s < r; s += 1) {
if (
r === i || r === j || r === k ||
s === i || s === j || s === k
) continue;
const q1 = charges[r].position;
const q2 = charges[s].position;
// Let l(t) := q1 + (q2 - q1) * t.
// L := { l(t) : 0 <= t <= 1 } is the line segment
// between q1 and q2. L intersects the plane
// generated by p1 p2 p3 iff l(t) intersects with
// the plane for some 0 <= t <= 1.
//
// If L intersects the plane, q1 and q2 are on
// opposite sides of the plane generated by p1 p2 p3,
// so p1 p2 p3 can't be a face. If we want the
// polytope to be convex. Which we do.
//
// A point k is on the plane generated by p1 p2 p3 iff
// dot(k - p1, normal) = 0. Let n := normal.
//
// dot(l(t) - p1, n) = 0
// iff dot(q1 + (q2 - q1) * t - p1, n) = 0
// iff dot(q1 - p1, n) + dot(q2 - q2, n) * t = 0
// iff t = dot(p1 - q1, n) / dot(q2 - q2, n)
const t = (
p5.Vector.dot(p5.Vector.sub(p1, q1), normal) /
p5.Vector.dot(p5.Vector.sub(q2, q1), normal)
);
plane_separates_vertices ||= t >= 0 && t <= 1;
if (plane_separates_vertices) break;
euler_formula ||= charges.length * 2 - faces.length == 4;
if (euler_formula) break;
}
if (plane_separates_vertices || euler_formula) break;
}
if (euler_formula) return;
if (!plane_separates_vertices) {
faces.push([p1, p2, p3]);
}
}
}
}
}
function draw_faces(radius) {
// fix OpenGL stacking alpha behaviour
faces.sort((a, b) => face_dist_sq(b) - face_dist_sq(a));
push();
strokeWeight(2);
stroke(0x00);
fill(0xbf, 0x7f);
for ([p1, p2, p3] of faces) {
beginShape(TRIANGLES);
vertex(p1.x * radius, p1.y * radius, p1.z * radius);
vertex(p2.x * radius, p2.y * radius, p2.z * radius);
vertex(p3.x * radius, p3.y * radius, p3.z * radius);
endShape();
}
pop();
}
function draw_skeleton(radius) {
push();
noStroke();
fill(0xff);
sphere(4);
stroke(0xbf);
for (let charge of charges) {
line(
0,
0,
0,
charge.position.x * radius,
charge.position.y * radius,
charge.position.z * radius,
);
}
pop();
}
function make_charges(n) {
charges = [];
for (let i = 0; i < n; i += 1) {
let position;
if (i === 0) {
position = createVector(0, -1, 0);
} else {
position = p5.Vector.random3D();
}
charges.push({
position: position,
velocity: createVector(),
acceleration: createVector(),
color: red,
});
}
}
function draw_charges(radius) {
push();
noStroke();
for (let charge of charges.values()) {
ambientMaterial(charge.color);
let position = charge.position.copy();
position.mult(radius);
push();
translate(position.x, position.y, position.z);
sphere(15);
pop();
}
pop();
}
function draw_sphere(radius, n_axis_circles) {
stroke(0x3f);
noFill();
if (surface === SURFACE_NONE) n_axis_circles = 0;
else if (surface === SURFACE_EARTH) n_axis_circles = 2;
push();
rotateX(TAU / 4);
draw_circles(
radius,
n_axis_circles,
color(0x00, 0x9f, 0xff),
color(0xff, 0x9f, 0x00),
);
pop();
push();
rotateY(TAU / 4);
draw_circles(
radius,
n_axis_circles,
color(0xff, 0x00, 0xff),
color(0x00, 0xff, 0x00),
);
pop();
if (surface === SURFACE_EARTH) {
push();
noStroke();
noFill();
tint(0xff, 0x9f);
texture(earth_image);
rotateY(TAU / 4);
sphere(radius);
pop();
}
}
function draw_circles(radius, n_circles, pole_1_color, pole_2_color) {
push();
stroke(pole_1_color);
translate(0, 0, -radius);
point(0, 0);
pop();
for (let i = 1; i < n_circles; i += 1) {
const angle = map(i, 0, n_circles - 1, -TAU / 4, TAU / 4);
const circle_radius = radius * cos(angle);
push();
translate(0, 0, radius * sin(angle));
circle(0, 0, circle_radius * 2);
pop();
}
push();
stroke(pole_2_color);
translate(0, 0, radius);
point(0, 0);
pop();
}
function make_lights() {
let light = createVector(0, 1, -1);
light.normalize();
directionalLight(0x1f, 0x1f, 0x1f, light);
ambientLight(0xbf);
}
function keyPressed() {
if (key == ' ') {
toggle_physics();
} else if (key == 'd') {
set_surface((surface + 1) % 3);
} else if (key == 'f') {
toggle_skeleton();
} else if (key == 'g') {
toggle_polytope();
} else if (key >= '0' && key <= '9') {
make_charges(int(key));
faces = [];
}
}
function toggle_physics() {
physics = !physics;
checkbox_physics.checked = physics;
}
function set_surface(value) {
surface = value;
for (let button of buttons_surface) button.disabled = false;
buttons_surface[value].disabled = true;
}
function toggle_skeleton() {
skeleton = !skeleton;
checkbox_skeleton.checked = skeleton;
}
function toggle_polytope() {
polytope = !polytope;
checkbox_polytope.checked = polytope;
}