Click anywhere on the canvas to place a point. Its dual line appears instantly, drawn using the standard duality map p=(a,b)↦ℓ:y=ax−b. Drag existing points to move them.
<!-- Geometric duality demo: points in the plane dual to lines y=ax-b -->
<p class="hint">Click on the canvas to place up to 5 points. Each point <b>P = (a, b)</b> maps to the dual line <b>y = ax − b</b>. When 3 points are <b>collinear</b>, their 3 dual lines become <b>concurrent</b> (orange dot). Drag points to explore.</p>
<div class="canvas-wrap">
<canvas id="cv" width="480" height="300"></canvas>
<p class="canvas-label">Point space — click to place points; their dual lines are drawn in the same view</p>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Clear all</button>
<button id="btn-example" type="button">Load collinear example</button>
</div>
/* Layout for the duality demo canvas and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { position: relative; display: inline-block; width: 100%; }
canvas { display: block; width: 100%; max-width: 480px; height: auto;
border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; background: #f8fafb; }
.canvas-label { font-size: .78rem; color: #888; margin: .3rem 0 0; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.collinear { color: #0a7d33; }
.status.normal { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Geometric Duality interactive demo
// Standard point-line duality: point P=(a,b) dualizes to line y=ax-b
// Collinear points produce concurrent dual lines — the core duality theorem
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 480, H = 300;
// Canvas coordinate system: origin at center, y-axis up, scaled by SCALE px per unit
const CX = W / 2, CY = H / 2, SCALE = 60;
// Array of world-space points [a, b]; dragging index
let points = [];
let dragging = null;
// Convert between canvas pixels and world coordinates
function toWorld(px, py) { return [(px - CX) / SCALE, -(py - CY) / SCALE]; }
function toCanvas(wx, wy) { return [CX + wx * SCALE, CY - wy * SCALE]; }
// Evaluate the dual line y=ax-b at a given x
// The standard duality map: point (a,b) <-> line y=ax-b
function dualLineY(wx, a, b) { return a * wx - b; }
// Return canvas endpoints for the dual line of point (a,b) clipped to canvas width
function clipLine(a, b) {
const x0 = (0 - CX) / SCALE, x1 = (W - CX) / SCALE;
const [cx0, cy0] = toCanvas(x0, dualLineY(x0, a, b));
const [cx1, cy1] = toCanvas(x1, dualLineY(x1, a, b));
return [cx0, cy0, cx1, cy1];
}
// Return true when the first 3 points are collinear (cross-product area near zero)
function areCollinear(pts) {
if (pts.length < 3) return false;
const [ax, ay] = pts[0], [bx, by] = pts[1], [cx, cy] = pts[2];
return Math.abs((bx - ax) * (cy - ay) - (cx - ax) * (by - ay)) < 0.04;
}
// Compute intersection of two dual lines y=a1*x-b1 and y=a2*x-b2
// Solving a1*x-b1 = a2*x-b2 gives x=(b1-b2)/(a1-a2)
// Note: numerator is b1-b2, not b2-b1
function dualIntersection(p1, p2) {
const [a1, b1] = p1, [a2, b2] = p2;
if (Math.abs(a1 - a2) < 1e-9) return null;
const x = (b1 - b2) / (a1 - a2);
const y = dualLineY(x, a1, b1);
return [x, y];
}
// Return the single concurrent point of the first 3 dual lines, or null if they do not meet
function getConcurrentPoint(pts) {
if (pts.length < 3) return null;
const i01 = dualIntersection(pts[0], pts[1]);
const i02 = dualIntersection(pts[0], pts[2]);
const i12 = dualIntersection(pts[1], pts[2]);
const valid = [i01, i02, i12].filter(Boolean);
if (valid.length < 2) return null;
const [x0, y0] = valid[0];
const scale = Math.max(1, Math.abs(x0), Math.abs(y0));
const EPS = 0.02;
const allClose = valid.every(([x, y]) =>
Math.abs(x - x0) / scale < EPS && Math.abs(y - y0) / scale < EPS
);
return allClose ? valid[0] : null;
}
// Draw the x and y axes as faint grid lines
function drawAxes() {
ctx.save();
ctx.strokeStyle = '#d0d8e0';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
ctx.restore();
}
// Main render function: clear, axes, dual lines, concurrent dot, primal points, status
function draw() {
ctx.clearRect(0, 0, W, H);
drawAxes();
const collinear = areCollinear(points);
const concurrentPt = (points.length >= 3) ? getConcurrentPoint(points.slice(0, 3)) : null;
// Draw each dual line across the full canvas width
const COLORS = ['#e63946', '#2196f3', '#ff9800', '#9c27b0', '#00897b'];
points.forEach(([a, b], i) => {
const [cx0, cy0, cx1, cy1] = clipLine(a, b);
ctx.save();
ctx.strokeStyle = COLORS[i % COLORS.length];
ctx.lineWidth = 1.8;
ctx.globalAlpha = 0.7;
ctx.beginPath(); ctx.moveTo(cx0, cy0); ctx.lineTo(cx1, cy1); ctx.stroke();
ctx.restore();
});
// Draw an orange circle at the concurrent intersection when collinear
if (concurrentPt) {
const [cx, cy] = toCanvas(concurrentPt[0], concurrentPt[1]);
if (cx >= 0 && cx <= W && cy >= 0 && cy <= H) {
ctx.save();
ctx.fillStyle = '#ff9800';
ctx.strokeStyle = '#333';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, 2 * Math.PI);
ctx.fill(); ctx.stroke();
ctx.restore();
}
}
// Draw the primal points on top with labels P1, P2, ...
points.forEach(([a, b], i) => {
const [px, py] = toCanvas(a, b);
ctx.save();
ctx.fillStyle = COLORS[i % COLORS.length];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(px, py, 7, 0, 2 * Math.PI);
ctx.fill(); ctx.stroke();
ctx.fillStyle = '#222';
ctx.font = 'bold 11px system-ui';
ctx.fillText('P' + (i + 1), px + 9, py - 5);
ctx.restore();
});
// Update the status bar text and color class
const statusEl = document.getElementById('status');
if (points.length === 0) {
statusEl.textContent = 'Click to place a point.';
statusEl.className = 'status normal';
} else if (points.length < 3) {
statusEl.textContent = 'Place at least 3 points to see the duality in action.';
statusEl.className = 'status normal';
} else if (collinear) {
statusEl.textContent = '✓ Points are collinear → dual lines are concurrent! (orange dot)';
statusEl.className = 'status collinear';
} else {
statusEl.textContent = 'Points are in general position — dual lines form a triangle of intersections.';
statusEl.className = 'status normal';
}
}
// Return the index of a point under the cursor, or -1 if none
function hitTest(ex, ey) {
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (ex - rect.left) * scaleX;
const py = (ey - rect.top) * scaleY;
for (let i = 0; i < points.length; i++) {
const [a, b] = points[i];
const [cpx, cpy] = toCanvas(a, b);
const dx = px - cpx, dy = py - cpy;
if (dx * dx + dy * dy < 100) return i;
}
return -1;
}
cv.addEventListener('mousedown', e => {
const idx = hitTest(e.clientX, e.clientY);
if (idx >= 0) { dragging = idx; return; }
if (points.length >= 5) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
points.push(toWorld(px, py));
draw();
});
cv.addEventListener('mousemove', e => {
if (dragging === null) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
points[dragging] = toWorld(px, py);
draw();
});
cv.addEventListener('mouseup', () => { dragging = null; });
cv.addEventListener('mouseleave', () => { dragging = null; });
// Touch event support for mobile devices
cv.addEventListener('touchstart', e => {
e.preventDefault();
const t = e.touches[0];
const idx = hitTest(t.clientX, t.clientY);
if (idx >= 0) { dragging = idx; return; }
if (points.length >= 5) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (t.clientX - rect.left) * scaleX;
const py = (t.clientY - rect.top) * scaleY;
points.push(toWorld(px, py));
draw();
}, { passive: false });
cv.addEventListener('touchmove', e => {
e.preventDefault();
if (dragging === null) return;
const t = e.touches[0];
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (t.clientX - rect.left) * scaleX;
const py = (t.clientY - rect.top) * scaleY;
points[dragging] = toWorld(px, py);
draw();
}, { passive: false });
cv.addEventListener('touchend', () => { dragging = null; });
document.getElementById('btn-reset').addEventListener('click', () => { points = []; draw(); });
// Load a preset collinear configuration to demonstrate duality
document.getElementById('btn-example').addEventListener('click', () => {
// Three points on y=-1 are collinear; their dual lines y=ax+1 all pass through (0,1)
points = [[-2, -1], [0, -1], [2, -1]];
draw();
});
draw();
<!-- Geometric duality demo: points in the plane dual to lines y=ax-b -->
<p class="hint">Click on the canvas to place up to 5 points. Each point <b>P = (a, b)</b> maps to the dual line <b>y = ax − b</b>. When 3 points are <b>collinear</b>, their 3 dual lines become <b>concurrent</b> (orange dot). Drag points to explore.</p>
<div class="canvas-wrap">
<canvas id="cv" width="480" height="300"></canvas>
<p class="canvas-label">Point space — click to place points; their dual lines are drawn in the same view</p>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Clear all</button>
<button id="btn-example" type="button">Load collinear example</button>
</div>
/* Layout for the duality demo canvas and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.canvas-wrap { position: relative; display: inline-block; width: 100%; }
canvas { display: block; width: 100%; max-width: 480px; height: auto;
border: 1px solid #cdd9e3; border-radius: 8px; cursor: crosshair; background: #f8fafb; }
.canvas-label { font-size: .78rem; color: #888; margin: .3rem 0 0; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .4rem 0; }
.status.collinear { color: #0a7d33; }
.status.normal { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Geometric Duality interactive demo
// Standard point-line duality: point P=(a,b) dualizes to line y=ax-b
// Collinear points produce concurrent dual lines — the core duality theorem
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = 480, H = 300;
// Canvas coordinate system: origin at center, y-axis up, scaled by SCALE px per unit
const CX = W / 2, CY = H / 2, SCALE = 60;
// Array of world-space points [a, b]; dragging index
let points = [];
let dragging = null;
// Convert between canvas pixels and world coordinates
function toWorld(px, py) { return [(px - CX) / SCALE, -(py - CY) / SCALE]; }
function toCanvas(wx, wy) { return [CX + wx * SCALE, CY - wy * SCALE]; }
// Evaluate the dual line y=ax-b at a given x
// The standard duality map: point (a,b) <-> line y=ax-b
function dualLineY(wx, a, b) { return a * wx - b; }
// Return canvas endpoints for the dual line of point (a,b) clipped to canvas width
function clipLine(a, b) {
const x0 = (0 - CX) / SCALE, x1 = (W - CX) / SCALE;
const [cx0, cy0] = toCanvas(x0, dualLineY(x0, a, b));
const [cx1, cy1] = toCanvas(x1, dualLineY(x1, a, b));
return [cx0, cy0, cx1, cy1];
}
// Return true when the first 3 points are collinear (cross-product area near zero)
function areCollinear(pts) {
if (pts.length < 3) return false;
const [ax, ay] = pts[0], [bx, by] = pts[1], [cx, cy] = pts[2];
return Math.abs((bx - ax) * (cy - ay) - (cx - ax) * (by - ay)) < 0.04;
}
// Compute intersection of two dual lines y=a1*x-b1 and y=a2*x-b2
// Solving a1*x-b1 = a2*x-b2 gives x=(b1-b2)/(a1-a2)
// Note: numerator is b1-b2, not b2-b1
function dualIntersection(p1, p2) {
const [a1, b1] = p1, [a2, b2] = p2;
if (Math.abs(a1 - a2) < 1e-9) return null;
const x = (b1 - b2) / (a1 - a2);
const y = dualLineY(x, a1, b1);
return [x, y];
}
// Return the single concurrent point of the first 3 dual lines, or null if they do not meet
function getConcurrentPoint(pts) {
if (pts.length < 3) return null;
const i01 = dualIntersection(pts[0], pts[1]);
const i02 = dualIntersection(pts[0], pts[2]);
const i12 = dualIntersection(pts[1], pts[2]);
const valid = [i01, i02, i12].filter(Boolean);
if (valid.length < 2) return null;
const [x0, y0] = valid[0];
const scale = Math.max(1, Math.abs(x0), Math.abs(y0));
const EPS = 0.02;
const allClose = valid.every(([x, y]) =>
Math.abs(x - x0) / scale < EPS && Math.abs(y - y0) / scale < EPS
);
return allClose ? valid[0] : null;
}
// Draw the x and y axes as faint grid lines
function drawAxes() {
ctx.save();
ctx.strokeStyle = '#d0d8e0';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
ctx.restore();
}
// Main render function: clear, axes, dual lines, concurrent dot, primal points, status
function draw() {
ctx.clearRect(0, 0, W, H);
drawAxes();
const collinear = areCollinear(points);
const concurrentPt = (points.length >= 3) ? getConcurrentPoint(points.slice(0, 3)) : null;
// Draw each dual line across the full canvas width
const COLORS = ['#e63946', '#2196f3', '#ff9800', '#9c27b0', '#00897b'];
points.forEach(([a, b], i) => {
const [cx0, cy0, cx1, cy1] = clipLine(a, b);
ctx.save();
ctx.strokeStyle = COLORS[i % COLORS.length];
ctx.lineWidth = 1.8;
ctx.globalAlpha = 0.7;
ctx.beginPath(); ctx.moveTo(cx0, cy0); ctx.lineTo(cx1, cy1); ctx.stroke();
ctx.restore();
});
// Draw an orange circle at the concurrent intersection when collinear
if (concurrentPt) {
const [cx, cy] = toCanvas(concurrentPt[0], concurrentPt[1]);
if (cx >= 0 && cx <= W && cy >= 0 && cy <= H) {
ctx.save();
ctx.fillStyle = '#ff9800';
ctx.strokeStyle = '#333';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, 2 * Math.PI);
ctx.fill(); ctx.stroke();
ctx.restore();
}
}
// Draw the primal points on top with labels P1, P2, ...
points.forEach(([a, b], i) => {
const [px, py] = toCanvas(a, b);
ctx.save();
ctx.fillStyle = COLORS[i % COLORS.length];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(px, py, 7, 0, 2 * Math.PI);
ctx.fill(); ctx.stroke();
ctx.fillStyle = '#222';
ctx.font = 'bold 11px system-ui';
ctx.fillText('P' + (i + 1), px + 9, py - 5);
ctx.restore();
});
// Update the status bar text and color class
const statusEl = document.getElementById('status');
if (points.length === 0) {
statusEl.textContent = 'Click to place a point.';
statusEl.className = 'status normal';
} else if (points.length < 3) {
statusEl.textContent = 'Place at least 3 points to see the duality in action.';
statusEl.className = 'status normal';
} else if (collinear) {
statusEl.textContent = '✓ Points are collinear → dual lines are concurrent! (orange dot)';
statusEl.className = 'status collinear';
} else {
statusEl.textContent = 'Points are in general position — dual lines form a triangle of intersections.';
statusEl.className = 'status normal';
}
}
// Return the index of a point under the cursor, or -1 if none
function hitTest(ex, ey) {
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (ex - rect.left) * scaleX;
const py = (ey - rect.top) * scaleY;
for (let i = 0; i < points.length; i++) {
const [a, b] = points[i];
const [cpx, cpy] = toCanvas(a, b);
const dx = px - cpx, dy = py - cpy;
if (dx * dx + dy * dy < 100) return i;
}
return -1;
}
cv.addEventListener('mousedown', e => {
const idx = hitTest(e.clientX, e.clientY);
if (idx >= 0) { dragging = idx; return; }
if (points.length >= 5) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
points.push(toWorld(px, py));
draw();
});
cv.addEventListener('mousemove', e => {
if (dragging === null) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
points[dragging] = toWorld(px, py);
draw();
});
cv.addEventListener('mouseup', () => { dragging = null; });
cv.addEventListener('mouseleave', () => { dragging = null; });
// Touch event support for mobile devices
cv.addEventListener('touchstart', e => {
e.preventDefault();
const t = e.touches[0];
const idx = hitTest(t.clientX, t.clientY);
if (idx >= 0) { dragging = idx; return; }
if (points.length >= 5) return;
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (t.clientX - rect.left) * scaleX;
const py = (t.clientY - rect.top) * scaleY;
points.push(toWorld(px, py));
draw();
}, { passive: false });
cv.addEventListener('touchmove', e => {
e.preventDefault();
if (dragging === null) return;
const t = e.touches[0];
const rect = cv.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (t.clientX - rect.left) * scaleX;
const py = (t.clientY - rect.top) * scaleY;
points[dragging] = toWorld(px, py);
draw();
}, { passive: false });
cv.addEventListener('touchend', () => { dragging = null; });
document.getElementById('btn-reset').addEventListener('click', () => { points = []; draw(); });
// Load a preset collinear configuration to demonstrate duality
document.getElementById('btn-example').addEventListener('click', () => {
// Three points on y=-1 are collinear; their dual lines y=ax+1 all pass through (0,1)
points = [[-2, -1], [0, -1], [2, -1]];
draw();
});
draw();
The demo highlights the dual intersection point whenever the three lines are concurrent, letting you see the duality in both directions at once.
Comments
Loading comments...