Compare commits

...

7 commits

Author SHA1 Message Date
root
0fad69fda8 interface: bigger now 2025-04-27 10:11:26 +00:00
root
72a24ce131 keyboard shortcuts; ignore when some inputs active 2025-04-27 10:11:07 +00:00
root
afd3392611 bugfix: wrong polytope faces
reproduce:
1. polytope on
2. polytope off
3. toggle physics on then off (want big change)
4. polytope on
2025-04-27 10:09:18 +00:00
root
9194832b01 interface: <input type="number"> for particles 2025-04-27 10:08:00 +00:00
root
be6d6ba000 interface: set charge magnitude 2025-04-27 10:05:59 +00:00
root
642d1c5cdd custom charge magnitude
also: s/charge/particle
to reduce confusion
2025-04-27 10:04:48 +00:00
root
5e04ad107f physics oopsie: coulomb's law is 1/r^2 not 1/r 2025-04-27 07:57:42 +00:00
3 changed files with 135 additions and 65 deletions

View file

@ -7,7 +7,8 @@
<script src="lib/p5.js"></script>
<style>
:root {
--aside-size: 218px;
--aside-width: 220px;
--aside-height: 256px;
}
.js {
display: none;
@ -24,7 +25,7 @@ body {
background-color: #1b1b1b;
overflow: hidden;
display: grid;
grid-template-columns: var(--aside-size) auto;
grid-template-columns: var(--aside-width) auto;
color: white;
}
aside {
@ -38,8 +39,10 @@ aside > container {
flex-wrap: wrap;
gap: 12px;
}
aside button {
aside button, aside input[type="number"] {
font-size: 20px;
}
aside button {
min-width: 27px;
}
aside input[type="checkbox"] {
@ -54,7 +57,7 @@ aside > container > div {
@media screen and (orientation:portrait) {
body {
grid-template-columns: none;
grid-template-rows: auto var(--aside-size);
grid-template-rows: auto var(--aside-height);
}
aside {
order: 1;
@ -68,15 +71,20 @@ aside > container > div {
<body>
<aside class="js">
<container>
<div style="width:100%">
<div>
<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>
@ -86,16 +94,21 @@ aside > container > div {
<button id="button-surface-earth" onclick="set_surface(SURFACE_EARTH)">Earth</button>
</div>
<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>
<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>
</div>
</container>
</aside>
@ -108,6 +121,11 @@ 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");</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>
</body>
</html>

100
sketch.js
View file

@ -1,7 +1,7 @@
let camera;
let red;
let charges = [];
let particles = [];
let faces = [];
let sphere_radius;
@ -14,13 +14,16 @@ 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;
let aside_width;
let aside_height;
function preload() {
@ -29,7 +32,9 @@ function preload() {
function setup() {
createCanvas(0, 0, WEBGL);
aside_size = int(getComputedStyle(document.documentElement).getPropertyValue('--aside-size').replace('px', ''));
const css = getComputedStyle(document.documentElement);
aside_width = int(css.getPropertyValue('--aside-width').replace('px', ''));
aside_height = int(css.getPropertyValue('--aside-height').replace('px', ''));
windowResized();
camera = createCamera();
@ -51,13 +56,16 @@ 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_size, windowHeight);
resizeCanvas(windowWidth - aside_width, windowHeight);
} else {
resizeCanvas(windowWidth, windowHeight - aside_size);
resizeCanvas(windowWidth, windowHeight - aside_height);
}
}
@ -70,11 +78,16 @@ function draw() {
camera.centerZ = 0;
make_lights();
if (physics) move_charges(charges);
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_charges(sphere_radius);
draw_particles(sphere_radius);
if (skeleton) draw_skeleton(sphere_radius);
if (polytope) {
if (polytope_if_fast) {
if (physics || faces.length === 0) find_faces();
draw_faces(sphere_radius);
}
@ -88,27 +101,27 @@ function face_dist_sq([v1, v2, v3]) {
function find_faces() {
faces = [];
for (let i = 2; i < charges.length; i += 1) {
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 = charges[i].position;
const p2 = charges[j].position;
const p3 = charges[k].position;
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 < charges.length; r += 1) {
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 = charges[r].position;
const q2 = charges[s].position;
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
@ -133,7 +146,7 @@ function find_faces() {
);
plane_separates_vertices ||= t >= 0 && t <= 1;
if (plane_separates_vertices) break;
euler_formula ||= charges.length * 2 - faces.length == 4;
euler_formula ||= particles.length * 2 - faces.length == 4;
if (euler_formula) break;
}
if (plane_separates_vertices || euler_formula) break;
@ -170,21 +183,23 @@ function draw_skeleton(radius) {
fill(0xff);
sphere(4);
stroke(0xbf);
for (let charge of charges) {
for (let particle of particles) {
line(
0,
0,
0,
charge.position.x * radius,
charge.position.y * radius,
charge.position.z * radius,
particle.position.x * radius,
particle.position.y * radius,
particle.position.z * radius,
);
}
pop();
}
function make_charges(n) {
charges = [];
function make_particles(n) {
faces = [];
input_particles.value = `${n}`;
particles = [];
for (let i = 0; i < n; i += 1) {
let position;
if (i === 0) {
@ -192,21 +207,22 @@ function make_charges(n) {
} else {
position = p5.Vector.random3D();
}
charges.push({
particles.push({
position: position,
velocity: createVector(),
acceleration: createVector(),
charge: charge,
color: red,
});
}
}
function draw_charges(radius) {
function draw_particles(radius) {
push();
noStroke();
for (let charge of charges.values()) {
ambientMaterial(charge.color);
let position = charge.position.copy();
for (let particle of particles) {
ambientMaterial(particle.color);
let position = particle.position.copy();
position.mult(radius);
push();
translate(position.x, position.y, position.z);
@ -284,7 +300,26 @@ 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') {
@ -292,8 +327,8 @@ function keyPressed() {
} else if (key == 'g') {
toggle_polytope();
} else if (key >= '0' && key <= '9') {
make_charges(int(key));
faces = [];
if (document.activeElement === input_particles) return;
make_particles(int(key));
}
}
@ -317,3 +352,10 @@ 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,32 +1,42 @@
function move_charges(charges) {
for (let charge of charges) {
charge.acceleration.setMag(0);
function move_particles(particles, force_constant) {
for (let particle of particles) {
particle.acceleration.setMag(0);
}
for (let i = 0; i < charges.length; i += 1) {
for (let i = 0; i < particles.length; i += 1) {
for (let j = 0; j < i; j += 1) {
const displacement = p5.Vector.sub(
charges[i].position,
charges[j].position,
particles[i].position,
particles[j].position,
);
let acceleration_mag = 1 / displacement.mag() * 0.001;
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 ai;
if (acceleration_mag === Infinity) {
let aj;
if (force_mag === Infinity) {
ai = p5.Vector.random3D();
aj = p5.Vector.random3D();
} else {
ai = displacement.copy().normalize();
aj = ai.copy().mult(-1);
}
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);
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);
}
}
for (let charge of charges) {
charge.velocity = charge.velocity.add(charge.acceleration);
charge.position = charge.position.add(charge.velocity);
charge.position.normalize();
for (let particle of particles) {
particle.velocity = particle.velocity.add(particle.acceleration);
particle.position = particle.position.add(particle.velocity);
particle.position.normalize();
}
}