$sloprun.dev

Hash Avalanche

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 6 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

Two texts sit side by side differing by exactly one character, and SHA-256 fingerprints both of them on every keystroke: about sixty of the sixty-four hex digits come back different, and a 16×16 grid shows which of the 256 bits flipped. That is the avalanche effect — no partial credit, no getting warmer — which is why nobody can nudge a file toward a fingerprint they want. Press make B match A to watch the whole thing collapse to zero, then change one character in B a few times and notice the big number never strays far from 50%. The bottom panel proves it isn't luck: it hashes up to 256 more one-character edits of text A and plots how far each one moved — a tight pile around half, with the stingiest edit still flipping about 40% of the bits.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/hash-avalanche" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Hash Avalanche — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/hash-avalanche

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 932 lines 35.9 KB index.html
demos/hash-avalanche/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hash Avalanche — sloprun.dev</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }

/* Native UI (textarea caret, scrollbars, selection) must follow the theme too.
   Kept outside the token block above so that block stays verbatim. */
:root { color-scheme: light; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme: dark; } }
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

/* ---------- base ---------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; padding: 22px 16px 34px; line-height: 1.5; overflow-x: hidden; }
.wrap { max-width: 1040px; margin: 0 auto; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button, textarea { font: inherit; color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---------- header ---------- */
.eyebrow {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent); margin: 0 0 6px;
  display: flex; align-items: center; gap: 8px;
}
.eyebrow .led {
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft); flex: none;
}
h1 { font-size: clamp(22px, 5vw, 30px); line-height: 1.12; margin: 0 0 8px; letter-spacing: -.022em; }
.lede { margin: 0 0 18px; color: var(--muted); font-size: 15px; max-width: 68ch; }
.lede b { color: var(--ink); font-weight: 600; }

/* ---------- panels ---------- */
.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 15px;
}
.panel + .panel { margin-top: 13px; }
.phead {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin: 0 0 12px;
}
.ptitle {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted); margin: 0;
}
.pnote { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }

