仅在于其形式转载商用利用AI模型识别您绘制的形状并将其修正为标准的几何图形。html!DOCTYPE htmlhtml langzh-CNheadmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0, user-scalablenotitle手画图形自动修正 | AI 识别/titlestyle* {box-sizing: border-box;user-select: none;}body {margin: 0;min-height: 100vh;background: linear-gradient(145deg, #f5f7fa 0%, #e9edf5 100%);display: flex;justify-content: center;align-items: center;font-family: Segoe UI, Roboto, system-ui, -apple-system, sans-serif;padding: 16px;}.card {max-width: 700px;width: 100%;background: rgba(255, 255, 255, 0.7);backdrop-filter: blur(8px);-webkit-backdrop-filter: blur(8px);border-radius: 48px 48px 32px 32px;box-shadow: 0 20px 40px rgba(0, 20, 40, 0.2), 0 8px 16px rgba(0, 0, 0, 0.08);padding: 24px 24px 32px;transition: all 0.2s;border: 1px solid rgba(255, 255, 255, 0.5);}h1 {font-weight: 500;font-size: 1.8rem;letter-spacing: -0.5px;color: #1a2639;margin: 0 0 6px 0;display: flex;align-items: center;gap: 8px;}h1 small {font-size: 1rem;font-weight: 400;color: #4a5b6e;margin-left: 8px;opacity: 0.8;}.sub {color: #3a4a5e;margin-top: -4px;margin-bottom: 20px;font-weight: 300;font-size: 0.95rem;display: flex;justify-content: space-between;align-items: center;flex-wrap: wrap;}.canvas-wrapper {position: relative;background: #ffffff;border-radius: 28px;box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.06), 0 2px 4px rgba(0, 0, 0, 0.02);padding: 8px;margin-bottom: 20px;border: 1px solid rgba(255, 255, 255, 0.8);}#drawCanvas {display: block;width: 100%;height: auto;aspect-ratio: 1 / 1;background: #ffffff;border-radius: 24px;cursor: crosshair;touch-action: none; /* 避免触摸滚动 */box-shadow: 0 2px 8px rgba(0,0,0,0.03);}.toolbar {display: flex;flex-wrap: wrap;align-items: center;justify-content: center;gap: 12px 16px;margin: 16px 0 10px;}.btn-group {display: flex;gap: 8px;flex-wrap: wrap;justify-content: center;}.btn {background: #ffffffdd;border: none;padding: 10px 22px;border-radius: 60px;font-size: 0.95rem;font-weight: 500;color: #1e2b3a;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);backdrop-filter: blur(4px);background: rgba(255, 255, 255, 0.8);border: 1px solid rgba(255,255,255,0.6);transition: all 0.15s;display: inline-flex;align-items: center;gap: 6px;cursor: pointer;letter-spacing: 0.3px;}.btn-primary {background: #2c3e50;color: white;border: 1px solid #1f2e3d;box-shadow: 0 8px 16px -6px rgba(28, 52, 84, 0.2);}.btn-primary:hover {background: #1f3347;transform: scale(0.97);box-shadow: 0 4px 10px rgba(0,0,0,0.1);}.btn-primary:active {transform: scale(0.95);}.btn-outline {background: transparent;border: 1px solid #b0c0d0;color: #2c3e50;}.btn-outline:hover {background: #eef3fa;border-color: #8a9db0;}.btn:disabled {opacity: 0.5;pointer-events: none;filter: grayscale(0.3);}.badge {background: #d0dae8;padding: 4px 14px;border-radius: 40px;font-size: 0.8rem;color: #1a2a3a;font-weight: 500;}.status-area {background: #f0f4fc;border-radius: 40px;padding: 8px 20px;display: inline-block;font-size: 0.9rem;color: #1f3349;border: 1px solid #dce3ef;backdrop-filter: blur(2px);margin-top: 4px;}.footer {display: flex;justify-content: space-between;align-items: center;flex-wrap: wrap;margin-top: 12px;padding: 0 4px;}.hint {color: #4a5f78;font-size: 0.85rem;background: #eef3fa;padding: 6px 16px;border-radius: 30px;border: 1px solid #d3deec;}media (max-width: 480px) {.card { padding: 16px; }.btn { padding: 8px 16px; font-size: 0.85rem; }.toolbar { gap: 8px; }}/style/headbodydiv classcardh1✦ 图形修正small手画 → 标准/small/h1div classsubspan️ 绘制任意形状 · 点击修正/spanspan classbadge idshapeLabel⚡ 等待识别/span/divdiv classcanvas-wrappercanvas iddrawCanvas width600 height600/canvas/divdiv classtoolbardiv classbtn-groupbutton classbtn btn-primary idcorrectBtn✨ 修正图形/buttonbutton classbtn btn-outline idclearBtn️ 清空/button/divbutton classbtn btn-outline idundoBtn stylepadding:10px 18px;↩ 撤销/button/divdiv classfooterspan classstatus-area idstatusMessage✏️ 画一个圆、矩形或三角形/spanspan classhint⚡ 自动识别并修正/span/div/divscript(function(){// ----- 画布设置 -----const canvas document.getElementById(drawCanvas);const ctx canvas.getContext(2d);const rect canvas.getBoundingClientRect();const statusMsg document.getElementById(statusMessage);const shapeLabel document.getElementById(shapeLabel);// 尺寸 (画布逻辑 600x600)const W 600, H 600;canvas.width W;canvas.height H;// ----- 状态 -----let isDrawing false;let points []; // 当前笔画点集 {x, y}let strokes []; // 历史笔画 (用于撤销)let correctedShape null; // 修正后的图形数据 {type, points, ...}// 绘图样式const STROKE_COLOR #1f2e4a;const STROKE_WIDTH 3.5;const CORRECT_COLOR #d43f34;const CORRECT_WIDTH 3.2;// ----- 辅助函数 -----function getCanvasCoords(e) {const rect canvas.getBoundingClientRect();const scaleX canvas.width / rect.width; // 通常 canvas 逻辑尺寸与 CSS 尺寸比例const scaleY canvas.height / rect.height;let clientX, clientY;if (e.touches) {clientX e.touches[0].clientX;clientY e.touches[0].clientY;e.preventDefault();} else {clientX e.clientX;clientY e.clientY;}const x (clientX - rect.left) * scaleX;const y (clientY - rect.top) * scaleY;return { x: Math.min(W, Math.max(0, x)), y: Math.min(H, Math.max(0, y)) };}// 重绘全部 (包含修正图形)function redraw() {ctx.clearRect(0, 0, W, H);// 绘制所有保存的笔画 (strokes)ctx.lineCap round;ctx.lineJoin round;ctx.strokeStyle STROKE_COLOR;ctx.lineWidth STROKE_WIDTH;for (let stroke of strokes) {if (stroke.length 1) continue;ctx.beginPath();ctx.moveTo(stroke[0].x, stroke[0].y);for (let i 1; i stroke.length; i) {ctx.lineTo(stroke[i].x, stroke[i].y);}ctx.stroke();}// 绘制当前正在画的点 (如果 isDrawing 但 points 可能为空)if (points.length 1 !isDrawing) {// 如果不在绘制状态但有点用于显示最后一段(但通常不会)}// 如果有修正图形绘制修正结果 (覆盖)if (correctedShape) {ctx.save();ctx.strokeStyle CORRECT_COLOR;ctx.lineWidth CORRECT_WIDTH;ctx.setLineDash([6, 5]);ctx.shadowColor rgba(200, 60, 40, 0.2);ctx.shadowBlur 10;const pts correctedShape.points;if (pts pts.length 0) {ctx.beginPath();ctx.moveTo(pts[0].x, pts[0].y);for (let i 1; i pts.length; i) {ctx.lineTo(pts[i].x, pts[i].y);}// 如果是封闭图形 (矩形/三角形/圆用 closePath)if (correctedShape.type rectangle || correctedShape.type triangle || correctedShape.type circle) {ctx.closePath();}ctx.stroke();}ctx.restore();// 更新标签let label correctedShape.type.charAt(0).toUpperCase() correctedShape.type.slice(1);if (correctedShape.type circle) label ⭕ 圆;else if (correctedShape.type rectangle) label ▭ 矩形;else if (correctedShape.type triangle) label △ 三角形;else if (correctedShape.type line) label ━ 线段;shapeLabel.innerText label;statusMsg.innerText ✅ 已修正为 correctedShape.type;} else {// 没有修正图形显示提示if (strokes.length 0 points.length 0) {shapeLabel.innerText ⚡ 等待识别;statusMsg.innerText ✏️ 画一个圆、矩形或三角形;} else {// 有笔画但无修正shapeLabel.innerText 未修正;statusMsg.innerText ✏️ 点击 修正图形 识别;}}}// ----- 核心: 识别并修正图形 (基于简化点集) -----function recognizeAndCorrect() {// 合并所有笔画为一个点集 (用于识别)let allPoints [];for (let stroke of strokes) {allPoints allPoints.concat(stroke);}// 加上当前未保存的 points (如果正在画但没保存, 一般不会)if (points.length 0) {allPoints allPoints.concat(points);}// 如果点太少无法识别if (allPoints.length 6) {statusMsg.innerText ⚠️ 点太少请画一个完整形状;correctedShape null;redraw();return;}// 采样降噪均匀采样最多 80 个点 (提高性能)let sampled [];const step Math.max(1, Math.floor(allPoints.length / 80));for (let i 0; i allPoints.length; i step) {sampled.push(allPoints[i]);}if (sampled.length 4) sampled allPoints.slice(0, 20);// 1. 判断形状: 基于凸包 角度/距离特征 (简易)// 使用凸包算法 (Andrew monotone chain) 获取轮廓function convexHull(points) {if (points.length 3) return points.slice();const sorted points.slice().sort((a, b) a.x - b.x || a.y - b.y);const lower [];for (let p of sorted) {while (lower.length 2 cross(lower[lower.length-2], lower[lower.length-1], p) 0) lower.pop();lower.push(p);}const upper [];for (let p of sorted.reverse()) {while (upper.length 2 cross(upper[upper.length-2], upper[upper.length-1], p) 0) upper.pop();upper.push(p);}lower.pop();upper.pop();return lower.concat(upper);}function cross(o, a, b) {return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);}const hull convexHull(sampled);if (hull.length 3) {// 如果点太少或退化成线当作线段const first sampled[0], last sampled[sampled.length-1];correctedShape {type: line,points: [first, last]};redraw();statusMsg.innerText 识别为线段;return;}// 计算多边形面积 (用于判断)function polyArea(pts) {let area 0;const n pts.length;for (let i 0; i n; i) {const j (i1) % n;area pts[i].x * pts[j].y;area - pts[j].x * pts[i].y;}return Math.abs(area) / 2;}const area polyArea(hull);// 计算周长function perimeter(pts) {let p 0;for (let i 0; i pts.length; i) {const j (i1) % pts.length;p Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);}return p;}const perim perimeter(hull);// 圆形度: 4pi*area / perim^2const circularity (4 * Math.PI * area) / (perim * perim 0.001);// 判断形状类型let type line;let pts [];// 如果近似圆 (circularity 0.75 且 点数5)if (circularity 0.70 hull.length 5) {type circle;// 计算圆心和半径 (最小二乘)let cx 0, cy 0;for (let p of hull) { cx p.x; cy p.y; }cx / hull.length; cy / hull.length;let r 0;for (let p of hull) {r Math.hypot(p.x - cx, p.y - cy);}r / hull.length;// 生成圆 (32个点)const segs 32;pts [];for (let i 0; i segs; i) {const theta (i / segs) * 2 * Math.PI;pts.push({ x: cx r * Math.cos(theta), y: cy r * Math.sin(theta) });}statusMsg.innerText ⭕ 识别为圆形;}else {// 判断矩形 / 三角形 : 基于凸包顶点数量 角度const hullLen hull.length;// 计算角度 (近似)function angleAt(p, prev, next) {const v1 { x: prev.x - p.x, y: prev.y - p.y };const v2 { x: next.x - p.x, y: next.y - p.y };const dot v1.x * v2.x v1.y * v2.y;const cross v1.x * v2.y - v1.y * v2.x;let ang Math.atan2(cross, dot);if (ang 0) ang Math.PI;return ang;}// 如果 hull 顶点数为 3~5 且 近似直角或锐角if (hullLen 3 hullLen 5) {// 针对三角形 (3个顶点) 或 矩形 (4个顶点)// 计算角度let angles [];for (let i 0; i hullLen; i) {const prev hull[(i-1hullLen) % hullLen];const curr hull[i];const next hull[(i1) % hullLen];angles.push(angleAt(curr, prev, next));}const avgAngle angles.reduce((a,b) ab, 0) / angles.length;const minAngle Math.min(...angles);const maxAngle Math.max(...angles);// 判断三角形角度范围大且接近60度const angleRange maxAngle - minAngle;if (hullLen 3 || (hullLen 4 angleRange 0.8 avgAngle 0.9 avgAngle 1.8)) {type triangle;// 生成三角形: 取凸包 3 个顶点 (或近似)if (hullLen 3) {pts hull.slice();pts.push(hull[0]); // 闭合} else {// 如果多于3个点取三个角度最大的顶点 (最简单)let sorted hull.map((p, idx) ({ p, angle: angles[idx], idx }));sorted.sort((a,b) b.angle - a.angle);const top3 sorted.slice(0, 3).map(item item.p);pts top3.concat(top3[0]);}statusMsg.innerText △ 识别为三角形;}else if (hullLen 4 hullLen 6) {// 尝试矩形: 角度接近 90° (1.57 rad)const rightAngleCount angles.filter(a a 1.2 a 1.9).length;if (rightAngleCount 2) {type rectangle;// 取凸包 4 个顶点排序 (按角度)let sorted hull.map((p, idx) ({ p, angle: angles[idx], idx }));sorted.sort((a,b) a.angle - b.angle);// 取4个极值点 (实际上取最远点)const rectPts [];// 使用极值: 左右上下let minX Infinity, maxX -Infinity, minY Infinity, maxY -Infinity;for (let p of hull) {if (p.x minX) minX p.x;if (p.x maxX) maxX p.x;if (p.y minY) minY p.y;if (p.y maxY) maxY p.y;}const corners [{ x: minX, y: minY },{ x: maxX, y: minY },{ x: maxX, y: maxY },{ x: minX, y: maxY }];pts corners.concat(corners[0]);type rectangle;statusMsg.innerText ▭ 识别为矩形;} else {// 默认当作线段const first sampled[0], last sampled[sampled.length-1];pts [first, last];type line;statusMsg.innerText 识别为线段;}} else {// 线段const first sampled[0], last sampled[sampled.length-1];pts [first, last];type line;statusMsg.innerText 识别为线段;}} else {// 默认线const first sampled[0], last sampled[sampled.length-1];pts [first, last];type line;statusMsg.innerText 识别为线段;}