真要做得可靠,canvas是唯一合理起点;用dom元素模拟拼图会在吸附、坐标同步、移动端适配和完成判定上必然失败。

用 HTML 做拼图游戏,核心不是堆 div 或靠 CSS 背景定位“假装”切片,而是必须明确:你要的是可交互、可拖拽、可校验的拼图逻辑,不是静态展示。真要做得可靠,canvas 是唯一合理起点;用 DOM 元素模拟拼图,后期卡在吸附、坐标同步、移动端 touch 适配、完成判定上几乎是必然的。
为什么不能只用 div + background-position?
常见错误是把一张图切好,每个 div 用 background-position 显示不同区域。这看似简单,但实际会立刻撞墙:
- 图片缩放后,
background-position偏移值失效,块错位不可逆 - 拖拽时靠
transform移动元素,脱离原始网格坐标系,无法判断“是否邻接空位” - 完成判定只能比视觉位置,受抗锯齿、浮点误差、缩放影响,极易误判
- 移动端
touchmove事件坐标与clientX/Y不一致,不加preventDefault()会触发页面滚动
canvas 渲染拼图块的最小可行写法
你不需要一上来就写完整游戏,先让一块能动起来。关键三步:
- 创建
<canvas id="puzzle"></canvas>,设好固定宽高(如400x400),确保能被行数列数整除(比如 4×4 → 每块 100×100) - 用
Image加载图后,在隐藏 canvas 上绘制全图,再用ctx.getImageData()或直接ctx.drawImage()裁剪每块 —— 别用background-image - 维护一个二维数组
board,存每个格子当前显示哪块原始坐标,例如board[1][2] = {x: 0, y: 300}表示第 1 行第 2 列显示原图顶部第 3 行区域
重绘逻辑就一句话:redraw() 函数里遍历 board,对每个非空位格子调用 ctx.drawImage(img, srcX, srcY, pieceW, pieceH, dstX, dstY, pieceW, pieceH)。
空位判定和合法移动怎么写才不出错?
空位(blank)不是“某个 div 透明了”,而是一个明确的坐标对,比如 blankRow = 3, blankCol = 3。用户点击某块时,只允许它和空位相邻才交换:
- 相邻判断别用
Math.abs(i - j) === 1 || === 4这种索引差——那是线性排列假设,只适用于一维数组;二维下必须用行列计算:Math.abs(row - blankRow) === 1 && col === blankCol || Math.abs(col - blankCol) === 1 && row === blankRow - 打乱初始布局时,禁止用
for...Math.random() > 0.5随机交换:这会产生约 50% 不可解状态。正确做法是模拟真实滑动——从终局开始,随机选一个邻接空位的块,执行 N 次交换 - 拖拽中临时状态必须分离:
draggingPiece是独立对象,含原始索引、当前屏幕坐标、偏移量;松手时才决定是否写回board并更新空位坐标
完成判定别碰像素,只比索引
胜利条件不是“所有块看起来归位了”,而是 board[i][j] 的原始索引是否等于 i * cols + j(即恢复初始顺序)。空位本身不参与比对,但它位置决定了哪些块“可移动”。所以:
- 初始化时记录每个块的“目标索引”,存在
piece.targetIndex或直接存在board[i][j].target - 每次交换后,只检查非空位的所有格子:
board[i][j].target === i * cols + j - 一旦发现任一格子不符,立刻中断;全部通过才算赢
这个逻辑干净、稳定、不受缩放或设备像素比干扰。真正容易被忽略的是:空位坐标必须始终与 board 状态严格同步,哪怕只是打乱一步,也要同时更新 blankRow/blankCol 和对应 board 项 —— 错一次,后续所有移动都会错位。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











