sdof_simulator / script.js
Javedalam's picture
Upload 6 files
27be395 verified
/**
* Interactive SDOF Vibration Simulator
* Implements RK4 solver and real-time plotting
*/
// --- Constants & State ---
const state = {
m: 1.0,
k: 100,
c: 5.0,
F0: 10,
p: 5.0, // Forcing frequency (rad/s)
u0: 0.0,
v0: 0.0,
tMax: 10.0,
dt: 0.001
};
// --- DOM Elements ---
const elements = {
sliders: {
m: document.getElementById('mass-slider'),
k: document.getElementById('stiffness-slider'),
c: document.getElementById('damping-slider'),
F0: document.getElementById('force-slider'),
p: document.getElementById('p-slider'),
u0: document.getElementById('u0-slider'),
v0: document.getElementById('v0-slider')
},
values: {
m: document.getElementById('mass-val'),
k: document.getElementById('stiffness-val'),
c: document.getElementById('damping-val'),
F0: document.getElementById('force-val'),
p: document.getElementById('p-val'),
u0: document.getElementById('u0-val'),
v0: document.getElementById('v0-val')
},
stats: {
omegaN: document.getElementById('omega-n-val'),
zeta: document.getElementById('zeta-val'),
regime: document.getElementById('regime-val'),
peakDisp: document.getElementById('peak-disp-val'),
peakVel: document.getElementById('peak-vel-val')
},
canvas: {
disp: document.getElementById('disp-chart'),
vel: document.getElementById('vel-chart')
},
equation: {
math: document.getElementById('math-equation'),
sol: document.getElementById('math-solution')
}
};
// --- Physics Engine ---
/**
* Differential equations for SDOF system
* u' = v
* v' = (F0 * sin(p * t) - c * v - k * u) / m
*/
function derivatives(t, u, v) {
const u_prime = v;
const v_prime = (state.F0 * Math.sin(state.p * t) - state.c * v - state.k * u) / state.m;
return { u_prime, v_prime };
}
function solve() {
let t = 0;
let u = state.u0; // Initial displacement
let v = state.v0; // Initial velocity
const results = {
t: [],
u: [],
v: []
};
const steps = Math.ceil(state.tMax / state.dt);
for (let i = 0; i <= steps; i++) {
// Store current state
if (i % 10 === 0) { // Downsample for plotting (store every 10th point)
results.t.push(t);
results.u.push(u);
results.v.push(v);
}
// RK4 Steps
const k1 = derivatives(t, u, v);
const t2 = t + state.dt / 2;
const u2 = u + k1.u_prime * state.dt / 2;
const v2 = v + k1.v_prime * state.dt / 2;
const k2 = derivatives(t2, u2, v2);
const t3 = t + state.dt / 2;
const u3 = u + k2.u_prime * state.dt / 2;
const v3 = v + k2.v_prime * state.dt / 2;
const k3 = derivatives(t3, u3, v3);
const t4 = t + state.dt;
const u4 = u + k3.u_prime * state.dt;
const v4 = v + k3.v_prime * state.dt;
const k4 = derivatives(t4, u4, v4);
// Update state
u += (state.dt / 6) * (k1.u_prime + 2 * k2.u_prime + 2 * k3.u_prime + k4.u_prime);
v += (state.dt / 6) * (k1.v_prime + 2 * k2.v_prime + 2 * k3.v_prime + k4.v_prime);
t += state.dt;
}
return results;
}
/**
* Calculate system properties
*/
function calculateProperties() {
const omegaN = Math.sqrt(state.k / state.m);
const zeta = state.c / (2 * Math.sqrt(state.k * state.m));
let regime = '';
if (zeta < 0) regime = 'Unstable';
else if (zeta >= 0 && zeta < 1) regime = 'Underdamped';
else if (zeta === 1) regime = 'Critically Damped';
else regime = 'Overdamped';
return { omegaN, zeta, regime };
}
function updateEquationDisplay(props) {
// 1. Governing Equation
// mu'' + cu' + ku = F0sin(pt)
// mu'' + cu' + ku = F0sin(pt)
const eq = `${state.m.toFixed(1)}u'' + ${state.c.toFixed(1)}u' + ${state.k.toFixed(0)}u = ${state.F0 > 0 ? `${state.F0}sin(${state.p.toFixed(1)}t)` : '0'}`;
elements.equation.math.textContent = eq;
// 2. Solution Form
let solText = "";
if (state.F0 === 0) {
// Free Vibration
if (props.zeta < 1) {
solText = `Free Underdamped: u(t) = e^(-${(props.zeta * props.omegaN).toFixed(2)}t) * (A cos(${(props.omegaN * Math.sqrt(1 - props.zeta ** 2)).toFixed(2)}t) + B sin(...))`;
} else if (props.zeta === 1) {
solText = `Free Critically Damped: u(t) = (A + Bt) * e^(-${props.omegaN.toFixed(2)}t)`;
} else {
solText = `Free Overdamped: u(t) = A * e^(s1*t) + B * e^(s2*t)`;
}
} else {
// Forced Vibration
solText = `Forced Response: u(t) = u_h(t) [Transient] + u_p(t) [Steady State]`;
}
elements.equation.sol.textContent = solText;
}
// --- Rendering ---
function drawChart(canvas, timeData, valueData, color, label) {
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Setup scaling
const padding = 40;
const plotWidth = width - 2 * padding;
const plotHeight = height - 2 * padding;
const minVal = Math.min(...valueData);
const maxVal = Math.max(...valueData);
const range = maxVal - minVal || 1; // Avoid division by zero
// Helper to map coordinates
const mapX = (t) => padding + (t / state.tMax) * plotWidth;
const mapY = (val) => height - padding - ((val - minVal) / range) * plotHeight;
// Draw Grid
ctx.strokeStyle = '#334155';
ctx.lineWidth = 1;
ctx.beginPath();
// Zero line
if (minVal < 0 && maxVal > 0) {
const yZero = mapY(0);
ctx.moveTo(padding, yZero);
ctx.lineTo(width - padding, yZero);
}
ctx.stroke();
// Draw Curve
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(mapX(timeData[0]), mapY(valueData[0]));
for (let i = 1; i < timeData.length; i++) {
ctx.lineTo(mapX(timeData[i]), mapY(valueData[i]));
}
ctx.stroke();
// Draw Axes Labels (Simple)
ctx.fillStyle = '#94a3b8';
ctx.font = '12px Inter';
ctx.fillText('0', padding, height - padding + 15);
ctx.fillText(state.tMax + 's', width - padding - 20, height - padding + 15);
ctx.fillText(maxVal.toFixed(2), 5, padding + 5);
ctx.fillText(minVal.toFixed(2), 5, height - padding + 5);
}
let isUpdating = false;
function updateUI() {
if (isUpdating) return;
isUpdating = true;
requestAnimationFrame(() => {
// 1. Solve
console.log("Solving with state:", JSON.stringify(state));
const data = solve();
const props = calculateProperties();
console.log("Properties:", props);
updateEquationDisplay(props);
// 2. Update Stats
elements.stats.omegaN.textContent = props.omegaN.toFixed(2) + ' rad/s';
elements.stats.zeta.textContent = props.zeta.toFixed(3);
elements.stats.regime.textContent = props.regime;
// Peak values
const maxDisp = Math.max(...data.u.map(Math.abs));
const maxVel = Math.max(...data.v.map(Math.abs));
elements.stats.peakDisp.textContent = maxDisp.toFixed(3);
elements.stats.peakVel.textContent = maxVel.toFixed(3);
// 3. Draw Charts
// Resize canvas to match display size
[elements.canvas.disp, elements.canvas.vel].forEach(canvas => {
const rect = canvas.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
canvas.width = rect.width;
canvas.height = rect.height;
}
});
drawChart(elements.canvas.disp, data.t, data.u, '#38bdf8', 'Displacement');
drawChart(elements.canvas.vel, data.t, data.v, '#4ade80', 'Velocity');
isUpdating = false;
});
}
// --- Event Listeners ---
function handleSliderChange(key) {
return (e) => {
const val = parseFloat(e.target.value);
state[key] = val;
elements.values[key].textContent = val;
updateUI();
};
}
function init() {
console.log("Initializing SDOF Simulator v2");
// Attach listeners
elements.sliders.m.addEventListener('input', handleSliderChange('m'));
elements.sliders.k.addEventListener('input', handleSliderChange('k'));
elements.sliders.c.addEventListener('input', handleSliderChange('c'));
elements.sliders.F0.addEventListener('input', handleSliderChange('F0'));
elements.sliders.p.addEventListener('input', handleSliderChange('p'));
elements.sliders.u0.addEventListener('input', handleSliderChange('u0'));
elements.sliders.v0.addEventListener('input', handleSliderChange('v0'));
// Initial render
updateUI();
// Handle resize
window.addEventListener('resize', updateUI);
}
// Start
init();