手画图形自动修正HTML
2026/7/22 14:00:53 网站建设 项目流程

仅在于其形式转载商用

利用AI模型识别您绘制的形状,并将其修正为标准的几何图形。

```html

<!DOCTYPE html>

<html lang="zh-CN">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

<title>手画图形自动修正 | AI 识别</title>

<style>

* {

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>

</head>

<body>

<div class="card">

<h1>

✦ 图形修正

<small>手画 → 标准</small>

</h1>

<div class="sub">

<span>🖌️ 绘制任意形状 · 点击修正</span>

<span class="badge" id="shapeLabel">⚡ 等待识别</span>

</div>

<div class="canvas-wrapper">

<canvas id="drawCanvas" width="600" height="600"></canvas>

</div>

<div class="toolbar">

<div class="btn-group">

<button class="btn btn-primary" id="correctBtn">✨ 修正图形</button>

<button class="btn btn-outline" id="clearBtn">🗑️ 清空</button>

</div>

<button class="btn btn-outline" id="undoBtn" style="padding:10px 18px;">↩ 撤销</button>

</div>

<div class="footer">

<span class="status-area" id="statusMessage">✏️ 画一个圆、矩形或三角形</span>

<span class="hint">⚡ 自动识别并修正</span>

</div>

</div>

<script>

(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 = (i+1) % 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 = (i+1) % 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^2

const 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-1+hullLen) % hullLen];

const curr = hull[i];

const next = hull[(i+1) % hullLen];

angles.push(angleAt(curr, prev, next));

}

const avgAngle = angles.reduce((a,b) => a+b, 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 = '📏 识别为线段';

}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询