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="two-template-probe"></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 two-template-probe/ to a new name (one DNS label), set forkedFrom to { "name": "two-template-probe", "version": 1 }, 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": "two-template-probe",
"title": "Two-template probe",
"version": 1,
"claim": "For A·sin(2πft + φ), the sine and cosine correlations are (A/2)cos φ and (A/2)sin φ, so 2√(⟨x,sin⟩² + ⟨x,cos⟩²) recovers A at every phase while either correlation alone passes through zero.",
"summary": "Sweep the amplitude and phase of a 5-cycle sinusoid; the two correlations trade off as the phase turns and the plotted pair travels a circle of radius A/2, while the recovered-amplitude readout holds constant.",
"topics": [
"signals/correlation",
"math/complex-numbers"
],
"aliases": [
"phase problem",
"quadrature",
"sine and cosine templates",
"complex template"
],
"params": {
"amp": {
"type": "number",
"default": 0.8,
"min": 0.2,
"max": 1,
"label": "amplitude A"
},
"phase": {
"type": "integer",
"default": 0,
"min": 0,
"max": 360,
"label": "phase φ (degrees)"
}
},
"check": [
{
"q": "A component sits exactly at the template frequency, but with phase 90°. The sine-template correlation reads…",
"options": [
"A/2, as for any matched frequency",
"A",
"−A/2",
"zero; the cosine template is needed to detect it"
],
"answer": 3,
"why": "sin·cos averages to zero over whole cycles, so a 90°-shifted component is invisible to the sine template alone; the pair 2√(⟨x,sin⟩² + ⟨x,cos⟩²) recovers A at any phase."
},
{
"q": "A sine template correlated against a cosine of the same frequency gives…",
"options": [
"0.5, the same as for a matched sine template",
"1",
"zero, which is why the DFT probes with both at once",
"A/2, the component’s half-amplitude"
],
"answer": 2,
"why": "Two sinusoids at the same frequency, 90° apart, are orthogonal; the complex template contains both, so no phase goes undetected."
},
{
"q": "Euler’s formula e^(iθ) = cos θ + i·sin θ is useful for the DFT because…",
"options": [
"it removes the need for multiplication",
"a complex exponential supplies the sine and cosine templates at once",
"it removes the factor of 2π from the exponent",
"it applies only to angles smaller than 2π"
],
"answer": 1,
"why": "The complex template carries both phases, so the DFT detects a component at any phase, whereas a lone sine template would miss a cosine-phased component entirely."
}
],
"capabilities": [],
"height": 510,
"requires": [],
"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>Two-template probe</title>
<style>
:root {
color-scheme: light dark;
--bg: #f7f3ea;
--bg-card: #efe9da;
--border: #d8cfba;
--text: #211d14;
--text-dim: #6e6553;
--accent: #31597f;
--accent2: #b04e1b;
--hot: #a82433;
--ok: #3d6b4f;
--ink-rgb: 33, 29, 20;
--paper-rgb: 247, 243, 234;
--accent-rgb: 49, 89, 127;
--accent2-rgb: 176, 78, 27;
--hot-rgb: 168, 36, 51;
--ok-rgb: 61, 107, 79;
--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: #161410;
--bg-card: #1e1b15;
--border: #383225;
--text: #e9e3d3;
--text-dim: #9c917c;
--accent: #8fb8e0;
--accent2: #dd9355;
--hot: #df7a88;
--ok: #82bd97;
--ink-rgb: 233, 227, 211;
--paper-rgb: 22, 20, 16;
--accent-rgb: 143, 184, 224;
--accent2-rgb: 221, 147, 85;
--hot-rgb: 223, 122, 136;
--ok-rgb: 130, 189, 151;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body { background: transparent; color: var(--text); font-family: var(--serif); font-size: 15px; line-height: 1.5; }
.widget { background: var(--bg-card); border: 1px solid var(--border); border-radius: 4px; padding: 22px; }
.control-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }
.control-row label { font-size: 14px; color: var(--text-dim); white-space: nowrap; }
.control-row.sym { display: grid; grid-template-columns: 88px 1fr 1fr; gap: 12px 16px; align-items: center; }
.control-row.sym.single { grid-template-columns: 88px 1fr; }
.ctl { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ctl input[type="range"] { flex: 1; min-width: 0; }
.ctl .val { flex: none; width: 64px; text-align: right; font-size: 13px; }
@media (max-width: 600px) {
.control-row.sym { grid-template-columns: 64px 1fr; }
.control-row.sym .ctl:nth-of-type(2) { grid-column: 2; }
}
input[type="range"] { flex: 1; min-width: 110px; accent-color: var(--accent); }
.toggle { display: inline-flex; align-items: baseline; gap: 7px; font-size: 13.5px; color: var(--text-dim); cursor: pointer; }
.toggle input { accent-color: var(--accent2); }
.control-row .toggle { white-space: normal; }
.mono { font-family: var(--mono); }
.dim { color: var(--text-dim); font-size: 12.5px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.chip, .btn {
font-family: var(--mono); font-size: 12.5px; color: var(--text); background: transparent;
border: 1px solid var(--border); border-radius: 3px; padding: 6px 13px; cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.chip:hover, .btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary { border-color: var(--accent); color: var(--accent); }
.btn.playing, .btn.playing:hover { border-color: var(--hot); color: var(--hot); }
.btn.off { border-color: var(--hot); color: var(--hot); opacity: 0.85; }
.btn:disabled { opacity: 0.35; cursor: default; }
.btn:disabled:hover { border-color: var(--border); color: var(--text); }
.chip.sel-chip { border-color: var(--accent); color: var(--accent); }
.readout-row { display: flex; gap: 28px; flex-wrap: wrap; margin: 14px 0 0; }
.readout { text-align: left; }
.readout.inline { margin-left: auto; text-align: right; }
.readout .big { display: block; font-size: 30px; 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 small { color: var(--text-dim); font-size: 12.5px; }
@media (max-width: 560px) { .readout .big { font-size: 25px; } }
.plot { display: block; width: 100%; height: 220px; margin-top: 8px; }
.plot.tall { height: 280px; }
.plot.duo { height: 320px; }
.plot.phasor { height: 400px; }
.plot.spec { height: 230px; }
.mono, .ctl .val, .readout .big, .readout small { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div class="widget">
<div class="control-row sym single">
<label for="ph2-a">amplitude A</label>
<span class="ctl wide"><input type="range" id="ph2-a" min="20" max="100" value="80"><span class="mono val" id="ph2-a-label">0.80</span></span>
</div>
<div class="control-row sym single">
<label for="ph2-ph">phase φ</label>
<span class="ctl wide"><input type="range" id="ph2-ph" min="0" max="360" value="0"><span class="mono val" id="ph2-ph-label">0°</span></span>
</div>
<canvas id="ph2-canvas" class="plot duo"></canvas>
<div class="readout-row">
<div class="readout">
<span class="big mono" id="ph2-c">0.400</span>
<small>⟨x, sin⟩</small>
</div>
<div class="readout">
<span class="big mono" id="ph2-s">0.000</span>
<small>⟨x, cos⟩</small>
</div>
<div class="readout accent">
<span class="big mono" id="ph2-amp">0.800</span>
<small>recovered A = 2√(sum of squares)</small>
</div>
</div>
</div>
<script src="/w/_sdk/host.js?v=1"></script>
<script src="/w/_lib/dsp.js?v=1"></script>
<script src="widget.js?v=1"></script>
</body>
</html>
widget.js
/* Two-template probe: correlations against sin and cos plotted as one point; the recovered amplitude ignores phase. */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const { T, setupCanvas, hline, trace, wash, zero, signal } = DSP;
const F = 5;
function render() {
const A = $('ph2-a').value / 100;
const deg = +$('ph2-ph').value;
const ph = (deg * Math.PI) / 180;
$('ph2-a-label').textContent = A.toFixed(2);
$('ph2-ph-label').textContent = deg + '°';
const N = 700;
let c = 0, s = 0;
for (let i = 0; i < N; i++) {
const t = i / N;
const x = A * Math.sin(2 * Math.PI * F * t + ph);
c += x * Math.sin(2 * Math.PI * F * t);
s += x * Math.cos(2 * Math.PI * F * t);
}
c /= N;
s /= N;
const rec = 2 * Math.sqrt(c * c + s * s);
$('ph2-c').textContent = zero(c).toFixed(3);
$('ph2-s').textContent = zero(s).toFixed(3);
$('ph2-amp').textContent = rec.toFixed(3);
const { ctx, w, h } = setupCanvas($('ph2-canvas'));
const x0 = 14;
const pw = w - 28;
const topMid = h * 0.22;
const topAmp = h * 0.15;
ctx.clearRect(0, 0, w, h);
ctx.font = DSP.font(10);
hline(ctx, x0, w - 14, topMid, T.GRID);
trace(ctx, (t) => A * Math.sin(2 * Math.PI * F * t + ph), { x0, pw, mid: topMid, amp: topAmp, color: T.ACCENT2, lw: 1.8 });
DSP.label(ctx, 'signal · A sin(2π·5·t + φ)', x0 + 2, 12);
const planeR = Math.min(h * 0.27, w * 0.2);
const pcx = x0 + planeR + 26;
const pcy = h * 0.66;
const sc = planeR / 0.55;
ctx.strokeStyle = T.GRID;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pcx - planeR - 8, pcy);
ctx.lineTo(pcx + planeR + 8, pcy);
ctx.moveTo(pcx, pcy - planeR - 8);
ctx.lineTo(pcx, pcy + planeR + 8);
ctx.stroke();
ctx.strokeStyle = wash(0.2);
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.arc(pcx, pcy, (A / 2) * sc, 0, Math.PI * 2);
ctx.stroke();
ctx.setLineDash([]);
const px = pcx + c * sc;
const py = pcy - s * sc;
ctx.strokeStyle = T.ACCENT;
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.moveTo(pcx, pcy);
ctx.lineTo(px, py);
ctx.stroke();
ctx.fillStyle = T.INK;
ctx.beginPath();
ctx.arc(px, py, 4, 0, Math.PI * 2);
ctx.fill();
DSP.label(ctx, '⟨x, sin⟩ →', pcx + planeR + 12, pcy + 4);
ctx.save();
ctx.translate(pcx - 8, pcy - planeR + 4);
ctx.rotate(-Math.PI / 2);
DSP.label(ctx, '⟨x, cos⟩ →', 0, 0, T.DIM, 'right');
ctx.restore();
DSP.label(ctx, 'dashed circle: radius A/2; the point sits at angle φ', pcx + planeR + 22, pcy - planeR + 8);
DSP.label(ctx, 'recovered A = ' + rec.toFixed(2) + ' at every φ', pcx + planeR + 22, pcy - planeR + 26, T.ACCENT2);
}
['ph2-a', 'ph2-ph'].forEach((id) => $(id).addEventListener('input', () => {
render();
signal('interaction', { control: id });
}));
function applyParams(p) {
if (typeof p.amp === 'number') $('ph2-a').value = Math.round(p.amp * 100);
if (Number.isInteger(p.phase)) $('ph2-ph').value = p.phase;
render();
}
render();
DSP.onResize(render);
DSP.connect({ render, params: applyParams });
})();