functionality for adding multiple pendula

main
Benjamin Kraft 1 year ago
parent 75fdcebf73
commit 5f93512fac
  1. 75
      public/data/scripts/ts/manager.ts
  2. 10
      public/data/scripts/ts/pendulum.ts
  3. 88
      public/data/styles/color_picker.css
  4. 7
      public/data/styles/range_input.css
  5. 24
      public/index.html
  6. 2
      public/styles.css

@ -2,12 +2,13 @@ class Manager {
pendula: Pendulum[] = [] pendula: Pendulum[] = []
timescale = 1; timescale: number;
gravity = 9.81; gravity: number;
playing = false; playing = false;
static Size = 200; static SubSteps: number;
static Size = 20;
init(){ init(){
// @ts-ignore // @ts-ignore
@ -15,8 +16,9 @@ class Manager {
createApp({ createApp({
data() { data() {
return { return {
gravity: manager.gravity, gravity: 0,
timescale: manager.timescale, timescale: 0,
subSteps: 0,
playingBtn: "play" playingBtn: "play"
}; };
}, },
@ -26,18 +28,29 @@ class Manager {
}, },
timescale(newScale: number){ timescale(newScale: number){
manager.timescale = newScale; manager.timescale = newScale;
},
subSteps(newSteps: number){
Manager.SubSteps = newSteps;
} }
}, },
methods: { methods: {
togglePlay(){ togglePlay(){
manager.playing = !manager.playing; manager.playing = !manager.playing;
this.playingBtn = manager.playing ? "pause" : "play"; this.playingBtn = manager.playing ? "pause" : "play";
},
resetSimulationControls(){
this.gravity = 9.81;
this.timescale = 1;
this.subSteps = 30;
} }
}, },
mounted() {
this.resetSimulationControls();
},
name: "Simulation" name: "Simulation"
}).mount("#simulation"); }).mount("#simulation");
createApp({ let app = createApp({
data(){ data(){
return { return {
segmentCount: 1, segmentCount: 1,
@ -50,26 +63,52 @@ class Manager {
multiple: false, multiple: false,
pendulumCount: 10, pendulumCount: 10,
changeProperty: "angle", changeProperty: "angle",
changeAmount: 0.05, changeAmount: 0.0005,
changeIndex: 0 changeIndex: 0
} }
}, },
methods: { methods: {
add() { add() {
if (this.multiple){ 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 { } else {
let color = p.color(this.color)
let M = this.masses.slice(0, this.segmentCount); let M = this.masses.slice(0, this.segmentCount);
let L = this.lengths.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); let newPendulum = new Pendulum(M, L, color, this.startAngle);
manager.pendula.push(newPendulum); manager.pendula.push(newPendulum);
} }
p.colorMode(p.HSB, 100);
p.colorMode(p.RGB);
}, },
deleteAll(){ deleteAll(){
if (confirm("Delete all pendulums?")){ if (confirm("Delete all pendula?")){
manager.pendula.splice(0); manager.pendula.splice(0);
} }
}, },
@ -95,8 +134,16 @@ class Manager {
this.resetMasses(); this.resetMasses();
this.resetLengths(); this.resetLengths();
}, },
name: "Preparation" name: "Add Pendula"
}).mount("#preparation"); });
app.config.globalProperties.$filters = {
round(value: number, n: number){
if (!value)
return "0";
return +value.toFixed(n);
}
}
app.mount("#preparation");
} }

