效果
思路
独立滑块和拼图,通过实例方法组合使用,滑块和拼图均通过指定元素容器加载内容 通过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 = crvm. co = covm. clipWidth = clipWidthvm. clipHeight = clipHeightvm. bgWidth = bgWidthvm. bgHeight = bgHeightconst _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 } = vmif ( ! 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 = offsetLeftvm. $Indicator = $Indicatorlet image = new Image ( ) image. crossOrigin = 'Anonymous' ; image. src = srcreturn new Promise ( ( resolve, reject ) => { image. onerror = rejectimage. 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 = $DragWraplet 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. clientXstartY = 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>