(function () {
const SCALE = 36;
const SIZE = 280;
const ORIGIN = SIZE / 2;
const RANGE = 3;
function toPx(x, y) { return { px: ORIGIN + x * SCALE, py: ORIGIN - y * SCALE }; }
function fromPx(px, py) { return { x: (px - ORIGIN) / SCALE, y: -(py - ORIGIN) / SCALE }; }
function clamp(v) { return Math.max(-RANGE, Math.min(RANGE, v)); }
function round1(v) { return Math.round(v * 10) / 10; }
function gridSvg() {
let s = "";
for (let i = -RANGE; i <= RANGE; i++) {
const { px } = toPx(i, 0);
const { py } = toPx(0, i);
s += ``;
s += ``;
}
return s;
}
function arrowSvg(x, y, color) {
const { px, py } = toPx(x, y);
if (Math.hypot(x, y) < 0.02) return "";
const angle = Math.atan2(py - ORIGIN, px - ORIGIN);
const headLen = 9;
const h1x = px - headLen * Math.cos(angle - Math.PI / 7);
const h1y = py - headLen * Math.sin(angle - Math.PI / 7);
const h2x = px - headLen * Math.cos(angle + Math.PI / 7);
const h2y = py - headLen * Math.sin(angle + Math.PI / 7);
return `
`;
}
function handleSvg(x, y, color, id) {
const { px, py } = toPx(x, y);
return ``;
}
// ---------- Fig 1.1: vector addition ----------
let a = { x: 1.5, y: 1 };
let b = { x: 1, y: -1.2 };
function staticPartAdd() {
return gridSvg() + arrowSvg(b.x, b.y, "#a39d8e") + arrowSvg(a.x, a.y, "#2748c9");
}
function renderAddDynamic() {
const svg = document.getElementById("vec-add-svg");
if (!svg) return;
const sum = { x: a.x + b.x, y: a.y + b.y };
const guide = ``;
svg.innerHTML = staticPartAdd() + guide + arrowSvg(sum.x, sum.y, "#a8702b") +
handleSvg(a.x, a.y, "#2748c9", "a") + handleSvg(b.x, b.y, "#6e6a60", "b");
attachHandleListeners(svg, "vec-add");
const readout = document.getElementById("vec-add-readout");
if (readout) readout.textContent = `a = (${round1(a.x)}, ${round1(a.y)}) b = (${round1(b.x)}, ${round1(b.y)}) a + b = (${round1(sum.x)}, ${round1(sum.y)})`;
}
// ---------- Fig 1.2: matrix transformation ----------
let v = { x: 1.4, y: 0.8 };
let M = [[1, 0], [0, 1]];
function applyM(vec) {
return { x: M[0][0] * vec.x + M[0][1] * vec.y, y: M[1][0] * vec.x + M[1][1] * vec.y };
}
function renderTransformDynamic() {
const svg = document.getElementById("vec-transform-svg");
if (!svg) return;
const mv = applyM(v);
svg.innerHTML = gridSvg() + arrowSvg(mv.x, mv.y, "#a8702b") + arrowSvg(v.x, v.y, "#2748c9") +
handleSvg(v.x, v.y, "#2748c9", "v");
attachHandleListeners(svg, "vec-transform");
const readout = document.getElementById("vec-transform-readout");
if (readout) readout.textContent = `v = (${round1(v.x)}, ${round1(v.y)}) Mv = (${round1(mv.x)}, ${round1(mv.y)})`;
const eq = document.getElementById("vec-transform-eq");
if (eq && window.katex) {
katex.render(
`\\begin{bmatrix}${M[0][0]}&${M[0][1]}\\\\${M[1][0]}&${M[1][1]}\\end{bmatrix}\\begin{bmatrix}${round1(v.x)}\\\\${round1(v.y)}\\end{bmatrix}=\\begin{bmatrix}${round1(mv.x)}\\\\${round1(mv.y)}\\end{bmatrix}`,
eq
);
}
}
function syncMatrixInputs() {
["m00", "m01", "m10", "m11"].forEach((id, idx) => {
const el = document.getElementById(id);
if (el) el.value = [M[0][0], M[0][1], M[1][0], M[1][1]][idx];
});
}
// ---------- Drag state (module-level, listeners attached ONCE per svg, not per render) ----------
let dragTarget = null; // "a" | "b" | "v" | null
let activeSvg = null;
let activeWhich = null;
let pendingFrame = false;
function attachHandleListeners(svg, which) {
// Only the handle circles are recreated each render, so (re)bind pointerdown on them.
svg.querySelectorAll("[data-handle]").forEach((h) => {
h.onpointerdown = (e) => {
dragTarget = h.dataset.handle;
activeSvg = svg;
activeWhich = which;
try { svg.setPointerCapture(e.pointerId); } catch (err) {}
h.style.cursor = "grabbing";
e.preventDefault();
};
});
}
function handleMove(svg, which, clientX, clientY) {
const rect = svg.getBoundingClientRect();
const px = ((clientX - rect.left) / rect.width) * SIZE;
const py = ((clientY - rect.top) / rect.height) * SIZE;
const { x, y } = fromPx(px, py);
const cx = clamp(round1(x)), cy = clamp(round1(y));
if (which === "vec-add") {
if (dragTarget === "a") a = { x: cx, y: cy };
else if (dragTarget === "b") b = { x: cx, y: cy };
} else if (which === "vec-transform") {
v = { x: cx, y: cy };
}
if (pendingFrame) return;
pendingFrame = true;
requestAnimationFrame(() => {
pendingFrame = false;
if (which === "vec-add") renderAddDynamic();
else if (which === "vec-transform") renderTransformDynamic();
});
}
function wireGlobalPointerEvents() {
// Attached exactly once — never re-attached on render — which is the actual fix for the lag.
window.addEventListener("pointermove", (e) => {
if (!dragTarget || !activeSvg) return;
handleMove(activeSvg, activeWhich, e.clientX, e.clientY);
});
window.addEventListener("pointerup", () => { dragTarget = null; activeSvg = null; });
window.addEventListener("pointercancel", () => { dragTarget = null; activeSvg = null; });
}
function wireMatrixInputs() {
const ids = ["m00", "m01", "m10", "m11"];
ids.forEach((id, idx) => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener("input", () => {
const val = parseFloat(el.value);
const r = Math.floor(idx / 2), c = idx % 2;
M[r][c] = isNaN(val) ? 0 : val;
renderTransformDynamic();
});
});
document.querySelectorAll(".matrix-preset").forEach((btn) => {
btn.addEventListener("click", () => {
M = JSON.parse(btn.dataset.matrix);
syncMatrixInputs();
renderTransformDynamic();
});
});
}
function init() {
wireGlobalPointerEvents();
if (document.getElementById("vec-add-svg")) renderAddDynamic();
if (document.getElementById("vec-transform-svg")) {
syncMatrixInputs();
wireMatrixInputs();
renderTransformDynamic();
}
}
document.addEventListener("DOMContentLoaded", init);
})();