Compare commits
7 commits
2a29ee0c51
...
0fad69fda8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0fad69fda8 | ||
![]() |
72a24ce131 | ||
![]() |
afd3392611 | ||
![]() |
9194832b01 | ||
![]() |
be6d6ba000 | ||
![]() |
642d1c5cdd | ||
![]() |
5e04ad107f |
3 changed files with 135 additions and 65 deletions
54
index.html
54
index.html
|
@ -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
100
sketch.js
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue