/** * 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();