diff --git a/content/blog/why-signals-bend.md b/content/blog/why-signals-bend.md index 6d44d7e..49e77e6 100644 --- a/content/blog/why-signals-bend.md +++ b/content/blog/why-signals-bend.md @@ -140,7 +140,9 @@ A few things I didn't expect going in, now painfully clear from the data.
-**Humidity is a trade.** At 10 GHz, 20 to 30 mm of precipitable water is the sweet spot. Too dry and the surface refractivity stays low, too wet and the column destabilizes. At 24 GHz, drier is always better, because the 22.235 GHz water vapor line happily eats whatever signal the atmosphere just delivered. +**Humidity is a trade.** Precipitable water, or PWAT, is the total amount of water vapor in a column of atmosphere above you, expressed as the depth you would get if you condensed all of it into liquid and let it puddle on the ground. A PWAT of 25 mm means that if every water molecule above your antenna fell out of the sky at once, you would be standing in an inch of water. It is the number forecasters use to describe how wet the air is in bulk, and for microwave propagation it is the variable that matters, because the refractivity gradient that bends signals depends on water vapor more than any other single thing. + +At 10 GHz, 20 to 30 mm of PWAT is the sweet spot. Too dry and the surface refractivity stays low, too wet and the column destabilizes. At 24 GHz, drier is always better, because the 22.235 GHz water vapor line happily eats whatever signal the atmosphere just delivered.
diff --git a/static/js/nlos.js b/static/js/nlos.js index ff9ceec..3ee8f1d 100644 --- a/static/js/nlos.js +++ b/static/js/nlos.js @@ -302,8 +302,8 @@ drawAntenna(ctx, xA, groundY, h1px, COLORS.orange); drawAntenna(ctx, xB, groundY, h2px, COLORS.orange); - labelText(ctx, 'A', xA, groundY - h1px - 10, COLORS.orange, 'center'); - labelText(ctx, 'B', xB, groundY - h2px - 10, COLORS.orange, 'center'); + pillLabel(ctx, 'A', xA, groundY - h1px - 12, COLORS.orange, 'center', 'middle'); + pillLabel(ctx, 'B', xB, groundY - h2px - 12, COLORS.orange, 'center', 'middle'); s.setReadout('status', blocked ? 'BLOCKED' : 'CLEAR'); s.setReadout('clearance', `${clearanceM.toFixed(1)} m`); @@ -419,7 +419,7 @@ ctx.beginPath(); ctx.arc(cx, peakY, 3, 0, Math.PI * 2); ctx.fill(); - labelText(ctx, `bulge ${bulgeM.toFixed(1)} m`, cx, peakY - 8, COLORS.yellow, 'center'); + pillLabel(ctx, `bulge ${bulgeM.toFixed(1)} m`, cx, peakY - 10, COLORS.yellow, 'center', 'middle'); s.setReadout('horizonA', `${dH1.toFixed(1)} km`); s.setReadout('horizonB', `${dH2.toFixed(1)} km`); @@ -657,7 +657,7 @@ ctx.strokeStyle = COLORS.dim; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(xA, yLOS); ctx.lineTo(xB, yLOS); ctx.stroke(); - labelText(ctx, 'line of sight', (xA + xB) / 2, yLOS - 6, COLORS.dim, 'center'); + pillLabel(ctx, 'line of sight', (xA + xB) / 2, yLOS - 10, COLORS.dim, 'center', 'middle'); // knife edge ctx.fillStyle = '#3a4150'; @@ -689,7 +689,7 @@ ctx.lineTo(obsX + 17, obsTopY + 4 * dir); ctx.closePath(); ctx.fill(); - labelText(ctx, `h = ${obsH.toFixed(1)} m`, obsX + 20, (yLOS + obsTopY) / 2, COLORS.yellow, 'left', 'middle'); + pillLabel(ctx, `h = ${obsH.toFixed(1)} m`, obsX + 20, (yLOS + obsTopY) / 2, COLORS.yellow, 'left', 'middle'); } // Compute v @@ -718,8 +718,8 @@ drawAntenna(ctx, xA, yLOS, 0, COLORS.orange); drawAntenna(ctx, xB, yLOS, 0, COLORS.orange); - labelText(ctx, 'A', xA, yLOS + 14, COLORS.orange, 'center'); - labelText(ctx, 'B', xB, yLOS + 14, COLORS.orange, 'center'); + pillLabel(ctx, 'A', xA, yLOS + 18, COLORS.orange, 'center', 'middle'); + pillLabel(ctx, 'B', xB, yLOS + 18, COLORS.orange, 'center', 'middle'); // Distance bracket along the bottom const bracketY = obsBottomY + 18; @@ -775,11 +775,11 @@ ctx.beginPath(); ctx.moveTo(xOf(0), pY); ctx.lineTo(xOf(0), pY + pH); ctx.stroke(); ctx.beginPath(); ctx.moveTo(pX, yOf(6)); ctx.lineTo(pX + pW, yOf(6)); ctx.stroke(); ctx.restore(); - labelText(ctx, 'grazing (ν=0, 6 dB)', xOf(0) + 4, yOf(6) - 4, COLORS.dim); + pillLabel(ctx, 'grazing (ν=0, 6 dB)', xOf(0) + 6, yOf(6) - 10, COLORS.dim, 'left', 'middle'); // Region labels - labelText(ctx, 'free space', xOf(-2), pY + 14, COLORS.green); - labelText(ctx, 'deep shadow', xOf(3.2), pY + 14, COLORS.red); + pillLabel(ctx, 'free space', pX + 6, pY + 14, COLORS.green, 'left', 'middle'); + pillLabel(ctx, 'deep shadow', pX + pW - 6, pY + 14, COLORS.red, 'right', 'middle'); // J(v) curve ctx.strokeStyle = COLORS.blue; @@ -793,7 +793,7 @@ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); - labelText(ctx, 'J(ν) — ITU-R P.526', xOf(1.2), yOf(12), COLORS.blue); + pillLabel(ctx, 'J(ν) — ITU-R P.526', xOf(1.2), yOf(12), COLORS.blue, 'left', 'middle'); // Current point with readout annotation const px = xOf(v), py = yOf(lossDb); @@ -806,7 +806,15 @@ ctx.moveTo(px, py); ctx.lineTo(pX, py); ctx.stroke(); ctx.setLineDash([]); - labelText(ctx, `ν = ${v.toFixed(2)}, ${lossDb.toFixed(1)} dB`, px + 8, py - 8, COLORS.yellow); + { + const rtext = `ν = ${v.toFixed(2)}, ${lossDb.toFixed(1)} dB`; + ctx.save(); + ctx.font = '12px system-ui, sans-serif'; + const rw = ctx.measureText(rtext).width + 16; + ctx.restore(); + const rightSide = px + 8 + rw < pX + pW; + pillLabel(ctx, rtext, rightSide ? px + 8 : px - 8, py - 10, COLORS.yellow, rightSide ? 'left' : 'right', 'middle'); + } s.setReadout('v', v.toFixed(2)); s.setReadout('loss', `${lossDb.toFixed(1)} dB`); @@ -1263,12 +1271,12 @@ if (hit) { ctx.fillStyle = rayColor; ctx.beginPath(); ctx.arc(hit.x, hit.y, 5, 0, Math.PI * 2); ctx.fill(); - labelText(ctx, `${(hit.dxm / 1000).toFixed(1)} km`, hit.x, hit.y - 10, rayColor, 'center'); + pillLabel(ctx, `${(hit.dxm / 1000).toFixed(1)} km`, hit.x, hit.y - 12, rayColor, 'center', 'middle'); } // Antenna drawAntenna(ctx, xLaunch, launchGroundY, s.values.height * pxPerM_y, COLORS.orange); - labelText(ctx, 'TX', xLaunch + 8, launchGroundY - s.values.height * pxPerM_y - 6, COLORS.orange); + pillLabel(ctx, 'TX', xLaunch + 8, launchGroundY - s.values.height * pxPerM_y - 10, COLORS.orange, 'left', 'middle'); // Legend labelText(ctx, 'effective-Earth view (ground curves at d²/2R). Past rays fade as you move sliders.', margin.l + 4, margin.t + 14, COLORS.dim); @@ -1338,7 +1346,7 @@ ctx.lineTo(margin.l + pW, groundY - duct * pxPerM_y); ctx.stroke(); ctx.setLineDash([]); - labelText(ctx, `duct top (${duct} m)`, margin.l + pW - 6, groundY - duct * pxPerM_y - 4, COLORS.cyan, 'right'); + pillLabel(ctx, `duct top (${duct} m)`, margin.l + pW - 6, groundY - duct * pxPerM_y - 8, COLORS.cyan, 'right', 'middle'); // Within the duct, effective-earth ray curvature corresponds to dN/dh = -dN/thickness_km. // Outside, standard atmosphere. @@ -1579,7 +1587,7 @@ ctx.lineTo(ax + 12, ay + 4); ctx.closePath(); ctx.fill(); - labelText(ctx, annotText, ax + 34, ay, COLORS.yellow, 'left', 'middle'); + pillLabel(ctx, annotText, ax + 34, ay, COLORS.yellow, 'left', 'middle'); // ---- Right panel: ray paths ---- const qX = leftW + 55, qY = 30, qW = w - qX - 15, qH = h - 60; @@ -1618,9 +1626,9 @@ ctx.fillRect(qX, yrayOf(0), qW, qY + qH - yrayOf(0)); // Duct-band labels - if (active === 'surface') labelText(ctx, 'duct region (ground to ~100 m)', qX + 8, yrayOf(100) - 4, COLORS.cyan); - if (active === 'elevated') labelText(ctx, 'duct region (200–350 m)', qX + 8, yrayOf(350) - 4, COLORS.cyan); - if (active === 'evap') labelText(ctx, 'duct region (sea surface to ~30 m)', qX + 8, yrayOf(30) - 4, COLORS.cyan); + if (active === 'surface') pillLabel(ctx, 'duct region (ground to ~100 m)', qX + 8, yrayOf(100) - 10, COLORS.cyan, 'left', 'middle'); + if (active === 'elevated') pillLabel(ctx, 'duct region (200–350 m)', qX + 8, yrayOf(350) - 10, COLORS.cyan, 'left', 'middle'); + if (active === 'evap') pillLabel(ctx, 'duct region (sea surface to ~30 m)', qX + 8, yrayOf(30) - 10, COLORS.cyan, 'left', 'middle'); // Illustrative signal paths (schematic) ctx.strokeStyle = COLORS.green; @@ -1774,11 +1782,11 @@ ctx.beginPath(); ctx.moveTo(margin.l, yOf(ductTop)); ctx.lineTo(margin.l + pW, yOf(ductTop)); ctx.stroke(); ctx.beginPath(); ctx.moveTo(margin.l, yOf(ductBottom)); ctx.lineTo(margin.l + pW, yOf(ductBottom)); ctx.stroke(); ctx.setLineDash([]); - labelText(ctx, 'elevated duct', margin.l + 8, yOf(ductTop) + 14, COLORS.cyan); + pillLabel(ctx, 'elevated duct', margin.l + 8, yOf(ductTop) + 14, COLORS.cyan, 'left', 'middle'); // Earth-curvature annotation near the middle - labelText(ctx, `Earth bulge at midpoint: ${maxBulge.toFixed(0)} m (k = 4/3)`, - margin.l + pW / 2, margin.t + pH - 6, COLORS.dim, 'center'); + pillLabel(ctx, `Earth bulge at midpoint: ${maxBulge.toFixed(0)} m (k = 4/3)`, + margin.l + pW / 2, margin.t + pH - 10, COLORS.dim, 'center', 'middle'); // Terrain fill ctx.fillStyle = '#2b3039';