/* ---------- inputs ---------- */
.io { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; }
@media (max-width: 680px) { .io { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.flabel {
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  letter-spacing: .1em; text-transform: uppercase;
}
.tag {
  display: inline-grid; place-items: center; width: 18px; height: 18px;
  border-radius: 4px; background: var(--accent-soft); color: var(--accent);
  font-family: var(--font-mono); font-size: 11px; font-weight: 700; flex: none;
}
textarea {
  width: 100%; min-height: 74px; resize: vertical; padding: 9px 10px;
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  font-family: var(--font-mono); font-size: 13px; line-height: 1.55;
}
textarea:focus-visible { border-color: var(--accent); }

.controls { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; align-items: center; }
.btn {
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  padding: 7px 11px; font-family: var(--font-mono); font-size: 12px;
  cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn:disabled { opacity: .45; cursor: not-allowed; }
.btn.ok { color: var(--run); border-color: var(--run); background: var(--run-soft); }
.sep { flex: 1 1 0; min-width: 0; }

.presets { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 11px; align-items: center; }
.plabel { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.chip {
  background: var(--bg); border: 1px solid var(--line); border-radius: 999px;
  padding: 5px 11px; font-family: var(--font-mono); font-size: 11.5px;
  cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease;
}
.chip:hover { border-color: var(--accent); color: var(--accent); }
.chip[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }

/* ---------- text diff strip ---------- */
.diffwrap { margin-top: 13px; border-top: 1px dashed var(--line); padding-top: 11px; }
.dline { display: grid; grid-template-columns: 20px minmax(0, 1fr); align-items: start; }
.dline + .dline { margin-top: 2px; }
.dtag { font-family: var(--font-mono); font-size: 10px; color: var(--muted); line-height: 1.85; }
.dscroll { position: relative; overflow-x: auto; overflow-y: hidden; min-width: 0; }
.dline code { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.85; white-space: pre; }
mark.hit {
  background: var(--accent-soft); color: var(--accent); font-weight: 700;
  border-bottom: 2px solid var(--accent); border-radius: 3px; padding: 1px 1px 0;
}
.caret {
  display: inline-block; width: 3px; height: 1em; vertical-align: -.15em;
  background: var(--accent); border-radius: 2px;
}
.difftally { margin-top: 8px; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); }
.difftally b { color: var(--ink); font-weight: 600; }

/* ---------- hero readout ---------- */
.hero { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 22px; align-items: center; }
@media (max-width: 760px) { .hero { grid-template-columns: 1fr; } }
.big {
  font-family: var(--font-mono); font-weight: 600; letter-spacing: -.035em;
  font-size: clamp(48px, 14vw, 82px); line-height: .95;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.big .unit { font-size: .36em; color: var(--muted); margin-left: .1em; letter-spacing: 0; }
.bigcap { font-family: var(--font-mono); font-size: 12px; color: var(--muted); margin-top: 8px; }

.gaugewrap { position: relative; margin-top: 16px; }
.gauge { height: 12px; border-radius: 6px; background: var(--line); overflow: hidden; }
.gauge .fill {
  height: 100%; width: 0%; background: var(--accent); border-radius: 6px;
  transition: width .3s cubic-bezier(.2,.7,.3,1);
}
.gaugewrap .mid {
  position: absolute; left: 50%; top: -5px; bottom: -5px; width: 0;
  border-left: 2px dashed var(--muted); opacity: .7;
}
.gscale {
  display: flex; justify-content: space-between; margin-top: 6px;
  font-family: var(--font-mono); font-size: 10px; color: var(--muted); letter-spacing: .06em;
}
.gscale.center { height: 15px; }
.gscale .c { position: absolute; left: 50%; transform: translateX(-50%); }
.gscalewrap { position: relative; }

/* ---------- bit grid ---------- */
.bitbox { width: 100%; max-width: 288px; justify-self: end; }
@media (max-width: 760px) { .bitbox { justify-self: start; max-width: 340px; } }
.bits { display: grid; grid-template-columns: repeat(16, 1fr); gap: 3px; }
.bit { aspect-ratio: 1 / 1; border-radius: 2px; background: var(--line); transition: background .18s ease; }
.bit.on { background: var(--accent); }
.bitcap { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); margin-top: 9px; line-height: 1.5; }

/* ---------- stats ---------- */
.stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 9px; margin-top: 18px;
}
.stat { border: 1px solid var(--line); border-radius: 8px; padding: 9px 10px; background: var(--bg); min-width: 0; }
.stat .k {
  display: block; font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: .13em; text-transform: uppercase; color: var(--muted);
}
.stat .v {
  display: block; font-family: var(--font-mono); font-size: 15px;
  font-variant-numeric: tabular-nums; margin-top: 4px; overflow-wrap: anywhere;
}
.stat .v .sub { color: var(--muted); font-size: 12px; }

.chipstat {
  display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono);
  font-size: 11px; border-radius: 999px; padding: 4px 10px;
  border: 1px solid var(--line); background: var(--bg); color: var(--muted);
}
.chipstat .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.chipstat.ok { color: var(--run); border-color: var(--run); background: var(--run-soft); }

/* ---------- hex ladder ---------- */
.hexpair { margin-bottom: 9px; }
.hexpair:last-child { margin-bottom: 0; }
.hexline { position: relative; padding-left: 20px; display: grid; }
.hexline::before {
  content: attr(data-tag); position: absolute; left: 0; top: 0;
  font-family: var(--font-mono); font-size: 10px; color: var(--muted); line-height: 2.1;
}
.nib {
  font-family: var(--font-mono); font-size: var(--nfs, 12px); line-height: 2.1;
  text-align: center; color: var(--muted); border-bottom: 2px solid transparent; min-width: 0;
}
.nib.hit {
  color: var(--accent); font-weight: 700; background: var(--accent-soft);
  border-bottom-color: var(--accent); border-radius: 3px 3px 0 0;
}
.legend { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 11px; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); }
.legend i { font-style: normal; display: inline-flex; align-items: center; gap: 6px; }
.swatch { width: 11px; height: 11px; border-radius: 3px; background: var(--line); flex: none; }
.swatch.a { background: var(--accent); }

