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