(function(window) { const l = 42, // 滑块边长 r = 10, // 滑块半径 w = 260, // 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) { const canvas = createElement('canvas') canvas.width = width canvas.height = height return canvas } function createImg(onload) { 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 "./img/login/" + getRandomNumberByRange(1, 50) + ".jpg"; //return 'https://picsum.photos/300/150/?image=' + getRandomNumberByRange(0, 100) } function draw(ctx, operation, x, y) { 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() { this.initDOM() this.initImg() this.draw() this.bindEvents() } 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() { const img = createImg(() => { // this.canvasCtx.linewidth = 10; // this.canvasCtx.fillstyle = "rgba(88, 204, 245, 0.4)"; //设置背景颜色 // this.canvasCtx.stroke(); // this.canvasCtx.strokeStyle = 'red'; // this.canvasCtx.lineWidth = 2; // this.canvasCtx.strokeRect(0, 0, w, h); this.canvasCtx.drawImage(img, 0, 0, w, h) this.blockCtx.drawImage(img, 0, 0, w, h) // this.blockCtx.strokeRect(1, 1, w - 2, h - 2); // this.blockCtx.shadowColor = 'rgba(255,0,0,1)'; // this.blockCtx.shadowBlur = 10; // this.blockCtx.shadowOffsetX = 10; // this.blockCtx.shadowOffsetY = 10; // this.blockCtx.fillStyle = 'rgba(0,0,255,1.0)'; // this.blockCtx.fillRect(100,100,200,100); // this.blockCtx.globalCompositeOperation = 'source-in'; // this.blockCtx.globalCompositeOperation="source-atop"; // this.blockCtx.drawImage(img, 0, 0, w + 12, h + 12); // this.blockCtx.fillRect(0, 0, w + 12, h + 12); // this.blockCtx.fillStyle = "#FF0000"; // this.blockCtx.drawImage(img, 0, 0, w + 12, h + 12); // this.blockCtx.globalCompositeOperation = "source-in"; // this.blockCtx.fillRect(0, 0, w + 12, h + 12); // this.blockCtx.globalCompositeOperation = "source-over"; // this.blockCtx.globalCompositeOperation="source-over"; // this.blockCtx.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(this.halveAlpha(ImageData), 0, y) }) this.img = img } drawblock(ctx, width, height, type, img) { //这里是二合一函数,可以画出阴影部分也切割出拼图形状的函数 let { w, r, sliderleft } = this; //w宽度,r圆的半径sliderleft是滑块的初始位置 //这地方用随机数每次显示的位置都不同 var x = this.random(30, width - w - r - 1); //这里最大值为了不让滑块进入隐藏所以要减去滑块的宽度 有个半圆所以要减去半圆位置 var y = this.random(10, height - w - r - 1); if (type == "clip") { //这里要保证在两个东西要在同一个y值上 x = sliderleft; y = this.y; } else { this.x = x; this.y = y; } let pi = math.pi; //绘制 ctx.beginpath(); //left ctx.moveto(x, y); //top ctx.arc(x + (w + 5) / 2, y, r, -pi, 0, true); ctx.lineto(x + w + 5, y); //right ctx.arc(x + w + 5, y + w / 2, r, 1.5 * pi, 0.5 * pi, false); ctx.lineto(x + w + 5, y + w); //bottom ctx.arc(x + (w + 5) / 2, y + w, r, 0, pi, false); ctx.lineto(x, y + w); ctx.arc(x, y + w / 2, r, 0.5 * pi, 1.5 * pi, true); ctx.lineto(x, y); if (type == "clip") { ctx.shadowblur = 10; ctx.shadowcolor = "black"; } ctx.linewidth = 1; ctx.fillstyle = "rgba(0, 0, 0, 0.4)"; //设置背景颜色 ctx.stroke(); ctx[type](); if (img) { //这里为什么要在这里加载图片呢,因为这个高度是动态的必须计算完之后在放进去 //还有个原因是你要先剪裁在加载图片 ctx.drawimage(img, -this.x, 0, width, height); } } halveAlpha(ImageData) { var alpha = 0.5 // var alpha = 1 for (var i = 0, len = ImageData.data.length; i < len; i += 4) { // 改变每个像素的透明度 ImageData.data[i + 1] = ImageData.data[i + 1] * alpha; ImageData.data[i + 2] = ImageData.data[i + 2] * alpha; } return ImageData; } 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() { this.canvasCtx.clearRect(0, 0, w, h) this.blockCtx.clearRect(0, 0, w, h) this.block.width = w } bindEvents() { this.el.onselectstart = () => false this.refreshIcon.onclick = () => { this.reset() } let originX, originY, trail = [], isMouseDown = false this.block.addEventListener('mousedown', function(e) { originX = e.x, originY = e.y isMouseDown = true }) 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 { //alert("验证失败"); addClass(this.sliderContainer, 'sliderContainer_fail') this.fail && this.fail(); //验证失败后,1秒后重新加载图片 setTimeout(() => { this.reset() }, 1000) } }) } 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() { 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))