效果
源码 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>