/* ---------- sweep ---------- */
.histwrap { position: relative; margin-top: 4px; }
.hist { display: flex; align-items: flex-end; gap: 2px; height: 92px; }
.hbar { flex: 1 1 0; min-width: 0; background: var(--accent); opacity: .8; border-radius: 2px 2px 0 0; min-height: 2px; transition: height .25s ease; }
.histwrap .mid { position: absolute; left: 50%; top: -4px; bottom: -4px; width: 0; border-left: 2px dashed var(--muted); opacity: .7; }
.haxis { display: flex; justify-content: space-between; margin-top: 6px; font-family: var(--font-mono); font-size: 10px; color: var(--muted); }
.sweepline { margin-top: 12px; font-family: var(--font-mono); font-size: 12px; color: var(--muted); line-height: 1.7; }
.sweepline b { color: var(--ink); font-weight: 600; }
.sweepsay { margin: 10px 0 0; font-size: 14px; color: var(--muted); }
.sweepsay b { color: var(--ink); font-weight: 600; }

/* ---------- footer ---------- */
footer {
  margin-top: 16px; display: flex; justify-content: space-between; gap: 10px;
  flex-wrap: wrap; font-family: var(--font-mono); font-size: 11px; color: var(--muted);
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
}
</style>
</head>
<body>
<div class="wrap">

  <p class="eyebrow"><span class="led"></span>SHA-256 &middot; avalanche</p>
  <h1>Hash Avalanche</h1>
  <p class="lede">
    Two texts that differ by <b>one character</b> get two fingerprints that differ <b>everywhere</b> —
    about half of all 256 bits flip. That is why you cannot nudge a file toward a fingerprint you
    want: there is no "close", and no warmer or colder to follow.
  </p>

  <!-- ============ INPUTS ============ -->
  <section class="panel" aria-labelledby="h-in">
    <div class="phead">
      <h2 class="ptitle" id="h-in">the two texts</h2>
      <span class="pnote">re-hashes as you type</span>
    </div>

    <div class="io">
      <div class="field">
        <label class="flabel" for="ta"><span class="tag">A</span> text a</label>
        <textarea id="ta" spellcheck="false" autocomplete="off" autocapitalize="off" rows="3">transfer 100 dollars to alex on friday</textarea>
      </div>
      <div class="field">
        <label class="flabel" for="tb"><span class="tag">B</span> text b</label>
        <textarea id="tb" spellcheck="false" autocomplete="off" autocapitalize="off" rows="3">transfer 900 dollars to alex on friday</textarea>
      </div>
    </div>

    <div class="controls">
      <button class="btn" id="btnNudge" type="button">change one character in B</button>
      <button class="btn" id="btnSame" type="button">make B match A</button>
      <span class="sep"></span>
      <span class="chipstat" id="matchChip"><span class="dot"></span><span id="matchTxt">comparing</span></span>
    </div>

    <div class="presets" id="presets">
      <span class="plabel">try:</span>
    </div>

    <div class="diffwrap">
      <div class="dline"><span class="dtag">A</span><div class="dscroll" tabindex="0" role="group" aria-label="text A, changed characters highlighted"><code id="dA"></code></div></div>
      <div class="dline"><span class="dtag">B</span><div class="dscroll" tabindex="0" role="group" aria-label="text B, changed characters highlighted"><code id="dB"></code></div></div>
      <p class="difftally" id="tally"></p>
    </div>
  </section>

  <!-- ============ RESULT ============ -->
  <section class="panel" aria-labelledby="h-res">
    <div class="phead">
      <h2 class="ptitle" id="h-res">how much of the fingerprint moved</h2>
      <span class="pnote" id="engine">engine: —</span>
    </div>

    <div class="hero">
      <div>
        <div class="big"><span id="pct">—</span><span class="unit">%</span></div>
        <div class="bigcap">of the 256 bits changed &middot; a good hash sits near 50%</div>

        <div class="gaugewrap">
          <div class="gauge"><div class="fill" id="fill"></div></div>
          <span class="mid"></span>
        </div>
        <div class="gscalewrap">
          <div class="gscale"><span>0%</span><span>100%</span></div>
          <div class="gscale center"><span class="c">ideal 50%</span></div>
        </div>
      </div>

      <div class="bitbox">
        <div class="bits" id="bits" aria-hidden="true"></div>
        <p class="bitcap">each square is one bit of the 256-bit fingerprint &middot; filled = flipped</p>
      </div>
    </div>

    <div class="stats">
      <div class="stat"><span class="k">bits changed</span><span class="v"><span id="sBits">—</span> <span class="sub">/ 256</span></span></div>
      <div class="stat"><span class="k">hex digits changed</span><span class="v"><span id="sHex">—</span> <span class="sub">/ 64</span></span></div>
      <div class="stat"><span class="k">input difference</span><span class="v" id="sIn">—</span></div>
      <div class="stat"><span class="k">longest run kept</span><span class="v" id="sRun">—</span></div>
    </div>
  </section>

  <!-- ============ HEX ============ -->
  <section class="panel" aria-labelledby="h-hex">
    <div class="phead">
      <h2 class="ptitle" id="h-hex">the two fingerprints, digit by digit</h2>
      <div style="display:flex;gap:7px;flex-wrap:wrap">
        <button class="btn" id="copyA" type="button">copy A</button>
        <button class="btn" id="copyB" type="button">copy B</button>
      </div>
    </div>
    <div id="hexbox" aria-hidden="true"></div>
    <div class="legend">
      <i><span class="swatch"></span> same digit</i>
      <i><span class="swatch a"></span> different digit</i>
      <i>64 hex digits &middot; 256 bits</i>
    </div>
    <p class="sr" id="hexSr"></p>
  </section>

  <!-- ============ SWEEP ============ -->
  <section class="panel" aria-labelledby="h-sw">
    <div class="phead">
      <h2 class="ptitle" id="h-sw">other one-character edits of A</h2>
      <button class="btn" id="btnSweep" type="button">try a different batch</button>
    </div>
    <div class="histwrap">
      <div class="hist" id="hist"></div>
      <span class="mid"></span>
    </div>
    <div class="haxis"><span>35%</span><span>50%</span><span>65%</span></div>
    <p class="sweepline" id="sweepStats">—</p>
    <p class="sweepsay" id="sweepSay"></p>
  </section>

  <p class="sr" role="status" aria-live="polite" id="live"></p>

  <footer>
    <span>hash avalanche &middot; sloprun.dev</span>
    <span>SHA-256 &middot; nothing leaves this tab</span>
  </footer>
