The demo below shows two vectors in the plane. Drag the tip of each arrow to reposition it, then press Orthogonalize to run Gram-Schmidt step by step.
<!-- Gram-Schmidt interactive demo — two draggable vectors in the plane -->
<div class="hint">Drag the tips of the blue arrows to set two vectors, then press Orthogonalize. Watch Gram-Schmidt strip each vector of its overlap with the previous one.</div>
<canvas id="gs-canvas" width="380" height="280"></canvas>
<div class="status" id="gs-status">Drag the arrow tips, then press Orthogonalize.</div>
<div class="btns">
<button id="btn-run" type="button">Orthogonalize</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
canvas { display: block; background: #f4f7fa; border-radius: 10px; border: 1px solid #dce3eb;
cursor: crosshair; touch-action: none; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .45rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d4e8f; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Canvas dimensions
const canvas = document.getElementById('gs-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Origin is at the center of the canvas
const OX = W / 2, OY = H / 2;
const SCALE = 90;
// Current input vectors and computed basis vectors
let v1 = [1.4, 0.6];
let v2 = [0.5, 1.3];
let e1 = null, e2 = null;
let phase = 'init';
// Which vector tip is being dragged
let dragging = null;
function dot(a, b) { return a[0]*b[0] + a[1]*b[1]; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function scale(a, s) { return [a[0]*s, a[1]*s]; }
function sub(a, b) { return [a[0]-b[0], a[1]-b[1]]; }
function add(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
// Normalize a vector to unit length
function normalize(a) { const n = norm(a); return n < 1e-9 ? [1, 0] : scale(a, 1/n); }
function toScreen(v) { return [OX + v[0]*SCALE, OY - v[1]*SCALE]; }
function toWorld(px, py) { return [(px - OX)/SCALE, (OY - py)/SCALE]; }
// Draw a vector arrow with optional label and dashed style
function drawArrow(from, to, color, label, dashed) {
const [fx, fy] = toScreen(from);
const [tx, ty] = toScreen(to);
ctx.save();
ctx.strokeStyle = color; ctx.fillStyle = color;
ctx.lineWidth = 2.2;
if (dashed) ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(fx, fy); ctx.lineTo(tx, ty); ctx.stroke();
ctx.setLineDash([]);
// Draw the arrowhead at the tip
const ang = Math.atan2(ty - fy, tx - fx);
const hw = 8, hl = 14;
ctx.beginPath();
ctx.moveTo(tx, ty);
ctx.lineTo(tx - hl*Math.cos(ang - 0.35), ty - hl*Math.sin(ang - 0.35));
ctx.lineTo(tx - hl*Math.cos(ang + 0.35), ty - hl*Math.sin(ang + 0.35));
ctx.closePath(); ctx.fill();
if (label) {
ctx.font = 'bold 13px system-ui';
ctx.fillText(label, tx + 6, ty - 6);
}
ctx.restore();
}
// Draw a small right-angle symbol at the origin between two unit directions
function drawRightAngle(p, d1, d2, size) {
const s = size / SCALE;
const a = add(p, scale(d1, s));
const b = add(add(p, scale(d1, s)), scale(d2, s));
const c = add(p, scale(d2, s));
const pts = [a, b, c].map(toScreen);
ctx.save();
ctx.strokeStyle = '#888'; ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
ctx.lineTo(pts[1][0], pts[1][1]);
ctx.lineTo(pts[2][0], pts[2][1]);
ctx.stroke();
ctx.restore();
}
function drawAxes() {
ctx.save();
ctx.strokeStyle = '#c8d4de'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, OY); ctx.lineTo(W, OY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(OX, 0); ctx.lineTo(OX, H); ctx.stroke();
ctx.restore();
}
// Clear canvas and redraw everything for the current phase
function draw() {
ctx.clearRect(0, 0, W, H);
drawAxes();
const origin = [0, 0];
// Draw the original input vectors v1 and v2
drawArrow(origin, v1, '#1d3557', 'v₁', false);
drawArrow(origin, v2, '#457b9d', 'v₂', false);
if (phase === 'step1' || phase === 'done') {
// Draw the first basis vector e1 (normalized v1)
drawArrow(origin, e1, '#e63946', 'e₁', false);
}
if (phase === 'done') {
// Draw the projection of v2 onto e1 (dashed)
const proj = scale(e1, dot(v2, e1));
drawArrow(origin, proj, '#f4a261', null, true);
// Draw the second basis vector e2 (remainder after projection)
drawArrow(origin, e2, '#2a9d8f', 'e₂', false);
// Mark the right angle between e1 and e2 at the origin
drawRightAngle(origin, e1, e2, 14);
}
// Draw draggable dots at the tips of v1 and v2
[v1, v2].forEach((v, i) => {
const [sx, sy] = toScreen(v);
ctx.beginPath();
ctx.arc(sx, sy, 6, 0, 2*Math.PI);
ctx.fillStyle = i === 0 ? '#1d3557' : '#457b9d';
ctx.fill();
});
}
function setStatus(text, cls) {
const el = document.getElementById('gs-status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
// Run one step of Gram-Schmidt on button click
function runGS() {
if (phase === 'init' || phase === 'done') {
// Step 1 — normalize v1 to get e1
const n1 = norm(v1);
if (n1 < 0.05) { setStatus('v1 is too short — drag its tip away from the origin.', 'bad'); return; }
e1 = normalize(v1);
phase = 'step1';
draw();
setStatus(i18nFmt('Step 1: e1 = normalize(v1). Original length was %n%.', { n: norm(v1).toFixed(3) }), 'info');
return;
}
if (phase === 'step1') {
// Step 2 — subtract projection of v2 onto e1, then normalize the remainder
const proj = scale(e1, dot(v2, e1));
const remainder = sub(v2, proj);
const n2 = norm(remainder);
if (n2 < 0.05) { setStatus('The vectors are parallel — e2 does not exist. Move v2 to a different direction.', 'bad'); return; }
e2 = normalize(remainder);
const d = Math.abs(dot(e1, e2));
phase = 'done';
draw();
setStatus(i18nFmt('Step 2 done. |e1 · e2| = %d% (should be ≈ 0).', { d: d.toExponential(2) }), 'ok');
}
}
// Reset vectors to initial positions and clear computed basis
function reset() {
v1 = [1.4, 0.6];
v2 = [0.5, 1.3];
e1 = null; e2 = null;
phase = 'init';
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
}
// Pointer event handlers for dragging vector tips
function hitTest(px, py, v) {
const [sx, sy] = toScreen(v);
return Math.hypot(px - sx, py - sy) < 14;
}
canvas.addEventListener('pointerdown', e => {
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
if (hitTest(px, py, v1)) dragging = 'v1';
else if (hitTest(px, py, v2)) dragging = 'v2';
});
canvas.addEventListener('pointermove', e => {
if (!dragging) return;
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
const w = toWorld(px, py);
if (dragging === 'v1') v1 = w; else v2 = w;
phase = 'init'; e1 = null; e2 = null;
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
});
canvas.addEventListener('pointerup', () => { dragging = null; });
canvas.addEventListener('pointercancel', () => { dragging = null; });
document.getElementById('btn-run').addEventListener('click', runGS);
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
<!-- Gram-Schmidt interactive demo — two draggable vectors in the plane -->
<div class="hint">Drag the tips of the blue arrows to set two vectors, then press Orthogonalize. Watch Gram-Schmidt strip each vector of its overlap with the previous one.</div>
<canvas id="gs-canvas" width="380" height="280"></canvas>
<div class="status" id="gs-status">Drag the arrow tips, then press Orthogonalize.</div>
<div class="btns">
<button id="btn-run" type="button">Orthogonalize</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
canvas { display: block; background: #f4f7fa; border-radius: 10px; border: 1px solid #dce3eb;
cursor: crosshair; touch-action: none; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .45rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d4e8f; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Canvas dimensions
const canvas = document.getElementById('gs-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Origin is at the center of the canvas
const OX = W / 2, OY = H / 2;
const SCALE = 90;
// Current input vectors and computed basis vectors
let v1 = [1.4, 0.6];
let v2 = [0.5, 1.3];
let e1 = null, e2 = null;
let phase = 'init';
// Which vector tip is being dragged
let dragging = null;
function dot(a, b) { return a[0]*b[0] + a[1]*b[1]; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function scale(a, s) { return [a[0]*s, a[1]*s]; }
function sub(a, b) { return [a[0]-b[0], a[1]-b[1]]; }
function add(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
// Normalize a vector to unit length
function normalize(a) { const n = norm(a); return n < 1e-9 ? [1, 0] : scale(a, 1/n); }
function toScreen(v) { return [OX + v[0]*SCALE, OY - v[1]*SCALE]; }
function toWorld(px, py) { return [(px - OX)/SCALE, (OY - py)/SCALE]; }
// Draw a vector arrow with optional label and dashed style
function drawArrow(from, to, color, label, dashed) {
const [fx, fy] = toScreen(from);
const [tx, ty] = toScreen(to);
ctx.save();
ctx.strokeStyle = color; ctx.fillStyle = color;
ctx.lineWidth = 2.2;
if (dashed) ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(fx, fy); ctx.lineTo(tx, ty); ctx.stroke();
ctx.setLineDash([]);
// Draw the arrowhead at the tip
const ang = Math.atan2(ty - fy, tx - fx);
const hw = 8, hl = 14;
ctx.beginPath();
ctx.moveTo(tx, ty);
ctx.lineTo(tx - hl*Math.cos(ang - 0.35), ty - hl*Math.sin(ang - 0.35));
ctx.lineTo(tx - hl*Math.cos(ang + 0.35), ty - hl*Math.sin(ang + 0.35));
ctx.closePath(); ctx.fill();
if (label) {
ctx.font = 'bold 13px system-ui';
ctx.fillText(label, tx + 6, ty - 6);
}
ctx.restore();
}
// Draw a small right-angle symbol at the origin between two unit directions
function drawRightAngle(p, d1, d2, size) {
const s = size / SCALE;
const a = add(p, scale(d1, s));
const b = add(add(p, scale(d1, s)), scale(d2, s));
const c = add(p, scale(d2, s));
const pts = [a, b, c].map(toScreen);
ctx.save();
ctx.strokeStyle = '#888'; ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(pts[0][0], pts[0][1]);
ctx.lineTo(pts[1][0], pts[1][1]);
ctx.lineTo(pts[2][0], pts[2][1]);
ctx.stroke();
ctx.restore();
}
function drawAxes() {
ctx.save();
ctx.strokeStyle = '#c8d4de'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, OY); ctx.lineTo(W, OY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(OX, 0); ctx.lineTo(OX, H); ctx.stroke();
ctx.restore();
}
// Clear canvas and redraw everything for the current phase
function draw() {
ctx.clearRect(0, 0, W, H);
drawAxes();
const origin = [0, 0];
// Draw the original input vectors v1 and v2
drawArrow(origin, v1, '#1d3557', 'v₁', false);
drawArrow(origin, v2, '#457b9d', 'v₂', false);
if (phase === 'step1' || phase === 'done') {
// Draw the first basis vector e1 (normalized v1)
drawArrow(origin, e1, '#e63946', 'e₁', false);
}
if (phase === 'done') {
// Draw the projection of v2 onto e1 (dashed)
const proj = scale(e1, dot(v2, e1));
drawArrow(origin, proj, '#f4a261', null, true);
// Draw the second basis vector e2 (remainder after projection)
drawArrow(origin, e2, '#2a9d8f', 'e₂', false);
// Mark the right angle between e1 and e2 at the origin
drawRightAngle(origin, e1, e2, 14);
}
// Draw draggable dots at the tips of v1 and v2
[v1, v2].forEach((v, i) => {
const [sx, sy] = toScreen(v);
ctx.beginPath();
ctx.arc(sx, sy, 6, 0, 2*Math.PI);
ctx.fillStyle = i === 0 ? '#1d3557' : '#457b9d';
ctx.fill();
});
}
function setStatus(text, cls) {
const el = document.getElementById('gs-status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
// Run one step of Gram-Schmidt on button click
function runGS() {
if (phase === 'init' || phase === 'done') {
// Step 1 — normalize v1 to get e1
const n1 = norm(v1);
if (n1 < 0.05) { setStatus('v1 is too short — drag its tip away from the origin.', 'bad'); return; }
e1 = normalize(v1);
phase = 'step1';
draw();
setStatus(i18nFmt('Step 1: e1 = normalize(v1). Original length was %n%.', { n: norm(v1).toFixed(3) }), 'info');
return;
}
if (phase === 'step1') {
// Step 2 — subtract projection of v2 onto e1, then normalize the remainder
const proj = scale(e1, dot(v2, e1));
const remainder = sub(v2, proj);
const n2 = norm(remainder);
if (n2 < 0.05) { setStatus('The vectors are parallel — e2 does not exist. Move v2 to a different direction.', 'bad'); return; }
e2 = normalize(remainder);
const d = Math.abs(dot(e1, e2));
phase = 'done';
draw();
setStatus(i18nFmt('Step 2 done. |e1 · e2| = %d% (should be ≈ 0).', { d: d.toExponential(2) }), 'ok');
}
}
// Reset vectors to initial positions and clear computed basis
function reset() {
v1 = [1.4, 0.6];
v2 = [0.5, 1.3];
e1 = null; e2 = null;
phase = 'init';
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
}
// Pointer event handlers for dragging vector tips
function hitTest(px, py, v) {
const [sx, sy] = toScreen(v);
return Math.hypot(px - sx, py - sy) < 14;
}
canvas.addEventListener('pointerdown', e => {
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
if (hitTest(px, py, v1)) dragging = 'v1';
else if (hitTest(px, py, v2)) dragging = 'v2';
});
canvas.addEventListener('pointermove', e => {
if (!dragging) return;
const r = canvas.getBoundingClientRect();
const px = (e.clientX - r.left) * (W / r.width);
const py = (e.clientY - r.top) * (H / r.height);
const w = toWorld(px, py);
if (dragging === 'v1') v1 = w; else v2 = w;
phase = 'init'; e1 = null; e2 = null;
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
});
canvas.addEventListener('pointerup', () => { dragging = null; });
canvas.addEventListener('pointercancel', () => { dragging = null; });
document.getElementById('btn-run').addEventListener('click', runGS);
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
setStatus('Drag the arrow tips, then press Orthogonalize.', '');
Comments
Loading comments...