Compare commits
7 Commits
c6a43ca622
...
2146620351
Author | SHA1 | Date |
---|---|---|
Benjamin Kraft | 2146620351 | 1 year ago |
Benjamin Kraft | 5f93512fac | 1 year ago |
Benjamin Kraft | 75fdcebf73 | 1 year ago |
Benjamin Kraft | c8a389b809 | 1 year ago |
Benjamin Kraft | 9bbcbe9e83 | 1 year ago |
Benjamin Kraft | 9c4d93af7a | 1 year ago |
Benjamin Kraft | b7491fa07f | 1 year ago |
@ -1,6 +1,6 @@ |
||||
{ |
||||
"display_name": "Pendulum", |
||||
"info_text": "Watch 500 Double-Pendulums diverge into chaos", |
||||
"info_text": "The laboratory for n-Pendula simulations, using 'Position Based Dynamics'.", |
||||
"visible": true, |
||||
"tags": ["Simulation"] |
||||
} |
After Width: | Height: | Size: 189 B |
After Width: | Height: | Size: 423 B |
After Width: | Height: | Size: 523 B |
Before Width: | Height: | Size: 318 B |
After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 218 B |
After Width: | Height: | Size: 598 B |
@ -1,31 +1,163 @@ |
||||
class Manager { |
||||
|
||||
nPendula: NPendulum[] = [] |
||||
|
||||
h = 0.07 |
||||
|
||||
constructor() { |
||||
p.colorMode(p.HSB, 100); |
||||
let count = 500; |
||||
for (let i = 0; i < count; i++){ |
||||
let rad = i / count / 1e3 + p.PI * 1.05; |
||||
let hue = i / count * 100; |
||||
let color = p.color(hue, 100, 100); |
||||
this.nPendula.push( |
||||
new NPendulum([200, 200], [2, 2], rad, color) |
||||
); |
||||
pendula: Pendulum[] = [] |
||||
|
||||
timescale: number; |
||||
gravity: number; |
||||
|
||||
playing = false; |
||||
|
||||
static SubSteps: number; |
||||
static Size = 20; |
||||
|
||||
init(){ |
||||
// @ts-ignore
|
||||
const {createApp} = Vue; |
||||
createApp({ |
||||
data() { |
||||
return { |
||||
gravity: 0, |
||||
timescale: 0, |
||||
subSteps: 0, |
||||
playingBtn: "play" |
||||
}; |
||||
}, |
||||
watch: { |
||||
gravity(newGravity: number) { |
||||
manager.gravity = newGravity; |
||||
}, |
||||
timescale(newScale: number){ |
||||
manager.timescale = newScale; |
||||
}, |
||||
subSteps(newSteps: number){ |
||||
Manager.SubSteps = newSteps; |
||||
} |
||||
}, |
||||
methods: { |
||||
togglePlay(){ |
||||
manager.playing = !manager.playing; |
||||
this.playingBtn = manager.playing ? "pause" : "play"; |
||||
}, |
||||
resetSimulationControls(){ |
||||
this.gravity = 9.81; |
||||
this.timescale = 1; |
||||
this.subSteps = 30; |
||||
} |
||||
}, |
||||
mounted() { |
||||
this.resetSimulationControls(); |
||||
}, |
||||
name: "Simulation" |
||||
}).mount("#simulation"); |
||||
|
||||
let app = createApp({ |
||||
data(){ |
||||
return { |
||||
segmentCount: 1, |
||||
maxSegmentCount: 30, |
||||
masses: [], |
||||
lengths: [], |
||||
startAngle: 90, |
||||
color: "#ffffff", |
||||
rainbow: false, |
||||
multiple: false, |
||||
pendulumCount: 10, |
||||
changeProperty: "angle", |
||||
changeAmount: 0.0005, |
||||
changeIndex: 0 |
||||
} |
||||
}, |
||||
methods: { |
||||
add() { |
||||
if (this.multiple){ |
||||
let changeAmount = this.changeAmount; |
||||
let changeIndex = this.changeIndex; |
||||
let color = p.color(this.color); |
||||
p.colorMode(p.HSB, 100); |
||||
for (let i = 0; i < this.pendulumCount; i++){ |
||||
let M = this.masses.slice(0, this.segmentCount); |
||||
let L = this.lengths.slice(0, this.segmentCount); |
||||
let startAngle = this.startAngle; |
||||
let progress = i / this.pendulumCount - 0.5; |
||||
switch (this.changeProperty){ |
||||
case "angle": |
||||
startAngle += progress * 360 * changeAmount; |
||||
break; |
||||
case "mass": |
||||
M[changeIndex] += progress * M[changeIndex] * changeAmount; |
||||
break; |
||||
case "length": |
||||
L[changeIndex] += progress * L[changeIndex] * changeAmount; |
||||
break; |
||||
} |
||||
if (this.rainbow){ |
||||
let hue = (progress + 0.5) * 100; |
||||
color = p.color(hue, 100, 100); |
||||
} |
||||
let newPendulum = new Pendulum(M, L, color, startAngle); |
||||
manager.pendula.push(newPendulum); |
||||
} |
||||
p.colorMode(p.RGB); |
||||
} else { |
||||
let M = this.masses.slice(0, this.segmentCount); |
||||
let L = this.lengths.slice(0, this.segmentCount); |
||||
let color = p.color(this.color); |
||||
let newPendulum = new Pendulum(M, L, color, this.startAngle); |
||||
manager.pendula.push(newPendulum); |
||||
} |
||||
|
||||
}, |
||||
deleteAll(){ |
||||
if (confirm("Delete all pendula?")){ |
||||
manager.pendula.splice(0); |
||||
} |
||||
}, |
||||
resetMasses(){ |
||||
for (let i = 0; i < this.maxSegmentCount; i++) |
||||
this.masses[i] = 1; |
||||
}, |
||||
resetLengths() { |
||||
for (let i = 0; i < this.maxSegmentCount; i++) |
||||
this.lengths[i] = 1; |
||||
}, |
||||
normalize(){ |
||||
let L: [number] = this.lengths; |
||||
let sum = L.slice(0, this.segmentCount).reduce((p, n) => p + n); |
||||
let maxLength = Manager.Size / 2; |
||||
let factor = maxLength / sum; |
||||
|
||||
for (let i = 0; i < this.segmentCount; i++) |
||||
this.lengths[i] *= factor; |
||||
} |
||||
}, |
||||
mounted() { |
||||
this.resetMasses(); |
||||
this.resetLengths(); |
||||
}, |
||||
name: "Add Pendula" |
||||
}); |
||||
app.config.globalProperties.$filters = { |
||||
round(value: number, n: number){ |
||||
if (!value) |
||||
return "0"; |
||||
return +value.toFixed(n); |
||||
} |
||||
} |
||||
p.colorMode(p.RGB); |
||||
app.mount("#preparation"); |
||||
|
||||
} |
||||
|
||||
update(){ |
||||
this.nPendula.forEach(p => p.update(this.h)); |
||||
if (this.playing) { |
||||
const h = this.timescale / Math.max(p.frameRate(), 30); |
||||
this.pendula.forEach(p => p.update(h)); |
||||
} |
||||
} |
||||
|
||||
draw(){ |
||||
p.push() |
||||
p.translate(p.width / 2, p.height / 2); |
||||
this.nPendula.forEach(p => p.draw()); |
||||
this.pendula.forEach(p => p.draw()); |
||||
p.pop(); |
||||
} |
||||
|
||||
|
@ -1,100 +1,99 @@ |
||||
const g = 9.81 |
||||
|
||||
class NPendulum { |
||||
|
||||
pendula: Pendulum[] = [] |
||||
|
||||
constructor(lengths, masses, startRad, color) { |
||||
switch (lengths.length) { |
||||
case 1: |
||||
this.pendula.push(new Pendulum(lengths[0], masses[0], startRad, color)); |
||||
break; |
||||
case 2: |
||||
let p1 = new Pendulum(lengths[0], masses[0], startRad, color); |
||||
let p2 = new Pendulum(lengths[1], masses[1], startRad, color); |
||||
p1.calcAcc = function(pendula){ |
||||
let p2 = pendula[1]; |
||||
return -g / this.l * p.sin(this.rad) - p2.l * p2.m / this.l / (this.m + p2.m) |
||||
* (p.cos(this.rad - p2.rad) * p2.acc + p.sin(this.rad - p2.rad) * p.pow(p2.vel, 2)); |
||||
} |
||||
p2.calcAcc = function (pendula){ |
||||
let p1 = pendula[0]; |
||||
return -g / this.l * p.sin(this.rad) - p1.l / this.l |
||||
* (p.cos(p1.rad - this.rad) * p1.acc - p.sin(p1.rad - this.rad) * p.pow(p1.vel, 2)); |
||||
} |
||||
this.pendula.push(p1, p2); |
||||
break; |
||||
} |
||||
this.pendula[0].origin = p.createVector(0, 0); |
||||
} |
||||
class Pendulum { |
||||
|
||||
updateOrigins(){ |
||||
this.pendula.forEach((p, i) => { |
||||
if (i > 0){ |
||||
let before = this.pendula[i - 1]; |
||||
p.origin = p5.Vector.add(before.origin, before.pos); |
||||
} |
||||
}); |
||||
} |
||||
X: Vector[] = []; |
||||
V: Vector[] = []; |
||||
|
||||
update(h){ |
||||
this.pendula.forEach((p, i) => { |
||||
p.update(h, this.pendula); |
||||
}); |
||||
this.updateOrigins(); |
||||
} |
||||
size: number; |
||||
|
||||
draw(){ |
||||
this.pendula.forEach(p => p.draw()); |
||||
constructor(readonly M: number[], readonly L: number[], readonly color: p5.Color, startAngle: number) { |
||||
console.assert(M.length === L.length, M, L, "Masses and Lengths are not of equal length!"); |
||||
|
||||
this.size = M.length; |
||||
|
||||
startAngle *= Math.PI / 180; |
||||
let direction = new Vector(Math.sin(startAngle), Math.cos(startAngle)); |
||||
let currentPosition = new Vector(0, 0); |
||||
for (let i = 0; i < this.size; i++){ |
||||
currentPosition.add(Vector.Mult(direction, L[i])); |
||||
this.X.push(currentPosition.copy()); |
||||
this.V.push(new Vector(0, 0)); |
||||
} |
||||
} |
||||
|
||||
} |
||||
// using position based dynamics
|
||||
update(h: number) { |
||||
h /= Manager.SubSteps; |
||||
|
||||
for (let k = 0; k < Manager.SubSteps; k++) { |
||||
|
||||
class Pendulum { |
||||
// Classic PBD needs multiple loops
|
||||
// Here, I can put all operations safely into one single loop,
|
||||
// because the positions and velocities in X, V are sorted
|
||||
// from the pendulum's origin to it's end which means
|
||||
// that only direct neighbours affect each other
|
||||
|
||||
l: number |
||||
m: number |
||||
let previousP = new Vector(0, 0); |
||||
for (let i = 0; i < this.size; i++) { |
||||
|
||||
acc: number = 0 |
||||
vel: number = 0 |
||||
rad: number |
||||
// apply external force (gravity)
|
||||
this.V[i].addC(0, manager.gravity * h); |
||||
|
||||
origin: p5.Vector |
||||
// euler step
|
||||
let currentP = Vector.Add(this.X[i],Vector.Mult(this.V[i], h)); |
||||
|
||||
color: p5.Color |
||||
// solve distance constraint
|
||||
let w1 = i === 0 ? 0 : 1 / this.M[i - 1]; |
||||
let w2 = 1 / this.M[i]; |
||||
|
||||
let s = Vector.Sub(previousP, currentP); |
||||
let n = s.copy(); |
||||
n.normalize(); |
||||
let l = s.mag(); |
||||
|
||||
constructor(l, m, rad, color) { |
||||
this.l = l; |
||||
this.m = m; |
||||
this.rad = rad; |
||||
this.color = color; |
||||
} |
||||
let deltaP1 = Vector.Mult(n, -w1 / (w1 + w2) * (l - this.L[i])); |
||||
let deltaP2 = Vector.Mult(n, w2 / (w1 + w2) * (l - this.L[i])); |
||||
|
||||
calcAcc(pendula){ |
||||
return -g / this.l * p.sin(this.rad); |
||||
} |
||||
previousP.add(deltaP1); |
||||
currentP.add(deltaP2); |
||||
|
||||
// integrate
|
||||
if (i > 0){ |
||||
this.V[i - 1] = Vector.Mult(Vector.Sub(previousP, this.X[i - 1]), 1 / h); |
||||
this.X[i - 1] = previousP; |
||||
} |
||||
|
||||
update(h, pendula = []){ |
||||
this.acc = this.calcAcc(pendula); |
||||
this.vel += this.acc * h; |
||||
this.rad += this.vel * h; |
||||
previousP = currentP; |
||||
} |
||||
|
||||
this.V[this.size - 1] = Vector.Mult(Vector.Sub(previousP, this.X[this.size - 1]), 1 / h); |
||||
this.X[this.size - 1] = previousP; |
||||
} |
||||
} |
||||
|
||||
draw(){ |
||||
let pos = this.pos; |
||||
p.push(); |
||||
p.translate(this.origin); |
||||
|
||||
p.stroke(this.color); |
||||
p.strokeWeight(3); |
||||
p.line(0, 0, pos.x, pos.y); |
||||
p.ellipse(pos.x, pos.y, this.m * 5, this.m * 5); |
||||
p.pop(); |
||||
} |
||||
p.strokeWeight(1); |
||||
p.fill(255); |
||||
|
||||
let scale = p.height * 0.95 / Manager.Size; |
||||
let p1 = new Vector(0, 0); |
||||
for (let p2 of this.X){ |
||||
p2 = p2.copy(); |
||||
p2.mult(scale); |
||||
p.line(p1.x, p1.y, p2.x, p2.y); |
||||
p1 = p2.copy(); |
||||
} |
||||
|
||||
get pos(){ |
||||
return p5.Vector.mult(p.createVector(p.sin(this.rad), p.cos(this.rad)), this.l); |
||||
for (let i = 0; i < this.size; i++){ |
||||
let p2 = this.X[i].copy(); |
||||
p2.mult(scale); |
||||
let r = Math.sqrt(this.M[i] * 10); |
||||
p.ellipse(p2.x, p2.y, r * 2, r * 2); |
||||
} |
||||
|
||||
p.pop(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,38 @@ |
||||
class Vector { |
||||
constructor(public x: number, public y: number) {} |
||||
|
||||
copy(){ |
||||
return new Vector(this.x, this.y); |
||||
} |
||||
|
||||
static Add(v1: Vector, v2: Vector){ |
||||
return new Vector(v1.x + v2.x, v1.y + v2.y); |
||||
} |
||||
static Sub(v1: Vector, v2: Vector){ |
||||
return new Vector(v1.x - v2.x, v1.y - v2.y); |
||||
} |
||||
static Mult(v: Vector, n: number){ |
||||
return new Vector(v.x * n, v.y * n); |
||||
} |
||||
|
||||
add(v: Vector){ |
||||
this.x += v.x; |
||||
this.y += v.y; |
||||
} |
||||
addC(x: number, y: number){ |
||||
this.x += x; |
||||
this.y += y; |
||||
} |
||||
mult(n: number){ |
||||
this.x *= n; |
||||
this.y *= n; |
||||
} |
||||
|
||||
mag(){ |
||||
return Math.sqrt(this.x * this.x + this.y * this.y); |
||||
} |
||||
|
||||
normalize(){ |
||||
this.mult(1 / this.mag()); |
||||
} |
||||
} |
@ -1,7 +0,0 @@ |
||||
{ |
||||
"collision": false, |
||||
"colorPicker": false, |
||||
"cookie": false, |
||||
"prototypes": false, |
||||
"technical": false |
||||
} |
@ -1,23 +0,0 @@ |
||||
{ |
||||
"project": { |
||||
"name": "pendulum", |
||||
"author": "BenjoCraeft", |
||||
"version": "0.0.0", |
||||
"playerCounts": [], |
||||
"online": { |
||||
"iceServers": [ |
||||
{"urls": "stun:stun.l.google.com:19302"}, |
||||
{ |
||||
"urls": "turn:numb.viagenie.ca", |
||||
"credential": "muazkh", |
||||
"username": "webrtc@live.com" |
||||
} |
||||
] |
||||
} |
||||
}, |
||||
"frameWork": { |
||||
"frameRate": 60, |
||||
"width": null, |
||||
"height": null |
||||
} |
||||
} |
@ -1,88 +0,0 @@ |
||||
#color_picker{ |
||||
width: 300px; |
||||
height: 25%; |
||||
margin: 20px; |
||||
margin-top: 50px; |
||||
border: 5px solid #000; |
||||
background-color: #000; |
||||
-webkit-user-select: none; |
||||
-moz-user-select: none; |
||||
-ms-user-select: none; |
||||
user-select: none; |
||||
position: relative; |
||||
} |
||||
#color_picker_numeric{ |
||||
width: 80%; |
||||
padding: 5%; |
||||
margin: 5%; |
||||
background-color: #888; |
||||
border-radius: 10px; |
||||
overflow: hidden; |
||||
} |
||||
.color_picker_rgb{ |
||||
float: left; |
||||
width: 22%; |
||||
height: 35px; |
||||
font-size: 25px; |
||||
color: #000; |
||||
} |
||||
.color_picker_rgb:nth-child(1){ |
||||
margin-right: 10%; |
||||
margin-left: 3%; |
||||
background-color: #F00; |
||||
|
||||
} |
||||
.color_picker_rgb:nth-child(2){ |
||||
background-color: #0F0; |
||||
} |
||||
.color_picker_rgb:nth-child(3){ |
||||
margin-left: 10%; |
||||
background-color: #00F; |
||||
color: #FFF; |
||||
} |
||||
#color_picker_hex{ |
||||
width: 50%; |
||||
height: 30px; |
||||
font-size: 25px; |
||||
margin: 10% 25% 0 25%; |
||||
} |
||||
#saturation{ |
||||
position: relative; |
||||
width: calc(100% - 33px); |
||||
height: 100%; |
||||
background: linear-gradient(to right, #FFF 0%, #F00 100%); |
||||
float: left; |
||||
margin-right: 6px; |
||||
} |
||||
#value { |
||||
width: 100%; |
||||
height: 100%; |
||||
background: linear-gradient(to top, #000 0%, rgba(255,255,255,0) 100%); |
||||
} |
||||
#sb_picker{ |
||||
border: 2px solid; |
||||
border-color: #FFF; |
||||
position: absolute; |
||||
width: 14px; |
||||
height: 14px; |
||||
border-radius: 10px; |
||||
bottom: 50px; |
||||
left: 50px; |
||||
box-sizing: border-box; |
||||
z-index: 10; |
||||
} |
||||
#hue { |
||||
width: 27px; |
||||
height: 100%; |
||||
position: relative; |
||||
float: left; |
||||
background: linear-gradient(to bottom, #F00 0%, #F0F 17%, #00F 34%, #0FF 50%, #0F0 67%, #FF0 84%, #F00 100%); |
||||
} |
||||
#hue_picker { |
||||
position: absolute; |
||||
background: #000; |
||||
border-bottom: 1px solid #000; |
||||
top: 0; |
||||
width: 27px; |
||||
height: 2px; |
||||
} |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 240 KiB |