Vue_滑块拼图验证组件

效果

源码 https://files-cdn.cnblogs.com/files/linyisonger/Vue.Examples-master.zip

在这里插入图片描述

主要思路

当图片加载完成时,获取该图片的宽高,将后两个canvas的宽高重新赋值。 – 尺寸的自适应

将图片渲染到两个canvas上,背景canvas用来挖出拼图,前景canvas用来只保留拼图。

实现

  • 随机图片
  • 随机位置
  • 自定义图片集合
  • 自定义允许误差

主要代码

sliderVerification.vue

<template>
  <div class="container">
    <div class="image">
      <img ref="image" v-if="currentImage" :src="currentImage" @load="onImgLoad()" />
      <canvas ref="background"></canvas>
      <canvas ref="foreground" :style="{left:foregroundLeft }"></canvas>
    </div>

    <div
      class="slider-bar"
      @mousemove="mousemove"
      @mouseup="mouseup"
      @mouseleave="mouseleave"
      :style="{height:boxSideLength}"
    >
      <div class="text">拖动滑块完成拼图</div>
      <div
        class="box"
        :style="{left:sliderBoxLeft,height:boxSideLength,width:boxSideLength }"
        @mousedown="mousedown"
        @touchstart="touchstart"
        @touchmove="touchmove"
        @touchend="touchend"
      ></div>
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-property-decorator";
import { getRandom } from "./sliderVerification";
const defaultDeviation = 4;
@Component
export default class SliderVerification extends Vue {
  @Prop({ type: Number, default: defaultDeviation }) deviation?: number; // 偏差大小
  @Prop({ type: Array }) images?: Array<string>; // 所有的图片
  private currentImage = ""; // 当前的图片
  private randomX = 0; // 随机的位置
  private moveX = 0; // 移动的位置
  private moveStart = false; // 是否开始移动  MouseDown使用
  private moveStartX = 0; // 移动开始X位置
  private imgWidth = 0; // 图片宽度
  private imgHeight = 0; // 图片高度
  private boxSideLength = 40; // 移动块的变长和移动条的高
  private puzzleMargin = 60; // 拼图边距
  private puzzleSideLength = 30; // 拼图边长
  // 移动块位置
  get sliderBoxLeft() {
    return this.moveXInRange + "px";
  }
  // 前景位置
  get foregroundLeft() {
    return this.moveXInRange - this.randomX + this.boxSideLength / 2 + "px";
  }
  // 移动位置在范围内的值
  get moveXInRange() {
    if (this.moveX < 0) return 0;
    if (this.moveX > this.imgWidth - this.boxSideLength)
      return this.imgWidth - this.boxSideLength;
    return this.moveX;
  }
  created() {
    if (!this.images || this.images.length == 0) {
      console.error("图片不能为空");
      return;
    }
    this.randomImage();
  }

