先不废话,请看演示。
公司要搞这么个微信活动,可现在没有前端开发,没办法,身为打杂总监只好临时顶下这个空缺了。先找了一些 JS 代码,试用了下都不太理想,好一点的写的又太复杂,改起来有难度,干脆撸起袖子自己干。
基本需求
有一个固定区域,被拆分成 c*r 个同等大小的碎片,拿走其中一块,靠近缺口的块可以向缺口方向移动;当拼出原来的图样视为完成。
依照此需求,需要经历 加载图片-》拆分图片-》随机打散-》移动碎片-》判定完成 这些步骤。为了更有可玩性,能自行选择自己的图片就更妙了。
下面就重点说明下各个步骤,为编写方便,引入 jQuery 作为辅助库。
加载图片
首先当然是载入图片,计算宽高,对比拼图区域的尺寸进行缩放,如果比例不同,还得“裁剪”掉多余的部分。
/**
* 加载图片
* cal 的回调参数为:
* ox 横向偏移
* oy 纵向偏移
* this 指向载入的图片的 jQuery 对象
* @param {String} src 图片路径
* @param {int} w 额定宽
* @param {int} h 额定高
* @param {Fucntion} cal 加载完成后的回调方法
*/
function loadr(src, w, h, cal) {
var img = new Image();
img.onload = function() {
var xw = img.width ;
var xh = img.height;
var zw = xh * w / h;
if (zw > xw) {
// 宽度优先
img.width = w;
img.height = xh * w / xw;
xh = (h - img.height) / 2;
xw = 0;
} else {
// 高度优先
img.height = h;
img.width = xw * h / xh;
xw = (w - img.width ) / 2;
xh = 0;
}
cal.call(img, xw, xh);
};
img.src = src ;
}
以上的“裁剪”仅仅是计算出偏移,然后将其传递给加载就绪的回调函数。
拆分图片
图有了,已缩放,现在需要“拆分”成碎片。这里自然不是真的切割了,而是将图片 clone 出 c*r 片,然后利用负的坐标定位,其实质是用一个块遮盖了“切除”的部分,仅显示需要的碎片部分。
/**
* 拆分图片
* @param {jQuery} that 容器对象
* @param {int} cols 行
* @param {int} rows 列
* @param {int} ew 板块宽度
* @param {int} eh 板块高度
* @param {int} ox 图片横向偏移
* @param {int} oy 图片纵向偏移
* @param {Image} im 图片对象
*/
function split(that, cols, rows, ew, eh, ox, oy, im) {
that.empty();
for(var j = 0 ; j < rows; j ++) {
for(var i = 0 ; i < cols; i ++) {
var k = i + j * rows;
var pic = $('<div class="pt-pic"></div>');
pic.attr("id", "pt-pic-"+k);
pic.data("idx", k);
pic.appendTo(that);
pic.css ({
"position": "relative",
"overflow": "hidden",
"border" : "0",
"width" : ew + "px",
"height" : eh + "px"
});
var img = $(im.cloneNode());
img.appendTo(pic);
img.css ({
"position": "absolute",
"z-index" : "88",
"border" : "0",
"left" : (0 - i * ew + ox) + "px",
"top" : (0 - j * eh + oy) + "px"
});
// 因边框可能影响宽高计算, 故边框单独用一个块来放
var bor = $('<div class="pt-bor"></div>');
bor.appendTo(pic);
bor.css ({
"position": "absolute",
"z-index" : "99",
"width" : "100%",
"height" : "100%"
});
// 由于样式宽高并不含边框, 故再次计算尺寸的偏移量
bor.css ({
"width" : (2 * bor.width () - bor.outerWidth ()) + "px",
"height" : (2 * bor.height() - bor.outerHeight()) + "px"
});
}
}
}
稍微注意,为方便人眼分辨碎片,最好给碎片加个边框,但加边框必然影响坐标的计算,故在图片上再覆盖一层,边框设在他上面,就算加个撕裂效果的透明图做边框都没问题了。这样碎片内图片的偏移坐标的计算就少了些麻烦了。
随机打散
这游戏当然是跟电脑玩了,总不能自己打散自己玩吧?但这个打散不能给每个图片一个随机位置,那很可能你永远也拼不回去了。小时拿那种拼图游戏板整人就干过这种事,故意抠下来把头和脚交换再打散,然后跟其他小朋友打赌。所以程序也得守规矩一块一块的移动。
/**
* 打散图片
* @param {jQuery} that 容器对象
* @param {int} cols 列
* @param {int} rows 行
* @param {int} rand 打散步数
*/
function upset(that, cols, rows, rand) {
var v ;
var r = Math.floor(Math.random() * cols * rows);
var hole = that.children().eq(r).addClass("pt-pix");
var part ;
var step = [];
var dbug = [];
for(var i = 0, j = rand; i < j; i ++) {
var x = cols - 1;
var y = rows - 1;
var z = cols;
var rx = r % cols;
var ry = Math.floor(r / cols);
var rv = [];
if (rx > 0 && rx < x) {
rv.push(r - 1, r + 1); // 可左右移动
} else
if (rx > 0) {
rv.push(r - 1); // 可向左移动
} else
{
rv.push(r + 1); // 可向右移动
}
if (ry > 0 && ry < y) {
rv.push(r - z, r + z); // 可上下移动
} else
if (ry > 0) {
rv.push(r - z); // 可向上移动
} else
{
rv.push(r + z); // 可向下移动
}
// 排除来源位置
if (step.length > 0) {
v = step[step.length - 1];
v = $.inArray(v, rv);
if (v > -1) {
rv.splice(v, 1 );
}
}
// 排除回旋位置
if (step.length > 2 && rv.length > 1) {
v = step[step.length - 3];
v = $.inArray(v, rv);
if (v > -1) {
rv.splice(v, 1 );
}
}
// 随机方向
r = rv[Math.floor(Math.random()* rv.length)];
v = hole.index();
step.push(v);
// 交换位置
part = that.children().eq( r );
if (r < v) {
part.insertBefore(hole);
hole.insertBefore(that.children().eq(r));
} else {
hole.insertBefore(part);
part.insertBefore(that.children().eq(v));
}
// 调试步骤
if (r == v + 1) {
dbug.push("左");
} else
if (r == v - 1) {
dbug.push("右");
} else
if (r > v) {
dbug.push("上");
} else
if (r < v) {
dbug.push("下");
}
}
// 攻略
dbug = dbug.reverse().join(" "); alert(dbug);
console.log( "攻略: "+dbug+"\r\n此非最优解, 仅为随机打散时的逆向步骤, 上下左右为相对缺口的板块, 祝您玩的开心!" );
}
把打散的步骤记录下来,然后反转数组,就是攻略啦。
不过随机时需要避免往回走,否则出现 左->右->左 这类情况就不好玩了;还得避免其他循环,如 上->右->下->左 这样的,这会回到原点,等于什么也没干;但更大的循环没想好怎么处理,暂时不去纠结了。
移动判定
移动碎片到缺口,也就是交换碎片与缺口的位置。左右移动很简单,序号大的 insertBefore 序号小的即可。上下移动有个小坑,开始自己没注意,我原本想不管横向还是纵向,没有两次 insertBefore 搞不定的,但是如果 3 和 7 交换位置(3x3, 0~8),3 移动到 7 前,7 再移动到 3 前,此时原来的 3 变成了 6。的确,没有什么是不能两次 insertBefore 解决的,但还得考虑让序号大的先动。
/**
* 移动板块
* @param {jQuery} that 容器对象
* @param {int} cols 列数
* @param {int} rows 行数
* @param {jQuery} hole 缺口对象
* @param {jQuery} part 板块对象
*/
function mover(that, cols, rows, hole, part) {
var move = false ;
var i = part.index();
var j = hole.index();
var ix = i % cols;
var jx = j % cols;
var iy = Math.floor(i / cols);
var jy = Math.floor(j / cols);
if (iy == jy) { // 在同一行
move = ix == jx + 1 // 可向左边移动
|| ix == jx - 1; // 可向右边移动
} else
if (ix == jx) { // 在同一列
move = iy == jy + 1 // 可向上移动
|| iy == jy - 1; // 可向下移动
}
// 互换位置
if (move) {
if (i < j ) {
part.insertBefore(hole);
hole.insertBefore(that.children().eq(i));
} else {
hole.insertBefore(part);
part.insertBefore(that.children().eq(j));
}
}
// 判断是否拼图完成
move = true;
for (i = 0, j = cols * rows; i < j; i ++) {
if (that.children().eq(i).data("idx") != i) {
move = false;
}
}
return move;
}
判断是否完成就来个笨办法吧,依次遍历所有碎片,只要有一个没对上序号就是还没成功。
未处理滑动事件,以后闲了再加吧。
整合游戏程序
上面分散的几个函数用起来还是不太方便,整合成一个 jQuery 插件。
/**
* 拼图游戏
* @param {String} src 图片路径
* @param {int} cols 列数
* @param {int} rows 行数
* @param {int} rand 打散步数
*/
$.fn.hsPintu = function(src, cols, rows, rand) {
var that = $(this);
var srz = that.data("src");
var img = that.data("img");
var aw = that.width ();
var ah = that.height();
var ew = aw / rows;
var eh = ah / cols;
// 状态: 0 进行中, 1 成功, 2 结束
that.data("hsPintuStatus", 2);
that.data("cols", cols);
that.data("rows", rows);
/**
* img 存在且 src 没变化
* 则不需要再次加载图片
* 直接取出存储好的数据
*/
if (img && srz === src) {
var ox = that.data("pos_x");
var oy = that.data("pos_y");
console.log("Note: 图片无变化");
split(that, cols, rows, ew, eh, ox, oy, img );
// 未给 rand 则仅拆分而不打散
if (rand === undefined) return;
upset(that, cols, rows, rand);
that.data("hsPintuStatus", 0);
that.trigger("hsPintuLaunch");
} else
loadr(src, aw, ah, function(ox, oy) {
that.data("src", src );
that.data("img", this);
that.data("pos_x", ox);
that.data("pos_y", oy);
console.log("Note: 载入新图片");
split(that, cols, rows, ew, eh, ox, oy, this);
// 未给 rand 则仅拆分而不打散
if (rand === undefined) return;
upset(that, cols, rows, rand);
that.data("hsPintuStatus", 0);
that.trigger("hsPintuLaunch");
});
// 已经初始化过就不要再绑定事件了
if (! that.data("hsPintuInited")) {
that.data("hsPintuInited", 1);
that.on("click", ".pt-pic:not(.pt-pix)", function() {
if (that.data("hsPintuStatus") === 0) {
var cols =that.data("cols");
var rows =that.data("rows");
var hole =that.children(".pt-pix");
if (mover(that, cols, rows, hole, $(this))) {
that.data("hsPintuStatus", 1);
that.trigger("hsPintuFinish");
}
}
});
}
return this;
};
用 $("#pt-box").hsPintu(图片URL, 列数, 行数[, 随机步数]);
即可初始化拼图游戏了, 拼图区域需要固定宽高;随机步数参数不提供时,仅拆解不打散。
图片没变化时没必要重新加载,避免下时间损耗。当然了,更好的办法是再判断行、列和区域尺寸,没变化则直接排列好碎片。懒得写了,先这样吧。
选择任意图片
上面都是固定的图片,参与感不好,让用户自行“上传”图片岂不更有意思。其实不必真的上传到服务器,既然“缩放”、“裁剪”上面都有了,直接加载本地图片不就好了嘛。
/**
* 预载文件
* @param {Function} cal 回调函数
* @returns {jQuery} 当前文件节点
*/
$.fn.hsFileLoad = function(cal) {
this.each(function() {
var that = this;
if (window.FileReader) {
var fr = new FileReader( );
fr.onloadend = function(e) {
cal.call(that, e.target.result);
}; cal.call(that);
$.each( this.files, function(i, fo) {
fr.readAsDataURL( fo );
});
} else
if (this.getAsDataURL) {
cal.call(that, that.getAsDataURL());
} else {
cal.call(that, that.value);
}
});
return this;
};
这段代码也能从我的开源项目内找到 预载文件 方法,此工具包还有些其他的文件上传预览类的方法,这是我对 bootstrap-fileinput 没有图片裁剪功能(与最终服务端处理后的结果一致)而“一气之下”自己写的一点零散代码。
完整的代码及演示可在 这里 看到,有朋友说看不到图,但图片我用的百度图片搜索的缩略图,不清楚怎么回事,看不到可以自己从本地选择图片。只是那个“加载”(Image.onload)和“切片”(Image.cloneNode)比较耗时,比较大的图片请耐心等等。
当然了,也可以光顾我们的活动页玩一把 拼图抽奖。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。