← back to Ken

kalshi-dash/risk-knob.html

175 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Ken · Risk Level Knob</title>
<style>
  :root{ --bg:#0a0e14; --panel:#0f1622; --ink:#e6edf3; --muted:#7d8da3; --cyan:#22d3ee; }
  *{box-sizing:border-box}
  html,body{margin:0;height:100%}
  body{background:radial-gradient(1200px 700px at 50% -10%,#12202e 0%,var(--bg) 60%);
    color:var(--ink);font:15px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;
    display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:22px;overflow:hidden}
  h1{font-size:13px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin:0;font-weight:700}
  .stage{position:relative;width:340px;height:340px;display:grid;place-items:center;touch-action:none;user-select:none;cursor:grab}
  .stage:active{cursor:grabbing}
  svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
  /* ambient glow behind the knob */
  .halo{position:absolute;width:250px;height:250px;border-radius:50%;filter:blur(38px);opacity:.55;
    transition:background .6s ease;animation:breathe 3.4s ease-in-out infinite}
  @keyframes breathe{0%,100%{transform:scale(.96);opacity:.42}50%{transform:scale(1.04);opacity:.6}}
  /* knob face */
  .knob{position:absolute;width:190px;height:190px;border-radius:50%;
    background:radial-gradient(120px 120px at 38% 30%,#1c2836,#0c131c 70%);
    border:1px solid rgba(255,255,255,.08);
    box-shadow:0 18px 40px rgba(0,0,0,.55),inset 0 2px 6px rgba(255,255,255,.06),inset 0 -18px 30px rgba(0,0,0,.5);
    display:grid;place-items:center;transition:transform .05s linear}
  .knob::before{content:"";position:absolute;inset:14px;border-radius:50%;
    background:conic-gradient(from 0deg,rgba(255,255,255,.03),rgba(255,255,255,0) 25%,rgba(255,255,255,.05) 50%,rgba(255,255,255,0) 75%,rgba(255,255,255,.03))}
  /* pointer notch that rotates with the knob */
  .pointer{position:absolute;top:12px;left:50%;width:6px;height:34px;border-radius:4px;margin-left:-3px;
    background:linear-gradient(#fff,#9fe8ff);box-shadow:0 0 14px var(--cyan),0 0 4px #fff}
  /* center readout (counter-rotated so it stays upright) */
  .readout{position:absolute;text-align:center;pointer-events:none}
  .val{font-size:62px;font-weight:800;letter-spacing:-1px;line-height:1;font-variant-numeric:tabular-nums;
    text-shadow:0 0 22px rgba(34,211,238,.25)}
  .unit{font-size:12px;color:var(--muted);letter-spacing:2px;margin-top:2px}
  .risk{margin-top:8px;font-size:13px;font-weight:800;letter-spacing:2px;transition:color .5s ease}
  .hint{font-size:12px;color:var(--muted);max-width:420px;text-align:center}
  .row{display:flex;gap:10px;align-items:center}
  button{background:#13202e;color:var(--ink);border:1px solid rgba(255,255,255,.14);border-radius:10px;
    padding:9px 16px;font-size:13px;font-weight:600;letter-spacing:.4px;cursor:pointer;transition:.15s}
  button:hover{border-color:var(--cyan);color:var(--cyan)}
  .cmd{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#9fe8ff;
    background:#0c1420;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 12px;letter-spacing:.2px}
  .legend{display:flex;gap:16px;font-size:11px;color:var(--muted);letter-spacing:.5px}
  .legend b{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;vertical-align:middle}
</style>
</head>
<body>
  <h1>Ken · Risk Level</h1>

  <div class="stage" id="stage">
    <div class="halo" id="halo"></div>
    <!-- arc gauge -->
    <svg viewBox="0 0 340 340" aria-hidden="true">
      <defs>
        <linearGradient id="arc" x1="0" y1="1" x2="1" y2="0">
          <stop offset="0"   stop-color="#ef4444"/>
          <stop offset=".38" stop-color="#f59e0b"/>
          <stop offset=".62" stop-color="#eab308"/>
          <stop offset="1"   stop-color="#22c55e"/>
        </linearGradient>
      </defs>
      <!-- track -->
      <path id="track" fill="none" stroke="rgba(255,255,255,.07)" stroke-width="10" stroke-linecap="round"/>
      <!-- value arc -->
      <path id="fill" fill="none" stroke="url(#arc)" stroke-width="10" stroke-linecap="round"
            style="filter:drop-shadow(0 0 6px rgba(34,211,238,.35))"/>
      <g id="ticks" stroke="rgba(255,255,255,.18)" stroke-width="2"></g>
    </svg>

    <div class="knob" id="knob"><div class="pointer"></div></div>
    <div class="readout">
      <div class="val" id="val">75</div>
      <div class="unit">MIN CONFIDENCE</div>
      <div class="risk" id="risk">BALANCED</div>
    </div>
  </div>

  <div class="legend">
    <span><b style="background:#ef4444"></b>50–64 High risk</span>
    <span><b style="background:#f59e0b"></b>65–74 Elevated</span>
    <span><b style="background:#eab308"></b>75–84 Balanced</span>
    <span><b style="background:#22c55e"></b>85–99 Conservative</span>
  </div>

  <div class="hint" id="hint">Drag the knob to set how selective Ken is. Higher = fewer, higher-conviction real trades (lower risk). Lower = more trades on thinner edges (higher risk).</div>

  <div class="row">
    <button id="demo">▶ Demo sweep</button>
    <span class="cmd" id="cmd">set-confidence.sh 75</span>
  </div>

<script>
(() => {
  // gauge geometry: 270° arc, from 135° (bottom-left) sweeping clockwise to 45° (bottom-right)
  const MIN=50, MAX=99, A0=135, A1=405;             // degrees (A1=45+360)
  const CX=170, CY=170, R=132;
  const rad = d => (d-90) * Math.PI/180;
  const ptOn = (d,r=R) => [CX + r*Math.cos(rad(d)), CY + r*Math.sin(rad(d))];
  const arcPath = (from,to,r=R) => {
    const [x0,y0]=ptOn(from,r), [x1,y1]=ptOn(to,r);
    const large = (to-from) > 180 ? 1 : 0;
    return `M${x0} ${y0} A${r} ${r} 0 ${large} 1 ${x1} ${y1}`;
  };
  const val2ang = v => A0 + (v-MIN)/(MAX-MIN)*(A1-A0);

  document.getElementById('track').setAttribute('d', arcPath(A0, A1));
  // ticks
  const ticks=document.getElementById('ticks');
  for(let v=MIN; v<=MAX; v+=(v%5===0?5:5)){ if(v%5) continue;
    const a=val2ang(v); const [x0,y0]=ptOn(a,R+9),[x1,y1]=ptOn(a, v%25===0? R+20 : R+15);
    const ln=document.createElementNS('http://www.w3.org/2000/svg','line');
    ln.setAttribute('x1',x0);ln.setAttribute('y1',y0);ln.setAttribute('x2',x1);ln.setAttribute('y2',y1);
    ticks.appendChild(ln);
  }

  const fill=document.getElementById('fill'), knob=document.getElementById('knob'),
        valEl=document.getElementById('val'), riskEl=document.getElementById('risk'),
        halo=document.getElementById('halo'), cmd=document.getElementById('cmd');

  const zone = v => v<65 ? {t:'HIGH RISK',c:'#ef4444'} : v<75 ? {t:'ELEVATED',c:'#f59e0b'}
                  : v<85 ? {t:'BALANCED',c:'#eab308'} : {t:'CONSERVATIVE',c:'#22c55e'};

  let cur=75;
  function render(v){
    v=Math.round(v);
    const a=val2ang(v);
    fill.setAttribute('d', arcPath(A0, a));
    knob.style.transform = `rotate(${a-270}deg)`;   // pointer starts at top (270°→0 offset)
    valEl.textContent=v;
    const z=zone(v); riskEl.textContent=z.t; riskEl.style.color=z.c;
    halo.style.background=`radial-gradient(circle,${z.c}66,transparent 70%)`;
    cmd.textContent=`set-confidence.sh ${v}`;
  }
  render(cur);

  // ── entrance "motion graphic": needle sweeps min→current with ease-out ──
  function sweep(to, from=MIN, dur=1400){
    const t0=performance.now();
    (function step(now){
      const t=Math.min(1,(now-t0)/dur), e=1-Math.pow(1-t,3);
      render(from + (to-from)*e);
      if(t<1) requestAnimationFrame(step); else { cur=to; render(cur); }
    })(t0);
  }
  requestAnimationFrame(()=>sweep(cur));
  document.getElementById('demo').onclick=()=>{ sweep(cur, MIN, 1300); };

  // ── drag to turn ──
  const stage=document.getElementById('stage');
  let dragging=false;
  const angOf = (cx,cy,x,y) => { let d=Math.atan2(y-cy, x-cx)*180/Math.PI + 90; if(d<0)d+=360; return d; };
  function fromEvent(ev){
    const r=stage.getBoundingClientRect();
    const cx=r.left+r.width/2, cy=r.top+r.height/2;
    const p = ev.touches ? ev.touches[0] : ev;
    let a=angOf(cx,cy,p.clientX,p.clientY);
    // map pointer angle onto the 135→405 sweep
    if(a<A0-10 && a<90) a+=360;
    a=Math.max(A0,Math.min(A1,a));
    const v=MIN+(a-A0)/(A1-A0)*(MAX-MIN);
    cur=v; render(v);
  }
  const down=e=>{dragging=true; fromEvent(e); e.preventDefault();};
  const move=e=>{ if(dragging) fromEvent(e); };
  const up=()=>dragging=false;
  stage.addEventListener('mousedown',down); window.addEventListener('mousemove',move); window.addEventListener('mouseup',up);
  stage.addEventListener('touchstart',down,{passive:false}); window.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',up);
})();
</script>
</body>
</html>