Embed
The script finds its own origin, so nothing else is needed; the widget runs in a sandboxed frame and cannot touch your page.
<script src="https://learn.mimmsy.com/learn-widget.js"></script>
<learn-widget name="complex-exp"></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 complex-exp/ to a new name (one DNS label), set forkedFrom to { "name": "complex-exp", "version": 3 }, 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": "complex-exp",
"title": "Complex exponential",
"version": 3,
"claim": "For z = a + bi, e^z has length eᵃ and angle b radians, as the partial products of (1 + z/n)ⁿ show.",
"summary": "Drag z in the left plane; the right plane draws the spiral of partial products (1 + z/n)ⁿ for n = 1024, and readouts compare the measured length and angle to eᵃ and b.",
"topics": [
"math/eulers-formula",
"math/parameterization"
],
"aliases": [
"e to the z",
"exp",
"complex exponential",
"spiral"
],
"check": [
{
"q": "e^(1 + iπ/2) has",
"options": [
"length 1, angle 90°",
"length e, angle 90°",
"length e, angle 1 rad",
"length 1, angle 180°"
],
"answer": 1,
"why": "Length eᵃ = e¹ and angle b = π/2 = 90°."
},
{
"q": "If z is purely imaginary (a = 0), e^z lies",
"options": [
"on the real axis",
"on the unit circle",
"at the origin",
"outside the unit circle"
],
"answer": 1,
"why": "e⁰ = 1, so the length is exactly 1 and only the angle b varies."
},
{
"q": "Extending eˣ to complex inputs is justified because…",
"options": [
"the defining recipe uses only addition and multiplication, which complex numbers have",
"any formula accepts any kind of input",
"iθ is small enough that the error vanishes",
"eˣ and eⁱᶿ are unrelated functions that share a name"
],
"answer": 0,
"why": "The limit (1 + z/n)ⁿ runs unmodified on complex z, and its output defines the extension."
},
{
"q": "Which property of the exponential survives the extension to complex inputs?",
"options": [
"e^(z+w) = e^z · e^w",
"e^z is greater than zero for every input",
"e^z is an increasing function",
"e^z is always a real number"
],
"answer": 0,
"why": "What survives is what can be proved from the recipe using only addition and multiplication; the other three are statements about real-number order, which complex numbers do not have."
}
],
"height": 440,
"requires": [
"eulers-chain"
],
"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>Complex exponential</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; }
.mono { font-family: var(--mono); }
.plot { display: block; width: 100%; height: 220px; margin-top: 8px; }
.plot.tall { height: 320px; }
.plot.interactive { cursor: grab; touch-action: none; }
.plot.interactive:active { cursor: grabbing; }
.readout-row { display: flex; gap: 28px; flex-wrap: wrap; margin: 14px 0 0; }
.readout { text-align: left; }
.readout .big { display: block; font-size: 28px; font-weight: 600; color: var(--accent); line-height: 1.1; min-width: 5ch; }
.readout.accent .big { color: var(--accent2); }
.readout small { color: var(--text-dim); font-size: 12.5px; }
</style>
</head>
<body>
<div class="widget">
<canvas id="ex-canvas" class="plot tall interactive"></canvas>
<div class="readout-row">
<div class="readout">
<span class="big mono" id="ex-z">0.00 + 1.57i</span>
<small>z = a + bi · drag it</small>
</div>
<div class="readout">
<span class="big mono" id="ex-len">1.00 / 1.00</span>
<small>length: measured / predicted e ᵃ</small>
</div>
<div class="readout accent">
<span class="big mono" id="ex-ang">90° / 90°</span>
<small>angle: measured / predicted b rad</small>
</div>
</div>
</div>
<script src="/w/_sdk/host.js?v=1"></script>
<script src="widget.js?v=1"></script>
</body>
</html>
widget.js
/* Complex exponential: drag z = a + bi; the partial products of (1 + z/n)^n spiral out to e^z, checked against e^a and b. */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const rootStyle = getComputedStyle(document.documentElement);
const tv = (n) => rootStyle.getPropertyValue(n).trim();
const wash = (a) => 'rgba(' + tv('--ink-rgb') + ',' + a + ')';
let ACCENT, ACCENT2, HOT, DIM, INK, GRID;
function readTheme() {
ACCENT = tv('--accent'); ACCENT2 = tv('--accent2'); HOT = tv('--hot');
DIM = tv('--text-dim'); INK = tv('--text'); GRID = wash(0.07);
}
function setupCanvas(canvas) {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = canvas.clientWidth, h = canvas.clientHeight;
canvas.width = Math.max(1, w * dpr);
canvas.height = Math.max(1, h * dpr);
const ctx = canvas.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
return { ctx, w, h };
}
function drawArrow(ctx, x0, y0, x1, y1, color, lw) {
ctx.strokeStyle = color; ctx.lineWidth = lw;
ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
const ang = Math.atan2(y1 - y0, x1 - x0);
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - 9 * Math.cos(ang - 0.4), y1 - 9 * Math.sin(ang - 0.4));
ctx.lineTo(x1 - 9 * Math.cos(ang + 0.4), y1 - 9 * Math.sin(ang + 0.4));
ctx.closePath(); ctx.fill();
}
const EX_N = 1024;
let exZ = { re: 0, im: Math.PI / 2 };
let exDrag = false;
const fmtZ = (z) =>
z.re.toFixed(2) + (z.im < 0 ? ' − ' : ' + ') + Math.abs(z.im).toFixed(2) + 'i';
function exCompute() {
const sr = 1 + exZ.re / EX_N;
const si = exZ.im / EX_N;
const pts = [{ re: 1, im: 0 }];
let re = 1, im = 0;
for (let s = 1; s <= EX_N; s++) {
const nre = re * sr - im * si;
const nim = re * si + im * sr;
re = nre; im = nim;
if (s % 8 === 0 || s === EX_N) pts.push({ re, im });
}
return { pts, final: { re, im } };
}
function renderEx() {
const { pts, final } = exCompute();
const mLen = Math.hypot(final.re, final.im);
const pLen = Math.exp(exZ.re);
const norm = (d) => ((d % 360) + 360) % 360;
const mAng = norm((Math.atan2(final.im, final.re) * 180) / Math.PI);
const pAng = norm((exZ.im * 180) / Math.PI);
$('ex-z').textContent = fmtZ(exZ);
$('ex-len').textContent = mLen.toFixed(2) + ' / ' + pLen.toFixed(2);
$('ex-ang').textContent = Math.round(mAng) + '° / ' + Math.round(pAng) + '°';
const { ctx, w, h } = setupCanvas($('ex-canvas'));
ctx.clearRect(0, 0, w, h);
ctx.font = '10px ui-monospace, Menlo, monospace';
const lc = { x: w * 0.24, y: h / 2 };
const lR = Math.min(w * 0.2, h / 2 - 24);
const lsc = lR / 2.6;
ctx.strokeStyle = GRID;
ctx.beginPath();
ctx.moveTo(lc.x - lR - 6, lc.y); ctx.lineTo(lc.x + lR + 6, lc.y);
ctx.moveTo(lc.x, lc.y - lR - 6); ctx.lineTo(lc.x, lc.y + lR + 6);
ctx.stroke();
ctx.strokeStyle = wash(0.12);
ctx.beginPath(); ctx.arc(lc.x, lc.y, lsc, 0, Math.PI * 2); ctx.stroke();
const zx = lc.x + exZ.re * lsc, zy = lc.y - exZ.im * lsc;
drawArrow(ctx, lc.x, lc.y, zx, zy, ACCENT, 2);
ctx.fillStyle = INK;
ctx.beginPath(); ctx.arc(zx, zy, 4.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = DIM; ctx.textAlign = 'center'; ctx.fillText('input z · drag', lc.x, 16);
ctx.fillStyle = ACCENT; ctx.fillText('z', zx + 10, zy - 8);
drawArrow(ctx, lc.x + lR + 14, h / 2, w * 0.47, h / 2, DIM, 1.2);
ctx.fillStyle = DIM; ctx.fillText('(1 + z/n)ⁿ', (lc.x + lR + w * 0.47) / 2 + 4, h / 2 - 10);
const rc = { x: w * 0.73, y: h / 2 };
const rR = Math.min(w * 0.21, h / 2 - 24);
const maxR = Math.max(1.45, mLen * 1.12);
const sc2 = rR / maxR;
ctx.strokeStyle = GRID;
ctx.beginPath();
ctx.moveTo(rc.x - rR - 6, rc.y); ctx.lineTo(rc.x + rR + 6, rc.y);
ctx.moveTo(rc.x, rc.y - rR - 6); ctx.lineTo(rc.x, rc.y + rR + 6);
ctx.stroke();
ctx.strokeStyle = wash(0.12);
ctx.beginPath(); ctx.arc(rc.x, rc.y, sc2, 0, Math.PI * 2); ctx.stroke();
ctx.strokeStyle = ACCENT2; ctx.lineWidth = 1.8;
ctx.beginPath();
pts.forEach((p, i) => {
const x = rc.x + p.re * sc2, y = rc.y - p.im * sc2;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
const tpx = rc.x + pLen * Math.cos(exZ.im) * sc2;
const tpy = rc.y - pLen * Math.sin(exZ.im) * sc2;
ctx.strokeStyle = HOT; ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.arc(tpx, tpy, 8, 0, Math.PI * 2); ctx.stroke();
ctx.fillStyle = INK;
ctx.beginPath(); ctx.arc(rc.x + final.re * sc2, rc.y - final.im * sc2, 4, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = DIM; ctx.textAlign = 'center';
ctx.fillText('output e^z · ring = predicted', rc.x, 16);
ctx.fillText('start: 1', rc.x + sc2, rc.y + 16);
}
function wire() {
const exCanvas = $('ex-canvas');
const exPointer = (e) => {
const r = exCanvas.getBoundingClientRect();
const lc = { x: r.width * 0.24, y: r.height / 2 };
const lR = Math.min(r.width * 0.2, r.height / 2 - 24);
const lsc = lR / 2.6;
return {
re: (e.clientX - r.left - lc.x) / lsc,
im: -(e.clientY - r.top - lc.y) / lsc,
};
};
const set = (e) => {
const p = exPointer(e);
exZ.re = clamp(p.re, -2.4, 2.4);
exZ.im = clamp(p.im, -2.4, 2.4);
renderEx();
};
exCanvas.addEventListener('pointerdown', (e) => {
exDrag = true;
try { exCanvas.setPointerCapture(e.pointerId); } catch (err) {}
set(e);
});
exCanvas.addEventListener('pointermove', (e) => { if (exDrag) set(e); });
exCanvas.addEventListener('pointerup', () => { exDrag = false; });
exCanvas.addEventListener('pointercancel', () => { exDrag = false; });
let t;
window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(renderEx, 120); });
}
readTheme();
wire();
renderEx();
LearnWidget.connect().then((host) => {
host.on('theme.changed', () => { readTheme(); renderEx(); });
readTheme();
renderEx();
});
})();