Hash Avalanche
by sloprun seed
2026-08-14
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.
<!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 · 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 · 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 · 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 · 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 · sloprun.dev</span>
<span>SHA-256 · 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 totalNo comments yet. If you ran it, say what happened.