$sloprun.dev

QR Code Maker

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

Type a link or a note and a real, scannable QR code assembles itself as you type — nothing uploaded, nothing fetched, the entire encoder lives in the page. Under the black squares sits the actual standard: byte-mode bit packing, Reed–Solomon check codewords over GF(256), the zigzag walk that threads data through the grid, and all eight mask patterns scored so the tidiest one wins. It opens on sloprun.dev as a version 2 symbol; tap the "a long note" chip and watch it jump to version 11, while the panel below shows how many codewords are suddenly there purely to survive damage. Then switch the modules to dots, pull the size slider up, and save the PNG.

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/qr-generator" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="QR Code Maker — 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/qr-generator

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 853 lines 34.4 KB index.html
demos/qr-generator/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QR Code Maker — 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); }

/* ---- derived surfaces (theme-paired, same rules as the tokens above) ---- */
:root { --wash: #F1F0EC; --shadow: 0 1px 2px rgba(20,22,26,.05), 0 8px 26px rgba(20,22,26,.07); --dim: .42; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --wash: #15181D; --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.42); --dim: .38; }
}
:root[data-theme="dark"] { --wash: #15181D; --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.42); --dim: .38; }

/* ---- shell ---- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-size: 16px; line-height: 1.55; overflow-x: hidden; -webkit-text-size-adjust: 100%; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 26px 16px 34px; }
h1 { font-size: 1.5rem; line-height: 1.2; margin: 0 0 7px; letter-spacing: -.015em; }
.lede { margin: 0; color: var(--muted); font-size: .95rem; max-width: 66ch; }
.lede b { color: var(--ink); font-weight: 600; }
.mono { font-family: var(--font-mono); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
footer { margin-top: 22px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }

.k-lab {
  font-family: var(--font-mono); font-size: .64rem; letter-spacing: .11em;
  text-transform: uppercase; color: var(--muted);
}
.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px;
}
.panel + .panel { margin-top: 14px; }
.panelhead { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-bottom: 11px; }

/* ---- layout ---- */
.grid {
  display: grid; grid-template-columns: minmax(0, 560px) minmax(0, 1fr);
  gap: 16px; align-items: start; margin-top: 18px;
}
@media (max-width: 760px) { .grid { grid-template-columns: 1fr; } }

/* ---- the code card ---- */
.stage {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 16px; box-shadow: var(--shadow);
}
.paper {
  background: #FFFFFF; border-radius: 8px; padding: 10px;
  display: flex; align-items: center; justify-content: center;
  min-height: 180px; border: 1px solid var(--line);
}
:root[data-theme="dark"] .paper { border-color: rgba(0,0,0,.3); }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .paper { border-color: rgba(0,0,0,.3); } }
#qr { display: block; width: auto; max-width: 100%; height: auto; border-radius: 3px; }
.stage.over #qr { opacity: .28; filter: grayscale(1); }
#qr { transition: opacity .18s ease, filter .18s ease; }

.stageline {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
  margin-top: 11px; font-family: var(--font-mono); font-size: .7rem; color: var(--muted);
}
.stageline b { color: var(--ink); font-weight: 600; }
.stageline .sep { opacity: .45; }
.stageline.bad, .stageline.bad b { color: var(--danger); }

.stageact { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 12px; }
.dlsize { font-family: var(--font-mono); font-size: .7rem; color: var(--muted); }
.saved { font-family: var(--font-mono); font-size: .7rem; color: var(--run); }

/* ---- controls ---- */
label.field { display: block; }
textarea, input[type="text"] {
  width: 100%; font-family: var(--font-mono); font-size: .82rem; line-height: 1.5;
  color: var(--ink); background: var(--wash); border: 1px solid var(--line);
  border-radius: 8px; padding: 9px 10px; resize: vertical;
}
textarea { min-height: 92px; }
textarea:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }

.btn {
  font-family: var(--font-mono); font-size: .73rem; letter-spacing: .02em;
  background: var(--bg); color: var(--ink); border: 1px solid var(--line);
  border-radius: 999px; padding: 6px 12px; min-height: 32px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px; text-decoration: none;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
.btn.primary { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.btn.primary:hover { background: var(--accent); color: var(--surface); }
:root[data-theme="dark"] .btn.primary:hover { color: #14161A; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) .btn.primary:hover { color: #14161A; } }
.btn[aria-disabled="true"], .btn:disabled { opacity: var(--dim); cursor: not-allowed; pointer-events: none; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* ---- slider ---- */
.sliderrow { display: flex; align-items: center; gap: 10px; }
input[type="range"] {
  -webkit-appearance: none; appearance: none; flex: 1 1 120px; min-width: 0;
  height: 22px; background: transparent; cursor: pointer;
}
input[type="range"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 999px; }
input[type="range"]::-webkit-slider-runnable-track { height: 4px; border-radius: 999px; background: var(--line); }
input[type="range"]::-moz-range-track { height: 4px; border-radius: 999px; background: var(--line); }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 16px; height: 16px; margin-top: -6px;
  border-radius: 50%; background: var(--accent); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--accent);
}
input[type="range"]::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--accent);
}
.px { font-family: var(--font-mono); font-size: .73rem; color: var(--ink); min-width: 8ch; text-align: right; }

/* ---- capacity meter ---- */
.meter { margin-top: 4px; }
.meterbar { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; }
.meterfill { height: 100%; width: 0%; background: var(--accent); border-radius: 999px; transition: width .18s ease, background-color .18s ease; }
.meterfill.bad { background: var(--danger); }
.meterfoot { display: flex; justify-content: space-between; gap: 8px; margin-top: 5px; font-family: var(--font-mono); font-size: .68rem; color: var(--muted); }
.meterfoot b { color: var(--ink); font-weight: 600; }

