1.学习目标
理解区域编码(Region Code,RC)
设计Cohen-Sutherland直线裁剪算法
编程实现Cohen-Sutherland直线裁剪算法
2.具体代码
-
1.具体算法
/**
* Cohen-Sutherland直线裁剪算法 - 优化版
* @author AI Assistant
* @license MIT
*/
// 区域编码常量 - 使用对象枚举以增强可读性
const RegionCode = Object.freeze({
INSIDE: 0, // 0000
LEFT: 1, // 0001
RIGHT: 2, // 0010
BOTTOM: 4, // 0100
TOP: 8 // 1000
});
/**
* 计算点的区域编码
* @param {number} x - 点的x坐标
* @param {number} y - 点的y坐标
* @param {Object} clipWindow - 裁剪窗口
* @returns {number} - 区域编码
*/
function computeCode(x, y, clipWindow) {
const { xmin, ymin, xmax, ymax } = clipWindow;
// 使用位运算计算区域编码
let code = RegionCode.INSIDE;
// 左/右测试
if (x < xmin) {
code |= RegionCode.LEFT;
} else if (x > xmax) {
code |= RegionCode.RIGHT;
}
// 下/上测试
if (y < ymin) {
code |= RegionCode.BOTTOM;
} else if (y > ymax) {
code |= RegionCode.TOP;
}
return code;
}
/**
* 计算线段与裁剪窗口边界的交点
* @param {number} code - 端点的区域编码
* @param {Point} p1 - 线段起点
* @param {Point} p2 - 线段终点
* @param {Object} clipWindow - 裁剪窗口
* @returns {Point} - 交点坐标
*/
function computeIntersection(code, p1, p2, clipWindow) {
const { xmin, ymin, xmax, ymax } = clipWindow;
const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
let x, y;
// 根据区域编码确定交点
if ((code & RegionCode.TOP) !== 0) {
// 与上边界相交
x = x1 + (x2 - x1) * (ymax - y1) / (y2 - y1);
y = ymax;
} else if ((code & RegionCode.BOTTOM) !== 0) {
// 与下边界相交
x = x1 + (x2 - x1) * (ymin - y1) / (y2 - y1);
y = ymin;
} else if ((code & RegionCode.RIGHT) !== 0) {
// 与右边界相交
y = y1 + (y2 - y1) * (xmax - x1) / (x2 - x1);
x = xmax;
} else if ((code & RegionCode.LEFT) !== 0) {
// 与左边界相交
y = y1 + (y2 - y1) * (xmin - x1) / (x2 - x1);
x = xmin;
}
return { x, y };
}
/**
* Cohen-Sutherland直线裁剪算法
* @param {Point} p1 - 线段起点 {x, y}
* @param {Point} p2 - 线段终点 {x, y}
* @param {Object} clipWindow - 裁剪窗口 {xmin, ymin, xmax, ymax}
* @returns {Object|null} - 裁剪后的线段坐标,如果线段完全在窗口外则返回null
*/
function cohenSutherlandClip(p1, p2, clipWindow) {
// 创建点的副本,避免修改原始数据
let point1 = { ...p1 };
let point2 = { ...p2 };
// 计算端点的区域编码
let code1 = computeCode(point1.x, point1.y, clipWindow);
let code2 = computeCode(point2.x, point2.y, clipWindow);
let isAccepted = false;
// 主循环
while (true) {
// 情况1: 两端点都在裁剪窗口内
if ((code1 | code2) === 0) {
isAccepted = true;
break;
}
// 情况2: 两端点都在裁剪窗口外的同一区域
else if ((code1 & code2) !== 0) {
break;
}
// 情况3: 线段部分在裁剪窗口内,需要裁剪
else {
// 选择一个在窗口外的端点
const outCode = code1 !== 0 ? code1 : code2;
// 计算交点
const intersection = computeIntersection(
outCode,
point1,
point2,
clipWindow
);
// 更新端点和区域编码
if (outCode === code1) {
point1 = intersection;
code1 = computeCode(point1.x, point1.y, clipWindow);
} else {
point2 = intersection;
code2 = computeCode(point2.x, point2.y, clipWindow);
}
}
}
// 返回裁剪结果
return isAccepted ? {
x1: point1.x,
y1: point1.y,
x2: point2.x,
y2: point2.y
} : null;
}
/**
* 绘制裁剪窗口
* @param {CanvasRenderingContext2D} ctx - Canvas上下文
* @param {Object} clipWindow - 裁剪窗口
* @param {Object} style - 绘制样式
*/
function drawClipWindow(ctx, clipWindow, style = {}) {
const { xmin, ymin, xmax, ymax } = clipWindow;
const {
strokeStyle = 'blue',
lineWidth = 2,
fillStyle = 'rgba(200, 220, 255, 0.1)'
} = style;
ctx.save();
// 设置样式
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
ctx.fillStyle = fillStyle;
// 绘制填充矩形
ctx.fillRect(xmin, ymin, xmax - xmin, ymax - ymin);
// 绘制边框
ctx.strokeRect(xmin, ymin, xmax - xmin, ymax - ymin);
// 绘制区域标签
ctx.font = '12px Arial';
ctx.fillStyle = 'rgba(0, 0, 100, 0.7)';
ctx.textAlign = 'center';
// 标记窗口四角的区域编码
const padding = 15;
ctx.fillText('1001', xmin - padding, ymin - padding); // 左上
ctx.fillText('1010', xmax + padding, ymin - padding); // 右上
ctx.fillText('0101', xmin - padding, ymax + padding); // 左下
ctx.fillText('0110', xmax + padding, ymax + padding); // 右下
ctx.restore();
}
/**
* 绘制线段
* @param {CanvasRenderingContext2D} ctx - Canvas上下文
* @param {Object} line - 线段数据
* @param {Object} style - 绘制样式
*/
function drawLine(ctx, line, style = {}) {
const { x1, y1, x2, y2 } = line;
const {
strokeStyle = 'red',
lineWidth = 1.5,
drawEndpoints = false,
endpointRadius = 4,
dashPattern = []
} = style;
ctx.save();
// 设置样式
ctx.strokeStyle = strokeStyle;
ctx.lineWidth = lineWidth;
if (dashPattern.length > 0) {
ctx.setLineDash(dashPattern);
}
// 绘制线段
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
// 绘制端点
if (drawEndpoints) {
ctx.fillStyle = strokeStyle;
// 起点
ctx.beginPath();
ctx.arc(x1, y1, endpointRadius, 0, Math.PI * 2);
ctx.fill();
// 终点
ctx.beginPath();
ctx.arc(x2, y2, endpointRadius, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
/**
* 获取线段区域编码的文本描述
* @param {number} code - 区域编码
* @returns {string} - 编码的二进制表示
*/
function getRegionCodeText(code) {
// 将编码转换为4位二进制字符串
return (code | 0).toString(2).padStart(4, '0');
}
// 导出所有函数和常量
export {
RegionCode,
computeCode,
cohenSutherlandClip,
drawClipWindow,
drawLine,
getRegionCodeText
};
-
2.服务器配置
const http = require('http');
const fs = require('fs');
const path = require('path');
const PORT = 3000;
// MIME类型映射
const mimeTypes = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon'
};
// 创建HTTP服务器
const server = http.createServer((req, res) => {
console.log(`请求: ${req.url}`);
// 处理主页请求
let filePath = '.' + req.url;
if (filePath === './') {
filePath = './cohenSutherlandDemo.html';
}
// 获取文件扩展名
const extname = path.extname(filePath);
// 设置默认的MIME类型
let contentType = mimeTypes[extname] || 'application/octet-stream';
// 读取文件
fs.readFile(filePath, (err, content) => {
if (err) {
if (err.code === 'ENOENT') {
// 文件未找到
res.writeHead(404);
res.end('404 Not Found');
} else {
// 服务器错误
res.writeHead(500);
res.end(`Server Error: ${err.code}`);
}
} else {
// 成功响应
// 添加正确的CORS头部,以允许ES模块加载
res.writeHead(200, {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*'
});
res.end(content, 'utf-8');
}
});
});
// 启动服务器
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}/`);
console.log('请使用浏览器访问上述地址查看Cohen-Sutherland算法演示');
});
-
3.HTML前端页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cohen-Sutherland直线裁剪算法演示</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<style>
:root {
--primary-color: #3f51b5;
--secondary-color: #f50057;
--success-color: #4caf50;
--bg-color: #f8f9fa;
--canvas-bg: #ffffff;
}
body {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
background-color: var(--bg-color);
margin: 0;
padding: 20px;
color: #333;
}
.container {
max-width: 1000px;
margin: 0 auto;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 20px;
}
h1 {
color: var(--primary-color);
text-align: center;
margin-bottom: 20px;
font-weight: bold;
font-size: 2rem;
}
.canvas-container {
position: relative;
margin: 20px 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
#canvas {
display: block;
background-color: var(--canvas-bg);
width: 100%;
height: 500px;
cursor: default;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-danger {
background-color: var(--secondary-color);
border-color: var(--secondary-color);
}
.btn-success {
background-color: var(--success-color);
border-color: var(--success-color);
}
.legend {
background-color: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 15px;
margin-top: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.legend-item {
display: flex;
align-items: center;
margin-right: 15px;
}
.legend-color {
width: 20px;
height: 3px;
margin-right: 8px;
border-radius: 2px;
}
.legend-point {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.blue {
background-color: var(--primary-color);
}
.red {
background-color: var(--secondary-color);
}
.green {
background-color: var(--success-color);
}
.status-bar {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
background-color: #f5f5f5;
font-family: monospace;
min-height: 40px;
}
.point-info {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 10px;
}
.info-card {
background-color: #f5f5f5;
border-radius: 5px;
padding: 10px;
width: 48%;
margin-bottom: 10px;
}
.code-display {
font-family: monospace;
font-weight: bold;
color: var(--primary-color);
}
footer {
text-align: center;
margin-top: 20px;
font-size: 0.8rem;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>Cohen-Sutherland直线裁剪算法演示</h1>
<div class="controls">
<button id="drawLineBtn" class="btn btn-primary">
<i class="bi bi-pencil"></i> 绘制新线段
</button>
<button id="clipBtn" class="btn btn-success">
<i class="bi bi-scissors"></i> 裁剪线段
</button>
<button id="resetBtn" class="btn btn-danger">
<i class="bi bi-trash"></i> 重置
</button>
<button id="toggleCodeBtn" class="btn btn-secondary">
<i class="bi bi-code-slash"></i> 显示/隐藏区域编码
</button>
</div>
<div class="canvas-container">
<canvas id="canvas"></canvas>
</div>
<div class="status-bar" id="statusBar">准备就绪。点击"绘制新线段"按钮开始。</div>
<div class="point-info">
<div class="info-card" id="point1Info">
<h5>起点:</h5>
<p>坐标:<span id="p1Coords">-</span></p>
<p>区域编码:<span class="code-display" id="p1Code">-</span></p>
</div>
<div class="info-card" id="point2Info">
<h5>终点:</h5>
<p>坐标:<span id="p2Coords">-</span></p>
<p>区域编码:<span class="code-display" id="p2Code">-</span></p>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color blue"></div>
<span>裁剪窗口</span>
</div>
<div class="legend-item">
<div class="legend-color red"></div>
<span>原始线段</span>
</div>
<div class="legend-item">
<div class="legend-color green"></div>
<span>裁剪后的线段</span>
</div>
<div class="legend-item">
<div class="legend-point red"></div>
<span>线段端点</span>
</div>
</div>
<footer>
Cohen-Sutherland直线裁剪算法 © 2023
</footer>
</div>
<script type="module">
// 导入优化后的Cohen-Sutherland模块
import {
RegionCode,
computeCode,
cohenSutherlandClip,
drawClipWindow,
drawLine,
getRegionCodeText
} from './cohenSutherland.js';
// DOM元素
const canvas = document.getElementById('canvas');
const statusBar = document.getElementById('statusBar');
const p1CoordsElem = document.getElementById('p1Coords');
const p2CoordsElem = document.getElementById('p2Coords');
const p1CodeElem = document.getElementById('p1Code');
const p2CodeElem = document.getElementById('p2Code');
// 调整Canvas以适应容器大小
function setupCanvas() {
// 获取容器的宽度,高度固定为500px
const containerWidth = canvas.parentElement.clientWidth;
canvas.width = containerWidth;
canvas.height = 500;
}
// 调用初始化
setupCanvas();
// 监听窗口大小变化,调整Canvas
window.addEventListener('resize', setupCanvas);
// 获取Canvas上下文
const ctx = canvas.getContext('2d');
// 裁剪窗口定义
const clipWindow = {
xmin: Math.round(canvas.width * 0.25),
ymin: Math.round(canvas.height * 0.25),
xmax: Math.round(canvas.width * 0.75),
ymax: Math.round(canvas.height * 0.75)
};
// 状态变量
let lines = [];
let isDrawing = false;
let startPoint = null;
let selectedLine = null;
let showRegionCodes = false;
// 更新状态栏
function updateStatus(message) {
statusBar.textContent = message;
}
// 更新点信息
function updatePointInfo(p1, p2) {
if (p1) {
p1CoordsElem.textContent = `(${Math.round(p1.x)}, ${Math.round(p1.y)})`;
const code = computeCode(p1.x, p1.y, clipWindow);
p1CodeElem.textContent = getRegionCodeText(code);
} else {
p1CoordsElem.textContent = '-';
p1CodeElem.textContent = '-';
}
if (p2) {
p2CoordsElem.textContent = `(${Math.round(p2.x)}, ${Math.round(p2.y)})`;
const code = computeCode(p2.x, p2.y, clipWindow);
p2CodeElem.textContent = getRegionCodeText(code);
} else {
p2CoordsElem.textContent = '-';
p2CodeElem.textContent = '-';
}
}
// 绘制所有元素
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制裁剪窗口
drawClipWindow(ctx, clipWindow, {
strokeStyle: '#3f51b5',
lineWidth: 2,
fillStyle: 'rgba(63, 81, 181, 0.05)'
});
// 绘制区域编码标记
if (showRegionCodes) {
// 绘制中心区域标记
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 中心区域
ctx.fillText('0000', (clipWindow.xmin + clipWindow.xmax) / 2, (clipWindow.ymin + clipWindow.ymax) / 2);
// 上方区域
ctx.fillText('1000', (clipWindow.xmin + clipWindow.xmax) / 2, clipWindow.ymin / 2);
// 下方区域
ctx.fillText('0100', (clipWindow.xmin + clipWindow.xmax) / 2, (clipWindow.ymax + canvas.height) / 2);
// 左方区域
ctx.fillText('0001', clipWindow.xmin / 2, (clipWindow.ymin + clipWindow.ymax) / 2);
// 右方区域
ctx.fillText('0010', (clipWindow.xmax + canvas.width) / 2, (clipWindow.ymin + clipWindow.ymax) / 2);
}
// 绘制所有线段
for (const line of lines) {
// 原始线段
drawLine(ctx, line, {
strokeStyle: '#f50057',
lineWidth: 1.5,
drawEndpoints: true,
endpointRadius: 4
});
// 如果有裁剪结果,绘制裁剪后的线段
if (line.clipped) {
drawLine(ctx, line.clipped, {
strokeStyle: '#4caf50',
lineWidth: 2.5,
dashPattern: [],
drawEndpoints: true,
endpointRadius: 4
});
}
}
// 如果正在绘制,显示预览线段
if (isDrawing && startPoint) {
const mousePos = canvas.mousePosition || { x: 0, y: 0 };
drawLine(ctx, {
x1: startPoint.x,
y1: startPoint.y,
x2: mousePos.x,
y2: mousePos.y
}, {
strokeStyle: 'rgba(245, 0, 87, 0.5)',
lineWidth: 1.5,
dashPattern: [5, 3],
drawEndpoints: true
});
}
}
// 为所有线段应用裁剪算法
function clipAllLines() {
if (lines.length === 0) {
updateStatus('没有线段可裁剪!');
return;
}
for (const line of lines) {
// 使用优化后的接口调用裁剪算法
line.clipped = cohenSutherlandClip(
{ x: line.x1, y: line.y1 },
{ x: line.x2, y: line.y2 },
clipWindow
);
}
redraw();
updateStatus(`已完成${lines.length}条线段的裁剪。`);
}
// 绑定按钮事件
document.getElementById('drawLineBtn').addEventListener('click', function() {
if (isDrawing) {
isDrawing = false;
startPoint = null;
canvas.style.cursor = 'default';
updateStatus('取消绘制线段。');
} else {
isDrawing = true;
startPoint = null;
canvas.style.cursor = 'crosshair';
updateStatus('请点击绘制线段的起点...');
}
});
document.getElementById('clipBtn').addEventListener('click', function() {
clipAllLines();
});
document.getElementById('resetBtn').addEventListener('click', function() {
lines = [];
isDrawing = false;
startPoint = null;
selectedLine = null;
canvas.style.cursor = 'default';
updatePointInfo(null, null);
updateStatus('已重置。点击"绘制新线段"按钮开始。');
redraw();
});
document.getElementById('toggleCodeBtn').addEventListener('click', function() {
showRegionCodes = !showRegionCodes;
redraw();
updateStatus(showRegionCodes ? '显示区域编码。' : '隐藏区域编码。');
});
// 跟踪鼠标位置
canvas.addEventListener('mousemove', function(e) {
const rect = canvas.getBoundingClientRect();
canvas.mousePosition = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
// 如果正在绘制,更新预览
if (isDrawing && startPoint) {
redraw();
}
});
// 处理鼠标点击
canvas.addEventListener('mousedown', function(e) {
if (!isDrawing) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (!startPoint) {
// 设置起点
startPoint = { x, y };
updateStatus('请点击绘制线段的终点...');
updatePointInfo({ x, y }, null);
} else {
// 设置终点,创建线段
const endPoint = { x, y };
// 创建新线段
const newLine = {
x1: startPoint.x,
y1: startPoint.y,
x2: x,
y2: y,
clipped: null
};
lines.push(newLine);
// 更新点信息
updatePointInfo(startPoint, endPoint);
// 重置绘制状态
startPoint = null;
isDrawing = false;
canvas.style.cursor = 'default';
updateStatus(`已添加线段 #${lines.length}。点击"裁剪线段"查看结果。`);
redraw();
}
});
// 初始绘制
redraw();
// 添加键盘快捷键
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
// ESC键取消绘制
if (isDrawing) {
isDrawing = false;
startPoint = null;
canvas.style.cursor = 'default';
updateStatus('取消绘制线段。');
redraw();
}
}
});
</script>
<!-- 添加Bootstrap图标 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
</body>
</html>
3.运行结果
4.详细介绍
# Cohen-Sutherland直线裁剪算法演示
这是一个交互式的Cohen-Sutherland直线裁剪算法演示项目,使用现代JavaScript和Canvas技术实现。
## 项目介绍
Cohen-Sutherland算法是一种经典的二维直线裁剪算法,用于确定一条线段是否与给定的矩形窗口相交,并计算交点。该算法使用区域编码(Region Code)来快速判断线段是否需要裁剪。
### 主要特点
- 交互式线段绘制和裁剪
- 美观的用户界面
- 实时显示区域编码
- 详细的算法步骤可视化
- 响应式设计,适应不同屏幕大小
## 文件结构
- `cohenSutherland.js` - 算法核心实现,使用现代ES模块
- `cohenSutherlandDemo.html` - 交互式演示界面
- `server.js` - 用于本地运行的简易HTTP服务器
- `README.md` - 项目说明文档
## 使用方法
1. 启动本地服务器:
```bash
node server.js
```
2. 打开浏览器访问 `http://localhost:3000`
3. 使用界面功能:
- 点击"绘制新线段"按钮,然后在画布上点击两次定义一条线段
- 点击"裁剪线段"按钮对绘制的线段进行裁剪
- 点击"显示/隐藏区域编码"按钮查看区域编码
- 点击"重置"按钮清除所有线段
## 区域编码说明
Cohen-Sutherland算法将二维平面划分为9个区域,使用4位二进制编码表示点所在的区域:
```
1001 | 1000 | 1010
----------------------
0001 | 0000 | 0010
----------------------
0101 | 0100 | 0110
```
每一位的含义:
- 第1位(LEFT):点在窗口左侧 (0001)
- 第2位(RIGHT):点在窗口右侧 (0010)
- 第3位(BOTTOM):点在窗口下方 (0100)
- 第4位(TOP):点在窗口上方 (1000)
中间区域(0000)表示点在窗口内部。
## 算法步骤
1. 计算线段两个端点P1(x1,y1)和P2(x2,y2)的区域编码code1和code2
2. 如果(code1 | code2) == 0,说明两点都在窗口内,直接接受该线段
3. 如果(code1 & code2) != 0,说明线段完全在窗口外的同一侧,直接拒绝该线段
4. 否则,线段部分在窗口内,需要裁剪:
- 选择一个在窗口外的端点
- 根据区域编码确定端点与窗口边界的交点
- 用交点替换原来的端点
- 重新计算新端点的区域编码
- 重复上述步骤,直到两点都在窗口内或线段被拒绝
## 技术栈
- 原生JavaScript (ES6+)
- HTML5 Canvas
- CSS3
- Bootstrap 5 (样式)
- Node.js (本地服务器)