Compare commits
No commits in common. '2146620351b3c9b6c5675ed5355296da6b9e3d12' and 'c6a43ca62299a964409d72667d2aeb64a025680e' have entirely different histories.
2146620351
...
c6a43ca622
@ -1,6 +1,6 @@ |
|||||||
{ |
{ |
||||||
"display_name": "Pendulum", |
"display_name": "Pendulum", |
||||||
"info_text": "The laboratory for n-Pendula simulations, using 'Position Based Dynamics'.", |
"info_text": "Watch 500 Double-Pendulums diverge into chaos", |
||||||
"visible": true, |
"visible": true, |
||||||
"tags": ["Simulation"] |
"tags": ["Simulation"] |
||||||
} |
} |
Before Width: | Height: | Size: 189 B |
Before Width: | Height: | Size: 423 B |
Before Width: | Height: | Size: 523 B |
After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 346 B |
Before Width: | Height: | Size: 218 B |
Before Width: | Height: | Size: 598 B |
@ -1,163 +1,31 @@ |
|||||||
class Manager { |
class Manager { |
||||||
|
|
||||||
pendula: Pendulum[] = [] |
nPendula: NPendulum[] = [] |
||||||
|
|
||||||
timescale: number; |
h = 0.07 |
||||||
gravity: number; |
|
||||||
|
|
||||||
playing = false; |
constructor() { |
||||||
|
|
||||||
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); |
||||||
for (let i = 0; i < this.pendulumCount; i++){ |
let count = 500; |
||||||
let M = this.masses.slice(0, this.segmentCount); |
for (let i = 0; i < count; i++){ |
||||||
let L = this.lengths.slice(0, this.segmentCount); |
let rad = i / count / 1e3 + p.PI * 1.05; |
||||||
let startAngle = this.startAngle; |
let hue = i / count * 100; |
||||||
let progress = i / this.pendulumCount - 0.5; |
let color = p.color(hue, 100, 100); |
||||||
switch (this.changeProperty){ |
this.nPendula.push( |
||||||
case "angle": |
new NPendulum([200, 200], [2, 2], rad, color) |
||||||
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(){ |
||||||
if (this.playing) { |
this.nPendula.forEach(p => p.update(this.h)); |
||||||
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.pendula.forEach(p => p.draw()); |
this.nPendula.forEach(p => p.draw()); |
||||||
p.pop(); |
p.pop(); |
||||||
} |
} |
||||||
|
|
||||||
|
@ -1,99 +1,100 @@ |
|||||||
class Pendulum { |
const g = 9.81 |
||||||
|
|
||||||
X: Vector[] = []; |
class NPendulum { |
||||||
V: Vector[] = []; |
|
||||||
|
pendula: Pendulum[] = [] |
||||||
size: number; |
|
||||||
|
constructor(lengths, masses, startRad, color) { |
||||||
constructor(readonly M: number[], readonly L: number[], readonly color: p5.Color, startAngle: number) { |
switch (lengths.length) { |
||||||
console.assert(M.length === L.length, M, L, "Masses and Lengths are not of equal length!"); |
case 1: |
||||||
|
this.pendula.push(new Pendulum(lengths[0], masses[0], startRad, color)); |
||||||
this.size = M.length; |
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); |
||||||
|
} |
||||||
|
|
||||||
startAngle *= Math.PI / 180; |
updateOrigins(){ |
||||||
let direction = new Vector(Math.sin(startAngle), Math.cos(startAngle)); |
this.pendula.forEach((p, i) => { |
||||||
let currentPosition = new Vector(0, 0); |
if (i > 0){ |
||||||
for (let i = 0; i < this.size; i++){ |
let before = this.pendula[i - 1]; |
||||||
currentPosition.add(Vector.Mult(direction, L[i])); |
p.origin = p5.Vector.add(before.origin, before.pos); |
||||||
this.X.push(currentPosition.copy()); |
|
||||||
this.V.push(new Vector(0, 0)); |
|
||||||
} |
} |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
// using position based dynamics
|
update(h){ |
||||||
update(h: number) { |
this.pendula.forEach((p, i) => { |
||||||
h /= Manager.SubSteps; |
p.update(h, this.pendula); |
||||||
|
}); |
||||||
|
this.updateOrigins(); |
||||||
|
} |
||||||
|
|
||||||
for (let k = 0; k < Manager.SubSteps; k++) { |
draw(){ |
||||||
|
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++) { |
|
||||||
|
|
||||||
// apply external force (gravity)
|
class Pendulum { |
||||||
this.V[i].addC(0, manager.gravity * h); |
|
||||||
|
|
||||||
// euler step
|
l: number |
||||||
let currentP = Vector.Add(this.X[i],Vector.Mult(this.V[i], h)); |
m: number |
||||||
|
|
||||||
// solve distance constraint
|
acc: number = 0 |
||||||
let w1 = i === 0 ? 0 : 1 / this.M[i - 1]; |
vel: number = 0 |
||||||
let w2 = 1 / this.M[i]; |
rad: number |
||||||
|
|
||||||
let s = Vector.Sub(previousP, currentP); |
origin: p5.Vector |
||||||
let n = s.copy(); |
|
||||||
n.normalize(); |
|
||||||
let l = s.mag(); |
|
||||||
|
|
||||||
let deltaP1 = Vector.Mult(n, -w1 / (w1 + w2) * (l - this.L[i])); |
color: p5.Color |
||||||
let deltaP2 = Vector.Mult(n, w2 / (w1 + w2) * (l - this.L[i])); |
|
||||||
|
|
||||||
previousP.add(deltaP1); |
|
||||||
currentP.add(deltaP2); |
|
||||||
|
|
||||||
// integrate
|
constructor(l, m, rad, color) { |
||||||
if (i > 0){ |
this.l = l; |
||||||
this.V[i - 1] = Vector.Mult(Vector.Sub(previousP, this.X[i - 1]), 1 / h); |
this.m = m; |
||||||
this.X[i - 1] = previousP; |
this.rad = rad; |
||||||
|
this.color = color; |
||||||
} |
} |
||||||
|
|
||||||
previousP = currentP; |
calcAcc(pendula){ |
||||||
|
return -g / this.l * p.sin(this.rad); |
||||||
} |
} |
||||||
|
|
||||||
this.V[this.size - 1] = Vector.Mult(Vector.Sub(previousP, this.X[this.size - 1]), 1 / h); |
update(h, pendula = []){ |
||||||
this.X[this.size - 1] = previousP; |
this.acc = this.calcAcc(pendula); |
||||||
} |
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(1); |
p.strokeWeight(3); |
||||||
p.fill(255); |
p.line(0, 0, pos.x, pos.y); |
||||||
|
p.ellipse(pos.x, pos.y, this.m * 5, this.m * 5); |
||||||
let scale = p.height * 0.95 / Manager.Size; |
p.pop(); |
||||||
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(); |
|
||||||
} |
|
||||||
|
|
||||||
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(); |
get pos(){ |
||||||
|
return p5.Vector.mult(p.createVector(p.sin(this.rad), p.cos(this.rad)), this.l); |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
@ -1,38 +0,0 @@ |
|||||||
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()); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,7 @@ |
|||||||
|
{ |
||||||
|
"collision": false, |
||||||
|
"colorPicker": false, |
||||||
|
"cookie": false, |
||||||
|
"prototypes": false, |
||||||
|
"technical": false |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
"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 |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
#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: 240 KiB After Width: | Height: | Size: 69 KiB |