  // 图片加载完成回调
  onImgLoad() {
    const image = this.$refs.image as Element;
    const width = image.clientWidth;
    const height = image.clientHeight;
    const background = this.$refs.background as any;
    const foreground = this.$refs.foreground as any;
    background.width = width;
    background.height = height;
    foreground.width = width;
    foreground.height = height;

    this.imgWidth = width;
    this.imgHeight = height;

    const backgroundContext = background.getContext("2d");
    const foregroundContext = foreground.getContext("2d");
    const img = new Image();
    img.src = (image as any).currentSrc;
    img.onload = () => {
      backgroundContext.drawImage(img, 0, 0, width, height);
      const position = this.getPosition();
      this.randomX = position.x;
      this.cutOutPuzzle(backgroundContext, position);
      this.cutOutPuzzle(foregroundContext, position);
      foregroundContext.clip();
      foregroundContext.drawImage(img, 0, 0, width, height);
    };
  }
  // 剪切拼图形状
  cutOutPuzzle(context: any, position: { x: number; y: number }) {
    const x = position.x;
    const y = position.y;
    const puzzleSideLengthHalf = this.puzzleSideLength / 2;
    const puzzleSideLengthHalfOneThird = puzzleSideLengthHalf / 3;
    context.beginPath();
    context.moveTo(x + puzzleSideLengthHalf, y + puzzleSideLengthHalf);
    context.lineTo(x - puzzleSideLengthHalf, y + puzzleSideLengthHalf);
    context.lineTo(x - puzzleSideLengthHalf, y + puzzleSideLengthHalfOneThird);
    context.arc(
      x - puzzleSideLengthHalf,
      y,
      puzzleSideLengthHalfOneThird,
      0.5 * Math.PI,
      -0.5 * Math.PI
    );
    context.lineTo(x - puzzleSideLengthHalf, y - puzzleSideLengthHalf);
    context.lineTo(x - puzzleSideLengthHalfOneThird, y - puzzleSideLengthHalf);
    context.arc(
      x,
      y - puzzleSideLengthHalf,
      puzzleSideLengthHalfOneThird,
      -1 * Math.PI,
      0 * Math.PI
    );
    context.lineTo(x + puzzleSideLengthHalf, y - puzzleSideLengthHalf);
    context.closePath();
    context.stroke();
    context.clip();
    context.fillStyle = "rgba(0,0,0,.6)";
    context.fill();
  }
  // 随机图片
  randomImage() {
    this.images = this.images as Array<string>;
    const currentImage = this.images[getRandom(0, this.images.length - 1)];
    if (currentImage == this.currentImage) this.onImgLoad();
    this.currentImage = currentImage;
  }
  // 随机位置
  getPosition() {
    return {
      x: getRandom(this.puzzleMargin, this.imgWidth - this.puzzleMargin),
      y: getRandom(this.puzzleMargin, this.imgHeight - this.puzzleMargin)
    };
  }
  // 成功回调
  @Emit() success(message: string) {
    return message;
  }
  // 错误回调
  @Emit() fail(message: string) {
    return message;
  }
  // 触摸开始
  touchstart(event: any) {
    this.moveStartX = event.touches[0].clientX;
  }
  // 触摸移动
  touchmove(event: any) {
    this.moveX = event.touches[0].clientX - this.moveStartX;
  }
  // 触摸结束
  touchend(event: any) {
    // 核对
    if (this.verify()) return;
    this.resetMoveSetting();
  }
  // 鼠标按下
  mousedown(event: any) {
    this.moveStart = true;
    this.moveStartX = event.x;
  }
  // 鼠标移动
  mousemove(event: any) {
    if (!this.moveStart) return;
    this.moveX = event.x - this.moveStartX;
  }
  // 鼠标抬起
  mouseup(event: any) {
    // 核对
    if (this.verify()) return;
    this.resetMoveSetting();
  }
  // 鼠标离开
  mouseleave() {
    this.resetMoveSetting();
  }
  // 验证位置 ✔
  verify() {
    this.deviation = this.deviation || defaultDeviation;
    if (
      Math.abs(this.moveXInRange - this.randomX + this.boxSideLength / 2) <
      this.deviation
    ) {
      this.success("verify success !");
      return true;
    }
    this.fail("verify fail !");
    return false;
  }
  //重置移动位置
  resetMoveSetting() {
    this.moveStart = false;
    this.moveX = 0;
    this.moveStartX = 0;
  }
}
</script>
<style lang="scss" scoped>
.container {
  width: 100%;
  height: 100%;
  background-color: #fff;
  .image {
    width: 100%;
    box-sizing: border-box;
    position: relative;
    img {
      width: 100%;
    }
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  }
  .slider-bar {
    width: 100%;
    background-color: #f8f8f8;
    position: relative;
    height: 40px;
    margin-top: 10px;
    /** 防止选中文字 */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    .text {
      height: 40px;
      text-align: center;
      line-height: 40px;
    }
    .box {
      position: absolute;
      top: 0;
      left: 0;
      height: 40px;
      width: 40px;
      background-color: #fff;
      box-shadow: 0 0 10px #ddd;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
    }
  }
}
</style>

使用

main.ts

import Vue from 'vue'
import App from './App.vue'
import sliderVerification from './components/sliderVerification/index'
Vue.config.productionTip = false
Vue.use(sliderVerification)
new Vue({
  render: h => h(App),
}).$mount('#app')

App.vue

<template>
  <div id="app">
    <div class="slider-verification-box">
      <SliderVerification
      ref="sliderVerification"
        :images="[require('./assets/xiaoai.jpeg'),require('./assets/xiaoai1.png')]"
        @success="success"
        @fail="fail"
      ></SliderVerification>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "./components/HelloWorld.vue";

@Component({
  components: {
    HelloWorld
  }
})
export default class App extends Vue {
  created() {}
  success(message: string) {
    console.log(message);
  }
  fail(message: string) {
    console.log(message);
    (this.$refs.sliderVerification as any).randomImage();
  }
}
</script>

<style lang="scss">
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
}
#app{
  display: flex;
  align-items: center;
  justify-content: center;
}
.slider-verification-box {
  box-sizing: border-box;
  width: 350px;
  padding: 10px;
  border: 1px solid #f4f4f4;
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林一怂儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值