xxxxxxxxxx
316
/*
Just drag and release the block!
Press 'a' on the keyboard to show/hide example
analytical solution satisfying u(0) = 132, u'(0) = 0.
(Click on the canvas first so that the key press is
recognized.)
Then, you can compare to numerical solution by dragging
block until its state is (132, 0), and releasing.
NOTES:
I made this simulation quickly, so the program may
benefit from refactoring, etc. Supporting code can be found in the project directory by clicking the arrow in the top left of the code editor.
The labels on the canvas are u and u', which worked better
for my explanatory purposes, but the code uses x and x'.
For a standalone simulation without the plot, see:
https://editor.p5js.org/highermathnotes/sketches/pq3qzh3hr
For a standalone phase plot without the simulation, see:
https://editor.p5js.org/highermathnotes/sketches/05td9xOPT7
To-do:
* adjust volume based on damping, as well as velocity
* add sliders for adjusting mass, damping, stiffness
* use grab cursor only on simulation (not on the plot)
*/
//VARIABLES
//simulation
let plot1, plot2; //graphics buffers
let win; //graphing window
let spring;
let block;
let ground;
let x, y; //graphing window coordinates of state vector (pos, vel)
let X, Y; //canvas coordinates of state vector
let X0; //X-coordinate of initial position (at equilibrium)
let dVelocities = []; //dragged velocities
let dVelocitiesSum; //sum of dragged velocities
let timeWindow = 10; //window for running average
let h = 0.1; //step size
let t = 0; //time
let released = true;
let slidingSound;
let soundLoop;
//phase plot
let p, v; //point of application, tangent vector
let m = 10; //number of horizontal grid points on each semi-axis
let n = 10; //number of vertical grid points on each semi-axis
let hSpace, vSpace; //px b/w horizontal, vertical grid points
let arrows = []; //arrows on the phase plane
let nTrajectory; //numerical trajectory curve
let aTrajectory; //analytical trajectory curve
let trajWeight = 5; //thickness of trajectories
let solnShown = false; //determines if aTrajectory is shown
//typesetting and labels
let x0Label;
let xLabel;
let xPrimeLabel;
let vLabel;
//CUSTOM FUNCTIONS
//first-order system
function xPrime(t, v) {
return v.y;
}
function yPrime(t, v) {
let m = block.mass;
let d = ground.damping;
let k = spring.stiffness;
return -(d / m) * v.y - (k / m) * v.x;
}
function vPrime(t, v) {
return createVector(xPrime(t, v), yPrime(t, v));
}
//example analytical solution with x(0) = 132, y(0) = 0
function soln(t) {
let x = -132 * exp(-2 * t) + 264 * exp(-t);
let y = 264 * exp(-2 * t) - 264 * exp(-t);
return createVector(x, y);
}
//PRELOAD, SETUP, AND DRAW
function preload() {
soundFormats('wav');
slidingSound = loadSound('sliding');
}
function setup() {
createCanvas(800, 400);
plot1 = createGraphics(width / 2, height);
plot2 = createGraphics(width / 2, height);
win = createGraphWindow(plot2.width / 2, plot2.height / 2, 1, 1);
hSpace = (plot2.width / 2) / m;
vSpace = (plot2.height / 2) / n;
spring = createSpring(0, height - 75);
X0 = spring.X;
spring.stiffness = 2;
block = createBlock(spring); //attach block to spring
block.mass = 1;
ground = createGround(height - (block.Y + block.height));
ground.damping = 0.3;
cursor('grab');
soundLoop = new p5.SoundLoop(onSoundLoop);
//create arrows
for (let i = -m; i <= m; i++) {
for (let j = -n; j <= n; j++) {
p = createVector(i * hSpace, j * vSpace);
v = createVector(xPrime(t, p), yPrime(t, p));
arrows.push(createArrow(win, p, v));
}
}
//create numerical trajectory (start with empty array of points)
nTrajectory = createCurve([], win);
//create example analytical trajectory with x(0) = 132, y(0) = 0
aTrajectory = createCurve([], win);
for (let i = 0; i < 500; i++){
aTrajectory.points.push([soln(i / 100).x, soln(i / 100).y]);
}
//KaTeX (mathematical typesetting)
//For background, see:
//https://discourse.processing.org/t/latex-in-processing/19691/6
plot1.parent('canvas');
x0Label = createP();
x0Label.parent('canvas');
x0Label.style('font-size', '20px');
x0Label.style('color', 'rgb(50, 50, 50)');
x0Label.position(X0 - 30, 100);
plot2.parent('canvas');
xLabel = createP();
xLabel.parent('canvas');
xLabel.style('font-size', '20px');
xLabel.style('color', 'rgb(232, 51, 232)');
xLabel.position(width - 28, 164);
xPrimeLabel = createP();
xPrimeLabel.parent('canvas');
xPrimeLabel.style('font-size', '20px');
xPrimeLabel.style('color', 'rgb(232, 51, 232)');
xPrimeLabel.position(3 * width / 4 - 8, -15);
}
function draw() {
//SIMULATION
plot1.background(229, 255, 234);
//equilibrium position line
plot1.stroke(100);
plot1.strokeWeight(1);
plot1.line(X0, 0, X0, height);
//label for equilibrium line
plot1.fill(229, 255, 234);
plot1.noStroke();
plot1.rect(X0-40, 115, 80, 40);
katex.render('u = 0', x0Label.elt);
//simulation components
ground.draw(plot1);
spring.draw(plot1);
block.draw(plot1);
//show contents of plot1
image(plot1, 0, 0);
//PHASE PLOT
//draw window
plot2.background(45);
plot2.stroke(255);
plot2.strokeWeight(1.5);
win.axis('horizontal', plot2);
win.axis('vertical', plot2);
//draw arrows
plot2.strokeWeight(1.2);
for (let i = 0; i < arrows.length; i++) {
arrows[i].draw(plot2, 12); //draw to plot2 with given length
}
//styling
plot2.fill(255);
plot2.strokeWeight(4);
plot2.stroke(232, 51, 232);
//label for horizontal axis
plot2.rect(plot2.width - 38, plot2.height / 2 - 18, 36, 36);
katex.render('\\textbf{\\textit{u}}', xLabel.elt);
//label for vertical axis
plot2.rect(plot2.width / 2 - 18, 2, 36, 36);
katex.render('\\textbf{\\textit{u}}^\\prime', xPrimeLabel.elt);
//assign coordinates
x = spring.position;
y = spring.velocity;
X = win.X(x);
Y = win.Y(y);
//analytical trajectory
if (solnShown) {
plot2.stroke(232, 51, 232);
plot2.strokeWeight(trajWeight);
aTrajectory.draw(plot2);
}
//numerical trajectory
if (released) {
plot2.strokeWeight(trajWeight);
plot2.stroke(255, 157, 91);
nTrajectory.points.push([x, y]);
nTrajectory.draw(plot2);
}
//draw current (pos, vel) point
plot2.strokeWeight(3);
plot2.stroke(255);
plot2.fill(232, 51, 232);
plot2.ellipse(X, Y, 15, 15);
//label current (pos, vel) point
let vLabel = `(${Math.round(x)}, ${Math.round(y)})`;
plot2.rectMode(CENTER);
plot2.rect(X, Y - 30, textWidth(vLabel) + 30, 30, 5, 5, 5, 5);
plot2.rectMode(CORNER);
plot2.fill(255);
plot2.noStroke();
plot2.textSize(18);
plot2.textAlign(CENTER, CENTER);
plot2.text(vLabel, X, Y - 30);
//show contents of plot 2
image(plot2, width / 2, 0);
//UPDATES
//update sound volume
slidingSound.setVolume(abs(spring.velocity) / 30);
//update state based on diff eq if mouse is released
if (released) {
//update spring.v, i.e. <spring.position, spring.velocity>
spring.v = rk(t, spring.v, vPrime, h); //Runge-Kutta
t += h;
}
//update velocity based on mouse position
//when it's grabbing the block;
//use a running average to smooth out sudden changes,
//so that sound isn't choppy
else {
//store recent velocities
dVelocities.push(movedX / (deltaTime / 1000));
if (dVelocities.length > timeWindow) {
dVelocities.shift(); //remove old velocities
}
//update spring velocity to equal running average velocity
dVelocitiesSum = 0;
for (let i = 0; i < dVelocities.length; i++) {
dVelocitiesSum += dVelocities[i];
}
spring.velocity = dVelocitiesSum / dVelocities.length;
}
}
//INTERACTIVITY
function mousePressed() {
released = false;
cursor('grabbing');
soundLoop.start();
}
function mouseDragged() {
released = false;
spring.length = mouseX - spring.leftX - 2 * spring.endLength;
}
function mouseReleased() {
released = true;
cursor('grab');
t = 0;
spring.velocity = 0;
nTrajectory.points = [[x, y]]; //start numerical trajectory at current point
}
//toggle analytical solution (show it or hide it)
function keyTyped() {
if (key === 'a') {
solnShown = !solnShown;
}
}
//SOUND LOOP CALLBACK
function onSoundLoop() {
slidingSound.play();
}