let camera; let red; let particles = []; 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 charge = -1; let buttons_surface; let checkbox_physics; let checkbox_skeleton; let checkbox_polytope; let input_particles; 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; input_particles = document.getElementById("input-particles"); input_particles.valueAsNumber = particles.length; } 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(); const polytope_is_slow = physics && particles.length > 40 || particles.length > 120; const polytope_if_fast = polytope && !polytope_is_slow; if (physics) { if (!polytope_if_fast) faces = []; move_particles(particles, 8e-4); } draw_particles(sphere_radius); if (skeleton) draw_skeleton(sphere_radius); if (polytope_if_fast) { 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 < particles.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 = particles[i].position; const p2 = particles[j].position; const p3 = particles[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 < particles.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 = particles[r].position; const q2 = particles[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 ||= particles.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 particle of particles) { line( 0, 0, 0, particle.position.x * radius, particle.position.y * radius, particle.position.z * radius, ); } pop(); } function make_particles(n) { faces = []; input_particles.value = `${n}`; particles = []; for (let i = 0; i < n; i += 1) { let position; if (i === 0) { position = createVector(0, -1, 0); } else { position = p5.Vector.random3D(); } particles.push({ position: position, velocity: createVector(), acceleration: createVector(), charge: charge, color: red, }); } } function draw_particles(radius) { push(); noStroke(); for (let particle of particles) { ambientMaterial(particle.color); let position = particle.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_particles(int(key)); } } 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; } function set_charge(value) { charge = value; for (let particle of particles) { particle.charge = value; } }