滑动验证考察的知识点:
1 JavaScript事件:
onmousedown:鼠标按下事件
onmousemove:鼠标移动事件
onmouseup:鼠标抬起事件
onMouseLeave:鼠标离开事件
2 CSS中 canvas 图片裁剪
效果图:
代码实现:
1 import React, { Component } from 'react'; 2 import { Icon, Spin } from 'antd'; 3 import './index.less'; 4 import SlideImgOnePng from '@/assets/images/slide_img_one.png'; 5 import SlideImgTwoPng from '@/assets/images/slide_img_two.png'; 6 import SlideImgThreePng from '@/assets/images/slide_img_three.png'; 7 import SlideImgFourPng from '@/assets/images/slide_img_four.png'; 8 import SlideImgFivePng from '@/assets/images/slide_img_five.png'; 9 import MyIcon from '@/global'; 10 import classNames from 'classnames'; 11 12 // const canWidth = 345; // 容器宽 13 // const canHeight = 215; // 容器高 14 const canWidth = 300; // 容器宽 15 const canHeight = 160; // 容器高 16 const canLitWidth = 42; // 图片滑块宽 17 const canLitR = 10; // 滑块附带小圆半径 18 const canLitL = canLitWidth + canLitR * 2 + 3; // 小拼图实际边长 19 20 // eslint-disable-next-line prefer-destructuring 21 const PI = Math.PI; // 圆周率 22 23 const time = 500; // 延迟时间 24 25 class BlockMove extends Component { 26 constructor(props) { 27 super(props); 28 console.log('BlockMove', props); 29 this.state = { 30 blockX: 0, // 小拼图X轴坐标 31 textMess: '向右移动拼接图片,完成验证', 32 type: 'process', // 验证状态,false为验证失败,true验证成功 33 loading: false, // 加载状态 34 canvasRand: '', 35 blockRand: '', 36 imgPath: '', 37 defaultImgList: [ 38 SlideImgOnePng, 39 SlideImgTwoPng, 40 SlideImgThreePng, 41 SlideImgFourPng, 42 SlideImgFivePng, 43 ], 44 limitEmit: true, 45 }; 46 47 this.y = 0; // 小拼图达到的Y轴坐标 48 this.x = 0; // 小拼图达到的X轴坐标 49 this.img = null; 50 this.limitEmit = true; // 触发频率 51 } 52 53 componentDidMount() { 54 this.props.onRef(this); 55 this.onMouseDown(); 56 this.init(); 57 } 58 59 init = () => { 60 this.y = 0; 61 this.x = 0; 62 this.setState( 63 { 64 type: 'process', 65 blockX: 0, 66 textMess: '向右移动拼接图片,完成验证', 67 canvasRand: `canvas${this.getRandomNumberByRange(0, 100)}`, 68 blockRand: `block${this.getRandomNumberByRange(101, 200)}`, 69 limitEmit: true, 70 }, 71 () => { 72 this.getImg(); 73 }, 74 ); 75 }; 76 77 /** 78 * @name onMouseDown 79 * @description 监听鼠标点击 80 */ 81 onMouseDown = () => { 82 const outBox = document.getElementById('out_mouse_img'); 83 const mouseBox = document.getElementById('mouse_img'); 84 const that = this; 85 86 // 鼠标按下操作 87 mouseBox.onmousedown = function (ev) { 88 console.log(ev); 89 const ev00 = ev || window.event; 90 const px = ev00.pageX; // 初始位置,对于整个页面来说,光标点击位置 91 const oL = this.offsetLeft; // 初始位置,对于有定位的父级,元素边框侧与父级边框侧的距离 初始为0 92 console.log(px, oL); 93 94 // 鼠标移动操作 95 document.onmousemove = function (evs) { 96 console.log(evs); 97 if (that.state.type === 'success') { 98 mouseBox.onmousedown = null; 99 document.onmousemove = null; 100 return; 101 } 102 const ev01 = evs || window.event; 103 const px1 = ev01.pageX; // 滑动后,当前鼠标所在位置 104 let oL1 = px1 - px + oL; // 距初始位置移动的距离 105 if (oL1 <= 0) { 106 oL1 = 0; 107 } else if (oL1 > outBox.clientWidth - mouseBox.clientWidth) { 108 oL1 = outBox.clientWidth - mouseBox.clientWidth; 109 } 110 that.setState({ blockX: oL1 }); 111 }; 112 113 // 鼠标放开 114 document.onmouseup = function () { 115 console.log('onmouseup'); 116 // document.onmousemove = null; 117 // document.onmouseup = null; 118 that.cancelMove(); 119 }; 120 }; 121 }; 122 123 /** 124 * @name 验证成功 125 * @description 验证成功,信息变化 126 */ 127 validateSuccess = () => { 128 const { limitEmit } = this.state; 129 console.log(limitEmit); 130 if (limitEmit) { 131 setTimeout(() => { 132 this.props.onSuccess(); 133 }, time); 134 } 135 this.setState({ 136 textMess: '验证成功', 137 type: 'success', 138 limitEmit: false, 139 }); 140 }; 141 142 /** 143 * @name 取消滑动 144 * @description 鼠标离开滑块,滑块停止滑动并复位 145 */ 146 cancelMove = () => { 147 const mouseBox = document.getElementById('mouse_img'); 148 const mouseLeft = mouseBox.offsetLeft; 149 console.log(mouseLeft, this.x); 150 document.onmousemove = null; 151 document.onmouseup = null; 152 if (mouseLeft !== 0) { 153 if (mouseLeft === this.x || (mouseLeft <= this.x + 3 && mouseLeft >= this.x - 3)) { 154 this.validateSuccess(); 155 } else { 156 console.log('init'); 157 this.setState({ 158 type: 'fail', 159 textMess: '验证失败', 160 }); 161 setTimeout(() => { 162 this.init(); 163 }, time); 164 } 165 } 166 }; 167 168 // 鼠标移开 169 onLeaveBlock = () => { 170 const mouseBox = document.getElementById('mouse_img'); 171 const mouseLeft = mouseBox.offsetLeft; 172 console.log('onLeaveBlock', mouseLeft, this.x); 173 if (mouseLeft !== 0) { 174 if (mouseLeft === this.x || (mouseLeft <= this.x + 3 && mouseLeft >= this.x - 3)) { 175 document.onmousemove = null; 176 document.onmouseup = null; 177 this.validateSuccess(); 178 } 179 } else { 180 console.log('leaveMouse'); 181 } 182 }; 183 184 /** 185 * @name getRandomNumberByRange 186 * @description 获取随机数 187 */ 188 getRandomNumberByRange = (start, end) => { 189 return Math.round(Math.random() * (end - start) + start); 190 }; 191 192 /** 193 * @name getImg 194 * @description 获取图片资源 195 * @description 196 */ 197 getImg = async () => { 198 const { defaultImgList } = this.state; 199 const index = Math.floor(Math.random() * defaultImgList.length); 200 this.setState( 201 { 202 imgPath: defaultImgList[index], 203 }, 204 () => { 205 this.drawInit(); 206 }, 207 ); 208 }; 209 210 /** 211 * @name drawInit 212 * @description 画图前处理 213 */ 214 drawInit = async () => { 215 const { canvasRand, blockRand, imgPath } = this.state; 216 const mycanvas = document.getElementById(canvasRand); 217 const myblock = document.getElementById(blockRand); 218 myblock.width = canWidth; // 等宽获取整个图片 219 const canvasCtx = mycanvas.getContext('2d'); 220 const blockCtx = myblock.getContext('2d'); 221 // 清空画布 222 canvasCtx.clearRect(0, 0, canWidth, canHeight); 223 blockCtx.clearRect(0, 0, canWidth, canHeight); 224 // 创建小图片滑块 225 this.img = document.createElement('img'); 226 // 随机位置创建拼图形状 227 this.x = this.getRandomNumberByRange(canLitL + 10, canWidth - (canLitL + 10)); 228 this.y = this.getRandomNumberByRange(10 + canLitR * 2, canHeight - (canLitL + 10)); 229 // console.log(this.x, this.y); 230 231 // 渲染图片 232 this.img.onload = async () => { 233 canvasCtx.drawImage(this.img, 0, 0, canWidth, canHeight); 234 blockCtx.drawImage(this.img, 0, 0, canWidth, canHeight); 235 const yAxis = this.y - canLitR * 2 - 1; // 小拼图实际的坐标 236 // console.log(yAxis); 237 const ImgData = blockCtx.getImageData(this.x - 4, yAxis - 1, canLitL, canLitL); 238 myblock.width = canLitL; // 小拼图的宽,隐藏抠图位置图片 239 blockCtx.putImageData(ImgData, 0, yAxis); 240 }; 241 this.img.src = imgPath; // 图片路径 242 243 this.draw(canvasCtx, this.x, this.y - 2, canLitWidth, 'fill'); 244 this.draw(blockCtx, this.x, this.y, canLitWidth, 'clip'); 245 }; 246 247 /** 248 * @name draw 249 * @description 画图公用方法 250 */ 251 draw = (ctx, x = 0, y = 0, w = 0, operation) => { 252 const r = canLitR; 253 ctx.beginPath(); 254 ctx.moveTo(x, y); 255 ctx.arc(x + w / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI); 256 ctx.lineTo(x + w, y); 257 ctx.arc(x + w + r - 2, y + w / 2, r, 1.21 * PI, 2.78 * PI); 258 ctx.lineTo(x + w, y + w); 259 ctx.lineTo(x, y + w); 260 ctx.arc(x + r - 2, y + w / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true); 261 ctx.lineTo(x, y); 262 ctx.lineWidth = 2; 263 ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; 264 ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'; 265 ctx.stroke(); 266 ctx.globalCompositeOperation = 'destination-over'; 267 268 // eslint-disable-next-line no-unused-expressions 269 operation === 'fill' ? ctx.fill() : ctx.clip(); 270 }; 271 272 render() { 273 console.log(this.state); 274 const { textMess, type, blockX, loading, canvasRand, blockRand } = this.state; 275 return ( 276 <div className="outDiv"> 277 <Spin spinning={loading}> 278 <div className="outDivNext"> 279 <MyIcon type="icon-refresh" className="my-icon-refresh" onClick={this.init} /> 280 <canvas id={canvasRand} className="outDivNoborder"></canvas> 281 <canvas id={blockRand} className="outDivLitBlock" style={{ left: blockX }}></canvas> 282 </div> 283 284 {/** 移动滑块 */} 285 <div id="out_mouse_img" className="outBkock" onMouseLeave={this.onLeaveBlock}> 286 <div 287 id="mouse_img" 288 className={classNames('moveBlock', type === 'fail' && 'move-fail-btn')} 289 style={{ marginLeft: blockX }} 290 > 291 {type === 'success' && <MyIcon type="icon-right-arrow" className="icon_check" />} 292 293 {type === 'process' && <MyIcon type="icon-more" className="icon_check" />} 294 295 {type === 'fail' && <MyIcon type="icon-fail" className="icon_check" />} 296 </div> 297 298 {/** 默认背景 */} 299 <div className="default-btn"> 300 {type === 'process' && <div>{blockX === 0 && textMess}</div>} 301 </div> 302 303 {/** 移动之后背景 */} 304 <div 305 className={classNames('default-btn move-btn', type === 'fail' && 'fail-btn')} 306 style={{ blockX }} 307 > 308 {type !== 'process' && <div style={{ color: '#fff' }}>{textMess}</div>} 309 </div> 310 </div> 311 </Spin> 312 </div> 313 ); 314 } 315 } 316 export default BlockMove;
推荐阅读:
网易易盾参考效果图:https://dun.163.com/trial/jigsaw
CSS canvas图片裁剪原理:https://www.cnblogs.com/huanglei-/p/8568405.html
JS实现拖动滑块验证:https://blog.csdn.net/tel13259437538/article/details/79822694