直接将以下代码保存为.html文件,双击即可在浏览器运行,无需依赖 Vue 工程、打包工具,内置 CDN 引入 Vue3,保留所有核心功能:标准圆展示、手绘捕捉、完整度评分、跨端兼容、重置功能。
<!DOCTYPEhtml><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport"content="width=device-width, initial-scale=1.0"><title>画圈完整度检测</title><!--引入Vue3CDN--><script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script><style>*{margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei",sans-serif;}.circle-check-container{width:100%;max-width:400px;margin:40px auto;text-align:center;}.circle-check-container h3{color:#333;margin-bottom:15px;}.tip{color:#666;font-size:14px;margin:10px0;line-height:1.5;}.score{font-size:16px;font-weight:600;color:#333;margin:15px0;}.score span{color:#f53f3f;font-size:18px;margin-left:4px;}.draw-canvas{border:1px solid #ddd;border-radius:4px;cursor:crosshair;margin:10px0;}.reset-btn{padding:8px 24px;background:#165dff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px;transition:background0.2s;}.reset-btn:hover{background:#0e4bdb;}/* 移动端适配 */@media(max-width:420px){.circle-check-container{padding:015px;}.draw-canvas{width:100%;}}</style></head><body><div id="app"><divclass="circle-check-container"><h3>画圈完整度检测</h3><pclass="tip">请在画布内沿标准圆手绘圆圈,松开鼠标/手指自动检测</p><divclass="score"v-if="score > -1">画圈完整度:<span>{{score}}</span>分</div><canvas ref="canvasRef"class="draw-canvas"width="400"height="400"@mousedown="startDraw"@mousemove="drawing"@mouseup="endDraw"@mouseleave="endDraw"@touchstart="handleTouchStart"@touchmove="handleTouchMove"@touchend="handleTouchEnd"></canvas><buttonclass="reset-btn"@click="resetDraw">重新绘制</button></div></div><script>const{createApp,ref,onMounted,onUnmounted}=Vue;createApp({setup(){// 画布引用constcanvasRef=ref(null);letctx=null;constcanvasWidth=400;constcanvasHeight=400;// 标准圆参数conststandardCircle={x:canvasWidth/2,y:canvasHeight/2,r:150,color:'#e5e7eb',lineWidth:2};// 手绘轨迹数据constdrawData={isDrawing:false,points:[],color:'#165dff',lineWidth:3};// 评分相关constscore=ref(-1);constscoreWeights={shapeFit:0.5,closeDegree:0.3,coverDegree:0.2};// 初始化画布onMounted(()=>{if(!canvasRef.value)return;ctx=canvasRef.value.getContext('2d');drawStandardCircle();});// 销毁清空onUnmounted(()=>{drawData.points=[];score.value=-1;});// 绘制标准圆functiondrawStandardCircle(){ctx.clearRect(0,0,canvasWidth,canvasHeight);ctx.beginPath();ctx.arc(standardCircle.x,standardCircle.y,standardCircle.r,0,2*Math.PI);ctx.strokeStyle=standardCircle.color;ctx.lineWidth=standardCircle.lineWidth;ctx.stroke();ctx.closePath();}// PC端开始绘制functionstartDraw(e){drawData.isDrawing=true;drawData.points=[];score.value=-1;const{x,y}=getCanvasXY(e);drawData.points.push({x,y});}// PC端绘制中functiondrawing(e){if(!drawData.isDrawing)return;const{x,y}=getCanvasXY(e);drawData.points.push({x,y});drawTrack();}// PC端结束绘制functionendDraw(){if(!drawData.isDrawing||drawData.points.length<10){drawData.isDrawing=false;drawStandardCircle();return;}drawData.isDrawing=false;calculateCompleteScore();}// 移动端触摸适配functionhandleTouchStart(e){e.preventDefault();consttouch=e.touches[0];startDraw(touch);}functionhandleTouchMove(e){e.preventDefault();consttouch=e.touches[0];drawing(touch);}functionhandleTouchEnd(e){e.preventDefault();endDraw();}// 获取Canvas内真实坐标functiongetCanvasXY(e){constrect=canvasRef.value.getBoundingClientRect();return{x:e.clientX-rect.left,y:e.clientY-rect.top};}// 绘制手绘轨迹functiondrawTrack(){drawStandardCircle();if(drawData.points.length<2)return;ctx.beginPath();ctx.moveTo(drawData.points[0].x,drawData.points[0].y);drawData.points.forEach((point,index)=>{if(index>0)ctx.lineTo(point.x,point.y);});ctx.strokeStyle=drawData.color;ctx.lineWidth=drawData.lineWidth;ctx.lineCap='round';ctx.lineJoin='round';ctx.stroke();ctx.closePath();}// 计算完整度评分functioncalculateCompleteScore(){const{points}=drawData;const{x:cx,y:cy,r:cr}=standardCircle;// 1. 形状贴合度letdistanceSum=0;points.forEach(({x,y})=>{constdis=Math.sqrt(Math.pow(x-cx,2)+Math.pow(y-cy,2));distanceSum+=1-Math.abs(dis-cr)/cr;});constshapeFit=Math.max(0,distanceSum/points.length);// 2. 闭合度conststart=points[0];constend=points[points.length-1];constcloseDis=Math.sqrt(Math.pow(start.x-end.x,2)+Math.pow(start.y-end.y,2));constcloseThreshold=2*Math.PI*cr*0.05;constcloseDegree=closeDis>closeThreshold?0:1-closeDis/closeThreshold;// 3. 轨迹覆盖度constangles=[];points.forEach(({x,y})=>{letangle=Math.atan2(y-cy,x-cx)*(180/Math.PI);if(angle<0)angle+=360;angles.push(angle);});angles.sort((a,b)=>a-b);letmaxGap=0;constangleCount=angles.length;for(leti=1;i<angleCount;i++){constgap=angles[i]-angles[i-1];maxGap=Math.max(maxGap,gap);}constlastGap=(360+angles[0])-angles[angleCount-1];maxGap=Math.max(maxGap,lastGap);constcoverDegree=1-maxGap/360;// 加权计算最终评分consttotalScore=(shapeFit*scoreWeights.shapeFit+closeDegree*scoreWeights.closeDegree+coverDegree*scoreWeights.coverDegree)*100;score.value=Math.round(Math.max(0,Math.min(100,totalScore)));}// 重置绘制functionresetDraw(){drawData.isDrawing=false;drawData.points=[];score.value=-1;drawStandardCircle();}return{canvasRef,score,startDraw,drawing,endDraw,handleTouchStart,handleTouchMove,handleTouchEnd,resetDraw};}}).mount('#app');</script></body></html>核心使用说明
运行方式:将代码保存为circle-check.html,直接用浏览器打开即可,无需任何额外配置;
操作流程:鼠标 / 手指按下画布开始画圈 → 松开后自动计算完整度(0-100 分)→ 点击「重新绘制」可清空轨迹重新检测;
跨端支持:完美兼容 PC 端(鼠标操作)和移动端(触摸操作),移动端会阻止默认滚动,保证绘制体验;
无效绘制判定:手绘轨迹点少于 10 个时,会判定为误触,自动清空轨迹,不进行评分。
可快速调整的参数(直接在代码中修改)
标准圆:修改standardCircle中的r(半径)、color(颜色),可调整标准圆大小和样式;
评分权重:修改scoreWeights中的数值,可调整「形状贴合度、闭合度、覆盖度」的评分占比(总和建议为 1);
手绘样式:修改drawData中的color(轨迹颜色)、lineWidth(线宽),可调整手绘轨迹的视觉效果;
画布大小:直接修改 canvas 标签的width/height,标准圆会自动居中适配。
关键核心原理(分模块详解)
下面拆解最核心的4 个模块,这是整个功能的底层逻辑,也是可按需调整的关键部分:
模块 1:Canvas 坐标捕捉与轨迹绘制
这是整个功能的基础,核心是获取鼠标 / 触摸点在 Canvas 内的真实坐标,并将连续的坐标点连接成轨迹。
坐标修正:鼠标 / 触摸的clientX/clientY是相对于浏览器视口的坐标,而 Canvas 有自己的独立坐标系,因此需要通过getBoundingClientRect()获取 Canvas 的视口位置,用视口坐标 - Canvas视口偏移量,得到Canvas 内的真实坐标,避免轨迹绘制偏移;
轨迹绘制:连续的坐标点形成数组后,通过 Canvas 的 2D 上下文 API 绘制:moveTo()定位起点 → lineTo()依次连接后续点 → stroke()描边形成轨迹;
视觉优化:设置lineCap: round(线条端点圆润)、lineJoin: round(线条拐角圆润),让手绘轨迹更符合实际画画的视觉效果;实时绘制时先重绘标准圆,避免轨迹重叠导致的模糊。
模块 2:形状贴合度计算(核心评价「圆不圆」)
目的是判断用户画的轨迹,每个点到标准圆圆心的距离,是否接近标准圆的半径,越接近则「越圆」,分值越高(0-1)。
计算单一点的偏差:对每个手绘坐标点,用欧几里得距离公式计算点到标准圆圆心的实际距离:
(x,y 是手绘点坐标,cx,cy 是标准圆圆心坐标)
计算单一点的贴合值:用1 - |实际距离 - 标准半径| / 标准半径,得到该点的贴合值(偏差越小,值越接近 1);
计算平均贴合度:将所有点的贴合值求和后除以点的总数,得到整体形状贴合度,同时做边界处理(Math.max(0, …)),避免出现负数(比如点离圆心过远时)。
模块 3:闭合度计算(核心评价「封没封口」)
目的是判断用户画的轨迹起点和终点是否重合 / 接近,越近则闭合度越高,分值越高(0-1)。
计算起点终点距离:同样用欧几里得距离公式,计算轨迹第一个点和最后一个点的直线距离;
设置闭合阈值:以 ** 标准圆周长的 5%** 为阈值(可调整),如果起点终点距离超过这个阈值,判定为「未封口」,闭合度直接为 0;
计算有效闭合度:如果距离小于阈值,用1 - 起点终点距离 / 阈值计算闭合度,距离越近,值越接近 1。
模块 4:轨迹覆盖度计算(核心评价「画没画全一圈」)
目的是判断用户画的轨迹是否覆盖了标准圆的 360°,避免只画半圈、1/4 圈却被判定为高完整度,覆盖越全面分值越高(0-1)。这是三个维度中稍复杂的一个,核心是将直角坐标转换为极角,分析角度的分布间隔:
直角坐标转极角:对每个手绘点,用Math.atan2(y - cy, x - cx)计算该点相对于标准圆圆心的极角(弧度),再转换为0-360° 的角度(负数角度加 360°,比如 - 10° 转为 350°);
角度排序:将所有点的角度按从小到大排序,方便计算相邻角度的间隔;
计算最大角度间隔:遍历排序后的角度,计算每两个相邻角度的差值(间隔),同时处理 360° 闭环(比如最后一个角度 350° 和第一个角度 10°,间隔是 20°,而非 - 340°),找到所有间隔中最大的那个;
计算覆盖度:用1 - 最大角度间隔 / 360°得到覆盖度,最大间隔越小(角度分布越均匀),覆盖度越接近 1(比如 360° 全覆盖时,最大间隔趋近于 0,覆盖度≈1;只画半圈时,最大间隔≈180°,覆盖度 = 0.5)。
三、最终评分的加权计算原理
三个维度的计算结果都是0-1 的量化值,最终需要转换为 0-100 分的完整度,核心是加权求和:
设置维度权重:根据业务需求给三个维度分配不同的权重(比如形状贴合度 50%、闭合度 30%、覆盖度 20%),权重总和建议为 1,方便计算;
加权求和:(形状贴合度×形状权重) + (闭合度×闭合权重) + (覆盖度×覆盖权重),得到 0-1 的总量化值;
分数转换与边界限制:总量化值 ×100,转换为 0-100 分,同时用Math.max(0, Math.min(100, …))限制分数范围(避免出现负分或超过 100 的分数),最后四舍五入为整数,得到最终完整度评分。