效果
思路
- 独立滑块和拼图,通过实例方法组合使用,滑块和拼图均通过指定元素容器加载内容
- 通过canvas 路径切片
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>拼图</title>
<style>
.slider-validator {
position: relative;
display: inline-block;
}
#refresh {
position: absolute;
top: 10px;
right: 20px;
width: 20px;
height: 20px;
background-image: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjY3MzYwNTYxMTA1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjI3MTEiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTcxLjczMzMzMyAyMjcuODRWMTUxLjQ2NjY2N2MwLTI1LjE3MzMzMy0xOS42MjY2NjctMzUuNDEzMzMzLTMzLjcwNjY2Ni0yNS4xNzMzMzRMMzI3LjY4IDI3Mi42NGEyMS4zMzMzMzMgMjEuMzMzMzMzIDAgMCAwIDAgMzUuNDEzMzMzbDIxMC4zNDY2NjcgMTQ2Ljc3MzMzNGEyMS43NiAyMS43NiAwIDAgMCAzMy43MDY2NjYtMTcuOTJWMzg2LjU2YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDEgMjMuMDQtMjEuMzMzMzMzIDI3MS43ODY2NjcgMjcxLjc4NjY2NyAwIDAgMSAyMzkuMzYgMjEwLjM0NjY2NiAyNjAuMjY2NjY3IDI2MC4yNjY2NjcgMCAwIDAgNi40LTU4LjAyNjY2NiAyNzEuMzYgMjcxLjM2IDAgMCAwLTI0OC43NDY2NjYtMjY4LjggMjAuOTA2NjY3IDIwLjkwNjY2NyAwIDAgMS0yMC4wNTMzMzQtMjAuOTA2NjY3ek00NDMuNzMzMzMzIDc5MC42MTMzMzN2ODUuMzMzMzM0YTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMzMuMjggMTcuNDkzMzMzbDIxMS4yLTE0Ny4yYTIxLjMzMzMzMyAyMS4zMzMzMzMgMCAwIDAgMC0zNC41NmwtMjExLjItMTQ3LjJhMjAuOTA2NjY3IDIwLjkwNjY2NyAwIDAgMC0zMy4yOCAxNy4wNjY2Njd2NTEuMmEyMC45MDY2NjcgMjAuOTA2NjY3IDAgMCAxLTIyLjYxMzMzMyAyMS4zMzMzMzMgMjcxLjc4NjY2NyAyNzEuNzg2NjY3IDAgMCAxLTIzOS43ODY2NjctMjEwLjM0NjY2NyAyNjAuMjY2NjY3IDI2MC4yNjY2NjcgMCAwIDAtNi40IDU4LjAyNjY2N0EyNzAuNTA2NjY3IDI3MC41MDY2NjcgMCAwIDAgNDIzLjY4IDc2OGEyMS43NiAyMS43NiAwIDAgMSAyMC4wNTMzMzMgMjIuNjEzMzMzeiIgZmlsbD0iIzY2NjY2NiIgcC1pZD0iMjcxMiI+PC9wYXRoPjwvc3ZnPg==");
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
cursor: pointer;
filter: brightness(0.1);
}
.loading-mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
background: rgba(255, 255, 255, 0.75);
}
.loading-mask svg {
animation: ani-loading 3s linear infinite;
}
@keyframes ani-loading {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
#puzzle {
position: relative;
border-radius: 15px;
overflow: hidden;
}
#drag-track {
position: relative;
height: 40px;
border: 1px solid #ddd;
border-radius: 3px;
background: #fff;
text-align: center;
line-height: 40px;
user-select: none;
}
.drag-wrap {
position: absolute;
top: 0;
left: 0;
background: rgba(224, 224, 224, 0.71);
}
.drag-wrap.success {
background: rgba(13, 143, 255, 0.82);
}
.drag-wrap.fail {
background: rgba(255, 110, 125, 0.83);
}
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
width: 40px;
height: 40px;
border-radius: 3px;
box-shadow: #9b9b9b 1px 1px 4px;
background: #fff;
cursor: pointer;
}
.drag-handle:after {
content: '';
display: block;
width: 100%;
height: 100%;
background: url("data:image/svg+xml;base64,PHN2ZyB0PSIxNjY3MzYwNjgyODU1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjM5MTkiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTY3LjMyNTA1IDU0Ny4xODUzNmMyMC45NzA2MTQtMjEuNDc5MTk3IDIwLjk3MDYxNC01Ni4zMDc0MjQgMC03Ny43OTA3MTRMMTg1LjI1MTE2OCA3Ny4xMTUzMzJjLTIwLjk3MTYzNy0yMS40NzcxNS01NC45NzUwNzktMjEuNDc3MTUtNzUuOTQ4NzYzIDAtMjAuOTczNjg0IDIxLjQ4NDMxNC0yMC45NzM2ODQgNTYuMzA5NDcgMCA3Ny43OTM3ODRsMzQ0LjE4ODAxNiAzNTMuMzgzNDQ2LTM0NC4xODgwMTYgMzUzLjM4NDQ2OWMtMjAuOTczNjg0IDIxLjQ4NDMxNC0yMC45NzM2ODQgNTYuMzExNTE3IDAgNzcuNzkyNzYgMjAuOTcxNjM3IDIxLjQ4MjI2NyA1NC45NzUwNzkgMjEuNDgyMjY3IDc1Ljk0ODc2MyAwbDM4Mi4wNzI4NTgtMzkyLjI4MDMzNyAwLjAwMTAyNC0wLjAwNDA5NHpNNDQwLjYwODAyIDE1NC45MDgwOTJsMzQ0LjE4NTk3IDM1My4zODM0NDYtMzQ0LjE4NTk3IDM1My4zODU0OTNjLTIwLjk3MzY4NCAyMS40ODQzMTQtMjAuOTczNjg0IDU2LjMxMTUxNyAwIDc3Ljc5Mjc2IDIwLjk3MjY2MSAyMS40ODIyNjcgNTQuOTc1MDc5IDIxLjQ4MjI2NyA3NS45NDk3ODYgMGwzODIuMDc0OTA1LTM5Mi4yODEzNjFjMjAuOTY2NTIxLTIxLjQ3ODE3NCAyMC45NjY1MjEtNTYuMzA3NDI0IDAtNzcuNzkwNzE0TDUxNi41NTU3NTkgNzcuMTE1MzMyYy0yMC45NzI2NjEtMjEuNDc3MTUtNTQuOTc1MDc5LTIxLjQ3NzE1LTc1Ljk0OTc4NiAwLTIwLjk3MTYzNyAyMS40ODMyOS0yMC45NzE2MzcgNTYuMzA5NDcgMC4wMDIwNDcgNzcuNzkyNzZ6IiBwLWlkPSIzOTIwIj48L3BhdGg+PC9zdmc+") no-repeat center /60% 60%;
opacity: 0.5;
}
.drag-handle:hover:after {
opacity: 1;
}
</style>
</head>
<body>
<div class="slider-validator">
<div id="puzzle"></div>
<div class="loading-mask">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
width="40" height="40">
<path d="M484 64h56c4.42 0 8 3.58 8 8v248c0 4.42-3.58 8-8 8h-56c-4.42 0-8-3.58-8-8V72c0-4.42 3.58-8 8-8z m0 632h56c4.42 0 8 3.58 8 8v248c0 4.42-3.58 8-8 8h-56c-4.42 0-8-3.58-8-8V704c0-4.42 3.58-8 8-8z m324.98-520.58l39.6 39.6c3.12 3.12 3.12 8.19 0 11.31L673.22 401.69c-3.12 3.12-8.19 3.12-11.31 0l-39.6-39.6c-3.12-3.12-3.12-8.19 0-11.31l175.36-175.36c3.13-3.13 8.19-3.13 11.31 0zM362.09 622.31l39.6 39.6c3.12 3.12 3.12 8.19 0 11.31L226.33 848.58c-3.12 3.12-8.19 3.12-11.31 0l-39.6-39.6c-3.12-3.12-3.12-8.19 0-11.31l175.36-175.36a7.985 7.985 0 0 1 11.31 0zM960 484v56c0 4.42-3.58 8-8 8H704c-4.42 0-8-3.58-8-8v-56c0-4.42 3.58-8 8-8h248c4.42 0 8 3.58 8 8z m-632 0v56c0 4.42-3.58 8-8 8H72c-4.42 0-8-3.58-8-8v-56c0-4.42 3.58-8 8-8h248c4.42 0 8 3.58 8 8z m520.58 324.98l-39.6 39.6c-3.12 3.12-8.19 3.12-11.31 0L622.31 673.22c-3.12-3.12-3.12-8.19 0-11.31l39.6-39.6c3.12-3.12 8.19-3.12 11.31 0l175.36 175.36c3.13 3.13 3.13 8.19 0 11.31zM401.69 362.09l-39.6 39.6c-3.12 3.12-8.19 3.12-11.31 0L175.42 226.33c-3.12-3.12-3.12-8.19 0-11.31l39.6-39.6c3.12-3.12 8.19-3.12 11.31 0l175.36 175.36a7.985 7.985 0 0 1 0 11.31z"
></path>
</svg>
</div>
<div id="refresh"></div>
</div>
<br>
<div id="drag-track" style="width: 400px"></div>
<script>
const PI = Math.PI, PI_05 = PI * 0.5, PI_15 = PI * 1.5
const _Vertices = [5, 1, 5, 1, 3, 0, 3, 2, 5, 5, 5, 5, 6, 3, 4, 3, 1, 5, 1, 5, 3, 6, 3, 4, 1, 1, 1, 1, 0, 3, 2, 3]
const createElement = (tag, attrs) => {
let $el = tag instanceof Node ? tag : document.createElement(tag)
if (attrs) {
Object.keys(attrs).forEach(k => $el.setAttribute(k, attrs[k]))
}
return $el
}
class Puzzle {
cr = 5
co = 2
Radian = null
DO = null
clipWidth = 0
clipHeight = 0
bgWidth = 0
bgHeight = 0
offsetLeft = 0
$Indicator = null
constructor(rootSelector) {
this.$Root = document.querySelector(rootSelector)
}
init({ cr = 5, co = 2, clipWidth = 60, clipHeight = 60, bgWidth = 300, bgHeight = 200 }) {
const vm = this
vm.cr = cr
vm.co = co
vm.clipWidth = clipWidth
vm.clipHeight = clipHeight
vm.bgWidth = bgWidth
vm.bgHeight = bgHeight
const _CR = Math.acos((cr - co) / cr)
vm.Radian = [
[PI_05 + _CR, PI_05 - _CR], [PI + _CR, PI - _CR], [_CR - PI_05, PI_15 - _CR], [_CR, -_CR],
[PI_15 - _CR, _CR - PI_05], [-_CR, _CR], [PI_05 - _CR, PI_05 + _CR], [PI - _CR, PI + _CR],
]
vm.DO = co - cr
}
setOffset(offsetLeft) {
this.$Indicator.style.left = `${offsetLeft}px`
}
load(src) {
const vm = this
const { bgWidth, bgHeight, clipWidth, clipHeight, DO, cr, Radian } = vm
if (!src) {
src = `https://picsum.photos/${bgWidth}/${bgHeight}`
}
const $ClipBg = createElement('canvas', { width: bgWidth, height: bgHeight })
const $ClipBlock = createElement('canvas', { width: bgWidth, height: bgHeight })
const $Indicator = createElement('img')
vm.$Root.style.cssText += `;width:${bgWidth}px;height:${bgHeight}px`
vm.$Root.innerHTML = ''
vm.$Root.append($ClipBg, $Indicator)
const boxCtx = $ClipBg.getContext('2d')
const blockCtx = $ClipBlock.getContext('2d')
const left = bgWidth / 2 + Math.random() * (bgWidth - clipWidth - cr * 2 - bgWidth / 2)
const top = bgHeight / 2 - clipHeight / 2
const xa = [left + DO, left, left - DO, left + clipWidth / 2, left + clipWidth + DO, left + clipWidth, left + clipWidth - DO]
const ya = [top + DO, top, top - DO, top + clipHeight / 2, top + clipHeight + DO, top + clipHeight, top + clipHeight - DO]
const _MODES = [1, 1, 1, 1].map(d => Math.random() > 0.5 ? 1 : -1)
let offsetLeft = _MODES[3] === 1 ? left - (cr - DO) : left;
$Indicator.style = `left:0;position:absolute;filter:drop-shadow(2px 5px 4px #000);transform:translateX(-${offsetLeft}px)`
vm.offsetLeft = offsetLeft
vm.$Indicator = $Indicator
let image = new Image()
image.crossOrigin = 'Anonymous';
image.src = src
return new Promise((resolve, reject) => {
image.onerror = reject
image.onload = _ => {
const draw = ctx => {
ctx.globalCompositeOperation = 'destination-over'
ctx.moveTo(left, top);
_MODES.forEach((type, index) => {
let _v = _Vertices.slice(index * 8, (index + 1) * 8)
if (type === 0) {
return ctx.lineTo(xa[_v[0]], ya[_v[1]]);
}
if (type > 0) {
ctx.arc(xa[_v[4]], ya[_v[5]], cr, ...(Radian[index]), false)
} else {
ctx.arc(xa[_v[6]], ya[_v[7]], cr, ...(Radian[index + 4]), true)
}
ctx.lineTo(xa[_v[2]], ya[_v[3]])
})
}
draw(blockCtx)
blockCtx.clip()
blockCtx.drawImage(image, 0, 0, bgWidth, bgHeight)
draw(boxCtx)
boxCtx.fillStyle = '#fff'
boxCtx.fill()
boxCtx.drawImage(image, 0, 0, bgWidth, bgHeight)
$Indicator.src = $ClipBlock.toDataURL('image/png', 1)
resolve()
}
})
}
}
class Slider {
$DragWrap = null
lock = false
turingTest = false
constructor({ root, endHandle, moveHandle }) {
const vm = this
let $Root = document.querySelector(root)
let $DragWrap = createElement('div', { class: 'drag-wrap' })
let $DragHandle = createElement('div', { class: 'drag-handle' })
$Root.innerHTML = '<span>向右拖动滑块填充拼图</span>'
$DragWrap.append($DragHandle)
$Root.append($DragWrap)
vm.$DragWrap = $DragWrap
let startX = 0, startY = 0, leftResult = 0
const MMH = e => {
leftResult = Math.min(Math.max(0, e.clientX - startX), $Root.clientWidth - $DragHandle.offsetWidth)
if (e.clientY - startY !== 0) {
vm.turingTest = true
}
$DragWrap.style.paddingLeft = `${leftResult}px`
moveHandle.call(this, leftResult)
}
const MUH = e => {
$Root.removeEventListener('mousemove', MMH)
$Root.removeEventListener('mouseup', MUH)
endHandle.call(this, leftResult)
vm.lock = true
}
$Root.addEventListener('mousedown', e => {
if (!vm.lock) {
startX = e.clientX
startY = e.clientY
$Root.addEventListener('mousemove', MMH)
$Root.addEventListener('mouseup', MUH)
}
})
}
reset() {
this.$DragWrap.classList.remove('fail', 'success')
this.$DragWrap.style.paddingLeft = `0px`
this.lock = false
}
setSuccess() {
this.$DragWrap.classList.remove('fail')
this.$DragWrap.classList.add('success')
}
setFail() {
this.$DragWrap.classList.remove('success')
this.$DragWrap.classList.add('fail')
}
}
function loadGroupPlugin() {
let pz = new Puzzle('#puzzle')
pz.init({
cr: 8,
co: 4,
clipWidth: 40,
clipHeight: 40,
bgWidth: 400,
bgHeight: 300,
})
let slider = new Slider({
root: '#drag-track',
endHandle(left) {
if (!this.turingTest) {
slider.setFail()
} else {
if (Math.abs(pz.offsetLeft - left) < 10) {
slider.setSuccess()
} else {
slider.setFail()
}
}
setTimeout(_ => {
slider.reset()
load()
}, 2000)
},
moveHandle(left) {
pz.setOffset(left)
}
})
let $loading = document.querySelector('.loading-mask')
let load = src => {
$loading.style.display = 'flex'
pz.load(src).then(d => {
slider.lock = false
$loading.style.display = 'none'
}).catch(d => {
$loading.innerText = '获取图片失败'
slider.lock = true
})
}
load('./c.jpg')
let $change = document.getElementById('refresh')
$change.addEventListener('click', _ => {
load()
})
}
loadGroupPlugin()
</script>
</body>
</html>