Show code
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Sutherland-Hodgman Clipping</title><style>body{margin:0;overflow:hidden}canvas{display:block}</style></head><body><canvas id="clipCanvas"></canvas><script>const canvas = document.getElementById('clipCanvas');const ctx = canvas.getContext('2d');let W, H;function resize() {W = window.innerWidth * 0.95;H = window.innerHeight * 0.7;canvas.width = W;canvas.height = H;}resize();window.addEventListener('resize', resize);
let state = {rotation:0,x:0,y:0,stage:0,running:true};
const WINDOW_SIZE = 400;
const WIN_X = (W - WINDOW_SIZE) / 2;
const WIN_Y = (H - WINDOW_SIZE) / 2;
// Base concave polygon vertices (local space centered at origin (0,0))
let originalVertices = [[-150,-100],[0,-200],[-100,-50],[-150,-150],[0,-80]];
function transformPolygon(vertices, centerX, centerY) {
return vertices.map(p => [
p[0] * Math.cos(state.rotation) - p[1] * Math.sin(state.rotation),
p[0] * Math.sin(state.rotation) + p[1] * Math.cos(state.rotation)
]).map(p => [p[0] + centerX, p[1] + centerY]);
}
function clipPolygon(subjectVertices, edgeP1, edgeP2) {
let outputList = [];
if (subjectVertices.length < 3) return [];
const isInside = (p) => {
// Check if point p is to the "left" or on the directed line P1 -> P2
// Cross product check: (P2 - P1) x (P - P1) >= 0 (assuming standard canvas coordinates Y-down)
// Since we are using a standardized plane, let's use the signed area approach for robustness.
const val = (edgeP2[1] - edgeP1[1]) * (p[0] - edgeP1[0]) - (edgeP2[0] - edgeP1[0]) * (p[1] - edgeP1[1]);
// For clipping against the window, "inside" means 'on the side of the interior'
return val >= 0; // Assuming P1->P2 defines the inward normal direction
};
let s = subjectVertices[subjectVertices.length - 1];
let sInside = isInside(s);
for (let i = 0; i < subjectVertices.length; i++) {
let e = subjectVertices[i];
let eInside = isInside(e);
if (eInside) {
if (!sInside) {
// Intersection point needed: crossing from outside to inside
let x_intersect = s[0] + (e[0] - s[0]) * s[1] / (e[1] - s[1]);
let y_intersect = s[1] + (e[1] - s[1]) * s[0] / (e[0] - s[0]);
// Using the line equation derived from P1 and P2 for intersection:
const dx = edgeP2[0] - edgeP1[0];
const dy = edgeP2[1] - edgeP1[1];
const num = (dx * (s[0] - e[0]) + dy * (s[1] - e[1]));
const den = dx * (s[0] - e[0]) + dy * (s[1] - e[1]);
// Simplified line intersection calculation for this specific setup:
let t;
if (Math.abs(e[1] - s[1]) > 1e-6) { // Avoid division by zero vertical lines
t = (edgeP2[0] - edgeP1[0]) * (s[1] - e[1]) / (edgeP2[0] - edgeP1[0]);
} else if (Math.abs(e[0] - s[0]) > 1e-6) { // Avoid division by zero horizontal lines
t = (edgeP2[1] - edgeP1[1]) * (s[0] - e[0]) / (edgeP2[1] - edgeP1[1]);
} else {
// Should not happen for non-degenerate edges
t = 0;
}
let ix, iy;
if (Math.abs(e[0] - s[0]) > Math.abs(e[1] - s[1])) { // Use X component change
ix = s[0] + t * (e[0] - s[0]);
iy = s[1] + t * (e[1] - s[1]);
} else { // Use Y component change
ix = s[0] + t * (e[0] - s[0]);
iy = s[1] + t * (e[1] - s[1]);
}
outputList.push([ix, iy]);
}
// Intersection point is added if needed, then the endpoint E
outputList.push(e);
} else {
if (sInside) {
// Crossing from inside to outside: intersection point only
let ix = s[0] + t * (e[0] - s[0]);
let iy = s[1] + t * (e[1] - s[1]);
outputList.push([ix, iy]);
}
}
s = e;
sInside = eInside;
}
return outputList;
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw Window (Fixed)
ctx.strokeStyle = '#888';
ctx.lineWidth = 2;
ctx.strokeRect(WIN_X, WIN_Y, WINDOW_SIZE, WINDOW_SIZE);
// Get current polygon vertices in world space
let currentPolygonVertices = transformPolygon(originalVertices, state.x, state.y);
// Draw Original Outline (Faint)
ctx.strokeStyle = 'rgba(100, 100, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
currentPolygonVertices.forEach((p, i) => {
if (i === 0) ctx.moveTo(p[0], p[1]);
else ctx.lineTo(p[0], p[1]);
});
ctx.closePath();
ctx.stroke();
// --- Clipping Visualization ---
let clippedResult = currentPolygonVertices;
const drawPoly = (vertices, fill=true) => {
if (vertices.length < 2) return;
ctx.beginPath();
ctx.moveTo(vertices[0][0], vertices[0][1]);
for (let i = 1; i < vertices.length; i++) {
ctx.lineTo(vertices[i][0], vertices[i][1]);
}
ctx.closePath();
if (fill) ctx.fillStyle = 'rgba(50, 200, 50, 0.6)';
ctx.fill();
ctx.strokeStyle = '#333';
ctx.stroke();
};
// Draw the intermediate clipping stages
let tempVertices = currentPolygonVertices;
for (let i = 0; i < 4; i++) {
if (i > 0) {
// Re-calculate the clipped result for visualization purposes
const winEdges = [
[[WIN_X, WIN_Y], [WIN_X + WINDOW_SIZE, WIN_Y]], // Bottom edge: P1->P2
[[WIN_X + WINDOW_SIZE, WIN_Y], [WIN_X + WINDOW_SIZE, WIN_Y + WINDOW_SIZE]], // Right edge
[[WIN_X + WINDOW_SIZE, WIN_Y + WINDOW_SIZE], [WIN_X, WIN_Y + WINDOW_SIZE]], // Top edge
[[WIN_X, WIN_Y + WINDOW_SIZE], [WIN_X, WIN_Y]] // Left edge
];
let currentStepPoly = transformPolygon(originalVertices, state.x, state.y);
// The SH algorithm needs to process the vertices sequentially:
// 1. Against Bottom Edge (P1->P2)
tempVertices = clipPolygon(currentStepPoly, winEdges[0][0], winEdges[0][1]);
if (i === 0 && tempVertices.length > 0) {
ctx.fillStyle = 'rgba(50, 200, 50, 0.6)'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.stroke();
}
// 2. Against Right Edge (P1->P2)
tempVertices = clipPolygon(tempVertices, winEdges[1][0], winEdges[1][1]);
if (i === 1 && tempVertices.length > 0) {
ctx.fillStyle = 'rgba(50, 200, 200, 0.6)'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.stroke();
}
// 3. Against Top Edge (P1->P2)
tempVertices = clipPolygon(tempVertices, winEdges[2][0], winEdges[2][1]);
if (i === 2 && tempVertices.length > 0) {
ctx.fillStyle = 'rgba(50, 200, 150, 0.6)'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.stroke();
}
// 4. Against Left Edge (P1->P2)
tempVertices = clipPolygon(tempVertices, winEdges[3][0], winEdges[3][1]);
if (i === 3 && tempVertices.length > 0) {
ctx.fillStyle = 'rgba(50, 200, 100, 0.6)'; ctx.fill();
ctx.strokeStyle = '#333'; ctx.stroke();
}
} else if (i === 0) {
// Draw the initial state before any clipping for clarity
drawPoly(tempVertices);
}
}
// Final Result Display (The last calculated tempVertices is the final result)
const finalResult = tempVertices;
ctx.fillStyle = 'rgba(255, 100, 50, 0.8)';
finalResult.forEach((p, i) => {
if (i === 0) ctx.beginPath();
else ctx.lineTo(p[0], p[1]);
});
ctx.closePath();
ctx.fill();
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.stroke();
// Vertex Count Display (Needs to be done outside the loop for cleanliness)
const vcBefore = originalVertices.length;
const vcAfter = finalResult.length;