代码结构
- 接口
- 主画布
- 滑块验证视图区域
- 滑块拖动条范围区
- 滑块
- 滑动验证组件组装
接口schema.tsx
import {
TStringDefaultType,
IStringConfigType
} from "@/components/FormComponents/types";
export type TSliderCaptchaEditData = Array<
IStringConfigType
>;
export interface ISliderCaptchaConfig {
width: TStringDefaultType;
height: TStringDefaultType;
}
export interface ISliderCaptchaSchema {
editData: TSliderCaptchaEditData;
config: ISliderCaptchaConfig;
}
const SliderCaptcha: ISliderCaptchaSchema = {
editData: [
{
key: "width",
name: "宽度",
type: "String"
},
{
key: "height",
name: "高度",
type: "String"
}
],
config: {
width: '300px',
height: '200px'
}
};
export default SliderCaptcha;
主画布
const MainCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
return (
<canvas width={props.width} height={props.height} ref={ref} />
)
})
滑块验证视图区域
const SliderCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
return (
<canvas style={{position: 'absolute', top: '0', left: '0'}} width={props.width} height={props.height} ref={ref} />
)
})
滑块拖动条范围区
const SliderBar = React.forwardRef((props:ISliderCaptchaConfig, ref) => {
return <div style={{width: props.width, height: '40px', background: '#f0f0f0', position: 'absolute', top: props.height}} />
})
滑块
const Slider = React.forwardRef<HTMLDivElement, Sliderprop>((props, ref) => {
return (
<div onMouseDown={props.onMouseDown} onMouseMove={props.onMouseMove} onMouseUp={props.onMouseUp} ref={ref} style={{width: '50px', height: '40px', background: '#fff', border: '1px solid #ccc', position: 'absolute', cursor: 'pointer', top: props.height, left: '0'}} />
)
})
滑动验证组件组装
- 初始化状态
- 定义drawCanvasimg函数
- 定义mousedown,mousemove,mouseup事件
const SliderCaptcha = ( props:ISliderCaptchaConfig) => {
const { width,height } = props;
// dom
const canvasRef = useRef<HTMLCanvasElement>(null)
const sliderCanvasRef = useRef<HTMLCanvasElement>(null)
const sliderRef = useRef<HTMLDivElement>(null)
// 拖动状态
const draggingRef = useRef(false)
// 验证状态
const VerifiedRef = useRef(false)
// 滑块偏移量
const offsetRef = useRef(0)
// 目标偏移量
const targetOffsetRef = useRef(0)
// 开始位置
let startX = 0
// 滑块位置
let sliderLeft = 0
const classNames = 'captcha-container'
//初始化画布渲染
useEffect(() => {
drawCanvasimg()
}, [])
//此处画布背景使用image函数,并且onload异步加载
const drawCanvasimg = useCallback(() => {
let img = new window.Image()
img.src = sliderImg
img.onload = () => {
drawCanvas(img)
}
}, [])
//ctx主画布绘制背景,绘制空缺区域,
//sliderCtx绘制滑块并置于初始位置,
const drawCanvas = (img: HTMLImageElement) => {
if (!canvasRef.current || !sliderCanvasRef.current || !sliderRef.current) return;
const ctx = canvasRef.current.getContext('2d')
const sliderCtx = sliderCanvasRef.current.getContext('2d')
if (!ctx || !sliderCtx) return;
// 清除画布
sliderCtx.clearRect(0, 0, sliderCanvasRef.current.width, sliderCanvasRef.current.height)
// 绘制背景
ctx.drawImage(img, 0, 0, canvasRef.current.width, canvasRef.current.height)
// 绘制目标区域
if (!targetOffsetRef.current) {
targetOffsetRef.current = Math.floor(Math.random() * (canvasRef.current.width - sliderRef.current.clientWidth))
// targetOffsetRef.current = 123
}
ctx.fillStyle = '#e8e8e8' // semi-transparent white
ctx.fillRect(targetOffsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
sliderRef.current.clientWidth, sliderRef.current.clientHeight)
// 绘制滑块
sliderCtx.drawImage(img, targetOffsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
sliderRef.current.clientWidth, sliderRef.current.clientHeight, offsetRef.current, (canvasRef.current.height - sliderRef.current.clientHeight) / 2,
sliderRef.current.clientWidth, sliderRef.current.clientHeight)
}
//滑块按下事件
//记录滑块初识位置startX,滑块左侧偏移量sliderLeft
const slidermouseDown = (e: React.MouseEvent | React.TouchEvent) => {
if (VerifiedRef.current) return
console.log('mouseDown')
draggingRef.current = true
startX = 'clientX' in e ? e.clientX : e.touches[0].clientX
sliderLeft = sliderRef.current?.offsetLeft || 0
}
//滑块移动事件
//记录触发事件e移动的距离,并计算滑块最新左侧偏移量,重绘画布
const onMouseMove = (e: React.MouseEvent | React.TouchEvent) => {
if (!draggingRef.current || !sliderRef.current) return;
const currentX = 'clientX' in e ? e.clientX : e.touches[0].clientX
let moveX = currentX - startX
let newLeft = sliderLeft + moveX
// 限制滑动范围
if (newLeft < 0) newLeft = 0
if (newLeft > parseInt(width) - sliderRef.current.clientWidth) {
newLeft = parseInt(width) - sliderRef.current.clientWidth
}
offsetRef.current = newLeft
sliderRef.current.style.left = `${newLeft}px`
drawCanvasimg()
}
//滑块松开事件
//判断滑块是否和预留空缺重合
const onMouseUp = () => {
if (!draggingRef.current || !sliderRef.current) return;
draggingRef.current = false
// 验证是否滑到正确位置
if (Math.abs(offsetRef.current - targetOffsetRef.current) < 5) {
VerifiedRef.current = true
sliderRef.current.style.background = '#91d5ff'
alert('验证成功!')
// if (typeof onSuccess === 'function') {
// onSuccess()
// }
} else {
offsetRef.current = 0
sliderRef.current.style.left = '0'
drawCanvasimg()
}
}
return (
<div className={classNames} >
<MainCanvas ref={canvasRef} width={width} height={height} />
<SliderCanvas ref={sliderCanvasRef} width={width} height={height} />
<SliderBar width={width} height={height} />
<Slider onMouseDown={slidermouseDown} onMouseMove={onMouseMove} onMouseUp={onMouseUp} ref={sliderRef} width={width} height={height} />
</div>
)
}
drawImage函数用法
drawImage 是 HTML5 Canvas API 中的一个方法,用于在画布上绘制图像。它可以绘制完整的图像,也可以绘制图像的某一部分,并且支持缩放和裁剪。以下是 drawImage 的详细用法:
- 绘制完整图像
context.drawImage(image, dx, dy);
image:要绘制的图像(HTMLImageElement、HTMLCanvasElement或HTMLVideoElement)。dx、dy:图像在画布上的目标坐标(左上角)。
- 绘制缩放图像
context.drawImage(image, dx, dy, dWidth, dHeight);
dWidth、dHeight:图像在画布上的目标宽度和高度(缩放)。
- 绘制图像的某一部分并缩放
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
sx、sy:源图像中裁剪区域的左上角坐标。sWidth、sHeight:源图像中裁剪区域的宽度和高度。dx、dy:裁剪区域在画布上的目标坐标。dWidth、dHeight:裁剪区域在画布上的目标宽度和高度(缩放)。
index.tsx
import React, { useCallback, useEffect, useRef, useMemo } from 'react'
import { ISliderCaptchaConfig } from "./schema";
import sliderImg from "@/assets/test.png";
import './index.less'
const MainCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
return (
<canvas width={props.width} height={props.height} ref={ref} />
)
})
const SliderCanvas = React.forwardRef<HTMLCanvasElement, ISliderCaptchaConfig>((props, ref) => {
.......
})
const Slider = React.forwardRef<HTMLDivElement, Sliderprop>((props, ref) => {
......
})
interface Sliderprop {
width: string,
height: string,
onMouseDown: React.MouseEventHandler<HTMLDivElement>,
onMouseMove: React.MouseEventHandler<HTMLDivElement>,
onMouseUp: React.MouseEventHandler<HTMLDivElement>
}
const SliderBar = React.forwardRef((props:ISliderCaptchaConfig, ref) => {
......
})
const SliderCaptcha = ( props:ISliderCaptchaConfig) => {
......
}
export default SliderCaptcha
index.less
.captcha-container {
position: relative;
text-align: center;
}