资讯动态

H5 Canvas连线题开发实战:拖拽绘制、坐标换算与判定逻辑

发布时间:2026/10/18 9:00:49 来源:尧图企业网站定制
简介这份资源面向前端初学者与互动教学应用开发者提供一套基于HTML5 Canvas与JavaScript的连线题完整实现方案可用于在线测验、教育游戏或认知测试场景。压缩包共4个文件包含1个html页面、1个js逻辑脚本和2个css样式文件整体仅9KB结构轻量便于快速集成与二次修改。核心内容围绕Canvas绘图、jQuery事件处理、连线判定算法、纵向与横向两种题型模板、取值回调及交互反馈展开并涉及坐标计算与性能优化思路。已有640人学习适合想通过小案例掌握Canvas交互开发、事件绑定与答题逻辑设计的读者参考可从中获取可运行的连线题原型、模板切换思路与回调验证机制作为课程作业或项目练手的起点。1. 从一道“连不上”的题说起这个 H5 连线题项目到底能干什么前阵子帮一个做在线教育的朋友看他们新上的互动题学生端反馈“拖拽没反应、线画不出来、对错判断全靠猜”。翻完代码发现问题不在业务逻辑而在 canvas 的坐标系和事件绑定上——这也是很多 H5 连线题项目的通病。今天要拆的这个项目就是一套用 H5 canvas js 实现的连线题方案核心解决三件事把左右两列选项画到画布上、让手指或鼠标能拖出一条跟随的线、松手时判断连到了哪个目标并给出对错反馈。它适合谁正在做题库、互动课件、轻量级答题页的开发者尤其是想用 jquery 快速搭原型、又不想引入重型图形库的场景。整套代码不依赖框架浏览器打开就能跑改起来也直接。2. 技术选型与整体架构为什么是 canvas 而不是 DOM 连线2.1 选型对比DOM SVG 与 canvas 的取舍做连线题第一反应可能是用 DOM 元素加 SVG 画线毕竟元素好定位、事件好绑定。但这个项目选了 canvas原因很实际连线题的核心交互是“拖拽过程中实时重绘一条跟随线”如果每次 mousemove 都去操作 SVG path 的 d 属性在低端安卓机上会明显掉帧。canvas 是一整块画布清屏加重绘的成本可控而且连线题的视觉元素简单——就是圆点、文字、线段不需要 DOM 的可访问性和样式继承。常见做法是选项文字用 canvas 的 fillText 绘制锚点用 arc 画圆连线用 moveTo/lineTo 描边。这样整个题目就是一张画布事件只绑在 canvas 上通过坐标反查命中了哪个锚点。代价是文字换行、样式微调得自己算但对连线题这种固定布局来说反而更可控。2.2 项目目录结构与模块划分这个项目没有构建工具就是一个静态页面加几个 js 文件结构很扁平lianxian/ ├── index.html # 页面入口放 canvas 容器 ├── css/ │ └── style.css # 画布尺寸、页面基础样式 ├── js/ │ ├── jquery.min.js # 事件绑定与 DOM 操作 │ ├── question.js # 题目数据与配置 │ ├── canvas.js # 画布初始化、绘制锚点与文字 │ ├── line.js # 连线绘制与拖拽逻辑 │ └── judge.js # 连线结果判定与反馈 └── images/ # 对错图标等静态资源question.js 里存题目数据canvas.js 负责把数据画出来line.js 处理交互judge.js 做判定。模块之间通过全局的题目对象和画布上下文通信没有用模块化打包好处是新手直接改文件就能看到效果坏处是变量都挂在 window 上改的时候要注意命名别冲突。2.3 数据驱动题目配置长什么样题目数据决定了画布上画什么、连什么算对。一个典型的配置如下// question.js var questionData { left: [ { id: l1, text: 苹果, x: 80, y: 120 }, { id: l2, text: 香蕉, x: 80, y: 200 }, { id: l3, text: 橘子, x: 80, y: 280 } ], right: [ { id: r1, text: 红色, x: 420, y: 120 }, { id: r2, text: 黄色, x: 420, y: 200 }, { id: r3, text: 橙色, x: 420, y: 280 } ], // 正确答案映射左 id 对应右 id answer: { l1: r1, l2: r2, l3: r3 } };x、y 是锚点在画布上的坐标单位是像素。answer 对象存正确配对判定时拿用户连的右 id 和这里比对。改题目只需要改这个文件不用动画布逻辑。注意坐标要跟 canvas 的实际宽高匹配如果 css 里把 canvas 缩放显示坐标换算会出问题后面避坑章节会细说。3. 核心绘制链路从清屏到锚点、文字、连线的完整实现3.1 画布初始化与高清屏适配canvas 在移动端有个经典坑css 尺寸和画布实际像素不一致导致线条模糊。项目里做了 devicePixelRatio 适配// canvas.js function initCanvas() { var canvas document.getElementById(questionCanvas); var ctx canvas.getContext(2d); var dpr window.devicePixelRatio || 1; var cssWidth 500; var cssHeight 400; // 实际像素放大 dpr 倍 canvas.width cssWidth * dpr; canvas.height cssHeight * dpr; canvas.style.width cssWidth px; canvas.style.height cssHeight px; // 缩放上下文让后续绘制按 css 像素走 ctx.scale(dpr, dpr); return { canvas: canvas, ctx: ctx, width: cssWidth, height: cssHeight }; }dpr 是设备像素比普通屏是 1高清屏可能是 2 或 3。canvas.width 设成 css 宽乘 dpr再用 ctx.scale 把绘制坐标系缩回 css 像素这样后面所有坐标都按 500x400 来写不用到处乘 dpr。如果跳过这步在高清屏上文字和线条会发虚这是血泪经验。3.2 绘制锚点与文字fillText 与 arc 的配合锚点是连线的起点和终点视觉上是个小圆点旁边带文字。绘制函数遍历题目数据// canvas.js function drawAnchors(ctx, data) { ctx.clearRect(0, 0, 500, 400); // 清屏 ctx.font 16px sans-serif; ctx.textAlign center; ctx.textBaseline middle; // 画左侧 data.left.forEach(function (item) { ctx.beginPath(); ctx.arc(item.x, item.y, 8, 0, Math.PI * 2); ctx.fillStyle #4a90d9; ctx.fill(); ctx.fillStyle #333; ctx.fillText(item.text, item.x - 40, item.y); }); // 画右侧 data.right.forEach(function (item) { ctx.beginPath(); ctx.arc(item.x, item.y, 8, 0, Math.PI * 2); ctx.fillStyle #d9534f; ctx.fill(); ctx.fillStyle #333; ctx.fillText(item.text, item.x 40, item.y); }); }clearRect 每次重绘前清空整块画布这是 canvas 动画的基本节奏。arc 的第三个参数是半径这里锚点半径 8 像素命中判定时也用这个值加一点容差。文字用 fillText 画在锚点旁边左侧文字往左偏 40 像素右侧往右偏避免压住圆点。textAlign 和 textBaseline 设成 center/middle让文字以给定坐标为中心省去手动算宽高。3.3 拖拽连线mousedown 到 mousemove 的状态机连线的交互本质是个小状态机按下时判断有没有命中锚点移动时更新临时线终点松开时做判定。用 jquery 绑事件// line.js var dragging false; var startAnchor null; var currentPos { x: 0, y: 0 }; $(canvas).on(mousedown touchstart, function (e) { var pos getEventPos(e); var anchor hitTest(pos, questionData.left); // 只允许从左往右连 if (anchor) { dragging true; startAnchor anchor; currentPos pos; } }); $(canvas).on(mousemove touchmove, function (e) { if (!dragging) return; currentPos getEventPos(e); redraw(); // 重绘所有锚点 当前拖拽线 }); $(canvas).on(mouseup touchend, function (e) { if (!dragging) return; var pos getEventPos(e); var endAnchor hitTest(pos, questionData.right); if (endAnchor) { saveConnection(startAnchor.id, endAnchor.id); } dragging false; startAnchor null; redraw(); });getEventPos 负责把鼠标或触摸事件的坐标换算成画布坐标触摸事件要取 touches[0]。hitTest 遍历锚点数组算事件坐标到锚点圆心的距离小于半径加容差就算命中。mousedown 只检测左侧锚点保证连线方向是从左到右避免用户从右往左连造成判定混乱。mousemove 里调 redraw把已保存的连线和当前拖拽的临时线一起画出来。3.4 坐标换算getBoundingClientRect 的正确用法事件坐标和画布坐标不是一回事页面滚动、canvas 在页面中的位置都会影响// line.js function getEventPos(e) { var rect canvas.getBoundingClientRect(); var clientX, clientY; if (e.type.indexOf(touch) 0) { clientX e.originalEvent.touches[0].clientX; clientY e.originalEvent.touches[0].clientY; } else { clientX e.clientX; clientY e.clientY; } return { x: clientX - rect.left, y: clientY - rect.top }; }getBoundingClientRect 返回 canvas 相对视口的位置clientX 减去 rect.left 就是画布内坐标。注意这里没有乘 dpr因为前面 ctx.scale 已经把绘制坐标系缩回 css 像素事件坐标也是 css 像素两边一致。如果页面有缩放或 canvas 被 css transform 过这套换算会偏得用更复杂的矩阵变换但常规布局够用。4. 判定与反馈连线结果怎么算对、怎么给用户后悔药4.1 连线存储与重绘已连的线不能丢用户可能连多条线每条都要记住并在重绘时画出来// line.js var connections []; // { from: l1, to: r1 } function saveConnection(fromId, toId) { // 同一个左锚点只保留最后一条线 connections connections.filter(function (c) { return c.from ! fromId; }); connections.push({ from: fromId, to: toId }); } function drawConnections(ctx) { connections.forEach(function (c) { var from findAnchor(c.from, questionData.left); var to findAnchor(c.to, questionData.right); ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.strokeStyle #4a90d9; ctx.lineWidth 2; ctx.stroke(); }); }saveConnection 里先过滤掉同一个左锚点的旧连线保证一个左项只连一个右项这是连线题的基本规则。drawConnections 根据 id 找到锚点坐标moveTo 到起点、lineTo 到终点、stroke 描边。重绘顺序是先画连线再画锚点这样锚点圆点盖在线段端点上视觉更干净。4.2 判定逻辑遍历 answer 映射逐条比对判定就是拿用户连的结果和正确答案比// judge.js function judgeAnswer() { var correct 0; var total Object.keys(questionData.answer).length; connections.forEach(function (c) { if (questionData.answer[c.from] c.to) { correct; } }); var allRight correct total connections.length total; showResult(allRight, correct, total); return allRight; }total 是正确答案的条目数correct 是连对的条数。allRight 要求连对数量等于总数且连线数量也等于总数防止用户少连几条却因为没连错而误判为全对。showResult 负责在页面上显示对错提示可以弹层也可以改文字。4.3 对错反馈与重置给用户一次重来的机会判定完要给出视觉反馈并且允许重置// judge.js function showResult(allRight, correct, total) { var tip document.getElementById(resultTip); if (allRight) { tip.textContent 全部连对共 total 题; tip.style.color #5cb85c; } else { tip.textContent 连对 correct / total 再试试; tip.style.color #d9534f; } } // 重置按钮 $(#resetBtn).on(click, function () { connections []; document.getElementById(resultTip).textContent ; redraw(); });反馈文案直接告诉用户连对几条比单纯说“错误”更有指导性。重置就是把 connections 清空、提示清空、重绘画布。这里用 jquery 绑按钮事件跟 canvas 事件分开逻辑清晰。注意 connections 是全局变量重置时直接赋空数组别用 length0 之外的花招简单直接。5. 避坑与排查连线题最容易翻车的五个地方5.1 现象移动端拖拽时页面跟着滚动原因touchmove 默认会触发页面滚动canvas 上的拖拽被页面滚动抢走。解决在 touchmove 事件里调 e.preventDefault()并且给 canvas 加 css 的 touch-action: none。注意 preventDefault 要放在判断 dragging 之后否则会阻止正常滚动。5.2 现象高清屏上线条和文字发虚原因只设了 canvas 的 css 尺寸没设实际像素尺寸浏览器把低分辨率画布拉伸显示。解决按 devicePixelRatio 放大 canvas.width/height再用 ctx.scale 缩回逻辑坐标参考 3.1 的 initCanvas。5.3 现象点击锚点没反应或者命中位置偏移原因事件坐标没减去 canvas 的 getBoundingClientRect().left/top或者页面滚动后 rect 变了没重新取。解决每次事件里实时调 getBoundingClientRect别缓存触摸事件记得取 touches[0] 而不是 e 本身。5.4 现象连完一条线再连同一起点旧线还在原因saveConnection 没做去重connections 里堆了多条同 from 的记录。解决push 之前先 filter 掉 from 相同的旧记录保证一个左锚点只有一条线。5.5 现象判定全对但用户其实少连了原因判定只统计连对的条数没校验连线总数。解决allRight 条件里加上 connections.length total两个条件都满足才算全对。6. 二次开发改造从固定题目到动态题库的落地思路这套代码跑通之后最自然的改造方向是把它接进真实题库。现在的题目数据写死在 question.js 里实际项目里题目通常来自接口。我的做法是保留绘制和交互逻辑不动只把 questionData 的赋值改成异步获取// 改造从接口拉题目 function loadQuestion(qid) { $.get(/api/question/ qid, function (res) { questionData res.data; // 结构保持 left/right/answer connections []; redraw(); }); }接口返回的数据结构跟原来保持一致这样 canvas.js、line.js、judge.js 一行都不用改。这是数据驱动的价值——只要契约不变渲染层就是稳定的。如果接口字段名不同加一层适配函数转换即可别去改绘制代码。第二个改造点是锚点坐标。现在 x、y 是写死的题目一多、选项一长就会重叠。常见做法是根据选项数量动态算坐标左侧 x 固定y 按 index 均分画布高度右侧同理。文字太长的话fillText 之前先用 measureText 量宽度超过阈值就截断加省略号或者换行绘制。这些都属于渲染层的增强不影响判定逻辑。第三个改造点是判定反馈的丰富度。现在只有文字提示可以加对错图标、把连错的线标红、连对的线标绿。实现方式是在 drawConnections 里根据 judge 结果给每条线不同的 strokeStyle。注意判定和绘制要解耦先算出每条线的对错状态存起来再统一重绘别在绘制函数里现算否则重绘频繁时性能会抖。最后一个容易忽略的点是移动端手势冲突。如果页面本身可以上下滚动canvas 又占了大半屏用户想滚动页面却触发了连线。我的习惯是给 canvas 加一个“编辑模式”开关非编辑模式下不绑拖拽事件用户点一下“开始连线”才激活。这样滚动和连线互不干扰体验上更接近真实答题场景。从那以后我每次做 canvas 交互都强制先跑一遍坐标换算和 dpr 适配再写业务逻辑——这两步翻车后面全白搭。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