JS滑动验证原理

滑动验证考察的知识点:

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;
View Code

推荐阅读:

网易易盾参考效果图: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

原文地址:https://www.cnblogs.com/terrymin/p/14968572.html