(function (window) { const l = 42, // 滑块边长 r = 10, // 滑块半径 w = 310, // canvas宽度 h = 155, // canvas高度 pi = math.pi const l = l + r * 2 // 滑块实际边长 function getrandomnumberbyrange(start, end) { return math.round(math.random() * (end - start) + start) } function createcanvas(width, height) { console.log("createcanvas"); const canvas = createelement('canvas') canvas.width = width canvas.height = height return canvas } function createimg(onload) { console.log("createimg"); const img = createelement('img') img.crossorigin = "anonymous" img.onload = onload img.onerror = () => { img.src = getrandomimg() } img.src = getrandomimg() return img } function createelement(tagname) { return document.createelement(tagname) } function addclass(tag, classname) { tag.classlist.add(classname) } function removeclass(tag, classname) { tag.classlist.remove(classname) } function getrandomimg() { return 'https://picsum.photos/300/150/?image=' + getrandomnumberbyrange(0, 100) } function draw(ctx, operation, x, y) { console.log("draw"); ctx.beginpath() ctx.moveto(x, y) ctx.lineto(x + l / 2, y) ctx.arc(x + l / 2, y - r + 2, r, 0, 2 * pi) ctx.lineto(x + l / 2, y) ctx.lineto(x + l, y) ctx.lineto(x + l, y + l / 2) ctx.arc(x + l + r - 2, y + l / 2, r, 0, 2 * pi) ctx.lineto(x + l, y + l / 2) ctx.lineto(x + l, y + l) ctx.lineto(x, y + l) ctx.lineto(x, y) ctx.fillstyle = '#fff' ctx[operation]() ctx.beginpath() ctx.arc(x, y + l / 2, r, 1.5 * pi, 0.5 * pi) ctx.globalcompositeoperation = "xor" ctx.fill() } function sum(x, y) { return x + y } function square(x) { return x * x } class jigsaw { constructor(el, success, fail) { this.el = el this.success = success this.fail = fail } init() { console.log("init"); this.initdom() this.initimg() this.draw() this.bindevents() } initdom() { console.log("initdom"); const canvas = createcanvas(w, h) // 画布 const block = canvas.clonenode(true) // 滑块 const slidercontainer = createelement('div') const refreshicon = createelement('div') const slidermask = createelement('div') const slider = createelement('div') const slidericon = createelement('span') const text = createelement('span') block.classname = 'block' slidercontainer.classname = 'slidercontainer' refreshicon.classname = 'refreshicon' slidermask.classname = 'slidermask' slider.classname = 'slider' slidericon.classname = 'slidericon' text.innerhtml = '向右滑动滑块填充拼图' text.classname = 'slidertext' const el = this.el el.appendchild(canvas) el.appendchild(refreshicon) el.appendchild(block) slider.appendchild(slidericon) slidermask.appendchild(slider) slidercontainer.appendchild(slidermask) slidercontainer.appendchild(text) el.appendchild(slidercontainer) object.assign(this, { canvas, block, slidercontainer, refreshicon, slider, slidermask, slidericon, text, canvasctx: canvas.getcontext('2d'), blockctx: block.getcontext('2d') }) } initimg() { console.log("initimg"); const img = createimg(() => { this.canvasctx.drawimage(img, 0, 0, w, h) this.blockctx.drawimage(img, 0, 0, w, h) const y = this.y - r * 2 + 2 const imagedata = this.blockctx.getimagedata(this.x, y, l, l) this.block.width = l this.blockctx.putimagedata(imagedata, 0, y) }) this.img = img } draw() { console.log("draw"); // 随机创建滑块的位置 this.x = getrandomnumberbyrange(l + 10, w - (l + 10)) this.y = getrandomnumberbyrange(10 + r * 2, h - (l + 10)) draw(this.canvasctx, 'fill', this.x, this.y) draw(this.blockctx, 'clip', this.x, this.y) } clean() { console.log("clean"); this.canvasctx.clearrect(0, 0, w, h) this.blockctx.clearrect(0, 0, w, h) this.block.width = w } bindevents() { console.log("bindevents"); this.el.onselectstart = () => false this.refreshicon.onclick = () => { this.reset() } let originx, originy, trail = [], ismousedown = false this.slider.addeventlistener('mousedown', function (e) { originx = e.x, originy = e.y ismousedown = true }) document.addeventlistener('mousemove', (e) => { if (!ismousedown) return false const movex = e.x - originx const movey = e.y - originy if (movex < 0 || movex + 38 >= w) return false this.slider.style.left = movex + 'px' var blockleft = (w - 40 - 20) / (w - 40) * movex this.block.style.left = blockleft + 'px' addclass(this.slidercontainer, 'slidercontainer_active') this.slidermask.style.width = movex + 'px' trail.push(movey) }) document.addeventlistener('mouseup', (e) => { if (!ismousedown) return false ismousedown = false if (e.x == originx) return false removeclass(this.slidercontainer, 'slidercontainer_active') this.trail = trail const {spliced, turingtest} = this.verify() if (spliced) { if (turingtest) { addclass(this.slidercontainer, 'slidercontainer_success') this.success && this.success() } else { addclass(this.slidercontainer, 'slidercontainer_fail') this.text.innerhtml = '再试一次' this.reset() } } else { addclass(this.slidercontainer, 'slidercontainer_fail') this.fail && this.fail() settimeout(() => { this.reset() }, 1000) } }) } verify() { console.log("verify"); const arr = this.trail // 拖动时y轴的移动距离 const average = arr.reduce(sum) / arr.length // 平均值 const deviations = arr.map(x => x - average) // 偏差数组 const stddev = math.sqrt(deviations.map(square).reduce(sum) / arr.length) // 标准差 const left = parseint(this.block.style.left) return { spliced: math.abs(left - this.x) < 10, turingtest: average !== stddev, // 只是简单的验证拖动轨迹,相等时一般为0,表示可能非人为操作 } } reset() { console.log("reset"); this.slidercontainer.classname = 'slidercontainer' this.slider.style.left = 0 this.block.style.left = 0 this.slidermask.style.width = 0 this.clean() this.img.src = getrandomimg() this.draw() } } window.jigsaw = { init: function (element, success, fail) { new jigsaw(element, success, fail).init(); } } }(window))