@ -22,11 +22,9 @@ class Pendulum {
// using position based dynamics // using position based dynamics
update(h: number) { update(h: number) {
const subSteps = 50; h /= Manager.SubSteps;
h /= subSteps; for (let k = 0; k < Manager.SubSteps; k++) {
for (let k = 0; k < subSteps; k++) {
// Classic PBD needs multiple loops // Classic PBD needs multiple loops
// Here, I can put all operations safely into one single loop, // Here, I can put all operations safely into one single loop,
@ -38,7 +36,7 @@ class Pendulum {
for (let i = 0; i < this.size; i++) { for (let i = 0; i < this.size; i++) {
// apply external force (gravity) // apply external force (gravity)
this.V[i].addC(0, manager.gravity * h * 50); this.V[i].addC(0, manager.gravity * h);
// euler step // euler step
let currentP = Vector.Add(this.X[i],Vector.Mult(this.V[i], h)); let currentP = Vector.Add(this.X[i],Vector.Mult(this.V[i], h));
@ -79,7 +77,7 @@ class Pendulum {
p.strokeWeight(1); p.strokeWeight(1);
p.fill(255); p.fill(255);
let scale = p.height / Manager.Size; let scale = p.height * 0.95 / Manager.Size;
let p1 = new Vector(0, 0); let p1 = new Vector(0, 0);
for (let p2 of this.X){ for (let p2 of this.X){
p2 = p2.copy(); p2 = p2.copy();

@ -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;
}

@ -2,6 +2,7 @@ input[type=range] {
-webkit-appearance: none; -webkit-appearance: none;
margin: 10px 0; margin: 10px 0;
width: 100%; width: 100%;
height: 20px;
background: none; background: none;
} }
input[type=range]:focus { input[type=range]:focus {
@ -9,7 +10,7 @@ input[type=range]:focus {
} }
input[type=range]::-webkit-slider-runnable-track { input[type=range]::-webkit-slider-runnable-track {
width: 100%; width: 100%;
height: 8.4px; height: 9px;
cursor: pointer; cursor: pointer;
animate: 0.2s; animate: 0.2s;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
@ -33,7 +34,7 @@ input[type=range]:focus::-webkit-slider-runnable-track {
} }
input[type=range]::-moz-range-track { input[type=range]::-moz-range-track {
width: 100%; width: 100%;
height: 8.4px; height: 9px;
cursor: pointer; cursor: pointer;
animate: 0.2s; animate: 0.2s;
box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d; box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
@ -52,7 +53,7 @@ input[type=range]::-moz-range-thumb {
} }
input[type=range]::-ms-track { input[type=range]::-ms-track {
width: 100%; width: 100%;
height: 8.4px; height: 9px;
cursor: pointer; cursor: pointer;
animate: 0.2s; animate: 0.2s;
background: transparent; background: transparent;

@ -7,7 +7,6 @@
<script src="data/lib/vue.global.js"></script> <script src="data/lib/vue.global.js"></script>
<script src="data/scripts/js/main.js" type="text/javascript"></script> <script src="data/scripts/js/main.js" type="text/javascript"></script>
<link href="styles.css" rel="stylesheet"> <link href="styles.css" rel="stylesheet">
<link href="data/styles/color_picker.css" rel="stylesheet">
<link href="data/styles/range_input.css" rel="stylesheet"> <link href="data/styles/range_input.css" rel="stylesheet">
<title>Pendulum</title> <title>Pendulum</title>
</head> </head>
@ -27,14 +26,14 @@
<span class="segment_label">({{ i - 1 }})</span> <span class="segment_label">({{ i - 1 }})</span>
<span class="segment_label">{{ masses[i - 1] }}kg</span> <span class="segment_label">{{ masses[i - 1] }}kg</span>
<input type="range" v-model.number="masses[i - 1]" min="0.1" max="10" step=".1"> <input type="range" v-model.number="masses[i - 1]" min="0.1" max="10" step=".1">
<span class="segment_label">{{ lengths[i - 1] }}m</span> <span class="segment_label">{{ $filters.round(lengths[i - 1], 1) }}m</span>
<input type="range" v-model.number="lengths[i - 1]" min="0.1" max="10" step=".1"> <input type="range" v-model.number="lengths[i - 1]" min="0.1" max="10" step=".1">
</template> </template>
</div> </div>
<div class="horizontal_group"> <div class="horizontal_group">
<button class="reset_segments_btn" @click="resetMasses" title="Reset masses to 1"></button> <button class="reset_btn" @click="resetMasses" title="Reset masses to 1"></button>
<button id="normalize_btn" @click="normalize" title="Normalize lengths to fit screen"></button> <button id="normalize_btn" @click="normalize" title="Normalize lengths to fit screen"></button>
<button class="reset_segments_btn" @click="resetLengths" title="Reset lengths to 1"></button> <button class="reset_btn" @click="resetLengths" title="Reset lengths to 1"></button>
</div> </div>
<label> <label>
Starting Angle: {{startAngle}}° Starting Angle: {{startAngle}}°
@ -75,13 +74,13 @@
<input type="range" v-model.number="changeIndex" min="0" :max="segmentCount - 1" step="1"> <input type="range" v-model.number="changeIndex" min="0" :max="segmentCount - 1" step="1">
</label> </label>
<label> <label>
Change Amount: {{ changeAmount * 100 }}% Change Amount: {{ $filters.round(changeAmount * 100, 3) }}%
<input type="range" v-model.number="changeAmount" min="0.0001" max="0.1" step="0.0001"> <input type="range" v-model.number="changeAmount" min="0.00001" max="0.001" step="0.00001">
</label> </label>
</template> </template>
<div class="horizontal_group"> <div class="horizontal_group">
<button @click="add" id="add_btn"></button> <button @click="add" id="add_btn" :title="'Add configured ' + (multiple ? 'pendula' : 'pendulum')"></button>
<button @click="deleteAll" id="delete_btn"></button> <button @click="deleteAll" id="delete_btn" title="Delete all pendula"></button>
</div> </div>
</fieldset> </fieldset>
<fieldset id="simulation"> <fieldset id="simulation">
@ -94,7 +93,14 @@
Timescale: x{{ timescale }} Timescale: x{{ timescale }}
<input type="range" v-model.number="timescale" min="0.01" max="3" step=".01"> <input type="range" v-model.number="timescale" min="0.01" max="3" step=".01">
</label> </label>
<button @click="togglePlay" id="play_btn" :class="playingBtn"></button> <label>
PBD Substeps: {{ subSteps }}
<input type="range" v-model.number="subSteps" min="1" max="100" step="1">
</label>
<div class="horizontal_group">
<button class="reset_btn" @click="resetSimulationControls" title="Reset simulation controls"></button>
<button @click="togglePlay" id="play_btn" :class="playingBtn" title="Pause/Resume simulation"></button>
</div>
</fieldset> </fieldset>
</div> </div>
<div id="canvas_holder"></div> <div id="canvas_holder"></div>

@ -150,7 +150,7 @@ button:active {
border-color: #9a9a9a; border-color: #9a9a9a;
} }
.reset_segments_btn { .reset_btn {
background-image: url("data/images/refresh.svg"); background-image: url("data/images/refresh.svg");
background-color: #cbcbcb; background-color: #cbcbcb;
border-color: #9a9a9a; border-color: #9a9a9a;

Loading…
Cancel
Save