Compare commits

..

No commits in common. "0fad69fda83795cf746713f0d1dda6cb289d6539" and "2a29ee0c51633cb2f77b737723dbe2cc987b6cec" have entirely different histories.

3 changed files with 65 additions and 135 deletions

View file

@ -7,8 +7,7 @@
<script src="lib/p5.js"></script>
<style>
:root {
--aside-width: 220px;
--aside-height: 256px;
--aside-size: 218px;
}
.js {
display: none;
@ -25,7 +24,7 @@ body {
background-color: #1b1b1b;
overflow: hidden;
display: grid;
grid-template-columns: var(--aside-width) auto;
grid-template-columns: var(--aside-size) auto;
color: white;
}
aside {
@ -39,10 +38,8 @@ aside > container {
flex-wrap: wrap;
gap: 12px;
}
aside button, aside input[type="number"] {
font-size: 20px;
}
aside button {
font-size: 20px;
min-width: 27px;
}
aside input[type="checkbox"] {
@ -57,7 +54,7 @@ aside > container > div {
@media screen and (orientation:portrait) {
body {
grid-template-columns: none;
grid-template-rows: auto var(--aside-height);
grid-template-rows: auto var(--aside-size);
}
aside {
order: 1;
@ -71,20 +68,15 @@ aside > container > div {
<body>
<aside class="js">
<container>
<div>
<div style="width:100%">
<button onclick="toggle_physics()">Physics</button>
<input id="checkbox-physics" type="checkbox" disabled>
</div>
<form style="display:grid;grid-template-columns:1fr auto;gap:5px;">
<input id="input-charge" type="range" min="-1.5" max="1.5" step="0.25"
style="min-width:100px;max-width:128px;"
oninput="set_charge(-Math.pow(10, input_charge.valueAsNumber))">
<input type="reset">
</form>
<div>
<button onclick="toggle_skeleton()">Skeleton</button>
<input id="checkbox-skeleton" type="checkbox" disabled>
<div></div>
</div>
<div>
<button onclick="toggle_polytope()">Polytope</button>
<input id="checkbox-polytope" type="checkbox" disabled>
</div>
@ -94,21 +86,16 @@ aside > container > div {
<button id="button-surface-earth" onclick="set_surface(SURFACE_EARTH)">Earth</button>
</div>
<div>
<button onclick="make_particles(0)">0</button>
<button onclick="make_particles(1)">1</button>
<button onclick="make_particles(2)">2</button>
<button onclick="make_particles(3)">3</button>
<button onclick="make_particles(4)">4</button>
<button onclick="make_particles(5)">5</button>
<button onclick="make_particles(6)">6</button>
<button onclick="make_particles(7)">7</button>
<button onclick="make_particles(8)">8</button>
<button onclick="make_particles(9)">9</button>
<div>
<input id="input-particles" type="number" min="0" max="360"
onkeypress="if (event.key === 'Enter') button_particles.click()">
<button id="button-particles" onclick="make_particles(min(360, max(0, input_particles.valueAsNumber)))">Create</button>
</div>
<button onclick="make_charges(0);faces=[]">0</button>
<button onclick="make_charges(1);faces=[]">1</button>
<button onclick="make_charges(2);faces=[]">2</button>
<button onclick="make_charges(3);faces=[]">3</button>
<button onclick="make_charges(4);faces=[]">4</button>
<button onclick="make_charges(5);faces=[]">5</button>
<button onclick="make_charges(6);faces=[]">6</button>
<button onclick="make_charges(7);faces=[]">7</button>
<button onclick="make_charges(8);faces=[]">8</button>
<button onclick="make_charges(9);faces=[]">9</button>
</div>
</container>
</aside>
@ -121,11 +108,6 @@ Source code: <a href="https://git.atomic.garden/root/sphere-electrons">https://g
</noscript>
<script src="thomson-problem.js"></script>
<script src="sketch.js"></script>
<script>
document.querySelectorAll(".js").forEach(e => e.style = "display:initial");
const input_charge = document.getElementById("input-charge");
input_charge.oninput();
const button_particles = document.getElementById("button-particles");
</script>
<script>document.querySelectorAll(".js").forEach(e => e.style = "display:initial");</script>
</body>
</html>

100
sketch.js
View file

@ -1,7 +1,7 @@
let camera;
let red;
let particles = [];
let charges = [];
let faces = [];
let sphere_radius;
@ -14,16 +14,13 @@ 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_width;
let aside_height;
let aside_size;
function preload() {
@ -32,9 +29,7 @@ function preload() {
function setup() {
createCanvas(0, 0, WEBGL);
const css = getComputedStyle(document.documentElement);
aside_width = int(css.getPropertyValue('--aside-width').replace('px', ''));
aside_height = int(css.getPropertyValue('--aside-height').replace('px', ''));
aside_size = int(getComputedStyle(document.documentElement).getPropertyValue('--aside-size').replace('px', ''));
windowResized();
camera = createCamera();
@ -56,16 +51,13 @@ function setup() {
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_width, windowHeight);
resizeCanvas(windowWidth - aside_size, windowHeight);
} else {
resizeCanvas(windowWidth, windowHeight - aside_height);
resizeCanvas(windowWidth, windowHeight - aside_size);
}
}
@ -78,16 +70,11 @@ function draw() {
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);
}
if (physics) move_charges(charges);
draw_particles(sphere_radius);
draw_charges(sphere_radius);
if (skeleton) draw_skeleton(sphere_radius);
if (polytope_if_fast) {
if (polytope) {
if (physics || faces.length === 0) find_faces();
draw_faces(sphere_radius);
}
@ -101,27 +88,27 @@ function face_dist_sq([v1, v2, v3]) {
function find_faces() {
faces = [];
for (let i = 2; i < particles.length; i += 1) {
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 = particles[i].position;
const p2 = particles[j].position;
const p3 = particles[k].position;
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 < particles.length; r += 1) {
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 = particles[r].position;
const q2 = particles[s].position;
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
@ -146,7 +133,7 @@ function find_faces() {
);
plane_separates_vertices ||= t >= 0 && t <= 1;
if (plane_separates_vertices) break;
euler_formula ||= particles.length * 2 - faces.length == 4;
euler_formula ||= charges.length * 2 - faces.length == 4;
if (euler_formula) break;
}
if (plane_separates_vertices || euler_formula) break;
@ -183,23 +170,21 @@ function draw_skeleton(radius) {
fill(0xff);
sphere(4);
stroke(0xbf);
for (let particle of particles) {
for (let charge of charges) {
line(
0,
0,
0,
particle.position.x * radius,
particle.position.y * radius,
particle.position.z * radius,
charge.position.x * radius,
charge.position.y * radius,
charge.position.z * radius,
);
}
pop();
}
function make_particles(n) {
faces = [];
input_particles.value = `${n}`;
particles = [];
function make_charges(n) {
charges = [];
for (let i = 0; i < n; i += 1) {
let position;
if (i === 0) {
@ -207,22 +192,21 @@ function make_particles(n) {
} else {
position = p5.Vector.random3D();
}
particles.push({
charges.push({
position: position,
velocity: createVector(),
acceleration: createVector(),
charge: charge,
color: red,
});
}
}
function draw_particles(radius) {
function draw_charges(radius) {
push();
noStroke();
for (let particle of particles) {
ambientMaterial(particle.color);
let position = particle.position.copy();
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);
@ -300,26 +284,7 @@ function make_lights() {
function keyPressed() {
if (key == ' ') {
if (
document.activeElement !== input_charge &&
document.activeElement !== document.body
) return;
toggle_physics();
} else if (key == 's') {
toggle_physics();
} else if (key == 'a') {
input_charge.valueAsNumber = 0;
input_charge.oninput();
} else if (key == '[') {
input_charge.valueAsNumber -= Number(input_charge.step);
input_charge.oninput();
} else if (key == ']') {
input_charge.valueAsNumber += Number(input_charge.step);
input_charge.oninput();
} else if (key == '-') {
make_particles(max(0, int(particles.length) - 1));
} else if (key == '=') {
make_particles(min(360, int(particles.length) + 1));
} else if (key == 'd') {
set_surface((surface + 1) % 3);
} else if (key == 'f') {
@ -327,8 +292,8 @@ function keyPressed() {
} else if (key == 'g') {
toggle_polytope();
} else if (key >= '0' && key <= '9') {
if (document.activeElement === input_particles) return;
make_particles(int(key));
make_charges(int(key));
faces = [];
}
}
@ -352,10 +317,3 @@ function toggle_polytope() {
polytope = !polytope;
checkbox_polytope.checked = polytope;
}
function set_charge(value) {
charge = value;
for (let particle of particles) {
particle.charge = value;
}
}

View file

@ -1,42 +1,32 @@
function move_particles(particles, force_constant) {
for (let particle of particles) {
particle.acceleration.setMag(0);
function move_charges(charges) {
for (let charge of charges) {
charge.acceleration.setMag(0);
}
for (let i = 0; i < particles.length; i += 1) {
for (let i = 0; i < charges.length; i += 1) {
for (let j = 0; j < i; j += 1) {
const displacement = p5.Vector.sub(
particles[i].position,
particles[j].position,
charges[i].position,
charges[j].position,
);
const force_mag = (
particles[i].charge * particles[j].charge
/ displacement.magSq()
* force_constant
);
// XXX possible extension: divide by charge's mass
const ai_mag = force_mag;
const aj_mag = force_mag;
let acceleration_mag = 1 / displacement.mag() * 0.001;
let ai;
let aj;
if (force_mag === Infinity) {
if (acceleration_mag === Infinity) {
ai = p5.Vector.random3D();
aj = p5.Vector.random3D();
} else {
ai = displacement.copy().normalize();
aj = ai.copy().mult(-1);
}
ai.mult(ai_mag);
aj.mult(aj_mag);
project_onto_plane(ai, particles[i].position);
project_onto_plane(aj, particles[j].position);
particles[i].acceleration.add(ai);
particles[j].acceleration.add(aj);
ai.mult(acceleration_mag);
let aj = p5.Vector.mult(ai, -1);
project_onto_plane(ai, charges[i].position);
project_onto_plane(aj, charges[j].position);
charges[i].acceleration.add(ai);
charges[j].acceleration.add(aj);
}
}
for (let particle of particles) {
particle.velocity = particle.velocity.add(particle.acceleration);
particle.position = particle.position.add(particle.velocity);
particle.position.normalize();
for (let charge of charges) {
charge.velocity = charge.velocity.add(charge.acceleration);
charge.position = charge.position.add(charge.velocity);
charge.position.normalize();
}
}