Spaces:
Running
Running
Create script.js
Browse files
script.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Interactive Travel-Time Curve
|
| 2 |
+
const ttCanvas = document.getElementById('travelTimeCanvas');
|
| 3 |
+
const ttCtx = ttCanvas.getContext('2d');
|
| 4 |
+
|
| 5 |
+
let receiverX = 100;
|
| 6 |
+
let isDragging = false;
|
| 7 |
+
|
| 8 |
+
const Vp = 200; // P-wave velocity m/s
|
| 9 |
+
const Vs = 120; // S-wave velocity m/s
|
| 10 |
+
const Vr = 100; // Rayleigh-wave velocity m/s
|
| 11 |
+
|
| 12 |
+
function drawTravelTime() {
|
| 13 |
+
const w = ttCanvas.width;
|
| 14 |
+
const h = ttCanvas.height;
|
| 15 |
+
ttCtx.clearRect(0, 0, w, h);
|
| 16 |
+
|
| 17 |
+
// Part 1: Survey Diagram (Top half)
|
| 18 |
+
const groundY = h * 0.2;
|
| 19 |
+
ttCtx.fillStyle = '#8b4513';
|
| 20 |
+
ttCtx.fillRect(0, groundY, w, h*0.3);
|
| 21 |
+
ttCtx.fillStyle = '#add8e6';
|
| 22 |
+
ttCtx.fillRect(0, 0, w, groundY);
|
| 23 |
+
|
| 24 |
+
// Source
|
| 25 |
+
ttCtx.fillStyle = 'red';
|
| 26 |
+
ttCtx.beginPath();
|
| 27 |
+
ttCtx.arc(50, groundY, 10, 0, Math.PI * 2);
|
| 28 |
+
ttCtx.fill();
|
| 29 |
+
ttCtx.fillText('Source', 40, groundY - 15);
|
| 30 |
+
|
| 31 |
+
// Receiver
|
| 32 |
+
ttCtx.fillStyle = 'blue';
|
| 33 |
+
ttCtx.fillRect(receiverX - 5, groundY - 20, 10, 20);
|
| 34 |
+
ttCtx.fillText('Receiver', receiverX - 25, groundY - 25);
|
| 35 |
+
|
| 36 |
+
// Part 2: Travel-Time Graph (Bottom half)
|
| 37 |
+
const graphOriginX = 50;
|
| 38 |
+
const graphOriginY = h * 0.5;
|
| 39 |
+
const graphHeight = h * 0.45;
|
| 40 |
+
const graphWidth = w - 60;
|
| 41 |
+
|
| 42 |
+
// Axes
|
| 43 |
+
ttCtx.strokeStyle = 'black';
|
| 44 |
+
ttCtx.beginPath();
|
| 45 |
+
ttCtx.moveTo(graphOriginX, graphOriginY);
|
| 46 |
+
ttCtx.lineTo(graphOriginX, graphOriginY + graphHeight);
|
| 47 |
+
ttCtx.lineTo(graphOriginX + graphWidth, graphOriginY + graphHeight);
|
| 48 |
+
ttCtx.stroke();
|
| 49 |
+
ttCtx.fillText('Time (T)', graphOriginX - 40, graphOriginY + graphHeight / 2);
|
| 50 |
+
ttCtx.fillText('Distance (X)', graphOriginX + graphWidth / 2, graphOriginY + graphHeight + 20);
|
| 51 |
+
|
| 52 |
+
// Plot lines
|
| 53 |
+
const plotX = receiverX - 50;
|
| 54 |
+
const maxDist = w - 100;
|
| 55 |
+
|
| 56 |
+
function plotWave(v, color) {
|
| 57 |
+
ttCtx.strokeStyle = color;
|
| 58 |
+
ttCtx.lineWidth = 2;
|
| 59 |
+
ttCtx.beginPath();
|
| 60 |
+
ttCtx.moveTo(graphOriginX, graphOriginY + graphHeight);
|
| 61 |
+
// Slope = 1/V. T = X/V. Y_pixel = T * scale_Y. X_pixel = X
|
| 62 |
+
// We invert T axis, so T=0 is at the top
|
| 63 |
+
let endT = maxDist / v;
|
| 64 |
+
let scaleY = graphHeight / (maxDist/Vr); // Scale based on slowest wave
|
| 65 |
+
let endY = graphOriginY + graphHeight - (endT * scaleY);
|
| 66 |
+
let endX = graphOriginX + maxDist;
|
| 67 |
+
// The line in the graph is T vs X, so Y axis is T.
|
| 68 |
+
// Y = origin + H - (T*scale)
|
| 69 |
+
// T = X/V
|
| 70 |
+
// Y = origin + H - (X/V * scaleY)
|
| 71 |
+
ttCtx.moveTo(graphOriginX, graphOriginY + graphHeight);
|
| 72 |
+
ttCtx.lineTo(graphOriginX+maxDist, graphOriginY+graphHeight - (maxDist/v * scaleY) );
|
| 73 |
+
ttCtx.stroke();
|
| 74 |
+
|
| 75 |
+
// Plot point for current receiver
|
| 76 |
+
let currentT = plotX / v;
|
| 77 |
+
let pointY = graphOriginY + graphHeight - (currentT * scaleY);
|
| 78 |
+
ttCtx.fillStyle = color;
|
| 79 |
+
ttCtx.beginPath();
|
| 80 |
+
ttCtx.arc(graphOriginX + plotX, pointY, 5, 0, Math.PI * 2);
|
| 81 |
+
ttCtx.fill();
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
plotWave(Vp, '#d90429');
|
| 85 |
+
plotWave(Vs, '#0077b6');
|
| 86 |
+
plotWave(Vr, '#2d6a4f');
|
| 87 |
+
|
| 88 |
+
ttCtx.lineWidth = 1;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
ttCanvas.addEventListener('mousedown', (e) => {
|
| 92 |
+
isDragging = true;
|
| 93 |
+
});
|
| 94 |
+
ttCanvas.addEventListener('mouseup', () => {
|
| 95 |
+
isDragging = false;
|
| 96 |
+
});
|
| 97 |
+
ttCanvas.addEventListener('mouseleave', () => {
|
| 98 |
+
isDragging = false;
|
| 99 |
+
});
|
| 100 |
+
ttCanvas.addEventListener('mousemove', (e) => {
|
| 101 |
+
if (isDragging) {
|
| 102 |
+
const rect = ttCanvas.getBoundingClientRect();
|
| 103 |
+
let x = e.clientX - rect.left;
|
| 104 |
+
if (x > 50 && x < ttCanvas.width - 50) {
|
| 105 |
+
receiverX = x;
|
| 106 |
+
drawTravelTime();
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
// Interactive Refraction
|
| 112 |
+
const refCanvas = document.getElementById('refractionCanvas');
|
| 113 |
+
const refCtx = refCanvas.getContext('2d');
|
| 114 |
+
const slider = document.getElementById('v2-slider');
|
| 115 |
+
const v2ValueSpan = document.getElementById('v2-value');
|
| 116 |
+
|
| 117 |
+
const V1 = 1000;
|
| 118 |
+
|
| 119 |
+
function drawRefraction() {
|
| 120 |
+
const w = refCanvas.width;
|
| 121 |
+
const h = refCanvas.height;
|
| 122 |
+
refCtx.clearRect(0, 0, w, h);
|
| 123 |
+
|
| 124 |
+
const V2 = slider.value;
|
| 125 |
+
v2ValueSpan.textContent = V2;
|
| 126 |
+
|
| 127 |
+
const interfaceY = h / 2;
|
| 128 |
+
|
| 129 |
+
// Layers
|
| 130 |
+
refCtx.fillStyle = '#f0e68c'; // Layer 1
|
| 131 |
+
refCtx.fillRect(0, 0, w, interfaceY);
|
| 132 |
+
refCtx.fillStyle = '#d2b48c'; // Layer 2
|
| 133 |
+
refCtx.fillRect(0, interfaceY, w, h);
|
| 134 |
+
refCtx.fillText(`V₁ = ${V1} m/s`, 10, 20);
|
| 135 |
+
refCtx.fillText(`V₂ = ${V2} m/s`, 10, interfaceY + 20);
|
| 136 |
+
|
| 137 |
+
// Incident Ray
|
| 138 |
+
const source = { x: 100, y: 0 };
|
| 139 |
+
const incidentPoint = { x: 250, y: interfaceY };
|
| 140 |
+
const angle1 = Math.atan((incidentPoint.x - source.x) / incidentPoint.y);
|
| 141 |
+
|
| 142 |
+
refCtx.strokeStyle = 'black';
|
| 143 |
+
refCtx.lineWidth = 2;
|
| 144 |
+
refCtx.beginPath();
|
| 145 |
+
refCtx.moveTo(source.x, source.y);
|
| 146 |
+
refCtx.lineTo(incidentPoint.x, incidentPoint.y);
|
| 147 |
+
refCtx.stroke();
|
| 148 |
+
|
| 149 |
+
// Refracted Ray (Snell's Law: sin(theta1)/V1 = sin(theta2)/V2)
|
| 150 |
+
const sin_theta2 = (V2 / V1) * Math.sin(angle1);
|
| 151 |
+
if (sin_theta2 < 1) { // No critical refraction yet
|
| 152 |
+
const angle2 = Math.asin(sin_theta2);
|
| 153 |
+
const endX = incidentPoint.x + (h / 2) * Math.tan(angle2);
|
| 154 |
+
const endY = h;
|
| 155 |
+
|
| 156 |
+
refCtx.beginPath();
|
| 157 |
+
refCtx.moveTo(incidentPoint.x, incidentPoint.y);
|
| 158 |
+
refCtx.lineTo(endX, endY);
|
| 159 |
+
refCtx.stroke();
|
| 160 |
+
|
| 161 |
+
// Draw angles
|
| 162 |
+
refCtx.strokeStyle = 'rgba(0,0,0,0.5)';
|
| 163 |
+
refCtx.lineWidth = 1;
|
| 164 |
+
refCtx.beginPath();
|
| 165 |
+
refCtx.moveTo(incidentPoint.x, incidentPoint.y-30);
|
| 166 |
+
refCtx.lineTo(incidentPoint.x, incidentPoint.y+30);
|
| 167 |
+
refCtx.stroke();
|
| 168 |
+
refCtx.beginPath();
|
| 169 |
+
refCtx.arc(incidentPoint.x, incidentPoint.y, 20, Math.PI/2 - angle1, Math.PI/2);
|
| 170 |
+
refCtx.stroke();
|
| 171 |
+
refCtx.fillText('θ₁', incidentPoint.x - 25, incidentPoint.y-5);
|
| 172 |
+
|
| 173 |
+
refCtx.beginPath();
|
| 174 |
+
refCtx.arc(incidentPoint.x, incidentPoint.y, 20, Math.PI/2, Math.PI/2 + angle2);
|
| 175 |
+
refCtx.stroke();
|
| 176 |
+
refCtx.fillText('θ₂', incidentPoint.x - 25, incidentPoint.y+20);
|
| 177 |
+
|
| 178 |
+
} else { // Critical refraction
|
| 179 |
+
refCtx.beginPath();
|
| 180 |
+
refCtx.moveTo(incidentPoint.x, incidentPoint.y);
|
| 181 |
+
refCtx.lineTo(w, incidentPoint.y);
|
| 182 |
+
refCtx.stroke();
|
| 183 |
+
refCtx.fillStyle = 'red';
|
| 184 |
+
refCtx.fillText('Critical Refraction!', incidentPoint.x + 10, incidentPoint.y-10);
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
slider.addEventListener('input', drawRefraction);
|
| 189 |
+
|
| 190 |
+
// Initial draws
|
| 191 |
+
window.onload = () => {
|
| 192 |
+
drawTravelTime();
|
| 193 |
+
drawRefraction();
|
| 194 |
+
};
|