use it elsewhere

Powers of two drill

Put it on your own page with two lines, or fork it and make it yours. It is CC-BY-4.0.

Embed

The script finds its own origin, so nothing else is needed; the widget runs in a sandboxed frame and cannot touch your page. Set the controls and the two lines update; the preview beneath shows what your readers get.

<script src="https://learn.mimmsy.com/learn-widget.js"></script> <learn-widget name="powers-of-two"></learn-widget>

It is CC-BY-4.0: keep the credit line the frame shows.

Preview

Fork

A fork is a copy whose manifest names its parent and the parent's version; lineage is kept forever, and the copy is yours to change. The library is its own repository: clone it, copy powers-of-two/ to a new name (one DNS label), set forkedFrom to { "name": "powers-of-two", "version": 2 }, change what you want, run npm run check, and open a pull request. Passing the check is the whole gate. Without a checkout, submit the same files to POST /api/widgets and it is served instantly as an unreviewed draft.

Source

The whole widget is these files; the repository has their history.

widget.json

{
  "name": "powers-of-two",
  "title": "Powers of two drill",
  "version": 2,
  "claim": "log₂ N is the exponent k with 2^k = N; for N ≤ 1024 the answer is at most 10.",
  "summary": "A streak drill: each prompt shows a power of two and you pick its exponent. The best streak persists between visits.",
  "topics": [
    "math/logarithms"
  ],
  "aliases": [
    "log2",
    "binary logarithm",
    "doublings"
  ],
  "params": {
    "maxK": {
      "type": "integer",
      "default": 10,
      "min": 2,
      "max": 20,
      "label": "largest exponent"
    }
  },
  "check": [
    {
      "q": "What is log₂ 256?",
      "options": [
        "6",
        "7",
        "8",
        "9"
      ],
      "answer": 2,
      "why": "2⁸ = 256."
    },
    {
      "q": "2¹⁰ = 1024, so log₂ 1024 is",
      "options": [
        "8",
        "10",
        "20",
        "512"
      ],
      "answer": 1,
      "why": "The logarithm is the exponent: 10."
    }
  ],
  "capabilities": [
    "storage"
  ],
  "height": 220,
  "requires": [
    "logarithms-ruler"
  ],
  "forkedFrom": null,
  "authors": []
}

index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Powers of two drill</title>
<style>
  :root {
    color-scheme: light dark;
    --bg-card: #efe9da;
    --border: #d8cfba;
    --text: #211d14;
    --text-dim: #6e6553;
    --accent: #31597f;
    --accent2: #b04e1b;
    --hot: #a82433;
    --ok: #3d6b4f;
    --ink-rgb: 33, 29, 20;
    --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg-card: #1e1b15;
      --border: #383225;
      --text: #e9e3d3;
      --text-dim: #9c917c;
      --accent: #8fb8e0;
      --accent2: #dd9355;
      --hot: #df7a88;
      --ok: #82bd97;
      --ink-rgb: 233, 227, 211;
    }
  }

  * { box-sizing: border-box; }
  html, body { margin: 0; }
  body { background: transparent; color: var(--text); font-family: var(--serif); }

  .widget {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 22px;
  }

  .prompt { font-family: var(--mono); font-size: 22px; margin: 2px 0 16px; }
  .prompt .n { color: var(--accent); }

  .chips { display: flex; gap: 8px; flex-wrap: wrap; }
  .btn {
    font-family: var(--mono); font-size: 15px; color: var(--text);
    background: transparent; border: 1px solid var(--border); border-radius: 3px;
    padding: 8px 16px; cursor: pointer; transition: border-color 0.15s, color 0.15s;
  }
  .btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
  .btn:disabled { cursor: default; }
  .btn.right { border-color: var(--ok); color: var(--ok); }
  .btn.wrong { border-color: var(--hot); color: var(--hot); }

  .msg { min-height: 20px; margin: 14px 0 0; font-size: 14px; color: var(--text-dim); }
  .msg.ok { color: var(--ok); }
  .msg.bad { color: var(--hot); }

  .readout-row { display: flex; gap: 28px; flex-wrap: wrap; margin: 16px 0 0; }
  .readout { text-align: left; }
  .readout .big {
    display: block; font-size: 26px; font-weight: 600; color: var(--accent);
    line-height: 1.1; min-width: 3ch;
  }
  .readout.accent .big { color: var(--accent2); }
  .readout small { color: var(--text-dim); font-size: 12.5px; }
