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