git / moonbite-network / templates/calculator.html · raw

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#0B0D12">
<title>The Cost of Waiting — MoonBite</title>
<meta name="description" content="Every week you wait, the same computer earns less MBITE. See exactly how much the delay costs you, on the real emission schedule.">
<link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg">
<link rel="stylesheet" href="/static/fonts/moonbite-fonts.css">
<style>
/* Doc 04: three roles. A heavy grotesque for display, a quiet sans for
   reading, a monospace for anything that is a number.

   Archivo Black and Inter are self-hosted from /static/fonts (latin subset,
   OFL-licensed). They are deliberately not loaded from Google Fonts: /privacy
   states the site loads nothing that phones home, and a webfont CDN sends
   every visitor's IP to Google on every page load. Every figure that is
   money, a hash, an address or a block height is monospace with tabular
   figures — amounts that shift width as they update look untrustworthy. */
:root{
  --font-display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif;
  --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  --font-data:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
h1,h2,.big,.reveal .big,.balance-amount,.elapsed,.mort-big{
  font-family:var(--font-display); font-weight:400; letter-spacing:-.02em;
}
.num,.mono,.addr,.cert .amt,.cl .v,.tot .v,.mcell .v,.odds .v,
.loss .v,.count .v,.zero-band .v,.total .v,.vault-v,td.n,th.n{
  font-family:var(--font-data); font-variant-numeric:tabular-nums;
}

* { margin:0; padding:0; box-sizing:border-box; }
body { background:#0B0D12; color:#F5F2EA;
  font-family:var(--font-body);
  overflow-x:hidden; -webkit-font-smoothing:antialiased; }
::selection { background:#D9A441; color:#0B0D12; }
a,button,select,input { -webkit-tap-highlight-color:transparent; font-family:inherit; }
.wrap { position:relative; z-index:1; max-width:540px; margin:0 auto; padding:0 18px 70px; }
.num { font-variant-numeric:tabular-nums; }
.kicker { letter-spacing:.3em; font-size:.62rem; color:#D9A441; text-transform:uppercase;
  font-weight:800; }

.head { text-align:center; padding-top:5vh; }
h1 { font-size:clamp(2rem,8.5vw,3.2rem); font-weight:800; line-height:1.03;
  letter-spacing:-.025em; margin:13px 0 10px; }
h1 .r { color:#D9A441; }   /* accent word: Moongold; red stays functional-only */
.lede { color:#8A8580; font-size:.96rem; line-height:1.62; max-width:410px; margin:0 auto; }

/* ------------------------------------------------------------ THE LOSS */
.loss { background:linear-gradient(140deg, rgba(200,64,47,.16), rgba(22,26,34,.6));
  border:1px solid rgba(200,64,47,.36); border-radius:20px; padding:24px 18px; margin-top:24px;
  text-align:center; box-shadow:0 22px 66px rgba(0,0,0,.5); }
.loss .k { font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase;
  font-weight:800; }
.loss .v { font-size:clamp(2.3rem,11vw,3.5rem); font-weight:800; color:#C8402F; line-height:1;
  margin-top:10px; letter-spacing:-.02em; }
.loss .u { font-size:.34em; color:#8A8580; font-weight:800; letter-spacing:.1em; }
.loss .sub { color:#F5F2EA; font-size:.92rem; margin-top:10px; font-weight:600; }
.loss .pct { display:inline-block; margin-top:12px; background:rgba(200,64,47,.2);
  border:1px solid rgba(200,64,47,.45); border-radius:999px; padding:6px 14px;
  font-size:.8rem; font-weight:800; color:#C8402F; letter-spacing:.05em; }

/* ---------------------------------------------------------- THE CURVE */
.chartcard { background:#161A22; border:1px solid rgba(217,164,65,.2); border-radius:20px;
  padding:20px 16px 14px; margin-top:16px; }
.chart-t { font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase;
  font-weight:800; margin-bottom:4px; }
.chart-s { color:#8A8580; font-size:.76rem; margin-bottom:14px; }
#decay { width:100%; height:210px; display:block; }

/* ----------------------------------------------------------- CONTROLS */
.panel { background:#161A22; border:1px solid rgba(245,242,234,.09); border-radius:20px;
  padding:20px 18px; margin-top:16px; }
.lab { display:block; font-size:.6rem; letter-spacing:.2em; color:#8A8580; text-transform:uppercase;
  font-weight:800; margin-bottom:9px; }
select, input[type=number] { width:100%; background:#0B0D12; color:#F5F2EA; font-size:.95rem;
  border:1px solid rgba(217,164,65,.28); border-radius:10px; padding:14px 12px; min-height:50px; }
select:focus, input[type=number]:focus { outline:none; border-color:#D9A441; }
.field + .field { margin-top:18px; }
.slider { width:100%; accent-color:#D9A441; height:34px; }
.slider-val { color:#D9A441; font-weight:800; }
.ends { display:flex; justify-content:space-between; font-size:.66rem; color:#8A8580;
  letter-spacing:.08em; }

/* ------------------------------------------------------------- RESULT */
.rows { margin-top:16px; }
.row { display:flex; align-items:center; gap:12px; padding:14px 0;
  border-top:1px solid rgba(245,242,234,.07); }
.row .when { flex:1; font-size:.9rem; color:#8A8580; font-weight:700; }
.row.now .when { color:#F5F2EA; }
.row .amt { font-size:1.05rem; font-weight:800; color:#8A8580; font-variant-numeric:tabular-nums; }
.row.now .amt { color:#2E9E6B; font-size:1.2rem; }
.row .pen { font-size:.72rem; color:#C8402F; font-weight:800; min-width:52px; text-align:right; }

.actions { display:flex; gap:10px; justify-content:center; flex-wrap:wrap; margin-top:24px; }
.btn { border:none; border-radius:10px; padding:15px 24px; min-height:50px; font-weight:800;
  font-size:.94rem; cursor:pointer; text-decoration:none; display:inline-flex;
  align-items:center; justify-content:center; gap:8px; }
.btn-gold { background:#D9A441; color:#0B0D12; box-shadow:0 12px 36px rgba(217,164,65,.28); }
.btn-ghost { background:transparent; color:#F5F2EA; border:1px solid rgba(245,242,234,.24); }
.fine { font-size:.73rem; color:#8A8580; line-height:1.72; text-align:center; margin-top:22px; }
.toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(16px);
  background:#2E9E6B; color:#04160E; font-weight:800; padding:12px 22px; border-radius:10px;
  opacity:0; transition:all .3s; z-index:60; pointer-events:none; font-size:.88rem; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
#card { display:none; }
/* Doc 17: visible keyboard focus on every interactive element. */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, [tabindex]:focus-visible {
  outline:2px solid #D9A441; outline-offset:3px; border-radius:6px;
}
</style>
</head>
<body>
{% include 'partials/nav.html' %}

<div class="wrap">
  <section class="head">
    <div class="kicker">THE EARLY MINER CALCULATOR</div>
    <h1>Every week you wait,<br>you mine <span class="r">less</span>.</h1>
    <p class="lede">Same machine, same electricity — a smaller share, forever.
      Here is what the delay actually costs, on MoonBite's real schedule.</p>
  </section>

  <!-- the number that matters -->
  <div class="loss">
    <div class="k">WAITING ONE YEAR COSTS YOU</div>
    <div class="v num" id="lossV">—<span class="u"> MBITE</span></div>
    <div class="sub" id="lossSub">—</div>
    <div class="pct" id="lossPct">—</div>
  </div>

  <!-- the collapse -->
  <div class="chartcard">
    <div class="chart-t">WHAT YOU MINE BY THE FIRST HALVING</div>
    <div class="chart-s">Depending on the day you start. The bar only falls.</div>
    <canvas id="decay"></canvas>
  </div>

  <!-- controls -->
  <div class="panel">
    <div class="field">
      <label class="lab" for="hw">YOUR HARDWARE (RANDOMX)</label>
      <select id="hw">
        <option value="2500">Laptop CPU (4–8 cores) — ~2.5 KH/s</option>
        <option value="12000" selected>Desktop CPU (Ryzen 9 class) — ~12 KH/s</option>
        <option value="44000">Server CPU (64-core EPYC) — ~44 KH/s</option>
        <option value="custom">Custom hashrate…</option>
      </select>
      <input type="number" id="hwCustom" placeholder="Hashes per second (H/s)" min="1"
             style="display:none; margin-top:9px;">
    </div>
    <div class="field">
      <label class="lab" for="net">NETWORK TODAY: <span class="slider-val" id="netLab">100 CPUs</span></label>
      <input type="range" id="net" class="slider" min="0" max="5" step="1" value="2">
      <div class="ends"><span>JUST YOU</span><span>100,000 CPUs</span></div>
    </div>
    <div class="rows" id="rows"></div>
  </div>

  <div class="actions">
    <a class="btn btn-gold" href="/start">Start mining now</a>
    <button class="btn btn-ghost" id="btnCard">Share this</button>
  </div>

  <p class="fine">
    Uses MoonBite's real schedule: {{ consensus.initial_subsidy_coins }} MBITE per block, one block
    every {{ consensus.block_time_min }} minutes ({{ '{:,}'.format(consensus.daily_emission_coins) }}
    MBITE emitted network-wide per day), halving at block
    {{ '{:,}'.format(consensus.halving_interval) }}. Assumes the network grows to ten times its
    current size over two years; difficulty retargets, so your earnings are your share of a fixed
    daily pie. Figures are in MBITE only — it has no market value, is not an investment or a
    security, and mining income is never guaranteed.
  </p>
</div>

{% include 'partials/footer.html' %}
<canvas id="card" width="1080" height="1080"></canvas>
<div class="toast" id="toast"></div>

<script nonce="{{ csp_nonce }}">
(function () {
  'use strict';
  var C = {
    reward: {{ consensus.initial_subsidy_coins }},
    blockSec: {{ consensus.block_time_sec }},
    blocksPerDay: {{ consensus.blocks_per_day }},
    daily: {{ consensus.daily_emission_coins }},
    halving: {{ consensus.halving_interval }},
    height: {{ consensus.height or 0 }}
  };
  var CPU = 12000;                       /* reference desktop CPU for the slider */
  var STEPS = [1,10,100,1000,10000,100000];
  var GROWTH_DAYS = 730;

  function $(id){ return document.getElementById(id); }
  function fmt(n){
    if (n >= 1000) return Math.round(n).toLocaleString('en-US');
    if (n >= 10) return n.toFixed(1);
    return n.toFixed(2);
  }
  /* clipboard.writeText rejects on insecure contexts, denied permission and
     some browsers; an uncaught rejection surfaces as a console error. */
  function copyText(s){
    try {
      if (navigator.clipboard && navigator.clipboard.writeText)
        return navigator.clipboard.writeText(s).catch(function(){});
    } catch(e){}
    return null;
  }
  function toast(m){ var t=$('toast'); t.textContent=m; t.classList.add('show');
    setTimeout(function(){ t.classList.remove('show'); },2200); }

  if (!CanvasRenderingContext2D.prototype.roundRect) {
    CanvasRenderingContext2D.prototype.roundRect=function(x,y,w,h,r){
      r=Math.min(r,w/2,h/2); this.moveTo(x+r,y); this.arcTo(x+w,y,x+w,y+h,r);
      this.arcTo(x+w,y+h,x,y+h,r); this.arcTo(x,y+h,x,y,r); this.arcTo(x,y,x+w,y,r);
      this.closePath(); return this; };
  }

  function myHash(){
    if ($('hw').value === 'custom'){ var v=parseFloat($('hwCustom').value); return v>0?v:CPU; }
    return parseFloat($('hw').value);
  }

  /* Day-by-day integration of your share of a fixed daily emission, with the
     network growing toward ten times its current size over two years. */
  function earned(startDay, endDay, my, todayCpus, finalCpus){
    var total = 0;
    for (var d = Math.floor(startDay); d < Math.floor(endDay); d++){
      var t = Math.min(1, d/GROWTH_DAYS);
      var netHs = Math.max((todayCpus + (finalCpus-todayCpus)*t) * CPU, my);
      total += C.daily * (my/netHs);
    }
    return total;
  }

  /* -------------------------------------------------------- the chart */
  var cv=$('decay'), g=cv.getContext('2d');
  var DPR=Math.min(window.devicePixelRatio||1,2);
  var bars=[], anim=0, animRAF=null;
  var reduced=window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  function drawChart(){
    var W=cv.clientWidth, H=210;
    if (cv.width!==W*DPR){ cv.width=W*DPR; cv.height=H*DPR; }
    cv.style.height=H+'px';
    g.setTransform(DPR,0,0,DPR,0,0);
    g.clearRect(0,0,W,H);
    if(!bars.length) return;

    var padB=42, padT=26, n=bars.length;
    var gap=8, bw=(W-gap*(n-1))/n;
    var max=bars[0].v || 1;

    bars.forEach(function(b,i){
      var full=(H-padB-padT)*(b.v/max);
      var h=full*anim;
      var x=i*(bw+gap), y=H-padB-h;
      var grad=g.createLinearGradient(0,y,0,y+h);
      if(i===0){ grad.addColorStop(0,'#E8C878'); grad.addColorStop(1,'#D9A441'); }
      else {
        var f=i/(n-1);
        grad.addColorStop(0,'rgba(217,164,65,'+(0.75-0.45*f)+')');
        grad.addColorStop(1,'rgba(200,64,47,'+(0.30+0.35*f)+')');
      }
      g.fillStyle=grad;
      g.beginPath(); g.roundRect(x,y,bw,Math.max(h,2),6); g.fill();
      /* value */
      g.fillStyle = i===0 ? '#F5F2EA' : '#8A8580';
      g.font = (i===0?'800 ':'700 ')+'11px system-ui,sans-serif';
      g.textAlign='center';
      g.fillText(fmt(b.v), x+bw/2, y-8);
      /* label */
      g.fillStyle = i===0 ? '#D9A441' : '#8A8580';
      g.font='700 9.5px system-ui,sans-serif';
      g.fillText(b.l1, x+bw/2, H-padB+16);
      if(b.l2){ g.fillStyle='rgba(138,133,128,.75)';
        g.fillText(b.l2, x+bw/2, H-padB+29); }
    });
    /* baseline */
    g.strokeStyle='rgba(245,242,234,.1)'; g.lineWidth=1;
    g.beginPath(); g.moveTo(0,H-padB+.5); g.lineTo(W,H-padB+.5); g.stroke();
  }

  function animate(){
    if(animRAF) cancelAnimationFrame(animRAF);
    if(reduced){ anim=1; drawChart(); return; }
    anim=0;
    (function step(){
      anim += (1-anim)*0.12 + 0.008;
      if(anim>=1){ anim=1; drawChart(); return; }
      drawChart(); animRAF=requestAnimationFrame(step);
    })();
  }

  /* ------------------------------------------------------- calculate */
  function render(animateBars){
    var idx=parseInt($('net').value,10);
    var todayCpus=STEPS[idx];
    $('netLab').textContent = todayCpus.toLocaleString('en-US') +
      (todayCpus===1 ? ' CPU (just you)' : ' CPUs');

    var my=myHash();
    var finalCpus=Math.min(todayCpus*10, 100000);
    var daysLeft=Math.max(1,(C.halving - C.height)/C.blocksPerDay);

    var marks=[0,91,182,273,365];
    var vals=marks.map(function(d){ return earned(d,daysLeft,my,todayCpus,finalCpus); });

    bars=[
      {v:vals[0], l1:'TODAY',   l2:''},
      {v:vals[1], l1:'+3',      l2:'MONTHS'},
      {v:vals[2], l1:'+6',      l2:'MONTHS'},
      {v:vals[3], l1:'+9',      l2:'MONTHS'},
      {v:vals[4], l1:'+1',      l2:'YEAR'}
    ];
    if(animateBars) animate(); else drawChart();

    var loss=vals[0]-vals[4];
    var pct = vals[0] > 0 ? Math.round(100*loss/vals[0]) : 0;
    $('lossV').innerHTML = fmt(loss)+'<span class="u"> MBITE</span>';
    $('lossSub').textContent = 'You would mine '+fmt(vals[0])+
      ' MBITE starting today — only '+fmt(vals[4])+' starting a year from now.';
    $('lossPct').textContent = '−'+pct+'% for waiting';

    $('rows').innerHTML = [
      ['Start today', vals[0], null, true],
      ['In 6 months', vals[2], Math.round(100-100*vals[2]/vals[0]), false],
      ['In 1 year',   vals[4], pct, false]
    ].map(function(r){
      return '<div class="row'+(r[3]?' now':'')+'">'+
        '<div class="when">'+r[0]+'</div>'+
        '<div class="amt num">'+fmt(r[1])+'</div>'+
        '<div class="pen">'+(r[2]===null?'':'−'+r[2]+'%')+'</div></div>';
    }).join('');

    window.__calc={ today:vals[0], year:vals[4], loss:loss, pct:pct, cpus:todayCpus };
  }

  $('hw').addEventListener('change', function(){
    $('hwCustom').style.display = this.value==='custom' ? 'block' : 'none';
    render(true);
  });
  $('hwCustom').addEventListener('input', function(){ render(false); });
  $('net').addEventListener('input', function(){ render(false); });
  $('net').addEventListener('change', function(){ render(true); });
  addEventListener('resize', function(){ drawChart(); });
  render(true);

  /* ------------------------------------------------------ share card */
  function shareText(){
    var c=window.__calc; if(!c) return '';
    return 'Waiting one year to start mining MoonBite costs me '+fmt(c.loss)+
      ' MBITE — '+c.pct+'% of what I would have. Being early is the whole game. moonbite.org/calculator';
  }
  $('btnCard').addEventListener('click', function(){
    var c=window.__calc; if(!c) return;
    if (navigator.share){ navigator.share({text:shareText()}).catch(function(){}); }
    else { copyText(shareText()); toast('Copied'); }
    var cv2=$('card'), x=cv2.getContext('2d');
    var bg=x.createLinearGradient(0,0,0,1080);
    bg.addColorStop(0,'#0B0D12'); bg.addColorStop(1,'#161A22');
    x.fillStyle=bg; x.fillRect(0,0,1080,1080);
    for(var i=0;i<80;i++){ x.globalAlpha=.12+Math.random()*.45; x.fillStyle='#F5F2EA';
      x.beginPath(); x.arc(Math.random()*1080,Math.random()*1080,Math.random()*1.8+.4,0,6.2832); x.fill(); }
    x.globalAlpha=1; x.textAlign='center';
    x.beginPath(); x.arc(540,168,72,0,6.2832); x.fillStyle='#F5F2EA'; x.fill();
    x.beginPath(); x.arc(579,113,56,0,6.2832); x.fillStyle='#0B0D12'; x.fill();
    x.fillStyle='#D9A441';
    x.beginPath(); x.roundRect(430,143,220,9,4.5); x.fill();
    x.beginPath(); x.roundRect(437,182,206,9,4.5); x.fill();
    x.fillStyle='#8A8580'; x.font='700 30px Arial';
    x.fillText('THE COST OF WAITING', 540, 300);
    x.fillStyle='#C8402F'; x.font='800 116px Arial';
    x.fillText('−'+fmt(c.loss), 540, 430);
    x.fillStyle='#F5F2EA'; x.font='800 44px Arial';
    x.fillText('MBITE', 540, 486);
    x.fillStyle='#F5F2EA'; x.font='400 36px Arial';
    x.fillText('is what one year of waiting costs me.', 540, 578);
    x.fillStyle='#8A8580'; x.font='400 32px Arial';
    x.fillText(fmt(c.today)+' MBITE if I start today', 540, 640);
    x.fillText('only '+fmt(c.year)+' if I start next year', 540, 688);
    x.fillStyle='#D9A441'; x.font='800 52px Arial';
    x.fillText('−'+c.pct+'%', 540, 780);
    x.fillStyle='#F5F2EA'; x.font='italic 34px Arial';
    x.fillText('Being early is the whole game.', 540, 862);
    x.fillStyle='#D9A441'; x.font='700 30px Arial';
    x.fillText('moonbite.org/calculator', 540, 938);
    x.fillStyle='rgba(245,242,234,.32)'; x.font='400 19px Arial';
    x.fillText('Estimate in MBITE only. Not an investment. Nothing is guaranteed.', 540, 1002);
    var a=document.createElement('a');
    a.download='moonbite-cost-of-waiting.png';
    a.href=cv2.toDataURL('image/png'); a.click();
  });
})();
</script>
</body>
</html>