示例
需求分析
在已有滑块组件的基础上,实现拼图验证组件,主要需求如下:
- 拼图组件 DOM 结构基于 canvas 实现,具体如下:
- canvas1 用于绘制背景图,作为拼图的底图,展示完整的图像。
- canvas2 用于绘制抠图,即拼图中需要拖动拼合的部分。
- 拼图组件的属性
- 背景图的 img :指定背景图的图片路径,用于在 canvas1 上绘制。
- 抠图的 img :指定抠图的图片路径,在 canvas2 上绘制。
- 抠图的 width :定义抠图的宽度,默认值为 40,可根据实际需求调整。
- 抠图的 Y 轴坐标 :确定抠图在背景图中的垂直位置,默认值为 0。
- 拼图组件的事件和方法
- 移动结束的事件 :当用户完成拖动拼图块的操作时触发,传递滑块的状态信息。
- 重置的方法 :用于将拼图组件恢复到初始状态,如将拼图块移回起始位置等。
实现细节
定义props
export interface PuzzleVerifyProps {
puzzleImg: string;
blockImg: string;
blockY?: number;
}
定义 emits
export interface PuzzleVerifyEmits {
(e: "moveEnd", state: SliderState, scaleX: number, scaleY: number): void;
}
定义expose
export interface PuzzleVerifyExpose {
reset: () => void;
}
编写template
<template>
<div ref="puzzle" :class="classes">
<canvas ref="puzzleCanvas" class="puzzle"></canvas>
<canvas ref="blockCanvas" class="block"></canvas>
<LdSliderVerify
ref="slide"
:draggable="true"
@thumbEnd="thumbEnd"
@thumbMove="thumbMove"
/>
</div>
</template>
组件样式
// config
@use "../../styles/variables" as *;
.ld-puzzle-verify {
position: relative;
.puzzle {
width: 100%;
height: 100%;
}
.block {
position: absolute;
top: 0;
left: 0;
}
}
组件源码
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from "vue";
import classNames from "classnames";
import LdSliderVerify, { type SliderState } from "./SliderVerify.vue";
export interface PuzzleVerifyProps {
puzzleImg: string;
blockImg: string;
blockY?: number;
}
export interface PuzzleVerifyEmits {
(e: "moveEnd", state: SliderState, scaleX: number, scaleY: number): void;
}
export interface PuzzleVerifyExpose {
reset: () => void;
}
defineOptions({
name: "LdPuzzleVerify",
});
const slide = ref<typeof LdSliderVerify | null>(null);
const puzzle = ref<HTMLDivElement | null>(null);
const puzzleCanvas = ref<HTMLCanvasElement | null>(null);
const blockCanvas = ref<HTMLCanvasElement | null>(null);
const puzzleCtx = ref<CanvasRenderingContext2D | null>(null);
const blockCtx = ref<CanvasRenderingContext2D | null>(null);
const scaleX = ref<number>(1);
const scaleY = ref<number>(1);
const props = withDefaults(defineProps<PuzzleVerifyProps>(), {
blockY: 0,
});
const classes = computed(() => {
return classNames("ld-puzzle-verify");
});
const emits = defineEmits<PuzzleVerifyEmits>();
const loadImage = (src: string) => {
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => resolve(img);
img.onerror = reject;
});
};
const drawImage = () => {
if (
!puzzleCtx.value ||
!blockCtx.value ||
!props.puzzleImg ||
!props.blockImg
)
return;
Promise.all([loadImage(props.puzzleImg), loadImage(props.blockImg)]).then(
(res) => {
const [puzzleImg, blockImg] = res;
// 宽高比
const aspectRatio = puzzleImg.width / puzzleImg.height;
puzzleCanvas.value!.width = puzzle.value!.offsetWidth;
puzzleCanvas.value!.height = puzzle.value!.offsetWidth / aspectRatio;
// 缩放比
scaleX.value = puzzleCanvas.value!.width / puzzleImg.width;
scaleY.value = puzzleCanvas.value!.height / puzzleImg.height;
blockCanvas.value!.width = blockImg.width * scaleX.value;
blockCanvas.value!.height = puzzleCanvas.value!.height;
puzzleCtx.value!.drawImage(
puzzleImg,
0,
0,
puzzleImg.width,
puzzleImg.height,
0,
0,
puzzleCanvas.value!.width,
puzzleCanvas.value!.height,
);
blockCtx.value!.drawImage(
blockImg,
0,
0,
blockImg.width,
blockImg.height,
0,
props.blockY * scaleY.value,
blockImg.width * scaleX.value,
blockImg.height * scaleY.value,
);
},
);
};
const thumbMove = (thumbState: SliderState) => {
blockCanvas.value!.style.left = thumbState.moveX + "px";
};
const thumbEnd = (thumbState: SliderState) => {
emits("moveEnd", thumbState, scaleX.value, scaleY.value);
};
const reset = () => {
blockCanvas.value!.style.left = "0px";
slide.value!.reset();
};
onMounted(() => {
puzzleCtx.value = puzzleCanvas.value!.getContext("2d");
blockCtx.value = blockCanvas.value!.getContext("2d");
drawImage();
});
watch(() => props.puzzleImg, drawImage);
defineExpose<PuzzleVerifyExpose>({
reset,
});
</script>
感谢阅读,敬请斧正!