|
@@ -0,0 +1,396 @@
|
|
|
+import React from "react";
|
|
|
+import { Spin } from "antd";
|
|
|
+import { sldComLanguage } from "@/utils/utils";
|
|
|
+import styles from "./index.less";
|
|
|
+
|
|
|
+const PI = Math.PI;
|
|
|
+
|
|
|
+function sum(x, y) {
|
|
|
+ return x + y;
|
|
|
+}
|
|
|
+function square(x) {
|
|
|
+ return x * x;
|
|
|
+}
|
|
|
+
|
|
|
+class SlideVerify extends React.Component {
|
|
|
+ constructor(props) {
|
|
|
+ super(props);
|
|
|
+ this.state = {
|
|
|
+ success: false,
|
|
|
+ containerActive: false,
|
|
|
+ containerSuccess: false,
|
|
|
+ containerFail: false,
|
|
|
+ sliderLeft: 0,
|
|
|
+ sliderWidth: 0,
|
|
|
+ loadBlock: true,
|
|
|
+ blockX: 0,
|
|
|
+ blockY: 0,
|
|
|
+ };
|
|
|
+
|
|
|
+ this.origin = { x: 0, y: 0 };
|
|
|
+ this.isMouseDown = false;
|
|
|
+ this.timestamp = 0;
|
|
|
+ this.trail = [];
|
|
|
+
|
|
|
+ this.blockRef = React.createRef();
|
|
|
+ this.canvasRef = React.createRef();
|
|
|
+
|
|
|
+ this.img = null;
|
|
|
+
|
|
|
+ this.handleStart = this.handleStart.bind(this);
|
|
|
+ this.handleMove = this.handleMove.bind(this);
|
|
|
+ this.handleEnd = this.handleEnd.bind(this);
|
|
|
+ this.reset = this.reset.bind(this);
|
|
|
+ this.refresh = this.refresh.bind(this);
|
|
|
+ }
|
|
|
+
|
|
|
+ componentDidMount() {
|
|
|
+ this.initCanvas();
|
|
|
+
|
|
|
+ document.addEventListener("mousemove", this.handleMove);
|
|
|
+ document.addEventListener("mouseup", this.handleEnd);
|
|
|
+
|
|
|
+ // 如果需要支持移动端,也加对应touch事件监听
|
|
|
+ document.addEventListener("touchmove", this.handleMove, { passive: false });
|
|
|
+ document.addEventListener("touchend", this.handleEnd);
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillUnmount() {
|
|
|
+ document.removeEventListener("mousemove", this.handleMove);
|
|
|
+ document.removeEventListener("mouseup", this.handleEnd);
|
|
|
+
|
|
|
+ document.removeEventListener("touchmove", this.handleMove);
|
|
|
+ document.removeEventListener("touchend", this.handleEnd);
|
|
|
+ }
|
|
|
+
|
|
|
+ draw = (ctx, x, y, l, r, operation) => {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(x, y);
|
|
|
+ ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
|
|
|
+ ctx.lineTo(x + l, y);
|
|
|
+ ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
|
|
|
+ ctx.lineTo(x + l, y + l);
|
|
|
+ ctx.lineTo(x, y + l);
|
|
|
+ ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
|
|
|
+ ctx.lineTo(x, y);
|
|
|
+ ctx.lineWidth = 2;
|
|
|
+ ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
|
|
|
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
|
|
|
+ ctx.stroke();
|
|
|
+ ctx[operation]();
|
|
|
+ ctx.globalCompositeOperation = "destination-over";
|
|
|
+ };
|
|
|
+
|
|
|
+ getRandomNumberByRange = (start, end) => {
|
|
|
+ return Math.round(Math.random() * (end - start) + start);
|
|
|
+ };
|
|
|
+
|
|
|
+ getRandomImg = (imgs, w, h) => {
|
|
|
+ if (!imgs || imgs.length === 0) {
|
|
|
+ return `https://picsum.photos/300/150?random=${Date.now()}`;
|
|
|
+ }
|
|
|
+ const idx = getRandomNumberByRange(0, imgs.length - 1);
|
|
|
+ return imgs[idx];
|
|
|
+ };
|
|
|
+
|
|
|
+ createImg = (imgs, onload) => {
|
|
|
+ const img = document.createElement("img");
|
|
|
+ img.crossOrigin = "Anonymous";
|
|
|
+ img.onload = onload;
|
|
|
+ img.onerror = () => {
|
|
|
+ img.src = this.getRandomImg(imgs);
|
|
|
+ };
|
|
|
+ img.src = this.getRandomImg(imgs);
|
|
|
+ return img;
|
|
|
+ };
|
|
|
+
|
|
|
+ initCanvas() {
|
|
|
+ const { w, h, l, r, imgs } = this.props;
|
|
|
+ const canvas = this.canvasRef.current;
|
|
|
+ const block = this.blockRef.current;
|
|
|
+ if (!canvas || !block) return;
|
|
|
+
|
|
|
+ this.canvasCtx = canvas.getContext("2d");
|
|
|
+ this.blockCtx = block.getContext("2d", { willReadFrequently: true });
|
|
|
+
|
|
|
+ this.img = this.createImg(imgs, () => {
|
|
|
+ this.setState({ loadBlock: false });
|
|
|
+
|
|
|
+ const L = l + r * 2 + 3;
|
|
|
+ const blockX = this.getRandomNumberByRange(L + 10, w - (L + 10));
|
|
|
+ const blockY = this.getRandomNumberByRange(10 + r * 2, h - (L + 10));
|
|
|
+
|
|
|
+ this.setState({ blockX, blockY });
|
|
|
+
|
|
|
+ if (this.canvasCtx && this.blockCtx) {
|
|
|
+ this.draw(this.canvasCtx, blockX, blockY, l, r, "fill");
|
|
|
+ this.draw(this.blockCtx, blockX, blockY, l, r, "clip");
|
|
|
+
|
|
|
+ this.canvasCtx.drawImage(this.img, 0, 0, w, h);
|
|
|
+ this.blockCtx.drawImage(this.img, 0, 0, w, h);
|
|
|
+
|
|
|
+ const _y = blockY - r * 2 - 1;
|
|
|
+ const imgData = this.blockCtx.getImageData(blockX, _y, L, L);
|
|
|
+ block.width = L;
|
|
|
+ this.blockCtx.putImageData(imgData, 0, _y);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.props.onImageLoad) this.props.onImageLoad();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ handleStart(e) {
|
|
|
+ if (this.state.success) return;
|
|
|
+
|
|
|
+ const nativeEvent = e.nativeEvent || e;
|
|
|
+
|
|
|
+ if (nativeEvent.touches && nativeEvent.touches.length > 0) {
|
|
|
+ this.origin.x = nativeEvent.touches[0].pageX;
|
|
|
+ this.origin.y = nativeEvent.touches[0].pageY;
|
|
|
+ } else if ("clientX" in nativeEvent && "clientY" in nativeEvent) {
|
|
|
+ this.origin.x = nativeEvent.clientX;
|
|
|
+ this.origin.y = nativeEvent.clientY;
|
|
|
+ }
|
|
|
+ this.isMouseDown = true;
|
|
|
+ this.trail = [];
|
|
|
+ this.timestamp = Date.now();
|
|
|
+ }
|
|
|
+
|
|
|
+ handleMove(e) {
|
|
|
+ if (!this.isMouseDown) return false;
|
|
|
+
|
|
|
+ // 阻止滚动等默认事件
|
|
|
+ e.preventDefault();
|
|
|
+
|
|
|
+ const nativeEvent = e.nativeEvent || e;
|
|
|
+
|
|
|
+ let moveX = 0;
|
|
|
+ let moveY = 0;
|
|
|
+
|
|
|
+ if (nativeEvent.touches && nativeEvent.touches.length > 0) {
|
|
|
+ moveX = nativeEvent.touches[0].pageX - this.origin.x;
|
|
|
+ moveY = nativeEvent.touches[0].pageY - this.origin.y;
|
|
|
+ } else if ("clientX" in nativeEvent && "clientY" in nativeEvent) {
|
|
|
+ moveX = nativeEvent.clientX - this.origin.x;
|
|
|
+ moveY = nativeEvent.clientY - this.origin.y;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { w } = this.props;
|
|
|
+
|
|
|
+ if (moveX < 0 || moveX + 38 >= w) return false;
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ sliderLeft: moveX,
|
|
|
+ sliderWidth: moveX,
|
|
|
+ containerActive: true,
|
|
|
+ containerFail: false,
|
|
|
+ containerSuccess: false,
|
|
|
+ });
|
|
|
+
|
|
|
+ this.trail.push(moveY);
|
|
|
+ }
|
|
|
+
|
|
|
+ handleEnd(e) {
|
|
|
+ if (!this.isMouseDown) return false;
|
|
|
+ this.isMouseDown = false;
|
|
|
+
|
|
|
+ const nativeEvent = e.nativeEvent || e;
|
|
|
+
|
|
|
+ let moveX;
|
|
|
+
|
|
|
+ if (nativeEvent.changedTouches && nativeEvent.changedTouches.length > 0) {
|
|
|
+ moveX = nativeEvent.changedTouches[0].pageX;
|
|
|
+ } else if ("clientX" in nativeEvent) {
|
|
|
+ moveX = nativeEvent.clientX;
|
|
|
+ } else {
|
|
|
+ moveX = this.origin.x;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (moveX === this.origin.x) return false;
|
|
|
+
|
|
|
+ const timestamp = Date.now() - this.timestamp;
|
|
|
+
|
|
|
+ const { spliced, TuringTest } = this.verify(
|
|
|
+ this.state.sliderLeft,
|
|
|
+ this.state.blockX,
|
|
|
+ this.props.accuracy
|
|
|
+ );
|
|
|
+
|
|
|
+ if (spliced) {
|
|
|
+ if (TuringTest) {
|
|
|
+ this.setState({
|
|
|
+ success: true,
|
|
|
+ containerSuccess: true,
|
|
|
+ containerFail: false,
|
|
|
+ sliderLeft: this.state.blockX,
|
|
|
+ sliderWidth: this.state.blockX,
|
|
|
+ });
|
|
|
+ this.props.onSuccess && this.props.onSuccess(timestamp);
|
|
|
+ } else {
|
|
|
+ this.setState({
|
|
|
+ containerFail: true,
|
|
|
+ containerSuccess: false,
|
|
|
+ sliderLeft: 0,
|
|
|
+ sliderWidth: 0,
|
|
|
+ });
|
|
|
+ this.props.onAgain && this.props.onAgain();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.setState({
|
|
|
+ containerFail: true,
|
|
|
+ containerSuccess: false,
|
|
|
+ sliderLeft: 0,
|
|
|
+ sliderWidth: 0,
|
|
|
+ });
|
|
|
+ this.refresh();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ verify(left, blockX, accuracy) {
|
|
|
+ const arr = this.trail;
|
|
|
+ const sum = (x, y) => x + y;
|
|
|
+ const square = (x) => x * x;
|
|
|
+
|
|
|
+ const average = arr.reduce(sum, 0) / arr.length || 0;
|
|
|
+ const deviations = arr.map((x) => x - average);
|
|
|
+ const stddev = Math.sqrt(
|
|
|
+ deviations.map(square).reduce(sum, 0) / arr.length || 0
|
|
|
+ );
|
|
|
+
|
|
|
+ const leftNum = parseInt(left);
|
|
|
+ accuracy = accuracy <= 1 ? 1 : accuracy > 10 ? 10 : accuracy;
|
|
|
+
|
|
|
+ return {
|
|
|
+ spliced: Math.abs(leftNum - blockX) <= accuracy,
|
|
|
+ TuringTest: average !== stddev,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ reset() {
|
|
|
+ const { w, h, l, r, imgs } = this.props;
|
|
|
+ this.setState({
|
|
|
+ success: false,
|
|
|
+ containerActive: false,
|
|
|
+ containerSuccess: false,
|
|
|
+ containerFail: false,
|
|
|
+ sliderLeft: 0,
|
|
|
+ sliderWidth: 0,
|
|
|
+ loadBlock: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ const block = this.blockRef.current;
|
|
|
+ if (block) block.style.left = "0px";
|
|
|
+ if (this.canvasCtx) this.canvasCtx.clearRect(0, 0, w, h);
|
|
|
+ if (this.blockCtx) this.blockCtx.clearRect(0, 0, w, h);
|
|
|
+ if (block) block.width = w;
|
|
|
+
|
|
|
+ this.img.src = this.getRandomImg(imgs);
|
|
|
+ }
|
|
|
+
|
|
|
+ refresh() {
|
|
|
+ this.reset();
|
|
|
+ this.props.onRefresh && this.props.onRefresh();
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ const { w, h, sliderText, show } = this.props;
|
|
|
+ const {
|
|
|
+ loadBlock,
|
|
|
+ containerActive,
|
|
|
+ containerSuccess,
|
|
|
+ containerFail,
|
|
|
+ sliderLeft,
|
|
|
+ sliderWidth,
|
|
|
+ success,
|
|
|
+ } = this.state;
|
|
|
+
|
|
|
+ // 容器className 组合
|
|
|
+ const containerCls = [
|
|
|
+ styles["slide-verify-slider"],
|
|
|
+ containerActive ? styles["container-active"] : "",
|
|
|
+ containerSuccess ? styles["container-success"] : "",
|
|
|
+ containerFail ? styles["container-fail"] : "",
|
|
|
+ ].join(" ");
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ id="slideVerify"
|
|
|
+ className={styles["slide-verify"]}
|
|
|
+ style={{ width: w }}
|
|
|
+ onSelect={(e) => e.preventDefault()}
|
|
|
+ >
|
|
|
+ <Spin spinning={loadBlock}>
|
|
|
+ <canvas
|
|
|
+ ref={this.canvasRef}
|
|
|
+ width={w}
|
|
|
+ height={h}
|
|
|
+ className={styles["slide-verify-canvas"]}
|
|
|
+ />
|
|
|
+
|
|
|
+ {show && (
|
|
|
+ <div
|
|
|
+ className={styles["slide-verify-refresh-icon"]}
|
|
|
+ onClick={this.refresh}
|
|
|
+ >
|
|
|
+ <i className={`iconfont ${styles["icon-refresh"]}`} />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <canvas
|
|
|
+ ref={this.blockRef}
|
|
|
+ width={w}
|
|
|
+ height={h}
|
|
|
+ className={styles["slide-verify-block"]}
|
|
|
+ style={{ left: `${sliderLeft}px`, position: "absolute", top: 0 }}
|
|
|
+ />
|
|
|
+
|
|
|
+ <div className={containerCls}>
|
|
|
+ <div
|
|
|
+ className={styles["slide-verify-slider-mask"]}
|
|
|
+ style={{ width: sliderWidth }}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className={styles["slide-verify-slider-mask-item"]}
|
|
|
+ style={{ left: sliderLeft }}
|
|
|
+ onMouseDown={this.handleStart}
|
|
|
+ onTouchStart={this.handleStart}
|
|
|
+ onTouchMove={this.handleMove}
|
|
|
+ onTouchEnd={this.handleEnd}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ draggable={false}
|
|
|
+ src={
|
|
|
+ success
|
|
|
+ ? require("./icons/success.png")
|
|
|
+ : require("./icons/right.png")
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <span className={styles["slide-verify-slider-text"]}>
|
|
|
+ {sliderText}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className={styles["bottom-action"]} onClick={this.refresh}>
|
|
|
+ <img src={require("./icons/refresh.png")} />
|
|
|
+ <span>{sldComLanguage('刷新')}</span>
|
|
|
+ </div>
|
|
|
+ </Spin>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+SlideVerify.defaultProps = {
|
|
|
+ l: 42,
|
|
|
+ r: 10,
|
|
|
+ w: 310,
|
|
|
+ h: 155,
|
|
|
+ sliderText: sldComLanguage("拖动滑块来填充拼图"),
|
|
|
+ accuracy: 5,
|
|
|
+ show: true,
|
|
|
+ imgs: [],
|
|
|
+ interval: 50,
|
|
|
+};
|
|
|
+
|
|
|
+export default SlideVerify;
|