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", |
"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, |
"visible": true, |
||||||
"tags": ["Simulation"] |
"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 { |
class Manager { |
||||||
|
|
||||||
nPendula: NPendulum[] = [] |
pendula: Pendulum[] = [] |
||||||
|
|
||||||
h = 0.07 |
timescale: number; |
||||||
|
gravity: number; |
||||||
|
|
||||||
constructor() { |
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); |
p.colorMode(p.HSB, 100); |
||||||
let count = 500; |
for (let i = 0; i < this.pendulumCount; i++){ |
||||||
for (let i = 0; i < count; i++){ |
let M = this.masses.slice(0, this.segmentCount); |
||||||
let rad = i / count / 1e3 + p.PI * 1.05; |
let L = this.lengths.slice(0, this.segmentCount); |
||||||
let hue = i / count * 100; |
let startAngle = this.startAngle; |
||||||
let color = p.color(hue, 100, 100); |
let progress = i / this.pendulumCount - 0.5; |
||||||
this.nPendula.push( |
switch (this.changeProperty){ |
||||||
new NPendulum([200, 200], [2, 2], rad, color) |
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); |
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); |
||||||
|
} |
||||||
|
} |
||||||
|
app.mount("#preparation"); |
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
update(){ |
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(){ |
draw(){ |
||||||
p.push() |
p.push() |
||||||
p.translate(p.width / 2, p.height / 2); |
p.translate(p.width / 2, p.height / 2); |
||||||
this.nPendula.forEach(p => p.draw()); |
this.pendula.forEach(p => p.draw()); |
||||||
p.pop(); |
p.pop(); |
||||||
} |
} |
||||||
|
|
||||||
|
@ -1,100 +1,99 @@ |
|||||||
const g = 9.81 |
class Pendulum { |
||||||
|
|
||||||
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); |
|
||||||
} |
|
||||||
|
|
||||||
updateOrigins(){ |
X: Vector[] = []; |
||||||
this.pendula.forEach((p, i) => { |
V: Vector[] = []; |
||||||
if (i > 0){ |
|
||||||
let before = this.pendula[i - 1]; |
size: number; |
||||||
p.origin = p5.Vector.add(before.origin, before.pos); |
|
||||||
|
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)); |
||||||
} |
} |
||||||
}); |
|
||||||
} |
} |
||||||
|
|
||||||
update(h){ |
// using position based dynamics
|
||||||
this.pendula.forEach((p, i) => { |
update(h: number) { |
||||||
p.update(h, this.pendula); |
h /= Manager.SubSteps; |
||||||
}); |
|
||||||
this.updateOrigins(); |
|
||||||
} |
|
||||||
|
|
||||||
draw(){ |
for (let k = 0; k < Manager.SubSteps; k++) { |
||||||
this.pendula.forEach(p => p.draw()); |
|
||||||
} |
|
||||||
|
|
||||||
} |
// 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
|
||||||
|
|
||||||
|
let previousP = new Vector(0, 0); |
||||||
|
for (let i = 0; i < this.size; i++) { |
||||||
|
|
||||||
class Pendulum { |
// apply external force (gravity)
|
||||||
|
this.V[i].addC(0, manager.gravity * h); |
||||||
|
|
||||||
l: number |
// euler step
|
||||||
m: number |
let currentP = Vector.Add(this.X[i],Vector.Mult(this.V[i], h)); |
||||||
|
|
||||||
acc: number = 0 |
// solve distance constraint
|
||||||
vel: number = 0 |
let w1 = i === 0 ? 0 : 1 / this.M[i - 1]; |
||||||
rad: number |
let w2 = 1 / this.M[i]; |
||||||
|
|
||||||
origin: p5.Vector |
let s = Vector.Sub(previousP, currentP); |
||||||
|
let n = s.copy(); |
||||||
|
n.normalize(); |
||||||
|
let l = s.mag(); |
||||||
|
|
||||||
color: p5.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])); |
||||||
|
|
||||||
|
previousP.add(deltaP1); |
||||||
|
currentP.add(deltaP2); |
||||||
|
|
||||||
constructor(l, m, rad, color) { |
// integrate
|
||||||
this.l = l; |
if (i > 0){ |
||||||
this.m = m; |
this.V[i - 1] = Vector.Mult(Vector.Sub(previousP, this.X[i - 1]), 1 / h); |
||||||
this.rad = rad; |
this.X[i - 1] = previousP; |
||||||
this.color = color; |
|
||||||
} |
} |
||||||
|
|
||||||
calcAcc(pendula){ |
previousP = currentP; |
||||||
return -g / this.l * p.sin(this.rad); |
|
||||||
} |
} |
||||||
|
|
||||||
update(h, pendula = []){ |
this.V[this.size - 1] = Vector.Mult(Vector.Sub(previousP, this.X[this.size - 1]), 1 / h); |
||||||
this.acc = this.calcAcc(pendula); |
this.X[this.size - 1] = previousP; |
||||||
this.vel += this.acc * h; |
} |
||||||
this.rad += this.vel * h; |
|
||||||
} |
} |
||||||
|
|
||||||
draw(){ |
draw(){ |
||||||
let pos = this.pos; |
|
||||||
p.push(); |
p.push(); |
||||||
p.translate(this.origin); |
|
||||||
p.stroke(this.color); |
p.stroke(this.color); |
||||||
p.strokeWeight(3); |
p.strokeWeight(1); |
||||||
p.line(0, 0, pos.x, pos.y); |
p.fill(255); |
||||||
p.ellipse(pos.x, pos.y, this.m * 5, this.m * 5); |
|
||||||
p.pop(); |
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(){ |
for (let i = 0; i < this.size; i++){ |
||||||
return p5.Vector.mult(p.createVector(p.sin(this.rad), p.cos(this.rad)), this.l); |
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 |