|
|
| Line 1: |
Line 1: |
| <!doctype html>
| |
| <html lang="en">
| |
| <head>
| |
| <meta charset="utf-8" />
| |
| <meta name="viewport" content="width=device-width,initial-scale=1" />
| |
| <title>EBL Dose Simulator — Hello Kitty Theme</title>
| |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
| |
| <style>
| |
| :root{
| |
| --pink:#ffb6d5;
| |
| --hotpink:#ff69b4;
| |
| --accent:#fff0f6;
| |
| --text:#333;
| |
| --card:#fff;
| |
| }
| |
| body{
| |
| margin:0;
| |
| font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial;
| |
| background: linear-gradient(180deg,var(--accent),#fff);
| |
| color:var(--text);
| |
| }
| |
| header{
| |
| display:flex;align-items:center;gap:16px;padding:18px 24px;background:var(--pink);box-shadow:0 6px 18px rgba(255,105,180,0.12);
| |
| }
| |
| .logo{
| |
| width:64px;height:64px;border-radius:12px;background:linear-gradient(180deg,var(--hotpink),#ffd1ea);display:flex;align-items:center;justify-content:center;box-shadow:0 6px 10px rgba(0,0,0,0.06);
| |
| }
| |
| .logo svg{width:46px;height:46px}
| |
| h1{font-size:20px;margin:0}
| |
| .container{max-width:1100px;margin:20px auto;padding:18px}
| |
| .grid{display:grid;grid-template-columns:320px 1fr;gap:18px}
| |
| .card{background:var(--card);border-radius:12px;padding:14px;box-shadow:0 8px 22px rgba(0,0,0,0.06)}
| |
| label{font-size:13px;display:block;margin-bottom:6px}
| |
| .small{font-size:12px;color:#666}
| |
| input[type=range]{width:100%}
| |
| .row{display:flex;gap:12px;align-items:center}
| |
| button{background:var(--hotpink);border:none;color:white;padding:8px 12px;border-radius:10px;cursor:pointer}
| |
| footer{margin-top:18px;text-align:center;color:#777;font-size:13px}
| |
| .muted{color:#666;font-size:13px}
| |
| canvas{background:transparent}
| |
| </style>
| |
| </head>
| |
| <body>
| |
| <header>
| |
| <div class="logo" aria-hidden>
| |
| <!-- Cute Hello-Kitty-like SVG (original, stylized cat) -->
| |
| <svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
| |
| <g fill="none" stroke="#ffffff" stroke-width="1.6">
| |
| <ellipse cx="32" cy="36" rx="18" ry="14" fill="#fff" stroke="none"/>
| |
| <path d="M18 18c-6 0-8-6-8-6s6 0 8 2c2 2 6 4 10 4s8-2 10-4c2-2 8-2 8-2s-2 6-8 6" fill="#fff" stroke="none"/>
| |
| </g>
| |
| <circle cx="24" cy="34" r="2.5" fill="#ff69b4"/>
| |
| <circle cx="40" cy="34" r="2.5" fill="#ff69b4"/>
| |
| <rect x="28" y="38" width="8" height="6" rx="3" fill="#ff69b4"/>
| |
| </svg>
| |
| </div>
| |
| <div>
| |
| <h1>EBL Dose Simulator — Hello Kitty Theme</h1>
| |
| <div class="small">Simulate exposure dose including forward- and backscattered electrons (proximity effect)</div>
| |
| </div>
| |
| </header>
| |
|
| |
|
| <main class="container">
| |
| <div class="grid">
| |
| <div class="card">
| |
| <div style="margin-bottom:10px">
| |
| <label for="widthSlider">Line width: <strong id="widthLabel">100 nm</strong></label>
| |
| <input id="widthSlider" type="range" min="10" max="400" value="100" step="1">
| |
| <div class="small muted">Change the width of the three parallel lines. Dose profile updates live.</div>
| |
| </div>
| |
|
| |
| <div style="margin-top:12px">
| |
| <label for="doseInput">Incident dose (a.u.):</label>
| |
| <input id="doseInput" type="number" value="1" step="0.1" style="width:100%" />
| |
| <div class="small muted">This is a scale factor — relative units (a.u.).</div>
| |
| </div>
| |
|
| |
| <hr style="margin:12px 0;border:none;border-top:1px solid #f1d7e3">
| |
|
| |
| <div>
| |
| <label>Proximity function parameters</label>
| |
| <div class="small">A two-Gaussian point-spread function is used: narrow forward-scatter (σ<sub>f</sub>) and broad backscatter (σ<sub>b</sub>) with weight η.</div>
| |
| <div style="margin-top:8px">
| |
| <label>Forward sigma (σ<sub>f</sub>, nm): <strong id="sfLabel">5</strong></label>
| |
| <input id="sfSlider" type="range" min="1" max="30" value="5">
| |
| </div>
| |
| <div style="margin-top:8px">
| |
| <label>Backscatter sigma (σ<sub>b</sub>, nm): <strong id="sbLabel">200</strong></label>
| |
| <input id="sbSlider" type="range" min="50" max="2000" value="200">
| |
| </div>
| |
| <div style="margin-top:8px">
| |
| <label>Backscatter weight (η): <strong id="etaLabel">0.4</strong></label>
| |
| <input id="etaSlider" type="range" min="0" max="0.9" step="0.01" value="0.4">
| |
| </div>
| |
| </div>
| |
|
| |
| <hr style="margin:12px 0;border:none;border-top:1px solid #f1d7e3">
| |
|
| |
| <div style="display:flex;gap:8px;justify-content:space-between">
| |
| <button id="resetBtn">Reset</button>
| |
| <div class="small muted">Tip: drag the line-width slider to see how proximity effects blur and add dose between lines.</div>
| |
| </div>
| |
| </div>
| |
|
| |
| <div class="card">
| |
| <canvas id="doseChart" height="220"></canvas>
| |
| <div style="display:flex;gap:14px;margin-top:10px;align-items:center">
| |
| <label style="display:flex;gap:6px;align-items:center"><input id="showPattern" type="checkbox" checked> Show pattern</label>
| |
| <label style="display:flex;gap:6px;align-items:center"><input id="showForward" type="checkbox" checked> Forward component</label>
| |
| <label style="display:flex;gap:6px;align-items:center"><input id="showBack" type="checkbox" checked> Backscatter component</label>
| |
| </div>
| |
| </div>
| |
| </div>
| |
|
| |
| <footer>
| |
| <div class="muted">Model: 1D cross-section of three parallel lines. Uses a two-Gaussian proximity function: PSF(r) = (1-η)*G(σf) + η*G(σb). Units are arbitrary — this is a relative-dose visualizer, useful for intuition and teaching.</div>
| |
| </footer>
| |
| </main>
| |
|
| |
| <script>
| |
| // --- Configuration & utilities ---
| |
| const LENGTH = 2000; // nm length of simulation window
| |
| const DX = 1; // nm resolution
| |
| const N = Math.floor(LENGTH/DX);
| |
| const x = new Array(N).fill(0).map((_,i)=>i*DX);
| |
|
| |
| function gaussian1d(sigma){
| |
| const twiceVar = 2*sigma*sigma;
| |
| const radius = Math.ceil(6*sigma/DX);
| |
| const kernel = new Float64Array(radius*2+1);
| |
| let sum = 0;
| |
| for(let i=-radius;i<=radius;i++){
| |
| const v = Math.exp(-(i*DX)*(i*DX)/twiceVar);
| |
| kernel[i+radius]=v; sum+=v;
| |
| }
| |
| for(let i=0;i<kernel.length;i++) kernel[i]/=sum;
| |
| return {kernel, radius};
| |
| }
| |
|
| |
| function convolve(signal, kernel, radius){
| |
| const out = new Float64Array(signal.length);
| |
| for(let i=0;i<signal.length;i++){
| |
| let s=0;
| |
| for(let k=-radius;k<=radius;k++){
| |
| const j=i+k;
| |
| if(j<0||j>=signal.length) continue;
| |
| s += signal[j]*kernel[k+radius];
| |
| }
| |
| out[i]=s;
| |
| }
| |
| return out;
| |
| }
| |
|
| |
| function makePattern(lineWidth){
| |
| // three parallel lines centered at 600, 1000, 1400 nm
| |
| const centers = [600,1000,1400];
| |
| const pat = new Float64Array(N);
| |
| const half = lineWidth/2;
| |
| for(let i=0;i<N;i++){
| |
| const xi = x[i];
| |
| for(const c of centers){
| |
| if(Math.abs(xi-c) <= half) { pat[i]=1; break; }
| |
| }
| |
| }
| |
| return pat;
| |
| }
| |
|
| |
| // --- UI elements ---
| |
| const widthSlider = document.getElementById('widthSlider');
| |
| const widthLabel = document.getElementById('widthLabel');
| |
| const doseInput = document.getElementById('doseInput');
| |
| const sfSlider = document.getElementById('sfSlider');
| |
| const sbSlider = document.getElementById('sbSlider');
| |
| const etaSlider = document.getElementById('etaSlider');
| |
| const sfLabel = document.getElementById('sfLabel');
| |
| const sbLabel = document.getElementById('sbLabel');
| |
| const etaLabel = document.getElementById('etaLabel');
| |
| const resetBtn = document.getElementById('resetBtn');
| |
| const showPattern = document.getElementById('showPattern');
| |
| const showForward = document.getElementById('showForward');
| |
| const showBack = document.getElementById('showBack');
| |
|
| |
| // --- Chart ---
| |
| const ctx = document.getElementById('doseChart').getContext('2d');
| |
| const chart = new Chart(ctx, {
| |
| type: 'line',
| |
| data: {
| |
| labels: x,
| |
| datasets: [
| |
| {label:'Total dose', data: new Array(N).fill(0), tension:0.2, borderWidth:2, pointRadius:0, spanGaps:true},
| |
| {label:'Pattern (direct)', data: new Array(N).fill(0), tension:0.2, borderWidth:1, pointRadius:0, borderDash:[4,4], hidden:!showPattern.checked},
| |
| {label:'Forward scatter', data: new Array(N).fill(0), tension:0.2, borderWidth:1, pointRadius:0, hidden:!showForward.checked},
| |
| {label:'Backscatter', data: new Array(N).fill(0), tension:0.2, borderWidth:1, pointRadius:0, hidden:!showBack.checked},
| |
| ]
| |
| },
| |
| options: {
| |
| animation:false,
| |
| plugins:{legend:{position:'top'}},
| |
| scales: {
| |
| x:{display:true,title:{display:true,text:'x (nm)'}},
| |
| y:{display:true,title:{display:true,text:'Relative dose (a.u.)'}}
| |
| }
| |
| }
| |
| });
| |
|
| |
| function updateChart(total, pattern, forwardComp, backComp){
| |
| chart.data.datasets[0].data = Array.from(total);
| |
| chart.data.datasets[1].data = Array.from(pattern);
| |
| chart.data.datasets[2].data = Array.from(forwardComp);
| |
| chart.data.datasets[3].data = Array.from(backComp);
| |
| chart.data.datasets[1].hidden = !showPattern.checked;
| |
| chart.data.datasets[2].hidden = !showForward.checked;
| |
| chart.data.datasets[3].hidden = !showBack.checked;
| |
| chart.update('none');
| |
| }
| |
|
| |
| function computeAndRender(){
| |
| const w = Number(widthSlider.value);
| |
| widthLabel.textContent = `${w} nm`;
| |
| const doseScale = Number(doseInput.value) || 1;
| |
| const sf = Number(sfSlider.value); sfLabel.textContent = sf;
| |
| const sb = Number(sbSlider.value); sbLabel.textContent = sb;
| |
| const eta = Number(etaSlider.value); etaLabel.textContent = eta;
| |
|
| |
| const pattern = makePattern(w);
| |
| // forward kernel
| |
| const {kernel: kf, radius: rf} = gaussian1d(sf);
| |
| const {kernel: kb, radius: rb} = gaussian1d(sb);
| |
|
| |
| const forwardComp = convolve(pattern, kf, rf).map(v=>v*(1-eta)*doseScale);
| |
| const backComp = convolve(pattern, kb, rb).map(v=>v*eta*doseScale);
| |
|
| |
| const total = new Float64Array(N);
| |
| for(let i=0;i<N;i++) total[i]=forwardComp[i]+backComp[i];
| |
|
| |
| updateChart(total, pattern.map(v=>v*doseScale), forwardComp, backComp);
| |
| }
| |
|
| |
| // --- events ---
| |
| [widthSlider, doseInput, sfSlider, sbSlider, etaSlider].forEach(el=>el.addEventListener('input',computeAndRender));
| |
| [showPattern, showForward, showBack].forEach(el=>el.addEventListener('change',computeAndRender));
| |
| resetBtn.addEventListener('click',()=>{
| |
| widthSlider.value=100; doseInput.value=1; sfSlider.value=5; sbSlider.value=200; etaSlider.value=0.4;
| |
| computeAndRender();
| |
| });
| |
|
| |
| // initial render
| |
| computeAndRender();
| |
| </script>
| |
| </body>
| |
| </html>
| |