Drag the endpoints of the two segments below. For each segment, the demo checks which side its two turns fall on relative to the other segment — four little signs, updated live.
<p class="hint">Drag the endpoints A, B, C, D. The demo computes the sign of a cross product (orientation) four times and checks whether the signs disagree on both sides — that disagreement is the whole crossing test.</p>
<svg id="stage" viewBox="0 0 360 260" class="stage"></svg>
<div class="signs" id="signs"></div>
<div class="status" id="status">Drag any point to move a segment.</div>
<div class="btns">
<button id="cross" type="button">Crossing example</button>
<button id="collinear" type="button">Collinear example</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { 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; }
.stage { width: 100%; max-width: 420px; height: 240px; background: #eef2f6; border: 1px solid #cdd9e3;
border-radius: 10px; touch-action: none; }
.seg-ab { stroke: #1d3557; stroke-width: 3; }
.seg-cd { stroke: #e63946; stroke-width: 3; }
.handle { cursor: grab; }
.handle:active { cursor: grabbing; }
.handle-ab { fill: #1d3557; stroke: #fff; stroke-width: 1.5; }
.handle-cd { fill: #e63946; stroke: #fff; stroke-width: 1.5; }
.label { font: 700 12px ui-monospace, monospace; fill: #445; pointer-events: none; }
.signs { display: flex; gap: .6rem; flex-wrap: wrap; margin: .55rem 0; font: 600 13px ui-monospace, monospace; }
.sign { padding: .2rem .5rem; border-radius: 6px; background: #e8eef3; border: 1px solid #cdd9e3; }
.sign.pos { background: #e6f6ea; border-color: #9fd8ae; color: #0a7d33; }
.sign.neg { background: #fdeaec; border-color: #f3aab1; color: #c92f3c; }
.sign.zero { background: #fff6df; border-color: #f0d78a; color: #9a7a08; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #9a7a08; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Orientation test for segment crossing. A segment AB crosses CD iff
// C and D fall on opposite sides of AB, and A and B fall on opposite sides of CD.
const EPS = 1e-6;
// Draggable endpoints of the two segments, in SVG viewBox coordinates.
let A = { x: 60, y: 60 };
let B = { x: 300, y: 200 };
let C = { x: 60, y: 200 };
let D = { x: 300, y: 60 };
const svgNS = 'http://www.w3.org/2000/svg';
const stage = document.getElementById('stage');
// Sign of the cross product (B-A) x (C-A): + = counter-clockwise, - = clockwise, 0 = collinear.
function orient(ax, ay, bx, by, cx, cy) {
const v = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
if (Math.abs(v) < EPS) return 0;
return v > 0 ? 1 : -1;
}
// Bounding-box check used only when three points are already known to be collinear.
function onSegment(ax, ay, bx, by, cx, cy) {
return Math.min(ax, bx) - EPS <= cx && cx <= Math.max(ax, bx) + EPS &&
Math.min(ay, by) - EPS <= cy && cy <= Math.max(ay, by) + EPS;
}
// The full test, general case plus the collinear special case.
function segmentsIntersect(p1, p2, p3, p4) {
const o1 = orient(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
const o2 = orient(p1.x, p1.y, p2.x, p2.y, p4.x, p4.y);
const o3 = orient(p3.x, p3.y, p4.x, p4.y, p1.x, p1.y);
const o4 = orient(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y);
let collinearTouch = false;
if (o1 === 0 && onSegment(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)) collinearTouch = true;
if (o2 === 0 && onSegment(p1.x, p1.y, p2.x, p2.y, p4.x, p4.y)) collinearTouch = true;
if (o3 === 0 && onSegment(p3.x, p3.y, p4.x, p4.y, p1.x, p1.y)) collinearTouch = true;
if (o4 === 0 && onSegment(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y)) collinearTouch = true;
const crosses = (o1 !== o2 && o3 !== o4) || collinearTouch;
return { o1, o2, o3, o4, crosses, anyZero: (o1 === 0 || o2 === 0 || o3 === 0 || o4 === 0) };
}
function signEl(labelText, value) {
const cls = value > 0 ? 'pos' : value < 0 ? 'neg' : 'zero';
const glyph = value > 0 ? '+' : value < 0 ? '−' : '0';
const span = document.createElement('span');
span.className = 'sign ' + cls;
span.textContent = i18nFmt('%name% = %glyph%', { name: labelText, glyph: glyph });
return span;
}
let dragging = null; // Which endpoint (if any) is currently being dragged.
function makeHandle(point, cls, key) {
const c = document.createElementNS(svgNS, 'circle');
c.setAttribute('r', 8);
c.setAttribute('class', 'handle ' + cls);
c.setAttribute('cx', point.x);
c.setAttribute('cy', point.y);
c.addEventListener('pointerdown', (e) => { dragging = key; e.target.setPointerCapture(e.pointerId); });
return c;
}
function labelEl(point, text, dx, dy) {
const t = document.createElementNS(svgNS, 'text');
t.setAttribute('x', point.x + dx);
t.setAttribute('y', point.y + dy);
t.setAttribute('class', 'label');
t.textContent = text;
return t;
}
function render() {
stage.innerHTML = '';
const lineAB = document.createElementNS(svgNS, 'line');
lineAB.setAttribute('x1', A.x); lineAB.setAttribute('y1', A.y);
lineAB.setAttribute('x2', B.x); lineAB.setAttribute('y2', B.y);
lineAB.setAttribute('class', 'seg-ab');
stage.appendChild(lineAB);
const lineCD = document.createElementNS(svgNS, 'line');
lineCD.setAttribute('x1', C.x); lineCD.setAttribute('y1', C.y);
lineCD.setAttribute('x2', D.x); lineCD.setAttribute('y2', D.y);
lineCD.setAttribute('class', 'seg-cd');
stage.appendChild(lineCD);
stage.appendChild(labelEl(A, 'A', -14, -10));
stage.appendChild(labelEl(B, 'B', 10, -10));
stage.appendChild(labelEl(C, 'C', -14, 18));
stage.appendChild(labelEl(D, 'D', 10, 18));
stage.appendChild(makeHandle(A, 'handle-ab', 'A'));
stage.appendChild(makeHandle(B, 'handle-ab', 'B'));
stage.appendChild(makeHandle(C, 'handle-cd', 'C'));
stage.appendChild(makeHandle(D, 'handle-cd', 'D'));
const r = segmentsIntersect(A, B, C, D);
const signs = document.getElementById('signs');
signs.innerHTML = '';
signs.appendChild(signEl('orient(A,B,C)', r.o1));
signs.appendChild(signEl('orient(A,B,D)', r.o2));
signs.appendChild(signEl('orient(C,D,A)', r.o3));
signs.appendChild(signEl('orient(C,D,B)', r.o4));
const status = document.getElementById('status');
if (r.crosses && r.anyZero) {
status.textContent = '⚠ Collinear case: a point lies exactly on the other line — checked with the on-segment rule.';
status.className = 'status warn';
} else if (r.crosses) {
status.textContent = '✓ The segments cross (signs split both ways).';
status.className = 'status ok';
} else {
status.textContent = '✗ The segments do not cross (signs agree on at least one side).';
status.className = 'status bad';
}
}
function pointFromEvent(e) {
const rect = stage.getBoundingClientRect();
const vb = stage.viewBox.baseVal;
const x = (e.clientX - rect.left) * (vb.width / rect.width);
const y = (e.clientY - rect.top) * (vb.height / rect.height);
return {
x: Math.max(10, Math.min(vb.width - 10, x)),
y: Math.max(10, Math.min(vb.height - 10, y)),
};
}
stage.addEventListener('pointermove', (e) => {
if (!dragging) return;
const p = pointFromEvent(e);
if (dragging === 'A') A = p; else if (dragging === 'B') B = p;
else if (dragging === 'C') C = p; else if (dragging === 'D') D = p;
render();
});
stage.addEventListener('pointerup', () => { dragging = null; });
stage.addEventListener('pointerleave', () => { dragging = null; });
function setCrossing() {
A = { x: 60, y: 60 }; B = { x: 300, y: 200 };
C = { x: 60, y: 200 }; D = { x: 300, y: 60 };
render();
}
function setCollinear() {
// Four collinear points with an overlapping stretch, to show the special case.
A = { x: 40, y: 130 }; B = { x: 200, y: 130 };
C = { x: 140, y: 130 }; D = { x: 320, y: 130 };
render();
}
document.getElementById('cross').addEventListener('click', setCrossing);
document.getElementById('collinear').addEventListener('click', setCollinear);
document.getElementById('reset').addEventListener('click', setCrossing);
render();
<p class="hint">Drag the endpoints A, B, C, D. The demo computes the sign of a cross product (orientation) four times and checks whether the signs disagree on both sides — that disagreement is the whole crossing test.</p>
<svg id="stage" viewBox="0 0 360 260" class="stage"></svg>
<div class="signs" id="signs"></div>
<div class="status" id="status">Drag any point to move a segment.</div>
<div class="btns">
<button id="cross" type="button">Crossing example</button>
<button id="collinear" type="button">Collinear example</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { 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; }
.stage { width: 100%; max-width: 420px; height: 240px; background: #eef2f6; border: 1px solid #cdd9e3;
border-radius: 10px; touch-action: none; }
.seg-ab { stroke: #1d3557; stroke-width: 3; }
.seg-cd { stroke: #e63946; stroke-width: 3; }
.handle { cursor: grab; }
.handle:active { cursor: grabbing; }
.handle-ab { fill: #1d3557; stroke: #fff; stroke-width: 1.5; }
.handle-cd { fill: #e63946; stroke: #fff; stroke-width: 1.5; }
.label { font: 700 12px ui-monospace, monospace; fill: #445; pointer-events: none; }
.signs { display: flex; gap: .6rem; flex-wrap: wrap; margin: .55rem 0; font: 600 13px ui-monospace, monospace; }
.sign { padding: .2rem .5rem; border-radius: 6px; background: #e8eef3; border: 1px solid #cdd9e3; }
.sign.pos { background: #e6f6ea; border-color: #9fd8ae; color: #0a7d33; }
.sign.neg { background: #fdeaec; border-color: #f3aab1; color: #c92f3c; }
.sign.zero { background: #fff6df; border-color: #f0d78a; color: #9a7a08; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.warn { color: #9a7a08; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Orientation test for segment crossing. A segment AB crosses CD iff
// C and D fall on opposite sides of AB, and A and B fall on opposite sides of CD.
const EPS = 1e-6;
// Draggable endpoints of the two segments, in SVG viewBox coordinates.
let A = { x: 60, y: 60 };
let B = { x: 300, y: 200 };
let C = { x: 60, y: 200 };
let D = { x: 300, y: 60 };
const svgNS = 'http://www.w3.org/2000/svg';
const stage = document.getElementById('stage');
// Sign of the cross product (B-A) x (C-A): + = counter-clockwise, - = clockwise, 0 = collinear.
function orient(ax, ay, bx, by, cx, cy) {
const v = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
if (Math.abs(v) < EPS) return 0;
return v > 0 ? 1 : -1;
}
// Bounding-box check used only when three points are already known to be collinear.
function onSegment(ax, ay, bx, by, cx, cy) {
return Math.min(ax, bx) - EPS <= cx && cx <= Math.max(ax, bx) + EPS &&
Math.min(ay, by) - EPS <= cy && cy <= Math.max(ay, by) + EPS;
}
// The full test, general case plus the collinear special case.
function segmentsIntersect(p1, p2, p3, p4) {
const o1 = orient(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
const o2 = orient(p1.x, p1.y, p2.x, p2.y, p4.x, p4.y);
const o3 = orient(p3.x, p3.y, p4.x, p4.y, p1.x, p1.y);
const o4 = orient(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y);
let collinearTouch = false;
if (o1 === 0 && onSegment(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y)) collinearTouch = true;
if (o2 === 0 && onSegment(p1.x, p1.y, p2.x, p2.y, p4.x, p4.y)) collinearTouch = true;
if (o3 === 0 && onSegment(p3.x, p3.y, p4.x, p4.y, p1.x, p1.y)) collinearTouch = true;
if (o4 === 0 && onSegment(p3.x, p3.y, p4.x, p4.y, p2.x, p2.y)) collinearTouch = true;
const crosses = (o1 !== o2 && o3 !== o4) || collinearTouch;
return { o1, o2, o3, o4, crosses, anyZero: (o1 === 0 || o2 === 0 || o3 === 0 || o4 === 0) };
}
function signEl(labelText, value) {
const cls = value > 0 ? 'pos' : value < 0 ? 'neg' : 'zero';
const glyph = value > 0 ? '+' : value < 0 ? '−' : '0';
const span = document.createElement('span');
span.className = 'sign ' + cls;
span.textContent = i18nFmt('%name% = %glyph%', { name: labelText, glyph: glyph });
return span;
}
let dragging = null; // Which endpoint (if any) is currently being dragged.
function makeHandle(point, cls, key) {
const c = document.createElementNS(svgNS, 'circle');
c.setAttribute('r', 8);
c.setAttribute('class', 'handle ' + cls);
c.setAttribute('cx', point.x);
c.setAttribute('cy', point.y);
c.addEventListener('pointerdown', (e) => { dragging = key; e.target.setPointerCapture(e.pointerId); });
return c;
}
function labelEl(point, text, dx, dy) {
const t = document.createElementNS(svgNS, 'text');
t.setAttribute('x', point.x + dx);
t.setAttribute('y', point.y + dy);
t.setAttribute('class', 'label');
t.textContent = text;
return t;
}
function render() {
stage.innerHTML = '';
const lineAB = document.createElementNS(svgNS, 'line');
lineAB.setAttribute('x1', A.x); lineAB.setAttribute('y1', A.y);
lineAB.setAttribute('x2', B.x); lineAB.setAttribute('y2', B.y);
lineAB.setAttribute('class', 'seg-ab');
stage.appendChild(lineAB);
const lineCD = document.createElementNS(svgNS, 'line');
lineCD.setAttribute('x1', C.x); lineCD.setAttribute('y1', C.y);
lineCD.setAttribute('x2', D.x); lineCD.setAttribute('y2', D.y);
lineCD.setAttribute('class', 'seg-cd');
stage.appendChild(lineCD);
stage.appendChild(labelEl(A, 'A', -14, -10));
stage.appendChild(labelEl(B, 'B', 10, -10));
stage.appendChild(labelEl(C, 'C', -14, 18));
stage.appendChild(labelEl(D, 'D', 10, 18));
stage.appendChild(makeHandle(A, 'handle-ab', 'A'));
stage.appendChild(makeHandle(B, 'handle-ab', 'B'));
stage.appendChild(makeHandle(C, 'handle-cd', 'C'));
stage.appendChild(makeHandle(D, 'handle-cd', 'D'));
const r = segmentsIntersect(A, B, C, D);
const signs = document.getElementById('signs');
signs.innerHTML = '';
signs.appendChild(signEl('orient(A,B,C)', r.o1));
signs.appendChild(signEl('orient(A,B,D)', r.o2));
signs.appendChild(signEl('orient(C,D,A)', r.o3));
signs.appendChild(signEl('orient(C,D,B)', r.o4));
const status = document.getElementById('status');
if (r.crosses && r.anyZero) {
status.textContent = '⚠ Collinear case: a point lies exactly on the other line — checked with the on-segment rule.';
status.className = 'status warn';
} else if (r.crosses) {
status.textContent = '✓ The segments cross (signs split both ways).';
status.className = 'status ok';
} else {
status.textContent = '✗ The segments do not cross (signs agree on at least one side).';
status.className = 'status bad';
}
}
function pointFromEvent(e) {
const rect = stage.getBoundingClientRect();
const vb = stage.viewBox.baseVal;
const x = (e.clientX - rect.left) * (vb.width / rect.width);
const y = (e.clientY - rect.top) * (vb.height / rect.height);
return {
x: Math.max(10, Math.min(vb.width - 10, x)),
y: Math.max(10, Math.min(vb.height - 10, y)),
};
}
stage.addEventListener('pointermove', (e) => {
if (!dragging) return;
const p = pointFromEvent(e);
if (dragging === 'A') A = p; else if (dragging === 'B') B = p;
else if (dragging === 'C') C = p; else if (dragging === 'D') D = p;
render();
});
stage.addEventListener('pointerup', () => { dragging = null; });
stage.addEventListener('pointerleave', () => { dragging = null; });
function setCrossing() {
A = { x: 60, y: 60 }; B = { x: 300, y: 200 };
C = { x: 60, y: 200 }; D = { x: 300, y: 60 };
render();
}
function setCollinear() {
// Four collinear points with an overlapping stretch, to show the special case.
A = { x: 40, y: 130 }; B = { x: 200, y: 130 };
C = { x: 140, y: 130 }; D = { x: 320, y: 130 };
render();
}
document.getElementById('cross').addEventListener('click', setCrossing);
document.getElementById('collinear').addEventListener('click', setCollinear);
document.getElementById('reset').addEventListener('click', setCrossing);
render();
Comments
Loading comments...