123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
<!DOCTYPE html>
<html lang="en">
<head><script>(function(){var n=0;function show(p,m){var el=document.createElement('div');el.style.cssText='position:fixed;left:0;right:0;background:#e04040;color:#fff;padding:6px 12px;z-index:'+(99999-n)+';font:13px/1.4 monospace;white-space:pre-wrap;border-bottom:1px solid #b02020;top:'+n*30+'px';el.textContent=p+m;document.body.appendChild(el);n++}window.onerror=function(msg,src,line){show('ERROR '+(src||'').split('/').pop()+':'+line+' \u2014 ',msg)};window.addEventListener('unhandledrejection',function(e){show('REJECT \u2014 ',e.reason&&e.reason.message||e.reason||'unknown')})})()</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Luminous Intensity Distribution</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #fff;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    font-family: Arial, Helvetica, sans-serif;
    padding: 12px;
  }
  h2 { margin-bottom: 4px; font-size: 18px; }
  .chart-container { position: relative; width: 100%; max-width: 700px; }
  #polar-container { aspect-ratio: 1 / 0.62; }
  #cross-container { aspect-ratio: 1 / 0.5; }
  #heatmap-container { aspect-ratio: 1 / 0.7; margin-top: 8px; }
  canvas { width: 100%; height: 100%; cursor: crosshair; }
  .tooltip {
    position: absolute; display: none; background: rgba(0,0,0,0.82);
    color: #fff; padding: 6px 10px; border-radius: 5px; font-size: 13px;
    pointer-events: none; white-space: nowrap; z-index: 10;
  }
  #readout, #readout2, #readoutHM {
    margin-top: 6px; font-size: 13px; color: #333; min-height: 18px;
  }
  .controls-table { margin-top: 12px; border-collapse: collapse; font-size: 13px; }
  .controls-table th { font-weight: 600; padding: 2px 6px; text-align: center; color: #666; font-size: 11px; }
  .controls-table td { padding: 3px 4px; vertical-align: middle; }
  .controls-table .param-label { font-weight: bold; font-size: 14px; text-align: right; padding-right: 6px; }
  .controls-table input[type=range] { width: 130px; vertical-align: middle; }
  .controls-table input[type=number] {
    width: 58px; font-size: 13px; font-weight: bold; text-align: center;
    border: 1px solid #ccc; border-radius: 3px; padding: 2px 4px; background: #fafafa;
  }
  .controls-table input[type=number]:focus { outline: none; border-color: #2e7d32; background: #fff; }
  .controls-table .cfg-input {
    width: 48px; font-size: 11px; text-align: center;
    border: 1px solid #ddd; border-radius: 2px; padding: 1px 2px; color: #666;
  }
  .controls-table .cfg-input:focus { outline: none; border-color: #999; }
  .section-title { font-size: 16px; font-weight: bold; margin-top: 18px; margin-bottom: 2px; }
  .legend { display: flex; align-items: center; gap: 14px; margin-top: 6px; font-size: 12px; color: #555; }
  .legend-item { display: flex; align-items: center; gap: 4px; }
  .legend-swatch { width: 20px; height: 3px; border-radius: 1px; }
  .color-legend { display: flex; align-items: center; gap: 8px; margin-top: 4px; font-size: 11px; color: #666; }
  .reflector-box {
    display: flex; align-items: center; gap: 10px; margin-top: 8px;
    padding: 6px 10px; background: #f8f6fa; border: 1px solid #d0c0e0;
    border-radius: 5px; font-size: 13px;
  }
  .reflector-box label { font-weight: bold; color: #555; }
  .reflector-box input[type=number] {
    width: 54px; font-size: 13px; font-weight: bold; text-align: center;
    border: 1px solid #ccc; border-radius: 3px; padding: 2px 4px;
  }
  .reflector-box input[type=number]:focus { outline: none; border-color: #7b1fa2; }
  .preset-btn {
    padding: 3px 7px; font-size: 11px; border: 1px solid #ccc;
    border-radius: 3px; background: #fff; cursor: pointer; color: #444;
  }
  .preset-btn:hover { background: #f0e8f5; border-color: #7b1fa2; }
  .preset-btn.active { background: #7b1fa2; color: #fff; border-color: #7b1fa2; }
  .power-section {
    margin-top: 10px; padding: 8px 12px; background: #f6f9ff;
    border: 1px solid #c0d0e8; border-radius: 5px; font-size: 13px;
    display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  }
  .power-section label { font-weight: bold; color: #555; }
  .power-section input[type=number] {
    width: 70px; font-size: 13px; font-weight: bold; text-align: center;
    border: 1px solid #ccc; border-radius: 3px; padding: 2px 4px;
  }
  .power-section .power-readout {
    padding: 3px 8px; background: #e8f0fe; border: 1px solid #b0c4de;
    border-radius: 3px; font-weight: bold; color: #1565c0; font-size: 12px;
  }
  .power-section .sep { color: #ccc; font-size: 16px; }
  .power-calc {
    margin-top: 6px; padding: 6px 12px; background: #fff8f0;
    border: 1px solid #e0d0b8; border-radius: 5px; font-size: 13px;
    display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  }
  .power-calc label { font-weight: bold; color: #555; }
  .power-calc input[type=number] {
    width: 80px; font-size: 13px; font-weight: bold; text-align: center;
    border: 1px solid #ccc; border-radius: 3px; padding: 2px 4px;
  }
  .power-calc .calc-result {
    padding: 3px 8px; background: #fef3e8; border: 1px solid #e0c8a0;
    border-radius: 3px; font-weight: bold; color: #c45e00; font-size: 12px;
  }
</style>
</head>
<body>

<h2>Luminous Intensity Distribution</h2>
<div id="readout">&nbsp;</div>
<div id="polar-container" class="chart-container">
  <canvas id="polarChart"></canvas>
  <div id="tooltip1" class="tooltip"></div>
</div>

<div class="section-title">Illuminance on Target Panel</div>
<table class="controls-table">
  <thead><tr><th></th><th>Value</th><th>Slider</th><th>Min</th><th>Max</th><th>Step</th></tr></thead>
  <tbody>
    <tr>
      <td class="param-label">h</td>
      <td><input type="number" id="hNum" value="100" step="0.5"></td>
      <td><input type="range" id="hSlider" min="1" max="200" step="0.5" value="100"></td>
      <td><input type="number" class="cfg-input" id="hMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="hMax" value="200" step="any"></td>
      <td><input type="number" class="cfg-input" id="hStep" value="0.5" step="any"></td>
    </tr>
    <tr>
      <td class="param-label">d</td>
      <td><input type="number" id="dNum" value="30" step="0.5"></td>
      <td><input type="range" id="dSlider" min="1" max="100" step="0.5" value="30"></td>
      <td><input type="number" class="cfg-input" id="dMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="dMax" value="100" step="any"></td>
      <td><input type="number" class="cfg-input" id="dStep" value="0.5" step="any"></td>
    </tr>
    <tr>
      <td class="param-label">cols</td>
      <td><input type="number" id="colsNum" value="12" step="1"></td>
      <td><input type="range" id="colsSlider" min="1" max="100" step="1" value="12"></td>
      <td><input type="number" class="cfg-input" id="colsMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="colsMax" value="100" step="any"></td>
      <td><input type="number" class="cfg-input" id="colsStep" value="1" step="any"></td>
    </tr>
    <tr>
      <td class="param-label">rows</td>
      <td><input type="number" id="rowsNum" value="14" step="1"></td>
      <td><input type="range" id="rowsSlider" min="1" max="100" step="1" value="14"></td>
      <td><input type="number" class="cfg-input" id="rowsMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="rowsMax" value="100" step="any"></td>
      <td><input type="number" class="cfg-input" id="rowsStep" value="1" step="any"></td>
    </tr>
    <tr>
      <td class="param-label">l (panel)</td>
      <td><input type="number" id="lNum" value="300" step="0.5"></td>
      <td><input type="range" id="lSlider" min="1" max="500" step="0.5" value="300"></td>
      <td><input type="number" class="cfg-input" id="lMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="lMax" value="500" step="any"></td>
      <td><input type="number" class="cfg-input" id="lStep" value="0.5" step="any"></td>
    </tr>
    <tr>
      <td class="param-label">w (panel)</td>
      <td><input type="number" id="wNum" value="340" step="0.5"></td>
      <td><input type="range" id="wSlider" min="1" max="500" step="0.5" value="340"></td>
      <td><input type="number" class="cfg-input" id="wMin" value="1" step="any"></td>
      <td><input type="number" class="cfg-input" id="wMax" value="500" step="any"></td>
      <td><input type="number" class="cfg-input" id="wStep" value="0.5" step="any"></td>
    </tr>
  </tbody>
</table>

<div class="reflector-box">
  <label>Reflectors:</label>
  <span style="font-size:12px;color:#666;">R =</span>
  <input type="number" id="reflNum" value="88" min="0" max="100" step="1">
  <span style="font-size:12px;color:#666;">%</span>
  <button class="preset-btn" data-r="100">Mirror</button>
  <button class="preset-btn active" data-r="88">Aluminum</button>
  <button class="preset-btn" data-r="75">White card</button>
  <button class="preset-btn" data-r="5">Black card</button>
  <button class="preset-btn" data-r="0">Void</button>
</div>

<div class="power-section">
  <label>Power per LED:</label>
  <input type="number" id="ledPower" value="500" min="0" step="10"> <span style="font-size:12px;color:#666;">mW</span>
  <span class="sep">|</span>
  <span class="power-readout" id="totalPowerOut">Total: 0 W</span>
  <span class="power-readout" id="panelAbsorbed">Efficiency: 0%</span>
  <span class="power-readout" id="planeReceived">Target receives: 0 W</span>
</div>
<div class="power-calc">
  <label>Energy calc:</label>
  <span style="font-size:12px;color:#666;">Time:</span>
  <input type="number" id="calcTime" value="60" min="0" step="1">
  <span style="font-size:12px;color:#666;">sec</span>
  <span class="calc-result" id="calcJoules">= 0 J/cm²</span>
  <span class="sep" style="color:#ccc;font-size:16px;">|</span>
  <span style="font-size:12px;color:#666;">Target:</span>
  <input type="number" id="calcTarget" value="5" min="0" step="0.1">
  <span style="font-size:12px;color:#666;">J/cm²</span>
  <span class="calc-result" id="calcTimeNeeded">= 0 sec</span>
</div>

<div class="legend">
  <div class="legend-item"><div class="legend-swatch" style="background:#1e64dc;height:4px;"></div> Cross-section (y=0)</div>
  <label style="display:flex;align-items:center;gap:4px;font-size:12px;color:#555;cursor:pointer;"><input type="checkbox" id="showLeds" checked> Show LEDs</label>
</div>
<div class="color-legend">
  <span>0%</span>
  <canvas id="colorLegendBar" width="120" height="12" style="width:120px;height:12px;border:1px solid #ccc;border-radius:2px;"></canvas>
  <span>100%</span>
  <span style="margin-left:4px;">(panel intensity)</span>
  <span id="deltaReadout" style="margin-left:12px;padding:2px 8px;background:#f0faf0;border:1px solid #b0d8b0;border-radius:3px;font-weight:bold;color:#2e7d32;"></span>
</div>

<div class="section-title">Top View (looking down at panel)</div>
<div id="readoutHM">&nbsp;</div>
<div id="heatmap-container" class="chart-container">
  <canvas id="heatmapChart"></canvas>
  <div id="tooltipHM" class="tooltip"></div>
</div>

<div class="section-title">Cross-Section (y = 0)</div>
<div id="readout2">&nbsp;</div>
<div id="cross-container" class="chart-container">
  <canvas id="crossChart"></canvas>
  <div id="tooltip2" class="tooltip"></div>
</div>

<script>
const dpr = window.devicePixelRatio || 1;

function intensity(angleDeg) { return Math.max(0, Math.cos(angleDeg * Math.PI / 180)); }

function illuminanceAt2D(dx, dy, h) {
  const r2 = dx * dx + dy * dy + h * h;
  const cosT = h / Math.sqrt(r2);
  return cosT * cosT * cosT / (h * h);
}

// Green color: 0 = white, 1 = green (#2e7d32)
function intensityColor(frac) {
  const f = Math.max(0, Math.min(1, frac));
  const r = Math.round(255 + (46 - 255) * f);
  const g = Math.round(255 + (125 - 255) * f);
  const b = Math.round(255 + (50 - 255) * f);
  return `rgb(${r},${g},${b})`;
}

(function drawColorLegend() {
  const c = document.getElementById('colorLegendBar');
  const cx = c.getContext('2d');
  for (let i = 0; i < 120; i++) { cx.fillStyle = intensityColor(i / 119); cx.fillRect(i, 0, 1, 12); }
})();

// ─── State ───
let hDist = 100, dSpacing = 30, nCols = 12, nRows = 14;
let panelL = 300, panelW = 340, reflectivity = 0.88;

// Source grid positions (centered at origin)
function getSourceGrid() {
  const sources = [];
  for (let r = 0; r < nRows; r++) {
    for (let c = 0; c < nCols; c++) {
      sources.push({
        x: (c - (nCols - 1) / 2) * dSpacing,
        y: (r - (nRows - 1) / 2) * dSpacing
      });
    }
  }
  return sources;
}

function getReflectorBounds() {
  return {
    xL: -nCols * dSpacing / 2, xR: nCols * dSpacing / 2,
    yL: -nRows * dSpacing / 2, yR: nRows * dSpacing / 2
  };
}

function totalIlluminance2D(px, py, h, sources) {
  const refl = getReflectorBounds();
  const outside = reflectivity > 0 && (px < refl.xL || px > refl.xR || py < refl.yL || py > refl.yR);
  if (outside) {
    let sum = 0;
    for (const s of sources) sum += (1 - reflectivity) * illuminanceAt2D(px - s.x, py - s.y, h);
    return sum;
  }
  let sum = 0;
  for (const s of sources) {
    sum += illuminanceAt2D(px - s.x, py - s.y, h);
    if (reflectivity > 0) {
      sum += reflectivity * illuminanceAt2D(px - (2 * refl.xL - s.x), py - s.y, h);
      sum += reflectivity * illuminanceAt2D(px - (2 * refl.xR - s.x), py - s.y, h);
      sum += reflectivity * illuminanceAt2D(px - s.x, py - (2 * refl.yL - s.y), h);
      sum += reflectivity * illuminanceAt2D(px - s.x, py - (2 * refl.yR - s.y), h);
    }
  }
  return sum;
}

// Cross-section along y=0
function totalIlluminanceX(x, h, sources) { return totalIlluminance2D(x, 0, h, sources); }

// ─── POLAR CHART ───
const polarCanvas = document.getElementById('polarChart');
const polarCtx = polarCanvas.getContext('2d');
const polarContainer = document.getElementById('polar-container');
const tooltip1 = document.getElementById('tooltip1');
const readout = document.getElementById('readout');
let pW, pH, pcx, pcy, pR, polarHighlight = null;

function polarResize() {
  const rect = polarContainer.getBoundingClientRect();
  pW = rect.width; pH = rect.height;
  polarCanvas.width = pW * dpr; polarCanvas.height = pH * dpr;
  polarCanvas.style.width = pW + 'px'; polarCanvas.style.height = pH + 'px';
  polarCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
  pcx = pW / 2; pcy = pH - 28; pR = Math.min(pW / 2 - 40, pH - 55);
  drawPolar();
}
function polarToXY(deg, r) {
  const rad = (deg - 90) * Math.PI / 180;
  return { x: pcx + r * Math.cos(rad), y: pcy + r * Math.sin(rad) };
}
function drawPolar() {
  const ctx = polarCtx; ctx.clearRect(0, 0, pW, pH);
  ctx.strokeStyle = '#bbb'; ctx.lineWidth = 0.6;
  for (const f of [0.25,0.5,0.75,1]) {
    ctx.beginPath(); ctx.setLineDash([4,4]); ctx.arc(pcx, pcy, pR*f, Math.PI, 2*Math.PI); ctx.stroke();
    ctx.setLineDash([]); ctx.fillStyle = '#999'; ctx.font = '11px Arial'; ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
    ctx.fillText(Math.round(f*100)+'%', pcx - pR*f - 4, pcy);
  }
  ctx.setLineDash([]);
  const angles = [-90,-60,-30,0,30,60,90];
  ctx.strokeStyle = '#bbb'; ctx.lineWidth = 0.6; ctx.setLineDash([4,4]);
  for (const deg of angles) { const p = polarToXY(deg, pR); ctx.beginPath(); ctx.moveTo(pcx, pcy); ctx.lineTo(p.x, p.y); ctx.stroke(); }
  ctx.setLineDash([]); ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(pcx-pR-25, pcy); ctx.lineTo(pcx+pR+25, pcy); ctx.stroke();
  ctx.fillStyle = '#000'; ctx.font = '13px Arial';
  for (const deg of angles) {
    const p = polarToXY(deg, pR+18);
    ctx.textAlign = deg < 0 ? 'right' : deg > 0 ? 'left' : 'center';
    ctx.textBaseline = deg === 0 ? 'bottom' : 'middle'; ctx.fillText(deg+'\u00B0', p.x, p.y);
  }
  ctx.beginPath();
  for (let deg = -90; deg <= 90; deg += 0.5) { const p = polarToXY(deg, pR*intensity(deg)); deg===-90 ? ctx.moveTo(p.x,p.y) : ctx.lineTo(p.x,p.y); }
  ctx.closePath(); ctx.fillStyle = 'rgba(46,125,50,0.08)'; ctx.fill();
  ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 2.5; ctx.beginPath();
  for (let deg = -90; deg <= 90; deg += 0.5) { const p = polarToXY(deg, pR*intensity(deg)); deg===-90 ? ctx.moveTo(p.x,p.y) : ctx.lineTo(p.x,p.y); }
  ctx.stroke();
  for (let deg = -90; deg <= 90; deg += 10) {
    const p = polarToXY(deg, pR*intensity(deg));
    ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI*2); ctx.fillStyle = '#2e7d32'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
  }
  if (polarHighlight) {
    const h = polarHighlight;
    ctx.strokeStyle = 'rgba(46,125,50,0.3)'; ctx.lineWidth = 1; ctx.setLineDash([3,3]);
    ctx.beginPath(); ctx.moveTo(pcx, pcy); ctx.lineTo(h.px, h.py); ctx.stroke(); ctx.setLineDash([]);
    ctx.beginPath(); ctx.arc(h.px, h.py, 6, 0, Math.PI*2); ctx.fillStyle = '#2e7d32'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
  }
}
function findNearestPolar(mx, my) {
  let best = null, bestDist = Infinity;
  for (let deg = -90; deg <= 90; deg += 1) {
    const p = polarToXY(deg, pR*intensity(deg)); const d = Math.hypot(p.x-mx, p.y-my);
    if (d < bestDist) { bestDist = d; best = { deg, val: intensity(deg), px: p.x, py: p.y }; }
  }
  return bestDist < 40 ? best : null;
}
polarCanvas.addEventListener('mousemove', (e) => {
  const rect = polarCanvas.getBoundingClientRect();
  const pt = findNearestPolar(e.clientX-rect.left, e.clientY-rect.top);
  if (pt) {
    polarHighlight = pt; tooltip1.style.display = 'block';
    let tx = pt.px+14, ty = pt.py-34; if (tx+160>pW) tx = pt.px-160; if (ty<0) ty = pt.py+14;
    tooltip1.style.left = tx+'px'; tooltip1.style.top = ty+'px';
    tooltip1.innerHTML = `<b>${pt.deg}\u00B0</b> &mdash; ${(pt.val*100).toFixed(1)}%`;
    readout.textContent = `Angle: ${pt.deg}\u00B0  |  Intensity: ${(pt.val*100).toFixed(1)}%`;
  } else { polarHighlight = null; tooltip1.style.display = 'none'; readout.innerHTML = '&nbsp;'; }
  drawPolar();
});
polarCanvas.addEventListener('mouseleave', () => { polarHighlight = null; tooltip1.style.display = 'none'; readout.innerHTML = '&nbsp;'; drawPolar(); });

// ─── HEATMAP (Top View) ───
const hmCanvas = document.getElementById('heatmapChart');
const hmCtx = hmCanvas.getContext('2d');
const hmContainer = document.getElementById('heatmap-container');
const tooltipHM = document.getElementById('tooltipHM');
const readoutHM = document.getElementById('readoutHM');
let hmW, hmH, hmMargin, hmPlotW, hmPlotH, hmOx, hmOy;

function hmResize() {
  const rect = hmContainer.getBoundingClientRect();
  hmW = rect.width; hmH = rect.height;
  hmCanvas.width = hmW * dpr; hmCanvas.height = hmH * dpr;
  hmCanvas.style.width = hmW + 'px'; hmCanvas.style.height = hmH + 'px';
  hmCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
  hmMargin = { top: 24, right: 20, bottom: 36, left: 44 };
  hmPlotW = hmW - hmMargin.left - hmMargin.right;
  hmPlotH = hmH - hmMargin.top - hmMargin.bottom;
  hmOx = hmMargin.left; hmOy = hmMargin.top;
  drawHeatmap();
}

function drawHeatmap() {
  const ctx = hmCtx; ctx.clearRect(0, 0, hmW, hmH);
  const h = hDist, sources = getSourceGrid();
  const plH = panelL / 2, plW = panelW / 2;
  const refl = getReflectorBounds();
  const xRange = Math.max(plH, Math.abs(refl.xL), Math.abs(refl.xR)) * 1.1;
  const yRange = Math.max(plW, Math.abs(refl.yL), Math.abs(refl.yR)) * 1.1;
  // Use equal scale on both axes: compute pixels-per-unit from whichever axis is tighter
  const pxPerUnitX = hmPlotW / (2 * xRange);
  const pxPerUnitY = hmPlotH / (2 * yRange);
  const pxPerUnit = Math.min(pxPerUnitX, pxPerUnitY);
  // Center both axes
  const xCen = hmOx + hmPlotW / 2;
  const yCen = hmOy + hmPlotH / 2;
  const xMax = hmPlotW / (2 * pxPerUnit);
  const yMax = hmPlotH / (2 * pxPerUnit);

  function xToP(x) { return xCen + x * pxPerUnit; }
  function yToP(y) { return yCen - y * pxPerUnit; }

  // Compute heatmap over the panel region
  const res = 100;
  let peak = 0;
  const data = [];
  for (let iy = 0; iy < res; iy++) {
    data[iy] = [];
    for (let ix = 0; ix < res; ix++) {
      const x = -plH + panelL * ix / (res - 1);
      const y = -plW + panelW * iy / (res - 1);
      const E = totalIlluminance2D(x, y, h, sources);
      data[iy][ix] = E;
      if (E > peak) peak = E;
    }
  }

  // Draw heatmap pixels
  const pxW = (xToP(plH) - xToP(-plH)) / res;
  const pxH = (yToP(-plW) - yToP(plW)) / res;
  for (let iy = 0; iy < res; iy++) {
    for (let ix = 0; ix < res; ix++) {
      const frac = peak > 0 ? data[iy][ix] / peak : 0;
      ctx.fillStyle = intensityColor(frac);
      const x = -plH + panelL * ix / (res - 1);
      const y = -plW + panelW * iy / (res - 1);
      ctx.fillRect(xToP(x) - pxW/2, yToP(y) - pxH/2, pxW + 1, pxH + 1);
    }
  }

  // Panel outline
  ctx.strokeStyle = '#2e7d32'; ctx.lineWidth = 2;
  ctx.strokeRect(xToP(-plH), yToP(plW), xToP(plH) - xToP(-plH), yToP(-plW) - yToP(plW));

  // Panel dimension labels
  ctx.fillStyle = '#2e7d32'; ctx.font = 'bold 11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
  ctx.fillText('l = ' + panelL.toFixed(1), (xToP(-plH) + xToP(plH)) / 2, yToP(-plW) + 4);
  ctx.save(); ctx.translate(xToP(plH) + 12, (yToP(plW) + yToP(-plW)) / 2);
  ctx.rotate(-Math.PI / 2); ctx.fillText('w = ' + panelW.toFixed(1), 0, 0); ctx.restore();

  // Reflector lines
  if (reflectivity > 0) {
    ctx.strokeStyle = '#888'; ctx.lineWidth = 1.5; ctx.setLineDash([]);
    const rl = xToP(refl.xL), rr = xToP(refl.xR), rt = yToP(refl.yR), rb = yToP(refl.yL);
    ctx.strokeRect(rl, rt, rr - rl, rb - rt);
    ctx.fillStyle = '#888'; ctx.font = '10px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
    ctx.fillText('R=' + Math.round(reflectivity * 100) + '%', (rl + rr) / 2, rt - 2);
  }

  // LED positions
  if (document.getElementById('showLeds').checked) {
    for (const s of sources) {
      const px = xToP(s.x), py = yToP(s.y);
      ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2);
      ctx.fillStyle = '#f5a623'; ctx.fill(); ctx.strokeStyle = '#d48b00'; ctx.lineWidth = 0.8; ctx.stroke();
    }
  }

  // Axes
  ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(hmOx, hmOy + hmPlotH); ctx.lineTo(hmOx + hmPlotW, hmOy + hmPlotH); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(hmOx, hmOy); ctx.lineTo(hmOx, hmOy + hmPlotH); ctx.stroke();
  ctx.fillStyle = '#000'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
  const xStep = niceStep(2 * xMax, 6);
  for (let xv = Math.ceil(-xMax / xStep) * xStep; xv <= xMax; xv += xStep) ctx.fillText(xv.toFixed(0), xToP(xv), hmOy + hmPlotH + 4);
  ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
  const yStep = niceStep(2 * yMax, 5);
  for (let yv = Math.ceil(-yMax / yStep) * yStep; yv <= yMax; yv += yStep) ctx.fillText(yv.toFixed(0), hmOx - 4, yToP(yv));
  ctx.textAlign = 'center'; ctx.fillText('x', hmOx + hmPlotW / 2, hmOy + hmPlotH + 22);
  ctx.save(); ctx.translate(12, hmOy + hmPlotH / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('y', 0, 0); ctx.restore();

  // Delta readout (over panel only)
  let minE = Infinity, maxE = 0;
  for (let iy = 0; iy < res; iy++) for (let ix = 0; ix < res; ix++) {
    if (data[iy][ix] < minE) minE = data[iy][ix];
    if (data[iy][ix] > maxE) maxE = data[iy][ix];
  }
  if (minE === Infinity) minE = 0;
  const minP = peak > 0 ? minE / peak * 100 : 0, maxP = peak > 0 ? maxE / peak * 100 : 0;
  document.getElementById('deltaReadout').textContent = '\u0394 ' + (maxP - minP).toFixed(1) + '%  (' + minP.toFixed(1) + '% \u2013 ' + maxP.toFixed(1) + '%)';

  // Store for hover
  hmCanvas._state = { xMax, yMax, plH, plW, peak, sources, h, xToP, yToP, xCen, yCen, pxPerUnit };
}

hmCanvas.addEventListener('mousemove', (e) => {
  const rect = hmCanvas.getBoundingClientRect();
  const mx = e.clientX - rect.left, my = e.clientY - rect.top;
  const st = hmCanvas._state; if (!st) return;
  const x = (mx - st.xCen) / st.pxPerUnit;
  const y = -(my - st.yCen) / st.pxPerUnit;
  if (Math.abs(x) <= st.plH && Math.abs(y) <= st.plW) {
    const E = totalIlluminance2D(x, y, st.h, st.sources);
    const pct = st.peak > 0 ? E / st.peak * 100 : 0;
    tooltipHM.style.display = 'block';
    let tx = mx + 14, ty = my - 30; if (tx + 180 > hmW) tx = mx - 180; if (ty < 0) ty = my + 14;
    tooltipHM.style.left = tx + 'px'; tooltipHM.style.top = ty + 'px';
    tooltipHM.innerHTML = `<b>(${x.toFixed(1)}, ${y.toFixed(1)})</b> &mdash; ${pct.toFixed(1)}%`;
    readoutHM.textContent = `x=${x.toFixed(1)}  y=${y.toFixed(1)}  |  Illuminance: ${pct.toFixed(1)}%`;
  } else { tooltipHM.style.display = 'none'; readoutHM.innerHTML = '&nbsp;'; }
});
hmCanvas.addEventListener('mouseleave', () => { tooltipHM.style.display = 'none'; readoutHM.innerHTML = '&nbsp;'; });

// ─── CROSS-SECTION (y=0) ───
const crCanvas = document.getElementById('crossChart');
const crCtx = crCanvas.getContext('2d');
const crContainer = document.getElementById('cross-container');
const tooltip2 = document.getElementById('tooltip2');
const readout2 = document.getElementById('readout2');
let crW, crH, crMargin, crPlotW, crPlotH, crOx, crOy;

function crResize() {
  const rect = crContainer.getBoundingClientRect();
  crW = rect.width; crH = rect.height;
  crCanvas.width = crW * dpr; crCanvas.height = crH * dpr;
  crCanvas.style.width = crW + 'px'; crCanvas.style.height = crH + 'px';
  crCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
  crMargin = { top: 30, right: 20, bottom: 36, left: 50 };
  crPlotW = crW - crMargin.left - crMargin.right;
  crPlotH = crH - crMargin.top - crMargin.bottom;
  crOx = crMargin.left; crOy = crMargin.top;
  drawCross();
}

function drawCross() {
  const ctx = crCtx; ctx.clearRect(0, 0, crW, crH);
  const h = hDist, sources = getSourceGrid();
  const plH = panelL / 2;
  const refl = getReflectorBounds();
  const xMax = Math.max(plH, Math.abs(refl.xL)) * 1.1;
  const res = 400;
  const sumData = [];
  let peak = 0;
  for (let i = 0; i <= res; i++) {
    const x = -xMax + 2 * xMax * i / res;
    const E = totalIlluminanceX(x, h, sources);
    sumData.push({ x, E }); if (E > peak) peak = E;
  }
  const yMax = peak > 0 ? peak * 1.05 : 1;
  function xToP(x) { return crOx + crPlotW * (x + xMax) / (2 * xMax); }
  function eToP(e) { return crOy + crPlotH * (1 - e / yMax); }

  // Color bar at top
  const barH = 14, barY = crOy - barH - 2;
  const barL = xToP(-plH), barR = xToP(plH);
  for (let px = Math.floor(barL); px <= Math.ceil(barR); px++) {
    const x = -plH + panelL * (px - barL) / (barR - barL);
    const E = totalIlluminanceX(x, h, sources);
    ctx.fillStyle = intensityColor(peak > 0 ? E / yMax : 0);
    ctx.fillRect(px, barY, 2, barH);
  }
  ctx.strokeStyle = '#2e7d32'; ctx.lineWidth = 1;
  ctx.strokeRect(barL, barY, barR - barL, barH);

  // Grid
  ctx.strokeStyle = '#ddd'; ctx.lineWidth = 0.5;
  for (let i = 0; i <= 5; i++) { const py = eToP(yMax * i / 5); ctx.beginPath(); ctx.moveTo(crOx, py); ctx.lineTo(crOx + crPlotW, py); ctx.stroke(); }
  const xStep = niceStep(2 * xMax, 8);
  for (let xv = Math.ceil(-xMax / xStep) * xStep; xv <= xMax; xv += xStep) { const px = xToP(xv); ctx.beginPath(); ctx.moveTo(px, crOy); ctx.lineTo(px, crOy + crPlotH); ctx.stroke(); }

  // Axes
  ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(crOx, crOy + crPlotH); ctx.lineTo(crOx + crPlotW, crOy + crPlotH); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(crOx, crOy); ctx.lineTo(crOx, crOy + crPlotH); ctx.stroke();
  ctx.fillStyle = '#000'; ctx.font = '11px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'top';
  for (let xv = Math.ceil(-xMax / xStep) * xStep; xv <= xMax; xv += xStep) ctx.fillText(xv.toFixed(0), xToP(xv), crOy + crPlotH + 4);
  ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; ctx.font = '11px Arial';
  for (let i = 0; i <= 5; i++) ctx.fillText(Math.round(i * 20) + '%', crOx - 4, eToP(yMax * i / 5));

  // Reflector lines
  if (reflectivity > 0) {
    ctx.save(); ctx.beginPath(); ctx.rect(crOx, crOy, crPlotW, crPlotH); ctx.clip();
    ctx.strokeStyle = '#888'; ctx.lineWidth = 2;
    const rl = xToP(refl.xL), rr = xToP(refl.xR);
    ctx.beginPath(); ctx.moveTo(rl, crOy); ctx.lineTo(rl, crOy + crPlotH); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(rr, crOy); ctx.lineTo(rr, crOy + crPlotH); ctx.stroke();
    ctx.restore();
  }

  // Clip for curve
  ctx.save(); ctx.beginPath(); ctx.rect(crOx, crOy, crPlotW, crPlotH); ctx.clip();
  // Fill
  ctx.beginPath(); ctx.moveTo(xToP(sumData[0].x), eToP(0));
  for (const pt of sumData) ctx.lineTo(xToP(pt.x), eToP(pt.E));
  ctx.lineTo(xToP(sumData[res].x), eToP(0)); ctx.closePath();
  ctx.fillStyle = 'rgba(46,125,50,0.06)'; ctx.fill();
  // Curve
  ctx.strokeStyle = '#1e64dc'; ctx.lineWidth = 2.5; ctx.beginPath();
  let first = true;
  for (const pt of sumData) { const px = xToP(pt.x), py = eToP(pt.E); first ? (ctx.moveTo(px, py), first = false) : ctx.lineTo(px, py); }
  ctx.stroke();
  ctx.restore();

  crCanvas._state = { xMax, yMax, peak, sources, h, xToP, eToP };
}

crCanvas.addEventListener('mousemove', (e) => {
  const rect = crCanvas.getBoundingClientRect();
  const mx = e.clientX - rect.left, my = e.clientY - rect.top;
  const st = crCanvas._state; if (!st) return;
  const x = (mx - crOx) / crPlotW * 2 * st.xMax - st.xMax;
  const E = totalIlluminanceX(x, st.h, st.sources);
  const pct = st.peak > 0 ? E / st.peak * 100 : 0;
  tooltip2.style.display = 'block';
  let tx = mx + 14, ty = my - 30; if (tx + 180 > crW) tx = mx - 180;
  tooltip2.style.left = tx + 'px'; tooltip2.style.top = ty + 'px';
  tooltip2.innerHTML = `<b>x = ${x.toFixed(1)}</b> &mdash; ${pct.toFixed(1)}%`;
  readout2.textContent = `x = ${x.toFixed(1)}  |  Illuminance: ${pct.toFixed(1)}% of peak`;
});
crCanvas.addEventListener('mouseleave', () => { tooltip2.style.display = 'none'; readout2.innerHTML = '&nbsp;'; });

function niceStep(range, targetTicks) {
  const rough = range / targetTicks;
  const mag = Math.pow(10, Math.floor(Math.log10(rough)));
  const norm = rough / mag;
  return (norm < 1.5 ? 1 : norm < 3.5 ? 2 : norm < 7.5 ? 5 : 10) * mag;
}

function redrawAll() { drawHeatmap(); drawCross(); updatePower(); }

// ─── Slider wiring ───
function wireParam(sId, nId, minId, maxId, stepId, getV, setV) {
  const sl = document.getElementById(sId), ni = document.getElementById(nId);
  const mi = document.getElementById(minId), ma = document.getElementById(maxId), st = document.getElementById(stepId);
  function sync(fromSl) {
    const v = parseFloat(fromSl ? sl.value : ni.value); if (isNaN(v)) return;
    setV(v); if (fromSl) ni.value = v; else sl.value = Math.max(parseFloat(sl.min), Math.min(parseFloat(sl.max), v));
    redrawAll();
  }
  sl.addEventListener('input', () => sync(true));
  ni.addEventListener('input', () => sync(false)); ni.addEventListener('change', () => sync(false));
  mi.addEventListener('change', () => { sl.min = mi.value; });
  ma.addEventListener('change', () => { sl.max = ma.value; });
  st.addEventListener('change', () => { sl.step = st.value; ni.step = st.value; });
}
wireParam('hSlider','hNum','hMin','hMax','hStep', ()=>hDist, v=>{hDist=v;});
wireParam('dSlider','dNum','dMin','dMax','dStep', ()=>dSpacing, v=>{dSpacing=v;});
wireParam('colsSlider','colsNum','colsMin','colsMax','colsStep', ()=>nCols, v=>{nCols=Math.max(1,Math.round(v));});
wireParam('rowsSlider','rowsNum','rowsMin','rowsMax','rowsStep', ()=>nRows, v=>{nRows=Math.max(1,Math.round(v));});
wireParam('lSlider','lNum','lMin','lMax','lStep', ()=>panelL, v=>{panelL=v;});
wireParam('wSlider','wNum','wMin','wMax','wStep', ()=>panelW, v=>{panelW=v;});

// ─── Reflectivity ───
const reflNumEl = document.getElementById('reflNum');
function setRefl(v) { reflectivity = v / 100; reflNumEl.value = v; updatePresetBtns(); redrawAll(); }
reflNumEl.addEventListener('input', () => setRefl(parseFloat(reflNumEl.value) || 0));
reflNumEl.addEventListener('change', () => setRefl(parseFloat(reflNumEl.value) || 0));
const presetBtns = document.querySelectorAll('.preset-btn');
function updatePresetBtns() { const r = Math.round(reflectivity * 100); presetBtns.forEach(b => b.classList.toggle('active', parseInt(b.dataset.r) === r)); }
presetBtns.forEach(b => b.addEventListener('click', () => setRefl(parseInt(b.dataset.r))));

// ─── Power ───
const ledPowerEl = document.getElementById('ledPower');
const totalPowerOutEl = document.getElementById('totalPowerOut');
const panelAbsorbedEl = document.getElementById('panelAbsorbed');
const planeReceivedEl = document.getElementById('planeReceived');
const calcTimeEl = document.getElementById('calcTime');
const calcJoulesEl = document.getElementById('calcJoules');
const calcTargetEl = document.getElementById('calcTarget');
const calcTimeNeededEl = document.getElementById('calcTimeNeeded');

// Irradiance from a single Lambertian source at (sx,sy,0) on a horizontal plane at height h
// at point (px,py,h): E = cos^3(theta) / (pi * h^2) per unit source power
// We already have illuminanceAt2D which returns cos^3/h^2, so divide by pi for power fraction.

// Irradiance from source at (sx,sy,0) on a VERTICAL wall point (wx,wy,wz):
// The source emits as cos(theta_s) where theta_s is angle from source normal (z-up).
// cos(theta_s) = wz / r
// The wall receives as cos(theta_w) where theta_w is angle from wall normal (inward).
// For a wall facing in the x-direction: cos(theta_w) = |wx - sx| / r
// For a wall facing in the y-direction: cos(theta_w) = |wy - sy| / r
// E_wall = (1/pi) * cos(theta_s) * cos(theta_w) / r^2
function wallIrradiance(sx, sy, wx, wy, wz, wallAxis) {
  const dx = wx - sx, dy = wy - sy;
  const r2 = dx * dx + dy * dy + wz * wz;
  const r = Math.sqrt(r2);
  if (r < 1e-9) return 0;
  const cosSource = wz / r; // source emission angle (from z-up normal)
  const cosWall = wallAxis === 'x' ? Math.abs(dx) / r : Math.abs(dy) / r;
  return cosSource * cosWall / (Math.PI * r2);
}

function updatePower() {
  const nTotal = nCols * nRows;
  const perLed_mW = parseFloat(ledPowerEl.value) || 0;
  const totalPower_W = perLed_mW * nTotal / 1000;
  const perLed_W = perLed_mW / 1000;
  const h = hDist, sources = getSourceGrid();
  const plH = panelL / 2, plW2 = panelW / 2;
  const refl = getReflectorBounds();

  // Power on target: integrate direct + reflected contributions separately
  // Direct: each real source emits P watts into hemisphere. Fraction on plane per source:
  //   integral of cos^3(theta)/(pi*h^2) dA over the target plane
  // Reflected: mirror sources contribute R * (same integral for mirror position)
  //   but this is energy redirected from sideways emission, not new energy.
  //   Total received can never exceed total emitted.
  const pSteps = 40;
  const pdx = panelL / pSteps, pdy = panelW / pSteps;
  let directSum = 0, reflectedSum = 0;
  const mirrorSources = [];
  if (reflectivity > 0) {
    const rb = getReflectorBounds();
    for (const s of sources) {
      mirrorSources.push({ x: 2 * rb.xL - s.x, y: s.y });
      mirrorSources.push({ x: 2 * rb.xR - s.x, y: s.y });
      mirrorSources.push({ x: s.x, y: 2 * rb.yL - s.y });
      mirrorSources.push({ x: s.x, y: 2 * rb.yR - s.y });
    }
  }
  for (let iy = 0; iy <= pSteps; iy++) for (let ix = 0; ix <= pSteps; ix++) {
    const x = -plH + ix * pdx, y = -plW2 + iy * pdy;
    let dE = 0, rE = 0;
    for (const s of sources) dE += illuminanceAt2D(x - s.x, y - s.y, h);
    for (const m of mirrorSources) rE += illuminanceAt2D(x - m.x, y - m.y, h);
    directSum += dE * pdx * pdy;
    reflectedSum += rE * pdx * pdy;
  }
  // Fraction of total emitted power landing on target
  // Direct fraction: what each real source contributes directly
  const directFrac = directSum / (nTotal * Math.PI);
  // Reflected fraction: mirror sources redirect R * their emission onto the plane
  // but the energy comes from the real sources' sideways emission, not new power
  const reflectedFrac = reflectivity * reflectedSum / (nTotal * Math.PI);
  // Total fraction capped at 1.0 (can't receive more than emitted)
  const totalFrac = Math.min(directFrac + reflectedFrac, 1.0);
  const planeRec_W = totalPower_W * totalFrac;
  const efficiency = totalPower_W > 0 ? (totalFrac * 100) : 0;

  totalPowerOutEl.textContent = 'Total: ' + fmtP(totalPower_W);
  panelAbsorbedEl.textContent = 'Efficiency: ' + efficiency.toFixed(1) + '%';
  planeReceivedEl.textContent = 'Target receives: ' + fmtP(planeRec_W);
  // Energy density: J/cm² = (power W / area cm²) * time s
  // Assuming l and w are in mm: area_cm2 = (l * w) / 100
  const area_cm2 = (panelL * panelW) / 100;
  const irradiance_W_cm2 = area_cm2 > 0 ? planeRec_W / area_cm2 : 0; // W/cm²
  const t = parseFloat(calcTimeEl.value) || 0;
  const energyDensity = irradiance_W_cm2 * t; // J/cm²
  calcJoulesEl.textContent = '= ' + fmtED(energyDensity);
  const tgt = parseFloat(calcTargetEl.value) || 0; // target J/cm²
  const tn = irradiance_W_cm2 > 0 ? tgt / irradiance_W_cm2 : Infinity;
  calcTimeNeededEl.textContent = tn === Infinity ? '= \u221E' : '= ' + fmtT(tn);
}
function fmtP(w) { return w >= 1 ? w.toFixed(2)+' W' : (w*1000).toFixed(1)+' mW'; }
function fmtED(jcm2) { return jcm2 >= 1 ? jcm2.toFixed(2)+' J/cm\u00B2' : (jcm2*1000).toFixed(1)+' mJ/cm\u00B2'; }
function fmtT(s) { return s >= 3600 ? (s/3600).toFixed(2)+' hr' : s >= 60 ? (s/60).toFixed(1)+' min' : s.toFixed(1)+' sec'; }
document.getElementById('showLeds').addEventListener('change', redrawAll);
ledPowerEl.addEventListener('input', updatePower);
calcTimeEl.addEventListener('input', updatePower);
calcTargetEl.addEventListener('input', updatePower);

// ─── URL params ───
const params = new URLSearchParams(window.location.search);
if (params.has('h')) { hDist = parseFloat(params.get('h')); document.getElementById('hSlider').value = hDist; document.getElementById('hNum').value = hDist; }
if (params.has('d')) { dSpacing = parseFloat(params.get('d')); document.getElementById('dSlider').value = dSpacing; document.getElementById('dNum').value = dSpacing; }
if (params.has('cols')) { nCols = parseInt(params.get('cols')); document.getElementById('colsSlider').value = nCols; document.getElementById('colsNum').value = nCols; }
if (params.has('rows')) { nRows = parseInt(params.get('rows')); document.getElementById('rowsSlider').value = nRows; document.getElementById('rowsNum').value = nRows; }
if (params.has('l')) { panelL = parseFloat(params.get('l')); document.getElementById('lSlider').value = panelL; document.getElementById('lNum').value = panelL; }
if (params.has('w')) { panelW = parseFloat(params.get('w')); document.getElementById('wSlider').value = panelW; document.getElementById('wNum').value = panelW; }
if (params.has('r')) { reflectivity = parseFloat(params.get('r')) / 100; reflNumEl.value = Math.round(reflectivity * 100); updatePresetBtns(); }

// ─── Init ───
window.addEventListener('resize', () => { polarResize(); hmResize(); crResize(); updatePower(); });
polarResize(); hmResize(); crResize(); updatePower();
</script>
</body>
</html>