网站建设 中国联盟网湘潭市哪里做网站
1.学习目标
理解区域编码(Region Code,RC)
设计Cohen-Sutherland直线裁剪算法
编程实现Cohen-Sutherland直线裁剪算法
2.具体代码
-
1.具体算法
/*** Cohen-Sutherland直线裁剪算法 - 优化版* @author AI Assistant* @license MIT*/// 区域编码常量 - 使用对象枚举以增强可读性
const RegionCode = Object.freeze({INSIDE: 0, // 0000LEFT: 1, // 0001RIGHT: 2, // 0010BOTTOM: 4, // 0100TOP: 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() {// 获取容器的宽度,高度固定为500pxconst containerWidth = canvas.parentElement.clientWidth;canvas.width = containerWidth;canvas.height = 500;}// 调用初始化setupCanvas();// 监听窗口大小变化,调整Canvaswindow.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 (本地服务器)