프런트 엔드 슬라이딩 퍼즐 검증 순수 js 결합 호출

프런트 엔드 슬라이딩 퍼즐 검증 순수 js 결합 호출

효과

사진 설명을 추가해주세요

생각의 기차

  • 독립적인 슬라이더와 퍼즐은 인스턴스 방식을 통해 조합하여 사용 슬라이더와 퍼즐 모두 지정된 요소 컨테이너를 통해 콘텐츠를 로드
  • 캔버스 경로로 슬라이스

코드

<!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
}

/**
 * 拼图控制类
 * [C]:突出|凹陷圆形
 */
class Puzzle {
      
      
  //[C]半径 circleRadio
  cr = 5
  //[C]嵌入边框偏移量 circleOffset
  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

    //radian [C]与边框的焦点和圆心的辐射角一半弧度值
    const _CR = Math.acos((cr - co) / cr)

    //[C]的绘制圆心集合
    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],
    ]
    //[C] 圆心偏移
    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]

    //随机各条边 [C]类型
    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) => {
      
      
            //0 1| 2  3 |4  5 |6  7
            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
    $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>

Supongo que te gusta

Origin blog.csdn.net/weixin_43954962/article/details/127652117
Recomendado
Clasificación