</style>
</head>
<body>
  <div class="widget">
    <div class="prompt">log&#8322; <span class="n" id="d-target">64</span> = ?</div>
    <div class="chips" id="d-opts"></div>
    <div class="msg" id="d-msg">Pick the exponent. Build a streak.</div>
    <div class="readout-row">
      <div class="readout">
        <span class="big mono" id="d-streak">0</span>
        <small>current streak</small>
      </div>
      <div class="readout accent">
        <span class="big mono" id="d-best">0</span>
        <small>best streak · saved</small>
      </div>
    </div>
  </div>
  <script src="/w/_sdk/host.js?v=1"></script>
  <script src="widget.js?v=1"></script>
</body>
</html>

widget.js

/* Powers of two drill: pick the exponent for 2^k, build a streak; the best streak persists through host storage. */
(() => {
  'use strict';

  const $ = (id) => document.getElementById(id);
  let host = null;
  let maxK = 10;
  let answer = 6;
  let streak = 0;
  let best = 0;
  let busy = false;

  const randInt = (lo, hi) => lo + Math.floor(Math.random() * (hi - lo + 1));

  function newQuestion() {
    busy = false;
    answer = randInt(1, maxK);
    $('d-target').textContent = String(Math.pow(2, answer));

    const opts = new Set([answer]);
    while (opts.size < 4) {
      const d = randInt(0, maxK + 2);
      if (d !== answer) opts.add(d);
    }
    const shuffled = [...opts].sort(() => Math.random() - 0.5);

    const wrap = $('d-opts');
    wrap.innerHTML = '';
    for (const k of shuffled) {
      const b = document.createElement('button');
      b.className = 'btn';
      b.textContent = String(k);
      b.addEventListener('click', () => pick(k, b));
      wrap.appendChild(b);
    }
  }

  function setMsg(text, cls) {
    const m = $('d-msg');
    m.textContent = text;
    m.className = 'msg' + (cls ? ' ' + cls : '');
  }

  async function recordBest() {
    if (streak <= best) return;
    best = streak;
    $('d-best').textContent = String(best);
    if (host && host.can('storage')) {
      try { await host.call('storage.set', { key: 'best', value: best }); }
      catch (e) {}
    }
  }

  function pick(k, btn) {
    if (busy) return;
    busy = true;
    $('d-opts').querySelectorAll('button').forEach((b) => { b.disabled = true; });

    const grade = (passed) => {
      if (host && typeof host.signal === 'function') host.signal(passed ? 'check.passed' : 'check.failed', { k, answer });
    };
    if (k === answer) {
      grade(true);
      btn.classList.add('right');
      streak += 1;
      $('d-streak').textContent = String(streak);
      setMsg('✓ ' + Math.pow(2, answer) + ' = 2^' + answer, 'ok');
      recordBest();
      setTimeout(newQuestion, 750);
    } else {
      grade(false);
      btn.classList.add('wrong');
      [...$('d-opts').children].forEach((b) => {
        if (b.textContent === String(answer)) b.classList.add('right');
      });
      streak = 0;
      $('d-streak').textContent = '0';
      setMsg('✗ it was 2^' + answer + ' = ' + Math.pow(2, answer), 'bad');
      setTimeout(newQuestion, 1300);
    }
  }

  newQuestion();

  LearnWidget.connect().then(async (h) => {
    host = h;
    if (h.params && Number.isInteger(h.params.maxK)) maxK = h.params.maxK;
    if (h.can('storage')) {
      try {
        const saved = await h.call('storage.get', { key: 'best' });
        if (typeof saved === 'number') { best = saved; $('d-best').textContent = String(best); }
      } catch (e) {}
    }
    newQuestion();
  });
})();