Spaces:
Running
Running
| // Interactive Travel-Time Curve | |
| const ttCanvas = document.getElementById('travelTimeCanvas'); | |
| const ttCtx = ttCanvas.getContext('2d'); | |
| let receiverX = 100; | |
| let isDragging = false; | |
| const Vp = 200; // P-wave velocity m/s | |
| const Vs = 120; // S-wave velocity m/s | |
| const Vr = 100; // Rayleigh-wave velocity m/s | |
| function drawTravelTime() { | |
| const w = ttCanvas.width; | |
| const h = ttCanvas.height; | |
| ttCtx.clearRect(0, 0, w, h); | |
| // Part 1: Survey Diagram (Top half) | |
| const groundY = h * 0.2; | |
| ttCtx.fillStyle = '#8b4513'; | |
| ttCtx.fillRect(0, groundY, w, h*0.3); | |
| ttCtx.fillStyle = '#add8e6'; | |
| ttCtx.fillRect(0, 0, w, groundY); | |
| // Source | |
| ttCtx.fillStyle = 'red'; | |
| ttCtx.beginPath(); | |
| ttCtx.arc(50, groundY, 10, 0, Math.PI * 2); | |
| ttCtx.fill(); | |
| ttCtx.fillText('Source', 40, groundY - 15); | |
| // Receiver | |
| ttCtx.fillStyle = 'blue'; | |
| ttCtx.fillRect(receiverX - 5, groundY - 20, 10, 20); | |
| ttCtx.fillText('Receiver', receiverX - 25, groundY - 25); | |
| // Part 2: Travel-Time Graph (Bottom half) | |
| const graphOriginX = 50; | |
| const graphOriginY = h * 0.5; | |
| const graphHeight = h * 0.45; | |
| const graphWidth = w - 60; | |
| // Axes | |
| ttCtx.strokeStyle = 'black'; | |
| ttCtx.beginPath(); | |
| ttCtx.moveTo(graphOriginX, graphOriginY); | |
| ttCtx.lineTo(graphOriginX, graphOriginY + graphHeight); | |
| ttCtx.lineTo(graphOriginX + graphWidth, graphOriginY + graphHeight); | |
| ttCtx.stroke(); | |
| ttCtx.fillText('Time (T)', graphOriginX - 40, graphOriginY + graphHeight / 2); | |
| ttCtx.fillText('Distance (X)', graphOriginX + graphWidth / 2, graphOriginY + graphHeight + 20); | |
| // Plot lines | |
| const plotX = receiverX - 50; | |
| const maxDist = w - 100; | |
| function plotWave(v, color) { | |
| ttCtx.strokeStyle = color; | |
| ttCtx.lineWidth = 2; | |
| ttCtx.beginPath(); | |
| ttCtx.moveTo(graphOriginX, graphOriginY + graphHeight); | |
| // Slope = 1/V. T = X/V. Y_pixel = T * scale_Y. X_pixel = X | |
| // We invert T axis, so T=0 is at the top | |
| let endT = maxDist / v; | |
| let scaleY = graphHeight / (maxDist/Vr); // Scale based on slowest wave | |
| let endY = graphOriginY + graphHeight - (endT * scaleY); | |
| let endX = graphOriginX + maxDist; | |
| // The line in the graph is T vs X, so Y axis is T. | |
| // Y = origin + H - (T*scale) | |
| // T = X/V | |
| // Y = origin + H - (X/V * scaleY) | |
| ttCtx.moveTo(graphOriginX, graphOriginY + graphHeight); | |
| ttCtx.lineTo(graphOriginX+maxDist, graphOriginY+graphHeight - (maxDist/v * scaleY) ); | |
| ttCtx.stroke(); | |
| // Plot point for current receiver | |
| let currentT = plotX / v; | |
| let pointY = graphOriginY + graphHeight - (currentT * scaleY); | |
| ttCtx.fillStyle = color; | |
| ttCtx.beginPath(); | |
| ttCtx.arc(graphOriginX + plotX, pointY, 5, 0, Math.PI * 2); | |
| ttCtx.fill(); | |
| } | |
| plotWave(Vp, '#d90429'); | |
| plotWave(Vs, '#0077b6'); | |
| plotWave(Vr, '#2d6a4f'); | |
| ttCtx.lineWidth = 1; | |
| } | |
| ttCanvas.addEventListener('mousedown', (e) => { | |
| isDragging = true; | |
| }); | |
| ttCanvas.addEventListener('mouseup', () => { | |
| isDragging = false; | |
| }); | |
| ttCanvas.addEventListener('mouseleave', () => { | |
| isDragging = false; | |
| }); | |
| ttCanvas.addEventListener('mousemove', (e) => { | |
| if (isDragging) { | |
| const rect = ttCanvas.getBoundingClientRect(); | |
| let x = e.clientX - rect.left; | |
| if (x > 50 && x < ttCanvas.width - 50) { | |
| receiverX = x; | |
| drawTravelTime(); | |
| } | |
| } | |
| }); | |
| // Interactive Refraction | |
| const refCanvas = document.getElementById('refractionCanvas'); | |
| const refCtx = refCanvas.getContext('2d'); | |
| const slider = document.getElementById('v2-slider'); | |
| const v2ValueSpan = document.getElementById('v2-value'); | |
| const V1 = 1000; | |
| function drawRefraction() { | |
| const w = refCanvas.width; | |
| const h = refCanvas.height; | |
| refCtx.clearRect(0, 0, w, h); | |
| const V2 = slider.value; | |
| v2ValueSpan.textContent = V2; | |
| const interfaceY = h / 2; | |
| // Layers | |
| refCtx.fillStyle = '#f0e68c'; // Layer 1 | |
| refCtx.fillRect(0, 0, w, interfaceY); | |
| refCtx.fillStyle = '#d2b48c'; // Layer 2 | |
| refCtx.fillRect(0, interfaceY, w, h); | |
| refCtx.fillText(`V₁ = ${V1} m/s`, 10, 20); | |
| refCtx.fillText(`V₂ = ${V2} m/s`, 10, interfaceY + 20); | |
| // Incident Ray | |
| const source = { x: 100, y: 0 }; | |
| const incidentPoint = { x: 250, y: interfaceY }; | |
| const angle1 = Math.atan((incidentPoint.x - source.x) / incidentPoint.y); | |
| refCtx.strokeStyle = 'black'; | |
| refCtx.lineWidth = 2; | |
| refCtx.beginPath(); | |
| refCtx.moveTo(source.x, source.y); | |
| refCtx.lineTo(incidentPoint.x, incidentPoint.y); | |
| refCtx.stroke(); | |
| // Refracted Ray (Snell's Law: sin(theta1)/V1 = sin(theta2)/V2) | |
| const sin_theta2 = (V2 / V1) * Math.sin(angle1); | |
| if (sin_theta2 < 1) { // No critical refraction yet | |
| const angle2 = Math.asin(sin_theta2); | |
| const endX = incidentPoint.x + (h / 2) * Math.tan(angle2); | |
| const endY = h; | |
| refCtx.beginPath(); | |
| refCtx.moveTo(incidentPoint.x, incidentPoint.y); | |
| refCtx.lineTo(endX, endY); | |
| refCtx.stroke(); | |
| // Draw angles | |
| refCtx.strokeStyle = 'rgba(0,0,0,0.5)'; | |
| refCtx.lineWidth = 1; | |
| refCtx.beginPath(); | |
| refCtx.moveTo(incidentPoint.x, incidentPoint.y-30); | |
| refCtx.lineTo(incidentPoint.x, incidentPoint.y+30); | |
| refCtx.stroke(); | |
| refCtx.beginPath(); | |
| refCtx.arc(incidentPoint.x, incidentPoint.y, 20, Math.PI/2 - angle1, Math.PI/2); | |
| refCtx.stroke(); | |
| refCtx.fillText('θ₁', incidentPoint.x - 25, incidentPoint.y-5); | |
| refCtx.beginPath(); | |
| refCtx.arc(incidentPoint.x, incidentPoint.y, 20, Math.PI/2, Math.PI/2 + angle2); | |
| refCtx.stroke(); | |
| refCtx.fillText('θ₂', incidentPoint.x - 25, incidentPoint.y+20); | |
| } else { // Critical refraction | |
| refCtx.beginPath(); | |
| refCtx.moveTo(incidentPoint.x, incidentPoint.y); | |
| refCtx.lineTo(w, incidentPoint.y); | |
| refCtx.stroke(); | |
| refCtx.fillStyle = 'red'; | |
| refCtx.fillText('Critical Refraction!', incidentPoint.x + 10, incidentPoint.y-10); | |
| } | |
| } | |
| slider.addEventListener('input', drawRefraction); | |
| // Initial draws | |
| window.onload = () => { | |
| drawTravelTime(); | |
| drawRefraction(); | |
| }; | |