/* ---- facts ---- */
.facts { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
@media (max-width: 400px) { .facts { grid-template-columns: 1fr; } }
.fact { background: var(--surface); padding: 8px 10px; min-width: 0; }
.fact .v { font-family: var(--font-mono); font-size: .84rem; color: var(--ink); font-variant-numeric: tabular-nums; word-break: break-word; }
.fact .v small { color: var(--muted); font-size: .72rem; }

.note { margin: 10px 0 0; font-size: .8rem; color: var(--muted); }
.alert {
  margin-top: 12px; border: 1px solid var(--danger); background: var(--surface);
  border-radius: 8px; padding: 10px 11px; font-size: .84rem;
}
.alert b { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .09em; text-transform: uppercase; color: var(--danger); display: block; margin-bottom: 3px; }
.alert[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<main class="wrap">
  <h1>QR Code Maker</h1>
  <p class="lede">Type anything — a link, a note, a wifi login — and it becomes a real scannable code,
    <b>built module by module right here in the page</b>. Nothing is uploaded, nothing is fetched.</p>

  <div class="grid">

    <!-- ============ the code ============ -->
    <section class="stage" id="stage" aria-labelledby="stageLab">
      <h2 class="sr-only" id="stageLab">The code</h2>
      <div class="paper">
        <canvas id="qr" role="img" aria-label="QR code"></canvas>
      </div>
      <p class="stageline" id="stageline"></p>
      <div class="stageact">
        <a class="btn primary" id="dl" download="qr-code.png" aria-disabled="true">Download PNG</a>
        <span class="dlsize" id="dlsize"></span>
        <span class="saved" id="saved" role="status"></span>
      </div>
    </section>

    <!-- ============ controls ============ -->
    <div>
      <section class="panel" aria-labelledby="inLab">
        <div class="panelhead">
          <span class="k-lab" id="inLab">what goes in the code</span>
        </div>
        <label class="field">
          <span class="sr-only">Text or link to encode</span>
          <textarea id="text" spellcheck="false" autocomplete="off" autocapitalize="off"
            aria-describedby="capFoot"></textarea>
        </label>
        <div class="meter">
          <div class="meterbar"><div class="meterfill" id="meterFill"></div></div>
          <div class="meterfoot">
            <span id="capFoot"></span>
            <span id="capRight"></span>
          </div>
        </div>
        <div class="alert" id="alert" role="status" hidden>
          <b>too long to fit</b>
          <span id="alertBody"></span>
        </div>
        <p class="note" style="margin-top:11px"><span class="k-lab">try one</span></p>
        <div class="chips" id="chips" style="margin-top:6px"></div>
      </section>

      <section class="panel" aria-labelledby="lookLab">
        <div class="panelhead"><span class="k-lab" id="lookLab">how it looks</span></div>

        <p class="k-lab" style="margin:0 0 5px">image size</p>
        <div class="sliderrow">
          <input type="range" id="size" min="180" max="1000" step="20" value="520" aria-label="image size in pixels">
          <span class="px" id="sizeOut"></span>
        </div>

        <p class="k-lab" style="margin:12px 0 6px">module shape</p>
        <div class="chips" role="group" aria-label="module shape">
          <button class="btn" type="button" data-style="square">squares</button>
          <button class="btn" type="button" data-style="round">rounded</button>
          <button class="btn" type="button" data-style="dot">dots</button>
        </div>
      </section>

      <section class="panel" aria-labelledby="factLab">
        <div class="panelhead"><span class="k-lab" id="factLab">what got built</span></div>
        <div class="facts" id="facts"></div>
        <p class="note">Error correction is fixed at level <b class="mono">M</b> — about 15% of the code
          can be smudged, torn or covered and a reader still gets your text back.</p>
      </section>
    </div>
  </div>

  <footer>demo · sloprun.dev</footer>
</main>

<script>
(function () {
  "use strict";
  var $ = function (id) { return document.getElementById(id); };

  /* =====================================================================
     QR encoder — byte mode, error correction level M, versions 1 through 20.
     Written from the spec: bit stream -> Reed-Solomon -> module grid -> mask.
     ===================================================================== */

  var MAXV = 20;

  /* per version (M): [ec codewords per block, blocks g1, data per g1 block,
                       blocks g2, data per g2 block] */
  var EC = {
    1:  [10, 1, 16, 0, 0],   2:  [16, 1, 28, 0, 0],   3:  [26, 1, 44, 0, 0],
    4:  [18, 2, 32, 0, 0],   5:  [24, 2, 43, 0, 0],   6:  [16, 4, 27, 0, 0],
    7:  [18, 4, 31, 0, 0],   8:  [22, 2, 38, 2, 39],  9:  [22, 3, 36, 2, 37],
    10: [26, 4, 43, 1, 44],  11: [30, 1, 50, 4, 51],  12: [22, 6, 36, 2, 37],
    13: [22, 8, 37, 1, 38],  14: [24, 4, 40, 5, 41],  15: [24, 5, 41, 5, 42],
    16: [28, 7, 45, 3, 46],  17: [28, 10, 46, 1, 47], 18: [26, 9, 43, 4, 44],
    19: [26, 3, 44, 11, 45], 20: [26, 3, 41, 13, 42]
  };

  /* alignment pattern centre coordinates */
  var ALIGN = {
    1: [], 2: [6,18], 3: [6,22], 4: [6,26], 5: [6,30], 6: [6,34],
    7: [6,22,38], 8: [6,24,42], 9: [6,26,46], 10: [6,28,50], 11: [6,30,54],
    12: [6,32,58], 13: [6,34,62], 14: [6,26,46,66], 15: [6,26,48,70],
    16: [6,26,50,74], 17: [6,30,54,78], 18: [6,30,56,82], 19: [6,30,58,86],
    20: [6,34,62,90]
  };

  function dataCodewords(v) { var t = EC[v]; return t[1] * t[2] + t[3] * t[4]; }
  function countBits(v) { return v < 10 ? 8 : 16; }
  function byteCapacity(v) { return Math.floor((dataCodewords(v) * 8 - 4 - countBits(v)) / 8); }

  var MAX_BYTES = byteCapacity(MAXV);

  /* ---------- GF(256), primitive polynomial 0x11D ---------- */
  function gmul(x, y) {
    var z = 0;
    for (var i = 7; i >= 0; i--) {
      z = (z << 1) ^ ((z >>> 7) * 0x11D);
      z ^= ((y >>> i) & 1) * x;
    }
    return z & 0xFF;
  }
  function rsDivisor(degree) {
    var r = new Uint8Array(degree);
    r[degree - 1] = 1;
    var root = 1;
    for (var i = 0; i < degree; i++) {
      for (var j = 0; j < degree; j++) {
        r[j] = gmul(r[j], root);
        if (j + 1 < degree) r[j] ^= r[j + 1];
      }
      root = gmul(root, 2);
    }
    return r;
  }
  function rsRemainder(data, div) {
    var res = new Uint8Array(div.length);
    for (var i = 0; i < data.length; i++) {
      var factor = data[i] ^ res[0];
      for (var k = 0; k < res.length - 1; k++) res[k] = res[k + 1];
      res[res.length - 1] = 0;
      for (var j = 0; j < res.length; j++) res[j] ^= gmul(div[j], factor);
    }
    return res;
  }

  /* ---------- bit stream + interleaving ---------- */
  function codewordsFor(bytes, v) {
    var bits = [];
    function put(val, len) { for (var i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); }
    put(0x4, 4);                      /* byte mode */
    put(bytes.length, countBits(v));
    for (var i = 0; i < bytes.length; i++) put(bytes[i], 8);

    var cap = dataCodewords(v) * 8;
    put(0, Math.min(4, cap - bits.length));      /* terminator */
    while (bits.length % 8 !== 0) bits.push(0);  /* to a whole byte */

    var cw = [];
    for (var b = 0; b < bits.length; b += 8) {
      var byte = 0;
      for (var k = 0; k < 8; k++) byte = (byte << 1) | bits[b + k];
      cw.push(byte);
    }
    var pad = [0xEC, 0x11], pi = 0;
    while (cw.length < dataCodewords(v)) cw.push(pad[pi++ & 1]);

    /* split into blocks, add check codewords, interleave */
    var t = EC[v], ecLen = t[0], blocks = [], off = 0, i2;
    for (i2 = 0; i2 < t[1]; i2++) { blocks.push(cw.slice(off, off + t[2])); off += t[2]; }
    for (i2 = 0; i2 < t[3]; i2++) { blocks.push(cw.slice(off, off + t[4])); off += t[4]; }

    var div = rsDivisor(ecLen), eccs = [], longest = 0;
    for (i2 = 0; i2 < blocks.length; i2++) {
      eccs.push(rsRemainder(blocks[i2], div));
      if (blocks[i2].length > longest) longest = blocks[i2].length;
    }
    var out = [];
    for (i2 = 0; i2 < longest; i2++)
      for (var j2 = 0; j2 < blocks.length; j2++)
        if (i2 < blocks[j2].length) out.push(blocks[j2][i2]);
    for (i2 = 0; i2 < ecLen; i2++)
      for (var j3 = 0; j3 < blocks.length; j3++) out.push(eccs[j3][i2]);

    return { codewords: out, ecLen: ecLen, blocks: blocks.length, dataLen: cw.length };
  }

  /* ---------- module grid ---------- */
  function buildGrid(v, codewords) {
    var size = 17 + 4 * v;
    var m = new Uint8Array(size * size);   /* 1 = dark */
    var fx = new Uint8Array(size * size);  /* 1 = function module, never masked */

    function set(r, c, val, isFn) {
      if (r < 0 || c < 0 || r >= size || c >= size) return;
      m[r * size + c] = val ? 1 : 0;
      if (isFn) fx[r * size + c] = 1;
    }
    function get(r, c) { return m[r * size + c]; }

    /* finders + separators */
    function finder(r0, c0) {
      for (var dr = -1; dr <= 7; dr++) for (var dc = -1; dc <= 7; dc++) {
        var d = Math.max(Math.abs(dr - 3), Math.abs(dc - 3));
        set(r0 + dr, c0 + dc, d !== 2 && d !== 4, true);
      }
    }
    finder(0, 0); finder(0, size - 7); finder(size - 7, 0);

    /* timing */
    for (var i = 0; i < size; i++) {
      if (!fx[6 * size + i]) set(6, i, i % 2 === 0, true);
      if (!fx[i * size + 6]) set(i, 6, i % 2 === 0, true);
    }

    /* alignment */
    var cs = ALIGN[v], last = cs.length - 1;
    for (var a = 0; a <= last; a++) for (var b = 0; b <= last; b++) {
      if ((a === 0 && b === 0) || (a === 0 && b === last) || (a === last && b === 0)) continue;
      for (var dr2 = -2; dr2 <= 2; dr2++) for (var dc2 = -2; dc2 <= 2; dc2++) {
        var dd = Math.max(Math.abs(dr2), Math.abs(dc2));
        set(cs[a] + dr2, cs[b] + dc2, dd !== 1, true);
      }
    }

    /* version block (7 and up) */
    if (v >= 7) {
      var rem = v;
      for (var q = 0; q < 12; q++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25);
      var vb = (v << 12) | rem;
      for (var k = 0; k < 18; k++) {
        var bit = (vb >>> k) & 1, aa = size - 11 + (k % 3), bb = Math.floor(k / 3);
        set(bb, aa, bit, true); set(aa, bb, bit, true);
      }
    }

    /* format bits — placed twice; drawn once now to reserve the cells */
    function formatWord(mask) {
      var d = (0 << 3) | mask;               /* level M = 0b00 */
      var r = d;
      for (var i = 0; i < 10; i++) r = (r << 1) ^ ((r >>> 9) * 0x537);
      return ((d << 10) | r) ^ 0x5412;
    }
    function drawFormat(w) {
      var i;
      for (i = 0; i <= 5; i++) set(i, 8, (w >>> i) & 1, true);
      set(7, 8, (w >>> 6) & 1, true);
      set(8, 8, (w >>> 7) & 1, true);
      set(8, 7, (w >>> 8) & 1, true);
      for (i = 9; i < 15; i++) set(8, 14 - i, (w >>> i) & 1, true);
      for (i = 0; i < 8; i++) set(8, size - 1 - i, (w >>> i) & 1, true);
      for (i = 8; i < 15; i++) set(size - 15 + i, 8, (w >>> i) & 1, true);
      set(size - 8, 8, 1, true);             /* the one module that is always dark */
    }
    drawFormat(formatWord(0));

    /* data, laid in a two-wide zigzag from the bottom right */
    var bitIdx = 0, total = codewords.length * 8, up = true;
    function nextBit() {
      if (bitIdx >= total) return 0;          /* remainder modules stay light */
      var byte = codewords[bitIdx >> 3], bit = (byte >>> (7 - (bitIdx & 7))) & 1;
      bitIdx++;
      return bit;
    }
    for (var right = size - 1; right >= 1; right -= 2) {
      if (right === 6) right = 5;             /* step over the timing column */
      for (var vert = 0; vert < size; vert++) {
        for (var s = 0; s < 2; s++) {
          var col = right - s, row = up ? (size - 1 - vert) : vert;
          if (fx[row * size + col]) continue;
          m[row * size + col] = nextBit();
        }
      }
      up = !up;
    }

    /* ---- masking: try all eight, keep the tidiest ---- */
    function maskAt(mk, r, c) {
      switch (mk) {
        case 0: return (r + c) % 2 === 0;
        case 1: return r % 2 === 0;
        case 2: return c % 3 === 0;
        case 3: return (r + c) % 3 === 0;
        case 4: return (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0;
        case 5: return ((r * c) % 2) + ((r * c) % 3) === 0;
        case 6: return (((r * c) % 2) + ((r * c) % 3)) % 2 === 0;
        default: return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0;
      }
    }
    function applyMask(mk) {
      for (var r = 0; r < size; r++) for (var c = 0; c < size; c++)
        if (!fx[r * size + c] && maskAt(mk, r, c)) m[r * size + c] ^= 1;
    }

    var RUN1 = [1,0,1,1,1,0,1,0,0,0,0], RUN2 = [0,0,0,0,1,0,1,1,1,0,1];
    function penalty() {
      var p = 0, i, j, run, cur, val;
      /* rule 1 — long same-colour runs */
      for (i = 0; i < size; i++) {
        cur = get(i, 0); run = 1;
        for (j = 1; j < size; j++) {
          val = get(i, j);
          if (val === cur) run++; else { if (run >= 5) p += 3 + run - 5; cur = val; run = 1; }
        }
        if (run >= 5) p += 3 + run - 5;
        cur = get(0, i); run = 1;
        for (j = 1; j < size; j++) {
          val = get(j, i);
          if (val === cur) run++; else { if (run >= 5) p += 3 + run - 5; cur = val; run = 1; }
        }
        if (run >= 5) p += 3 + run - 5;
      }
      /* rule 2 — 2x2 blocks of one colour */
      for (i = 0; i < size - 1; i++) for (j = 0; j < size - 1; j++) {
        var a = get(i, j);
        if (a === get(i, j + 1) && a === get(i + 1, j) && a === get(i + 1, j + 1)) p += 3;
      }
      /* rule 3 — shapes that look like a finder */
      for (i = 0; i < size; i++) for (j = 0; j + 10 < size; j++) {
        var okR1 = true, okR2 = true, okC1 = true, okC2 = true;
        for (var k = 0; k < 11; k++) {
          var rv = get(i, j + k), cv = get(j + k, i);
          if (rv !== RUN1[k]) okR1 = false;
          if (rv !== RUN2[k]) okR2 = false;
          if (cv !== RUN1[k]) okC1 = false;
          if (cv !== RUN2[k]) okC2 = false;
        }
        if (okR1) p += 40; if (okR2) p += 40; if (okC1) p += 40; if (okC2) p += 40;
      }
      /* rule 4 — drift away from a half-dark grid */
      var dark = 0;
      for (i = 0; i < m.length; i++) dark += m[i];
      p += Math.floor(Math.abs(dark * 100 / (size * size) - 50) / 5) * 10;
      return p;
    }

    var best = 0, bestScore = Infinity;
    for (var mk = 0; mk < 8; mk++) {
      applyMask(mk); drawFormat(formatWord(mk));
      var sc = penalty();
      if (sc < bestScore) { bestScore = sc; best = mk; }
      applyMask(mk);
    }
    applyMask(best); drawFormat(formatWord(best));

    return { size: size, modules: m, mask: best, penalty: bestScore };
  }

  function toBytes(str) {
    if (typeof TextEncoder === "function") return new TextEncoder().encode(str);
    var out = [], s = unescape(encodeURIComponent(str));
    for (var i = 0; i < s.length; i++) out.push(s.charCodeAt(i) & 0xFF);
    return new Uint8Array(out);
  }

  function makeQR(str) {
    var bytes = toBytes(str), v = 0;
    for (var i = 1; i <= MAXV; i++) { if (byteCapacity(i) >= bytes.length) { v = i; break; } }
    if (!v) return { over: true, bytes: bytes.length };
    var enc = codewordsFor(bytes, v);
    var g = buildGrid(v, enc.codewords);
    return {
      over: false, version: v, size: g.size, modules: g.modules, mask: g.mask,
      bytes: bytes.length, capacity: byteCapacity(v),
      blocks: enc.blocks, dataCw: enc.dataLen, ecCw: enc.ecLen * enc.blocks
    };
  }

  /* =====================================================================
     drawing
     ===================================================================== */
  var QUIET = 4;
  var DARK = "#111418", LIGHT = "#FFFFFF";

  function rrect(ctx, x, y, w, h, rad) {
    var r = Object.prototype.toString.call(rad) === "[object Array]" ? rad.slice() : [rad, rad, rad, rad];
    var lim = Math.min(w, h) / 2;
    for (var i = 0; i < 4; i++) r[i] = Math.max(0, Math.min(r[i], lim));
    ctx.beginPath();
    ctx.moveTo(x + r[0], y);
    ctx.lineTo(x + w - r[1], y); ctx.arcTo(x + w, y, x + w, y + r[1], r[1]);
    ctx.lineTo(x + w, y + h - r[2]); ctx.arcTo(x + w, y + h, x + w - r[2], y + h, r[2]);
    ctx.lineTo(x + r[3], y + h); ctx.arcTo(x, y + h, x, y + h - r[3], r[3]);
    ctx.lineTo(x, y + r[0]); ctx.arcTo(x, y, x + r[0], y, r[0]);
    ctx.closePath();
  }

  function drawEye(ctx, x, y, s, ro, rm, ri) {
    ctx.fillStyle = DARK; rrect(ctx, x, y, 7 * s, 7 * s, ro); ctx.fill();
    ctx.fillStyle = LIGHT; rrect(ctx, x + s, y + s, 5 * s, 5 * s, rm); ctx.fill();
    ctx.fillStyle = DARK; rrect(ctx, x + 2 * s, y + 2 * s, 3 * s, 3 * s, ri); ctx.fill();
  }

  function paint(canvas, qr, targetPx, shape) {
    var n = qr.size + QUIET * 2;
    var s = Math.max(1, Math.floor(targetPx / n));
    var dim = s * n;
    canvas.width = dim; canvas.height = dim;
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = LIGHT; ctx.fillRect(0, 0, dim, dim);

    var size = qr.size, m = qr.modules;
    function at(r, c) { return (r < 0 || c < 0 || r >= size || c >= size) ? 0 : m[r * size + c]; }
    function isEye(r, c) {
      return (r < 7 && c < 7) || (r < 7 && c >= size - 7) || (r >= size - 7 && c < 7);
    }

    var fancy = shape !== "square";
    ctx.fillStyle = DARK;

    for (var r = 0; r < size; r++) {
      for (var c = 0; c < size; c++) {
        if (!m[r * size + c]) continue;
        if (fancy && isEye(r, c)) continue;
        var x = (c + QUIET) * s, y = (r + QUIET) * s;
        if (shape === "square") {
          ctx.fillRect(x, y, s, s);
        } else if (shape === "dot") {
          ctx.beginPath();
          ctx.arc(x + s / 2, y + s / 2, s * 0.44, 0, Math.PI * 2);
          ctx.fill();
        } else {
          var R = s * 0.45;
          var up = at(r - 1, c), dn = at(r + 1, c), lf = at(r, c - 1), rt = at(r, c + 1);
          rrect(ctx, x, y, s, s, [
            (!up && !lf) ? R : 0, (!up && !rt) ? R : 0,
            (!dn && !rt) ? R : 0, (!dn && !lf) ? R : 0
          ]);
          ctx.fill();
        }
      }
    }

    if (fancy) {
      /* A corner radius of r modules stops covering the centre of the corner module
         once r passes 1.707, which would quietly damage a finder pattern. 1.2 keeps
         a comfortable margin, so the three finders stay exactly to spec and readers
         still lock on however the modules are styled. */
      var ro = s * 1.2;
      var rm = shape === "dot" ? s * 1.2 : s * 0.6;
      var ri = shape === "dot" ? s * 1.2 : s * 0.5;
      var q = QUIET * s, far = (QUIET + size - 7) * s;
      drawEye(ctx, q, q, s, ro, rm, ri);
      drawEye(ctx, far, q, s, ro, rm, ri);
      drawEye(ctx, q, far, s, ro, rm, ri);
    }
    return { px: dim, scale: s };
  }

  function paintEmpty(canvas, targetPx) {
    var n = 29, s = Math.max(1, Math.floor(targetPx / n)), dim = s * n;
    canvas.width = dim; canvas.height = dim;
    var ctx = canvas.getContext("2d");
    ctx.fillStyle = LIGHT; ctx.fillRect(0, 0, dim, dim);
    ctx.strokeStyle = "#D9D9D3"; ctx.lineWidth = Math.max(1, s * 0.5);
    var q = QUIET * s, far = (QUIET + 21 - 7) * s;
    [[q, q], [far, q], [q, far]].forEach(function (p) {
      rrect(ctx, p[0] + s * 0.5, p[1] + s * 0.5, 6 * s, 6 * s, s * 1.2); ctx.stroke();
    });
    return { px: dim, scale: s };
  }

  /* =====================================================================
     wiring
     ===================================================================== */
  var canvas = $("qr"), stage = $("stage"), dl = $("dl");
  var state = { shape: "square", px: 520 };
  var last = null, hrefTimer = 0;

  /* the slider and the shape buttons change only the drawing, so keep the last
     encode around instead of re-running Reed-Solomon on every drag event */
  var cache = { text: null, qr: null };
  function qrFor(text) {
    if (cache.text !== text) { cache.text = text; cache.qr = makeQR(text); }
    return cache.qr;
  }

  var PRESETS = [
    { label: "sloprun.dev", text: "https://sloprun.dev" },
    { label: "email", text: "mailto:[email protected]?subject=Hi%20there" },
    { label: "wifi login", text: "WIFI:T:WPA;S:Corner Cafe;P:flatwhite2024;;" },
    { label: "a long note", text: "Bring the blue folding chairs, the small speaker, and about four litres of water. We are on the far side of the field, past the two big oak trees, where the path bends left. If you get lost, walk toward the sound of the drums and you will find us." }
  ];

  function fmt(n) { return n.toLocaleString("en-US"); }

  function fact(label, value, sub) {
    return '<div class="fact"><div class="k-lab">' + label + '</div><div class="v">' +
      value + (sub ? ' <small>' + sub + '</small>' : '') + '</div></div>';
  }

  function showFacts(qr, out) {
    $("facts").innerHTML =
      fact("version", String(qr.version), "of 20") +
      fact("grid", qr.size + "×" + qr.size, "modules") +
      fact("data", fmt(qr.dataCw), "codewords") +
      fact("check", fmt(qr.ecCw), "codewords") +
      fact("blocks", String(qr.blocks), "level M") +
      fact("mask pattern", String(qr.mask), "of 0–7") +
      fact("quiet border", String(QUIET), "modules") +
      fact("image", out.px, "px · " + out.scale + " px/module");
  }

  function showBlankFacts(out) {
    $("facts").innerHTML =
      fact("version", "—") + fact("grid", "—") + fact("data", "—") + fact("check", "—") +
      fact("blocks", "—") + fact("mask pattern", "—") +
      fact("quiet border", String(QUIET), "modules") +
      fact("image", out.px, "px");
  }

  function disableDownload() {
    dl.setAttribute("aria-disabled", "true");
    dl.removeAttribute("href");
    $("dlsize").textContent = "";
  }

  function scheduleHref() {
    clearTimeout(hrefTimer);
    hrefTimer = setTimeout(function () {
      if (!last) { disableDownload(); return; }
      try {
        var url = canvas.toDataURL("image/png");
        dl.href = url;
        dl.removeAttribute("aria-disabled");
        $("dlsize").textContent = "~" + (Math.round(url.length * 0.75 / 102.4) / 10) + " KB";
      } catch (err) {
        disableDownload();
      }
    }, 180);
  }

  function render() {
    var text = $("text").value;
    var line = $("stageline"), alertBox = $("alert");
    $("sizeOut").textContent = state.px + " px";

    if (text.length === 0) {
      last = null;
      var e = paintEmpty(canvas, state.px);
      canvas.setAttribute("aria-label", "empty code area");
      stage.classList.remove("over");
      line.className = "stageline";
      line.innerHTML = "nothing to encode yet — type something in the box";
      alertBox.hidden = true;
      $("meterFill").style.width = "0%";
      $("meterFill").classList.remove("bad");
      $("capFoot").innerHTML = "<b>0</b> bytes";
      $("capRight").textContent = "up to " + fmt(MAX_BYTES);
      showBlankFacts(e);
      clearTimeout(hrefTimer);
      disableDownload();
      return;
    }

    var qr = qrFor(text);

    if (qr.over) {
      stage.classList.add("over");
      alertBox.hidden = false;
      var over = qr.bytes - MAX_BYTES;
      $("alertBody").textContent =
        "That is " + fmt(qr.bytes) + " bytes. The biggest code this page builds holds " +
        fmt(MAX_BYTES) + ", so trim about " + fmt(over) +
        " more. Shortening a link is usually the quickest fix.";
      line.className = "stageline bad";
      line.innerHTML = "<b>over capacity</b> <span class='sep'>·</span> " +
        (last ? "showing the last code that fit" : "nothing to show yet");
      if (last) {
        /* keep the stale code honest about the current size and shape */
        showFacts(last, paint(canvas, last, state.px, state.shape));
      } else {
        stage.classList.remove("over");
        showBlankFacts(paintEmpty(canvas, state.px));
        canvas.setAttribute("aria-label", "empty code area");
      }
      clearTimeout(hrefTimer);
      disableDownload();
      $("meterFill").style.width = "100%";
      $("meterFill").classList.add("bad");
      $("capFoot").innerHTML = "<b>" + fmt(qr.bytes) + "</b> bytes";
      $("capRight").textContent = fmt(MAX_BYTES) + " max";
      return;
    }

    last = qr;
    stage.classList.remove("over");
    alertBox.hidden = true;
    $("meterFill").classList.remove("bad");

    var out = paint(canvas, qr, state.px, state.shape);
    canvas.setAttribute("aria-label",
      "QR code, version " + qr.version + ", " + qr.size + " by " + qr.size + " modules, encoding: " +
      (text.length > 90 ? text.slice(0, 90) + "…" : text));

    line.className = "stageline";
    line.innerHTML =
      "built <span class='sep'>·</span> <b>version " + qr.version + "</b>" +
      " <span class='sep'>·</span> " + qr.size + "×" + qr.size + " modules" +
      " <span class='sep'>·</span> mask " + qr.mask;

    var pct = Math.min(100, Math.round(qr.bytes / qr.capacity * 100));
    $("meterFill").style.width = pct + "%";
    $("capFoot").innerHTML = "<b>" + fmt(qr.bytes) + "</b> of " + fmt(qr.capacity) + " bytes at v" + qr.version;
    $("capRight").textContent = fmt(MAX_BYTES) + " max";

    showFacts(qr, out);
    scheduleHref();
  }

  /* ---------- inputs ---------- */
  $("text").addEventListener("input", render);

  $("size").addEventListener("input", function () {
    state.px = parseInt(this.value, 10) || 520;
    render();
  });

  document.querySelectorAll("[data-style]").forEach(function (b) {
    b.addEventListener("click", function () {
      state.shape = b.getAttribute("data-style");
      document.querySelectorAll("[data-style]").forEach(function (o) {
        o.setAttribute("aria-pressed", String(o === b));
      });
      render();
    });
  });

  var chipWrap = $("chips");
  PRESETS.forEach(function (p) {
    var b = document.createElement("button");
    b.type = "button"; b.className = "btn"; b.textContent = p.label;
    b.addEventListener("click", function () {
      $("text").value = p.text;
      render();
      $("text").focus();
    });
    chipWrap.appendChild(b);
  });

  dl.addEventListener("click", function () {
    if (dl.getAttribute("aria-disabled") === "true") return;
    var s = $("saved");
    s.textContent = "saved qr-code.png";
    clearTimeout(dl._t);
    dl._t = setTimeout(function () { s.textContent = ""; }, 2600);
  });

  /* ---------- platform theme ---------- */
  window.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);
    }
  });

  /* ---------- boot ---------- */
  document.querySelector('[data-style="square"]').setAttribute("aria-pressed", "true");
  document.querySelectorAll('[data-style]:not([data-style="square"])').forEach(function (o) {
    o.setAttribute("aria-pressed", "false");
  });
  $("text").value = "https://sloprun.dev";
  $("size").value = String(state.px);
  render();
})();
</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