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-times-i"></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-times-i/ to a new name (one DNS label), set forkedFrom to { "name": "complex-times-i", "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-times-i",
"title": "Multiply by i",
"version": 3,
"claim": "Multiplying z by i rotates it a quarter turn counter-clockwise, swapping its coordinates and negating the new real part, so i²z = −z.",
"summary": "Drag z and see i·z, i²·z and i³·z fan out at quarter turns; readouts show the swapped, sign-flipped coordinates.",
"topics": [
"math/complex-numbers"
],
"aliases": [
"rotation by i",
"i squared",
"quarter turn"
],
"check": [
{
"q": "i·(3 + 2i) equals",
"options": [
"3 − 2i",
"−2 + 3i",
"2 − 3i",
"−3 − 2i"
],
"answer": 1,
"why": "(a + bi)·i = −b + ai: the coordinates swap and the new real part flips sign."
},
{
"q": "Multiplying by i twice sends z to",
"options": [
"z",
"−z",
"iz",
"|z|"
],
"answer": 1,
"why": "Two quarter turns make a half turn: i² = −1."
},
{
"q": "Multiplying a complex number by i…",
"options": [
"rotates it a quarter turn counterclockwise",
"doubles its length",
"reflects it across the real axis",
"rotates it a half turn"
],
"answer": 0,
"why": "Multiplication by i is a quarter turn counterclockwise about the origin. Two quarter turns make a half turn, which sends every point to its negative: i² = −1."
}
],
"height": 410,
"requires": [
"complex-point"
],
"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>Multiply by i</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; --accent-rgb:49,89,127; --accent2-rgb:176,78,27;
--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; --accent-rgb:143,184,224; --accent2-rgb:221,147,85;
}
}
* { 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.plane { height: 290px; }
.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 .big.hot { color: var(--hot); }
.readout .big.ok { color: var(--ok); }
.readout small { color: var(--text-dim); font-size: 12.5px; }
</style>
</head>
<body>
<div class="widget">
<canvas id="rot-canvas" class="plot plane interactive"></canvas>
<div class="readout-row">
<div class="readout">
<span class="big mono" id="rot-z">1.50 + 0.60i</span>
<small>z · drag it</small>
</div>
<div class="readout accent">
<span class="big mono" id="rot-iz">−0.60 + 1.50i</span>
<small>i z · coordinates swap, new real part flips</small>
</div>
<div class="readout">
<span class="big mono hot" id="rot-i2z">−1.50 − 0.60i</span>
<small>i²z · always equal to −z</small>
</div>
</div>
</div>
<script src="/w/_sdk/host.js?v=1"></script>
<script src="/w/_lib/complex-plane.js?v=1"></script>
<script src="widget.js?v=1"></script>
</body>
</html>
widget.js
/* Multiply by i: the quarter-turn fan z, iz, i²z, i³z. */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const CP = window.ComplexPlane;
const rotZ = { re: 1.5, im: 0.6 };
const timesI = (z) => ({ re: -z.im, im: z.re });
function render() {
const P = CP.planeBase($('rot-canvas'), 2.4, false);
const { ctx } = P;
const iz = timesI(rotZ);
const i2z = timesI(iz);
const i3z = timesI(i2z);
const r = Math.hypot(rotZ.re, rotZ.im) * P.sc;
const a0 = -Math.atan2(rotZ.im, rotZ.re);
ctx.strokeStyle = CP.wash(0.25);
ctx.lineWidth = 1;
ctx.setLineDash([3, 4]);
ctx.beginPath();
ctx.arc(P.cx, P.cy, r, a0, a0 - Math.PI / 2, true);
ctx.stroke();
ctx.setLineDash([]);
CP.drawArrow(ctx, P.cx, P.cy, P.X(i3z), P.Y(i3z), CP.wash(0.4), 1.6);
CP.drawArrow(ctx, P.cx, P.cy, P.X(i2z), P.Y(i2z), CP.T.HOT, 2);
CP.drawArrow(ctx, P.cx, P.cy, P.X(iz), P.Y(iz), CP.T.ACCENT2, 2);
CP.drawArrow(ctx, P.cx, P.cy, P.X(rotZ), P.Y(rotZ), CP.T.ACCENT, 2.4);
ctx.fillStyle = CP.T.INK;
ctx.beginPath();
ctx.arc(P.X(rotZ), P.Y(rotZ), 4.5, 0, Math.PI * 2);
ctx.fill();
CP.label(ctx, P, rotZ, 'z', CP.T.ACCENT, 12, -10);
CP.label(ctx, P, iz, 'i·z', CP.T.ACCENT2, -2, -12);
CP.label(ctx, P, i2z, 'i²·z = −z', CP.T.HOT, -6, 18);
CP.label(ctx, P, i3z, 'i³·z', CP.wash(0.55), 10, 16);
$('rot-z').textContent = CP.fmtZ(rotZ);
$('rot-iz').textContent = CP.fmtZ(iz);
$('rot-i2z').textContent = CP.fmtZ(i2z);
}
CP.readTheme();
CP.attachDrag($('rot-canvas'), 2.4, [rotZ], render);
render();
let t;
window.addEventListener('resize', () => { clearTimeout(t); t = setTimeout(render, 120); });
LearnWidget.connect().then((host) => {
host.on('theme.changed', () => { CP.readTheme(); render(); });
CP.readTheme();
render();
});
})();