UI polish: CRT scanlines, glow effects, boot animation

Pure CSS visual upgrades — zero JS overhead, no feature changes:
- CRT scanline overlay via body::after repeating gradient
- Boot-in animation (brightness flash + blur fade)
- Title flicker on load, persistent text-shadow glow
- Pulsing glow on active radio dots and connection indicator
- Shimmer gradient on progress bar
- Canvas sweep cursor and sparkline glow via shadowBlur
- Gradient fills on sparkline charts
- Hover states on cards, metric tiles, buttons (glow + scale)
- Custom thin scrollbar on log panel
- Tabular-nums for jitter-free metric updates
- Version badge in header

Made-with: Cursor
This commit is contained in:
drjones
2026-03-12 13:36:01 -07:00
parent 1bd2e21a79
commit f26cc60b53

View File

@@ -978,25 +978,30 @@ const char kHtml[] = R"HTML(
<title>CC1101 JAMMER</title><style> <title>CC1101 JAMMER</title><style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
html,body{background:#020504;color:#86f28a;font-family:'Courier New',monospace;font-size:12px;line-height:1.4} html,body{background:#020504;color:#86f28a;font-family:'Courier New',monospace;font-size:12px;line-height:1.4}
header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #1a3a1e;background:#030705} body::after{content:'';position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999;background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,.08) 2px,rgba(0,0,0,.08) 4px);opacity:.6}
h1{font-size:17px;letter-spacing:.12em;color:#a0f5a4} header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #1a3a1e;background:linear-gradient(180deg,#040a06 0%,#030705 100%)}
h1{font-size:17px;letter-spacing:.12em;color:#a0f5a4;text-shadow:0 0 8px rgba(134,242,138,.4)}
.sub{font-size:10px;color:#4fbf59;margin-top:2px} .sub{font-size:10px;color:#4fbf59;margin-top:2px}
#progWrap{height:3px;background:#060e07} #progWrap{height:3px;background:#060e07}
#progBar{height:3px;background:#4fbf59;width:0;transition:width .8s linear} #progBar{height:3px;background:linear-gradient(90deg,#2a8a2e,#86f28a,#2a8a2e);background-size:200% 100%;width:0;transition:width .8s linear;animation:progShimmer 3s linear infinite}
@keyframes progShimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
main{padding:10px;max-width:920px;margin:0 auto;display:flex;flex-direction:column;gap:8px} main{padding:10px;max-width:920px;margin:0 auto;display:flex;flex-direction:column;gap:8px}
.card{background:#030705;border:1px solid #1a3a1e;padding:10px} .card{background:#030705;border:1px solid #1a3a1e;padding:10px;transition:border-color .3s}
.card:hover{border-color:#2a5a2e}
.card h2{font-size:10px;color:#4fbf59;letter-spacing:.12em;text-transform:uppercase;border-bottom:1px solid #122814;padding-bottom:5px;margin-bottom:8px} .card h2{font-size:10px;color:#4fbf59;letter-spacing:.12em;text-transform:uppercase;border-bottom:1px solid #122814;padding-bottom:5px;margin-bottom:8px}
.banner{padding:14px;text-align:center;border:2px solid #1a3a1e} .banner{padding:14px;text-align:center;border:2px solid #1a3a1e;transition:all .4s ease}
.bt{font-size:22px;font-weight:bold;letter-spacing:.18em} .bt{font-size:22px;font-weight:bold;letter-spacing:.18em}
.bs{font-size:11px;margin-top:5px} .bs{font-size:11px;margin-top:5px}
.ban-on{border-color:#4fbf59;background:#060f07} .ban-on{border-color:#4fbf59;background:radial-gradient(ellipse at center,#0a1a0c 0%,#060f07 70%)}
.ban-off{border-color:#3a1218;background:#030504} .ban-off{border-color:#3a1218;background:#030504}
@keyframes pulse{0%,100%{box-shadow:0 0 6px rgba(134,242,138,.15)}50%{box-shadow:0 0 20px rgba(134,242,138,.45)}} @keyframes pulse{0%,100%{box-shadow:0 0 6px rgba(134,242,138,.15)}50%{box-shadow:0 0 24px rgba(134,242,138,.5),0 0 60px rgba(134,242,138,.1)}}
.ban-on{animation:pulse 2.2s ease-in-out infinite} .ban-on{animation:pulse 2.2s ease-in-out infinite}
.ban-on .bt{text-shadow:0 0 12px rgba(134,242,138,.6)}
.sg{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:5px} .sg{display:grid;grid-template-columns:repeat(auto-fill,minmax(105px,1fr));gap:5px}
.s{background:#020504;border:1px solid #1a3a1e;padding:6px 8px} .s{background:#020504;border:1px solid #1a3a1e;padding:6px 8px;transition:border-color .2s,background .2s}
.s:hover{border-color:#2a5a2e;background:#040a06}
.sl{font-size:8px;color:#4fbf59;text-transform:uppercase;letter-spacing:.1em;white-space:nowrap} .sl{font-size:8px;color:#4fbf59;text-transform:uppercase;letter-spacing:.1em;white-space:nowrap}
.sv{font-size:15px;font-weight:bold;margin-top:2px} .sv{font-size:15px;font-weight:bold;margin-top:2px;transition:color .3s}
.su{font-size:8px;color:#4fbf59;margin-left:1px} .su{font-size:8px;color:#4fbf59;margin-left:1px}
.warm{color:#f5d87c!important}.hot{color:#f28a86!important}.lo{color:#f5d87c!important}.crit{color:#f28a86!important} .warm{color:#f5d87c!important}.hot{color:#f28a86!important}.lo{color:#f5d87c!important}.crit{color:#f28a86!important}
.band{margin-bottom:10px} .band{margin-bottom:10px}
@@ -1007,25 +1012,42 @@ canvas.sp{height:44px;border:1px solid #122814;background:#020504}
.row{display:flex;flex-wrap:wrap;gap:10px} .row{display:flex;flex-wrap:wrap;gap:10px}
.col{flex:1;min-width:140px} .col{flex:1;min-width:140px}
.rrow{display:flex;gap:7px;align-items:center;margin-bottom:4px} .rrow{display:flex;gap:7px;align-items:center;margin-bottom:4px}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0} .dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;transition:all .3s}
.on{background:#86f28a}.off{background:#f28a86} .on{background:#86f28a;box-shadow:0 0 6px rgba(134,242,138,.7)}
@keyframes dotPulse{0%,100%{box-shadow:0 0 4px rgba(134,242,138,.5)}50%{box-shadow:0 0 10px rgba(134,242,138,.9)}}
.on{animation:dotPulse 1.5s ease-in-out infinite}
.off{background:#f28a86;box-shadow:0 0 4px rgba(242,138,134,.4)}
hr{border:none;border-top:1px solid #122814;margin:8px 0} hr{border:none;border-top:1px solid #122814;margin:8px 0}
label{font-size:10px;color:#4fbf59;display:block;margin-bottom:3px} label{font-size:10px;color:#4fbf59;display:block;margin-bottom:3px}
input[type=range]{width:100%;accent-color:#86f28a;margin:2px 0} input[type=range]{width:100%;accent-color:#86f28a;margin:2px 0}
input[type=number]{background:#020504;border:1px solid #1a3a1e;color:#86f28a;padding:3px 6px;font-family:inherit;font-size:11px;width:100%} input[type=number]{background:#020504;border:1px solid #1a3a1e;color:#86f28a;padding:3px 6px;font-family:inherit;font-size:11px;width:100%;transition:border-color .2s}
button{padding:6px 11px;background:#0a1e0c;color:#86f28a;border:1px solid #2a5a2e;cursor:pointer;font-family:inherit;font-size:11px;letter-spacing:.04em} input[type=number]:focus{border-color:#4fbf59;outline:none}
button:hover{background:#142a16} select:focus{border-color:#4fbf59;outline:none}
button{padding:6px 11px;background:#0a1e0c;color:#86f28a;border:1px solid #2a5a2e;cursor:pointer;font-family:inherit;font-size:11px;letter-spacing:.04em;transition:all .15s}
button:hover{background:#142a16;box-shadow:0 0 8px rgba(134,242,138,.15)}
button:active{transform:scale(.97)}
button.d{background:#140608;border-color:#4a1820;color:#f28a86} button.d{background:#140608;border-color:#4a1820;color:#f28a86}
button.d:hover{background:#200a10} button.d:hover{background:#200a10;box-shadow:0 0 8px rgba(242,138,134,.15)}
.err{color:#f28a86;font-size:10px;margin-top:2px} .err{color:#f28a86;font-size:10px;margin-top:2px}
pre{margin:0;padding:8px;background:#020504;border:1px solid #122814;height:28vh;overflow-y:auto;font-size:10px;line-height:1.6;color:#5fbf69} pre{margin:0;padding:8px;background:#020504;border:1px solid #122814;height:28vh;overflow-y:auto;font-size:10px;line-height:1.6;color:#5fbf69}
#connDot{width:9px;height:9px;border-radius:50%;background:#f28a86} pre::-webkit-scrollbar{width:4px}
pre::-webkit-scrollbar-track{background:#020504}
pre::-webkit-scrollbar-thumb{background:#1a3a1e;border-radius:2px}
#connDot{width:9px;height:9px;border-radius:50%;background:#f28a86;transition:background .3s,box-shadow .3s}
@keyframes bootIn{0%{opacity:0;filter:brightness(2) blur(2px)}100%{opacity:1;filter:brightness(1) blur(0)}}
body{animation:bootIn .6s ease-out}
@keyframes flicker{0%{opacity:1}3%{opacity:.4}6%{opacity:1}7%{opacity:.6}9%{opacity:1}100%{opacity:1}}
h1{animation:flicker .4s ease-out}
.sv{font-variant-numeric:tabular-nums}
</style></head><body> </style></head><body>
<header> <header>
<div><h1>CC1101 JAMMER</h1> <div><h1>CC1101 JAMMER</h1>
<div class="sub">ESP32-S3 &bull; 300320 MHz + 390436 MHz &bull; Dual-band FM noise sweep</div></div> <div class="sub">ESP32-S3 &bull; 300320 MHz + 390436 MHz &bull; Dual-band FM noise sweep</div></div>
<div style="text-align:right"> <div style="text-align:right;display:flex;align-items:center;gap:10px">
<div id="mission" style="font-size:9px;color:#4fbf59;margin-bottom:5px"></div> <div>
<div id="mission" style="font-size:9px;color:#4fbf59;margin-bottom:3px"></div>
<div style="font-size:8px;color:#1a3a1e;letter-spacing:.08em">v2.0 // LFSR+VCO</div>
</div>
<div id="connDot"></div> <div id="connDot"></div>
</div> </div>
</header> </header>
@@ -1239,13 +1261,17 @@ function drawSw(id,freq,ctr,span,active,trail,marks){
const g=ctx.createLinearGradient(cx-80,0,cx+80,0); const g=ctx.createLinearGradient(cx-80,0,cx+80,0);
g.addColorStop(0,'rgba(134,242,138,0)');g.addColorStop(.5,'rgba(134,242,138,0.22)');g.addColorStop(1,'rgba(134,242,138,0)'); g.addColorStop(0,'rgba(134,242,138,0)');g.addColorStop(.5,'rgba(134,242,138,0.22)');g.addColorStop(1,'rgba(134,242,138,0)');
ctx.fillStyle=g;ctx.fillRect(cx-80,0,160,H-14); ctx.fillStyle=g;ctx.fillRect(cx-80,0,160,H-14);
// Cursor line // Cursor line with glow
ctx.save();ctx.shadowColor='rgba(134,242,138,.6)';ctx.shadowBlur=6;
ctx.strokeStyle='#86f28a';ctx.lineWidth=2; ctx.strokeStyle='#86f28a';ctx.lineWidth=2;
ctx.beginPath();ctx.moveTo(cx,0);ctx.lineTo(cx,H-14);ctx.stroke(); ctx.beginPath();ctx.moveTo(cx,0);ctx.lineTo(cx,H-14);ctx.stroke();
// Cursor dot ctx.restore();
// Cursor dot with glow
ctx.save();ctx.shadowColor='rgba(134,242,138,.8)';ctx.shadowBlur=10;
ctx.fillStyle='#86f28a';ctx.beginPath();ctx.arc(cx,(H-14)/2,4,0,Math.PI*2);ctx.fill(); ctx.fillStyle='#86f28a';ctx.beginPath();ctx.arc(cx,(H-14)/2,4,0,Math.PI*2);ctx.fill();
ctx.restore();
// Top notch // Top notch
ctx.fillRect(Math.max(0,cx-2),0,4,4); ctx.fillStyle='#86f28a';ctx.fillRect(Math.max(0,cx-2),0,4,4);
} else if(!active){ } else if(!active){
ctx.strokeStyle='#2a1216';ctx.lineWidth=1; ctx.strokeStyle='#2a1216';ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(W/2,0);ctx.lineTo(W/2,H-14);ctx.stroke(); ctx.beginPath();ctx.moveTo(W/2,0);ctx.lineTo(W/2,H-14);ctx.stroke();
@@ -1270,11 +1296,13 @@ function drawSp(id,data,color,minH,maxH){
ctx.beginPath();let fs=true; ctx.beginPath();let fs=true;
data.forEach((v,i)=>{if(v===null){fs=true;return;}const x=i/(HS-1)*W,y=toY(v);if(fs){ctx.moveTo(x,H-4);ctx.lineTo(x,y);fs=false;}else ctx.lineTo(x,y);}); data.forEach((v,i)=>{if(v===null){fs=true;return;}const x=i/(HS-1)*W,y=toY(v);if(fs){ctx.moveTo(x,H-4);ctx.lineTo(x,y);fs=false;}else ctx.lineTo(x,y);});
ctx.lineTo(W,H-4);ctx.closePath(); ctx.lineTo(W,H-4);ctx.closePath();
ctx.fillStyle=color+'28';ctx.fill(); const gf=ctx.createLinearGradient(0,0,0,H);gf.addColorStop(0,color+'30');gf.addColorStop(1,color+'05');
// Line ctx.fillStyle=gf;ctx.fill();
// Line with glow
ctx.save();ctx.shadowColor=color;ctx.shadowBlur=4;
ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.beginPath();fs=true; ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.beginPath();fs=true;
data.forEach((v,i)=>{if(v===null){fs=true;return;}const x=i/(HS-1)*W,y=toY(v);if(fs){ctx.moveTo(x,y);fs=false;}else ctx.lineTo(x,y);}); data.forEach((v,i)=>{if(v===null){fs=true;return;}const x=i/(HS-1)*W,y=toY(v);if(fs){ctx.moveTo(x,y);fs=false;}else ctx.lineTo(x,y);});
ctx.stroke(); ctx.stroke();ctx.restore();
// Range labels // Range labels
ctx.fillStyle=color+'90';ctx.font='8px monospace'; ctx.fillStyle=color+'90';ctx.font='8px monospace';
ctx.textAlign='left';ctx.fillText(mn.toFixed(0),2,H-3); ctx.textAlign='left';ctx.fillText(mn.toFixed(0),2,H-3);
@@ -1349,7 +1377,7 @@ function applyTelemetry(t){
document.getElementById('hNow').className=ch(t.free_heap/1024); document.getElementById('hNow').className=ch(t.free_heap/1024);
drawSp('cT',hT,'#86f28a',20,90); drawSp('cT',hT,'#86f28a',20,90);
drawSp('cH',hH,'#4fbf59',0,320); drawSp('cH',hH,'#4fbf59',0,320);
document.getElementById('connDot').style.background='#86f28a'; const cd=document.getElementById('connDot');cd.style.background='#86f28a';cd.style.boxShadow='0 0 8px rgba(134,242,138,.7)';
} }
let logTick=0; let logTick=0;
@@ -1364,7 +1392,7 @@ async function poll(){
if(atBot)log.scrollTop=log.scrollHeight; if(atBot)log.scrollTop=log.scrollHeight;
} }
}catch(e){ }catch(e){
document.getElementById('connDot').style.background='#f28a86'; const cd=document.getElementById('connDot');cd.style.background='#f28a86';cd.style.boxShadow='0 0 8px rgba(242,138,134,.6)';
document.getElementById('bt').textContent='OFFLINE'; document.getElementById('bt').textContent='OFFLINE';
} }
setTimeout(poll, 1000); setTimeout(poll, 1000);