</div>

<script>
(function () {
  "use strict";

  /* ---------- platform theme ---------- */
  addEventListener("message", function (e) {
    var d = e && e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  var $ = function (id) { return document.getElementById(id); };
  var enc = new TextEncoder();

  /* ---------- SHA-256, in page, as a fallback ----------
     Used only when WebCrypto is unavailable (e.g. opened straight off disk).
     Same algorithm, same answers — just slower. */
  var K = [
    0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
    0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
    0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
    0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
    0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
    0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
    0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
    0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
  ];
  function rotr(x, n) { return (x >>> n) | (x << (32 - n)); }

  function sha256js(msg) {
    var H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
    var len = msg.length;
    var padded = len + 1;
    var zeros = (56 - (padded % 64) + 64) % 64;
    var total = padded + zeros + 8;
    var buf = new Uint8Array(total);
    buf.set(msg, 0);
    buf[len] = 0x80;
    var dv = new DataView(buf.buffer);
    var bits = len * 8;
    dv.setUint32(total - 8, Math.floor(bits / 4294967296) >>> 0);
    dv.setUint32(total - 4, bits >>> 0);

    var w = new Uint32Array(64), i, off;
    for (off = 0; off < total; off += 64) {
      for (i = 0; i < 16; i++) w[i] = dv.getUint32(off + i * 4);
      for (i = 16; i < 64; i++) {
        var x = w[i - 15], y = w[i - 2];
        var s0 = (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0;
        var s1 = (rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10)) >>> 0;
        w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
      }
      var a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
      for (i = 0; i < 64; i++) {
        var S1 = (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0;
        var ch = ((e & f) ^ (~e & g)) >>> 0;
        var t1 = (h + S1 + ch + K[i] + w[i]) >>> 0;
        var S0 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0;
        var mj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
        var t2 = (S0 + mj) >>> 0;
        h = g; g = f; f = e; e = (d + t1) >>> 0;
        d = c; c = b; b = a; a = (t1 + t2) >>> 0;
      }
      H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0;
      H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
      H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0;
      H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
    }
    var out = new Uint8Array(32), odv = new DataView(out.buffer);
    for (i = 0; i < 8; i++) odv.setUint32(i * 4, H[i]);
    return out;
  }

  var subtleOK = false;
  function digest(str) {
    var bytes = enc.encode(str);
    if (subtleOK) {
      return crypto.subtle.digest("SHA-256", bytes).then(function (b) {
        return new Uint8Array(b);
      }, function () {
        subtleOK = false;
        $("engine").textContent = "engine: in-page js";
        return sha256js(bytes);
      });
    }
    return Promise.resolve(sha256js(bytes));
  }

  var EMPTY_SHA = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
  function probe() {
    if (!(window.crypto && crypto.subtle && crypto.subtle.digest)) return Promise.resolve(false);
    try {
      return crypto.subtle.digest("SHA-256", new Uint8Array(0)).then(function (b) {
        return hex(new Uint8Array(b)) === EMPTY_SHA;
      }, function () { return false; });
    } catch (err) { return Promise.resolve(false); }
  }

  /* ---------- helpers ---------- */
  var HEXC = "0123456789abcdef";
  function hex(bytes) {
    var s = "";
    for (var i = 0; i < bytes.length; i++) s += HEXC[bytes[i] >> 4] + HEXC[bytes[i] & 15];
    return s;
  }
  var POP = new Uint8Array(256);
  for (var pi = 1; pi < 256; pi++) POP[pi] = (pi & 1) + POP[pi >> 1];

  function bitDiff(x, y) {
    var n = 0;
    for (var i = 0; i < 32; i++) n += POP[x[i] ^ y[i]];
    return n;
  }
  function flippedBits(x, y) {
    var out = new Uint8Array(256);
    for (var i = 0; i < 32; i++) {
      var v = x[i] ^ y[i];
      for (var b = 0; b < 8; b++) out[i * 8 + b] = (v >> (7 - b)) & 1;
    }
    return out;
  }
  function commonEdges(a, b) {
    var n = Math.min(a.length, b.length), p = 0, s = 0;
    while (p < n && a[p] === b[p]) p++;
    while (s < n - p && a[a.length - 1 - s] === b[b.length - 1 - s]) s++;
    return { pre: p, suf: s };
  }
  function longestSameRun(ha, hb) {
    var best = 0, run = 0;
    for (var i = 0; i < 64; i++) {
      if (ha[i] === hb[i]) { run++; if (run > best) best = run; } else run = 0;
    }
    return best;
  }

  /* ---------- state ---------- */
  var elA = $("ta"), elB = $("tb");
  var hexA = "", hexB = "", seq = 0;
  var lastCols = 0;

  var PRESETS = [
    { n: "money note", a: "transfer 100 dollars to alex on friday", b: "transfer 900 dollars to alex on friday" },
    { n: "pangram",    a: "the quick brown fox jumps over the lazy dog", b: "the quick brown fox jumps over the lazy cog" },
    { n: "file name",  a: "budget-2026-final.txt", b: "budget-2027-final.txt" },
    { n: "extra space", a: "hello world", b: "hello  world" },
    { n: "empty vs dot", a: "", b: "." }
  ];

  /* ---------- preset chips ---------- */
  (function buildPresets() {
    var box = $("presets");
    PRESETS.forEach(function (p, i) {
      var b = document.createElement("button");
      b.type = "button";
      b.className = "chip";
      b.textContent = p.n;
      b.setAttribute("aria-pressed", "false");
      b.addEventListener("click", function () {
        elA.value = p.a; elB.value = p.b;
        update();
        say("loaded example: " + p.n);
      });
      box.appendChild(b);
    });
  })();

  function syncChips() {
    var chips = $("presets").querySelectorAll(".chip");
    for (var i = 0; i < chips.length; i++) {
      var p = PRESETS[i];
      chips[i].setAttribute("aria-pressed", (p.a === elA.value && p.b === elB.value) ? "true" : "false");
    }
  }

  /* ---------- text diff strip ---------- */
  function renderTextDiff() {
    var a = elA.value, b = elB.value;
    var ed = commonEdges(a, b);
    var pre = ed.pre, suf = ed.suf, same = a === b;
    paint($("dA"), a, pre, a.length - suf, same);
    paint($("dB"), b, pre, b.length - suf, same);
    centerMark($("dA").parentNode);
    centerMark($("dB").parentNode);

    var changed = Math.max(a.length, b.length) - pre - suf;
    var t = $("tally");
    t.textContent = "";
    if (a === b) {
      t.appendChild(document.createTextNode("the two texts are identical"));
    } else {
      var word = changed === 1 ? "character" : "characters";
      var how = a.length === b.length ? "changed"
              : (b.length > a.length ? "added to B" : "removed from B");
      var strong = document.createElement("b");
      strong.textContent = changed + " " + word;
      t.appendChild(strong);
      t.appendChild(document.createTextNode(" " + how + " · at position " + pre +
        " · length " + a.length + " → " + b.length));
    }
    return { pre: pre, changed: changed, same: a === b };
  }

  /* keep the changed character in view even when the line is wider than the panel */
  function centerMark(line) {
    var m = line.querySelector("mark.hit, .caret");
    if (!m) { line.scrollLeft = 0; return; }
    var want = m.offsetLeft + m.offsetWidth / 2 - line.clientWidth / 2;
    line.scrollLeft = want > 0 ? want : 0;
  }

  function paint(node, str, from, to, same) {
    node.textContent = "";
    if (str === "") {
      var em = document.createElement("span");
      em.textContent = "(empty)";
      em.style.color = "var(--muted)";
      node.appendChild(em);
      return;
    }
    if (same) { node.appendChild(document.createTextNode(str)); return; }
    if (to <= from) {
      /* pure insertion on the other side: show where it lands */
      node.appendChild(document.createTextNode(str.slice(0, from)));
      var car = document.createElement("span");
      car.className = "caret";
      node.appendChild(car);
      node.appendChild(document.createTextNode(str.slice(from)));
      return;
    }
    node.appendChild(document.createTextNode(str.slice(0, from)));
    var m = document.createElement("mark");
    m.className = "hit";
    m.textContent = str.slice(from, to);
    node.appendChild(m);
    node.appendChild(document.createTextNode(str.slice(to)));
  }

  /* ---------- hex ladder ---------- */
  function pickCols() {
    var w = ($("hexbox").clientWidth || 320) - 22;
    var cands = [64, 32, 16, 8];
    for (var i = 0; i < cands.length; i++) if (w / cands[i] >= 14) return cands[i];
    return 8;
  }

  function lineEl(tag, mine, other, start, cols) {
    var d = document.createElement("div");
    d.className = "hexline";
    d.setAttribute("data-tag", tag);
    d.style.gridTemplateColumns = "repeat(" + cols + ", minmax(0, 1fr))";
    for (var i = start; i < start + cols; i++) {
      var s = document.createElement("span");
      s.className = "nib" + (mine.charAt(i) !== other.charAt(i) ? " hit" : "");
      s.textContent = mine.charAt(i);
      d.appendChild(s);
    }
    return d;
  }

  function renderHex(force) {
    var box = $("hexbox");
    var cols = pickCols();
    if (!force && cols === lastCols && box.childNodes.length) {
      /* same layout: just refresh the characters */
      var lines = box.querySelectorAll(".hexline");
      for (var li = 0; li < lines.length; li++) {
        var src = lines[li].getAttribute("data-tag") === "A" ? hexA : hexB;
        var oth = lines[li].getAttribute("data-tag") === "A" ? hexB : hexA;
        var kids = lines[li].childNodes;
        var base = Math.floor(li / 2) * cols;
        for (var ki = 0; ki < kids.length; ki++) {
          var idx = base + ki;
          kids[ki].textContent = src.charAt(idx);
          kids[ki].className = "nib" + (src.charAt(idx) !== oth.charAt(idx) ? " hit" : "");
        }
      }
      return;
    }
    lastCols = cols;
    var cell = (box.clientWidth - 22) / cols;
    box.style.setProperty("--nfs", Math.max(9, Math.min(14, Math.round(cell * 0.72))) + "px");
    var frag = document.createDocumentFragment();
    for (var c = 0; c < 64; c += cols) {
      var pair = document.createElement("div");
      pair.className = "hexpair";
      pair.appendChild(lineEl("A", hexA, hexB, c, cols));
      pair.appendChild(lineEl("B", hexB, hexA, c, cols));
      frag.appendChild(pair);
    }
    box.textContent = "";
    box.appendChild(frag);
  }

  /* ---------- bit grid ---------- */
  var bitCells = [];
  (function buildBits() {
    var box = $("bits"), frag = document.createDocumentFragment();
    for (var i = 0; i < 256; i++) {
      var d = document.createElement("div");
      d.className = "bit";
      frag.appendChild(d);
      bitCells.push(d);
    }
    box.appendChild(frag);
  })();

  function renderBits(flags) {
    for (var i = 0; i < 256; i++) {
      var on = flags[i] === 1;
      if (on !== (bitCells[i].className === "bit on")) bitCells[i].className = on ? "bit on" : "bit";
    }
  }

  /* ---------- main update ---------- */
  function update() {
    var mine = ++seq;
    var td = renderTextDiff();
    syncChips();
    $("btnNudge").disabled = elA.value.length === 0;

    return Promise.all([digest(elA.value), digest(elB.value)]).then(function (r) {
      if (mine !== seq) return;
      var ba = r[0], bb = r[1];
      hexA = hex(ba); hexB = hex(bb);

      var bits = bitDiff(ba, bb);
      var pct = bits / 256 * 100;
      var hexd = 0;
      for (var i = 0; i < 64; i++) if (hexA.charAt(i) !== hexB.charAt(i)) hexd++;

      $("pct").textContent = pct.toFixed(1);
      $("fill").style.width = pct + "%";
      $("sBits").textContent = bits;
      $("sHex").textContent = hexd;
      $("sIn").textContent = td.same ? "none" : (td.changed + " char" + (td.changed === 1 ? "" : "s"));
      var lrun = td.same ? 64 : longestSameRun(hexA, hexB);
      $("sRun").textContent = lrun + (lrun === 1 ? " digit" : " digits");

      var chip = $("matchChip"), txt = $("matchTxt");
      if (td.same) {
        chip.className = "chipstat ok";
        txt.textContent = "identical text → identical fingerprint";
      } else {
        chip.className = "chipstat";
        txt.textContent = bits + " of 256 bits differ";
      }

      renderBits(flippedBits(ba, bb));
      renderHex(false);
      $("hexSr").textContent = "Fingerprint A " + hexA + ". Fingerprint B " + hexB + ".";
      say(pct.toFixed(1) + " percent of bits changed, " + bits + " of 256, " + hexd + " of 64 hex digits");

      if (elA.value !== lastSweptA) scheduleSweep();
    });
  }

  var sayT = 0;
  function say(msg) {
    clearTimeout(sayT);
    sayT = setTimeout(function () { $("live").textContent = msg; }, 260);
  }

  /* ---------- the sweep: every one-character edit of A ---------- */
  var LETTERS = "abcdefghijklmnopqrstuvwxyz";
  function altChar(c, salt) {
    var lo = c.toLowerCase();
    var li = LETTERS.indexOf(lo);
    if (li >= 0) return LETTERS.charAt((li + 1 + (salt % 25)) % 26);
    if (c >= "0" && c <= "9") return String.fromCharCode(48 + ((+c + 1 + (salt % 9)) % 10));
    return LETTERS.charAt(salt % 26);
  }

  var sweepSeq = 0, sweepT = 0, lastSweptA = null;
  function scheduleSweep() {
    clearTimeout(sweepT);
    sweepT = setTimeout(function () { runSweep(false); }, 320);
  }

  function runSweep(force) {
    var text = elA.value;
    if (!force && text === lastSweptA) return;
    lastSweptA = text;
    var mine = ++sweepSeq;
    var stats = $("sweepStats"), say2 = $("sweepSay");
    if (!text.length) {
      drawHist(new Array(24).fill(0));
      stats.textContent = "text A is empty — nothing to edit";
      say2.textContent = "";
      return;
    }
    var chars = text.split("");
    var N = Math.min(256, Math.max(48, chars.length * 8));
    stats.textContent = "hashing " + N + " variants…";

    /* fresh random replacements every run, so the button really does re-roll */
    var salt = Math.floor(Math.random() * 100000);
    var jobs = [digest(text)];
    for (var i = 0; i < N; i++) {
      var v = chars.slice();
      var pos = i % chars.length;
      v[pos] = altChar(chars[pos], salt + i);
      jobs.push(digest(v.join("")));
    }

    return Promise.all(jobs).then(function (res) {
      if (mine !== sweepSeq) return;
      var base = res[0];
      var bins = new Array(24).fill(0);
      var sum = 0, lo = 101, hi = -1;
      for (var i = 1; i < res.length; i++) {
        var p = bitDiff(base, res[i]) / 256 * 100;
        sum += p;
        if (p < lo) lo = p;
        if (p > hi) hi = p;
        var idx = Math.floor((p - 35) / 30 * 24);
        if (idx < 0) idx = 0;
        if (idx > 23) idx = 23;
        bins[idx]++;
      }
      drawHist(bins);
      var mean = sum / N;
      stats.textContent = "";
      addStat(stats, N + " edits", null);
      addStat(stats, "mean ", mean.toFixed(1) + "%");
      addStat(stats, "lowest ", lo.toFixed(1) + "%");
      addStat(stats, "highest ", hi.toFixed(1) + "%");
      $("sweepSay").textContent = "Not one of those " + N + " edits left the fingerprint recognisable. " +
        "The stingiest change still flipped " + Math.round(lo) + "% of the bits.";
    });
  }

  function addStat(node, label, value) {
    if (node.childNodes.length) node.appendChild(document.createTextNode(" · "));
    node.appendChild(document.createTextNode(label));
    if (value !== null) {
      var b = document.createElement("b");
      b.textContent = value;
      node.appendChild(b);
    }
  }

  var histBars = [];
  (function buildHist() {
    var h = $("hist"), frag = document.createDocumentFragment();
    for (var i = 0; i < 24; i++) {
      var d = document.createElement("div");
      d.className = "hbar";
      d.style.height = "2px";
      frag.appendChild(d);
      histBars.push(d);
    }
    h.appendChild(frag);
    h.setAttribute("aria-hidden", "true");
  })();

  function drawHist(bins) {
    var max = 1;
    for (var i = 0; i < bins.length; i++) if (bins[i] > max) max = bins[i];
    for (i = 0; i < bins.length; i++) {
      histBars[i].style.height = Math.max(2, Math.round(bins[i] / max * 92)) + "px";
      histBars[i].style.opacity = bins[i] ? ".8" : ".18";
    }
  }

  /* ---------- copy ---------- */
  function flash(btn, label, good) {
    var old = btn.getAttribute("data-label") || btn.textContent;
    btn.setAttribute("data-label", old);
    btn.textContent = label;
    if (good) btn.classList.add("ok");
    setTimeout(function () {
      btn.textContent = old;
      btn.classList.remove("ok");
    }, 1300);
  }
  function copyText(text, btn) {
    var fallback = function () {
      try {
        var ta = document.createElement("textarea");
        ta.value = text;
        ta.setAttribute("readonly", "");
        ta.style.position = "fixed";
        ta.style.top = "-2000px";
        document.body.appendChild(ta);
        ta.select();
        var ok = document.execCommand("copy");
        document.body.removeChild(ta);
        flash(btn, ok ? "copied" : "cannot copy", ok);
      } catch (err) { flash(btn, "cannot copy", false); }
    };
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(function () {
          flash(btn, "copied", true);
        }, fallback);
        return;
      }
    } catch (err) { /* fall through */ }
    fallback();
  }

  /* ---------- wiring ---------- */
  elA.addEventListener("input", function () { update(); });
  elB.addEventListener("input", function () { update(); });

  $("btnNudge").addEventListener("click", function () {
    var a = elA.value;
    if (!a.length) return;
    var pos = Math.floor(Math.random() * a.length);
    var salt = Math.floor(Math.random() * 1000);
    elB.value = a.slice(0, pos) + altChar(a.charAt(pos), salt) + a.slice(pos + 1);
    update();
    say("changed one character in B at position " + pos);
  });

  $("btnSame").addEventListener("click", function () {
    elB.value = elA.value;
    update();
    say("B now matches A exactly");
  });

  $("btnSweep").addEventListener("click", function () {
    clearTimeout(sweepT);
    runSweep(true);
    say("running a new batch of one-character edits");
  });

  $("copyA").addEventListener("click", function () { copyText(hexA, this); });
  $("copyB").addEventListener("click", function () { copyText(hexB, this); });

  var rT = 0;
  function onResize() {
    clearTimeout(rT);
    rT = setTimeout(function () {
      if (pickCols() !== lastCols) renderHex(true);
    }, 120);
  }
  if (window.ResizeObserver) {
    new ResizeObserver(onResize).observe($("hexbox"));
  } else {
    addEventListener("resize", onResize);
  }

  /* ---------- go ---------- */
  probe().then(function (ok) {
    subtleOK = ok;
    $("engine").textContent = "engine: " + (ok ? "webcrypto" : "in-page js");
    return update();
  }).then(function () {
    renderHex(true);
    clearTimeout(sweepT);
    return runSweep(true);
  });
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong