puzzle-react
Version:
a puzzle verify component for react
299 lines (266 loc) • 9.39 kB
JSX
import React from "react";
import propTypes from "prop-types";
import "./index.css";
class PuzzleReact extends React.Component {
constructor(props) {
super(props);
this.state = {
backgroundContext: null,
slideContext: null,
goalContext: null,
offsetX: 0,
offsetY: 0,
isMousedown: false,
mouseClientX: 0,
slideOffsetX: 10,
distance: 0,
result: "default",
};
}
componentDidMount() {
this.initMoveWatcher();
this.initCanvasContext();
this.drawBackgroundImage();
}
componentWillUnmount() {
this.removeMoveWatcher();
}
/**
* @description 计算属性
*/
goalOffsetComputed = () => ({
left: `${this.state.offsetX}px`,
top: `${this.state.offsetY}px`,
});
slideOffsetComputed = () => ({
left: `${this.state.slideOffsetX + this.state.distance + 10}px`,
top: `${this.state.offsetY}px`,
});
puzzleButtonComputed = () => ({
left: `${this.state.distance}px`,
});
puzzleResultClassComputed = () => {
const classes = [
"puzzle-result",
(this.state.result == "success" || this.state.result == "fail") &&
`puzzle-result-${this.state.result}`,
]
return classes.join(" ");
};
puzzleResultTextComputed = () => {
const TEXT_ENMU = {
success: this.props.successText,
fail: this.props.failText,
};
return TEXT_ENMU[this.state.result];
};
/**
* @methods
*/
initMoveWatcher = () => {
const body = document.body;
body.addEventListener("mouseleave", this.onMouseLeave);
body.addEventListener("mousemove", this.onMouseMove);
body.addEventListener("mouseup", this.onMouseUp);
};
removeMoveWatcher = () => {
const body = document.body;
body.removeEventListener("mouseleave", this.onMouseLeave);
body.removeEventListener("mousemove", this.onMouseMove);
body.removeEventListener("mouseup", this.onMouseUp);
};
initCanvasContext = () => {
this.setState({
backgroundContext: this.getCanvasContext("#puzzle-background"),
slideContext: this.getCanvasContext("#puzzle-slide"),
goalContext: this.getCanvasContext("#puzzle-goal"),
});
};
getCanvasContext(selector) {
const canvas = document.querySelector(selector);
return canvas.getContext("2d");
}
drawBackgroundImage = async () => {
const img = await this.getImage();
if (img) {
const scale = this.props.width / this.props.height;
this.state.backgroundContext.drawImage(
img,
0,
0,
img.width,
img.width / scale,
0,
0,
this.props.width,
this.props.height
);
this.drawSlideImage();
}
};
getImage() {
return new Promise((resolve) => {
const img = document.getElementById("puzzle-img");
img.onload = () => {
resolve(img || false);
};
});
}
getBackgroundRandomArea = () => {
const x = ~~(Math.random() * 80 + 140);
const y = ~~((this.props.height - 50) / 2);
const imgData = this.state.backgroundContext.getImageData(x, y, 50, 50);
// this.state.offsetX = 10 + x;
// this.state.offsetY = 10 + y;
this.setState({
offsetX: 10 + x,
offsetY: 10 + y,
});
return imgData;
};
drawSlideImage = () => {
const imgData = this.getBackgroundRandomArea();
this.state.slideContext.putImageData(imgData, 0, 0);
};
onMouseDown = ({ clientX }) => {
this.setState({
isMousedown: true,
mouseClientX: clientX,
});
};
onMouseUp = () => {
this.resultVerify();
this.setState({
isMousedown: false,
mouseClientX: null,
});
};
onMouseMove = ({ clientX }) => {
if (this.state.isMousedown) {
if (clientX - this.state.mouseClientX > 0) {
this.setState({
distance: clientX - this.state.mouseClientX,
});
}
}
};
onMouseLeave = () => {
this.onMouseUp();
};
/**
* @description
* GAP 代表SLIDE 距离 GOAL 的偏差,其中 offsetX是生成GOAL的style left距离,
* 注意:是距离最外层父元素的距离而不是BACKGROUND的距离,减去10是因为BACKGROUND
* 距离最外层父元素有10间距偏差,然后this.slideOffsetX + this.distance是SLIDE
* 真正距离BACKGROUND的距离,因为拖动按钮是从BACKGROUND对齐的.
* 再次设置isMousedown = false,是因为校验完成之后不允许再次拖动,应该手动调用reset方法恢复出厂设置重新进行验证;
*/
resultVerify = () => {
var { isMousedown, offsetX, distance, result, slideOffsetX } =
this.state;
if (isMousedown) {
const GAP = Math.abs(offsetX - 10 - (slideOffsetX + distance));
result = GAP <= this.props.offsetDistance ? "success" : "fail";
isMousedown = false;
if (this.props.errorRetry && result == "fail") {
setTimeout(this.reset, 1000);
}
this.props.complete && this.props.complete(result);
this.setState({
isMousedown,
offsetX,
distance,
result,
});
}
};
reset = () => {
this.resetState();
this.drawSlideImage();
this.drawBackgroundImage();
};
resetState = () => {
this.setState({
isMousedown: false,
mouseClientX: 0,
distance: 0,
result: "default",
});
};
render() {
const { img, width, height } = this.props;
return (
<div className="puzzle">
<img crossOrigin="" hidden src={img} alt="" id="puzzle-img" />
<canvas
id="puzzle-background"
width={width}
height={height}
style={{ background: "transparent" }}
></canvas>
<canvas
style={this.slideOffsetComputed()}
width="50"
height="50"
id="puzzle-slide"
></canvas>
<canvas
style={this.goalOffsetComputed()}
width="50"
height="50"
id="puzzle-goal"
></canvas>
<div className="puzzle-box">
<div className="puzzle-handle">
<div
style={this.puzzleButtonComputed()}
onMouseDown={this.onMouseDown}
className="puzzle-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path
d="M10 17l5-5l-5-5v10z"
fill="currentColor"
></path>
</svg>
</div>
</div>
</div>
<div className={this.puzzleResultClassComputed()}>
{this.puzzleResultTextComputed()}
</div>
<div className="puzzle-refresh" onClick={this.reset}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
fill="currentColor"
></path>
</svg>
</div>
</div>
);
}
}
export default PuzzleReact;
PuzzleReact.propTypes = {
width: propTypes.number,
height: propTypes.number,
offsetDistance: propTypes.number,
successText: propTypes.string,
failText: propTypes.string,
img: propTypes.string,
errorRetry: propTypes.bool,
complete: propTypes.func,
};
PuzzleReact.defaultProps = {
width: 300,
height: 150,
offsetDistance: 10,
successText: "校验成功",
failText: "校验失败",
img: "https://i.loli.net/2021/08/30/lj1ciV3An8JDo6u.jpg",
errorRetry: true,
};