<div id="root"></div>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');

* {
  font-family: 'Press Start 2P', cursive;
	//font-size: 1.8vmin;
}

body {
  margin: 0;
}
View Compiled
const { useState, useRef, useCallback, useEffect, useReducer } = React;

const SLIDE_PUZZLE_SIZE = {
	height: '60vmin',
	width: '60vmin'
}

const BG =
	"https://images.ctfassets.net/w6gireivdm0c/61UEuQEwHoSoz3GIpNkF9e/1d18ada4da6c6de4912efa71f57d44aa/NEKO9V9A9131_TP_V4.jpg";
const SHUFFLE_SPEED = 60;
const MAX_SHUFFLE_COUNT = 100;
const LEVEL = {
	easy: 9,
	normal: 16,
	hard: 25
};
const BOARD_COLOR = "#997f5d";

const fitImage = async (url, h, w) => {
	const image = await loadImage(url).then((r) => r);

	const width = image.width;
	const height = image.height;

	const ratio = height / width;

	const fittedImageSize = {
		height: 0,
		width: 0
	};

	if (ratio >= 1) {
		fittedImageSize.height = height * (w / width);
		fittedImageSize.width = w;
	} else {
		fittedImageSize.height = h;
		fittedImageSize.width = width * (h / height);
	}

	return fittedImageSize;
};

const clamp = (value, min, max) => {
	if (value < min) return min;
	else if (value > max) return max;
	return value;
};
const loadImage = (url) => {
	return new Promise((resolve) => {
		const img = new Image();
		img.src = url;
		img.crossOrigin = "Anonymous";
		img.addEventListener("load", (e) => {
			resolve(e.target);
		});
	});
};
async function getSquareImage(url, sideLength) {
	const image = await loadImage(url).then((r) => r);

	const canvas = document.createElement("canvas");
	const ctx = canvas.getContext("2d");

	const ratio =
		image.height / image.width >= 1
			? sideLength / image.width
			: sideLength / image.height;
	canvas.width = sideLength; //image.width
	canvas.height = sideLength; //image.width

	ctx.drawImage(
		image,
		sideLength / 2 - (image.width * ratio) / 2,
		sideLength / 2 - (image.height * ratio) / 2,
		image.width * ratio,
		image.height * ratio
	);

	const result = canvas.toDataURL("image/jpeg");
	//console.log(result)

	return result;
}
async function divideImage(url, rows, cols) {
	const canvas = document.createElement("canvas");

	const ctx = canvas.getContext("2d");

	// const squareImageUrl = await getSquareImage(url).then(r => r)

	const image = await loadImage(url).then((r) => r);
	//console.log(image)

	canvas.width = image.width / rows;
	canvas.height = image.height / cols;

	const dividedImages = [];

	let count = 0;

	for (let i = 0; i < cols; i++) {
		for (let j = 0; j < rows; j++) {
			ctx.drawImage(
				image,
				(j * image.width) / rows,
				(i * image.height) / cols,
				image.width / rows,
				image.height / cols,
				0,
				0,
				canvas.width,
				canvas.height
			);

			const clipedImage = canvas.toDataURL("image/jpeg");

			dividedImages[count] = clipedImage;

			count++;
		}
	}
	//console.log(dividedImages)
	return dividedImages;
}

const initialState = {
	isLock: true,
	isShuffle: false,
	isComplete: false,
	bg: null,
	pieces: [],
	pieceLength: 0,
	width: 0,
	height: 0,
	trouble: 0
};

const slidePuzzleReducer = (state, action) => {
	switch (action.type) {
		case "RESIZE_SLIDE_PUZZLE": {
			if (!state.pieces.length) {
				return {
					...state
				};
			}

			const pieces = [...state.pieces];
			const pieceLength = state.pieceLength;
			const width = action.width;
			const height = action.height;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceWidth = width / rows;
					const pieceHeight = height / cols;

					pieces[count].x *= pieceWidth / pieces[count].width;
					pieces[count].y *= pieceHeight / pieces[count].height;
					pieces[count].basePointX *= pieceWidth / pieces[count].width;
					pieces[count].basePointY *= pieceHeight / pieces[count].height;
					pieces[count].height = pieceHeight;
					pieces[count].width = pieceWidth;

					count++;
				}
			}

			return {
				...state,
				pieces,
				height,
				width
			};
		}

		case "RESET_SLIDE_PUZZLE": {
			return {
				...state,
				pieces: [],
				pieceLength: 0,
				bg: null,
				isLock: true,
				isComplete: false,
				isShuffle: false,
				width: 0,
				height: 0,
				trouble: 0
			};
		}

		case "INITIALIZE_PIECES": {
			const pieces = [...state.pieces];
			const pieceLength = state.pieceLength;

			const height = action.height;
			const width = action.width;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			const pieceImages = action.pieceImages;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceHeight = height / cols;
					const pieceWidth = width / rows;

					const x = j * pieceHeight;
					const y = i * pieceHeight;

					pieces[count] = {
						...pieces[count],
						id: count,
						baseId: count,
						image: pieceImages[count],
						x,
						y,
						basePointX: x,
						basePointY: y,
						blank: count === pieceLength - 1 ? true : false,
						isDrag: false,
						isShuffle: false,
						height: pieceHeight,
						width: pieceWidth,
						offsetX: 0,
						offsetY: 0
					};

					count++;
				}
			}

			return {
				...state,
				pieces
			};
		}

		case "SHUFFLE_PIECES": {
			const pieces = [...state.pieces];

			pieces.map((piece) => ({
				...piece,
				isShuffle: false
			}));

			const pieceLength = pieces.length;

			const blank = pieces.find((piece) => piece.blank);

			const piecesNextToBlank = pieces.filter((piece, i) => {
				return (
					(piece.id === blank.id - 1 && blank.y === piece.y) || ///Math.sqrt(pieceLength) * piece.width - piece.width) ||
					(piece.id === blank.id + 1 && blank.y === piece.y) || // && piece.x !== 0を追加して空白の次が一行ずれていたらだめ
					(piece.id === blank.id - Math.sqrt(pieceLength) && blank.x === piece.x) ||
					(piece.id === blank.id + Math.sqrt(pieceLength) && blank.x === piece.x)
				);
			});

			console.log(piecesNextToBlank);

			const r = Math.floor(Math.random() * piecesNextToBlank.length);
			console.log(r);
			const movingPiece = piecesNextToBlank[r];
			console.log(movingPiece);

			if (!movingPiece) {
				return {
					...state,
					pieces
				};
			}

			if (movingPiece.isShuffle) {
				return {
					...state,
					pieces: pieces.map((piece) => ({
						...piece,
						isShuffle: false
					}))
				};
			}

			return {
				...state,
				pieces: pieces.map((piece) =>
					piece === movingPiece
						? {
								...piece,
								id: blank.id,
								x: blank.x,
								y: blank.y,
								basePointX: blank.x,
								basePointY: blank.y,
								isShuffle: true
						  }
						: piece === blank
						? {
								...piece,
								id: movingPiece.id,
								x: movingPiece.x,
								y: movingPiece.y,
								basePointX: movingPiece.x,
								basePointY: movingPiece.y,
								isShuffle: false
						  }
						: piece
				)
			};
		}

		case "SORT_PIECES": {
			const pieces = [...state.pieces].sort((a, b) => a.id - b.id);

			return {
				...state,
				pieces
			};
		}

		case "DRAG_START_PIECE": {
			const pieces = [...state.pieces].map((piece) =>
				piece.baseId === action.num
					? {
							...piece,
							isDrag: true
					  }
					: piece
			);

			return {
				...state,
				pieces
			};
		}

		case "LIMIT_MOVEMENT_PIECE": {
			const pieces = [...state.pieces];
			const width = state.width;
			const height = state.height;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (piece.x <= 0) {
						piece.offsetX = 0;
					}

					if (piece.x + piece.width >= width) {
						piece.offsetX = 0;
					}

					if (piece.y <= 0) {
						piece.offsetY = 0;
					}

					if (piece.y + piece.height >= height) {
						piece.offsetY = 0;
					}
				}
			});

			return {
				...state,
				pieces
			};
		}

		case "DRAG_MOVE_PIECE": {
			const pieces = [...state.pieces];
			const pieceLength = pieces.length;
			const elementOffsetX = action.elementOffsetX;
			const elementOffsetY = action.elementOffsetY;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (
						pieces[piece.id - 1] &&
						pieces[piece.id - 1].blank &&
						pieces[piece.id - 1].y === piece.y
					) {
						console.log("is-left");
						pieces[piece.id] = {
							...piece,
							offsetX: clamp(elementOffsetX, -piece.width, 0)
						};
					} else if (
						pieces[piece.id + 1] &&
						pieces[piece.id + 1].blank &&
						pieces[piece.id + 1].y === piece.y //同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
					) {
						console.log("is-right");

						pieces[piece.id] = {
							...piece,
							offsetX: clamp(elementOffsetX, 0, piece.width)
						};
					} else if (
						pieces[piece.id - Math.sqrt(pieceLength)] &&
						pieces[piece.id - Math.sqrt(pieceLength)].blank &&
						pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
					) {
						console.log("is-top");

						pieces[piece.id] = {
							...piece,
							offsetY: clamp(elementOffsetY, -piece.height, 0)
						};
					} else if (
						pieces[piece.id + Math.sqrt(pieceLength)] &&
						pieces[piece.id + Math.sqrt(pieceLength)].blank &&
						pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
					) {
						console.log("is-bottom");

						pieces[piece.id] = {
							...piece,
							offsetY: clamp(elementOffsetY, 0, piece.height)
						};
					}
				}
			});

			return {
				...state,
				pieces
			};
		}

		case "DRAG_END_PIECE": {
			const pieces = [...state.pieces];
			const pieceLength = pieces.length;

			const replacePiece = (targetPiece, blank) => {
				pieces[targetPiece.id] = {
					...pieces[targetPiece.id],
					id: blank.id,
					x: blank.basePointX,
					y: blank.basePointY,
					basePointX: blank.basePointX,
					basePointY: blank.basePointY,
					offsetX: 0,
					offsetY: 0
				};

				pieces[blank.id] = {
					...pieces[blank.id],
					id: targetPiece.id,
					x: targetPiece.basePointX,
					y: targetPiece.basePointY,
					basePointX: targetPiece.basePointX,
					basePointY: targetPiece.basePointY,
					offsetX: 0,
					offsetY: 0
				};
			};

			let trouble = 0;

			pieces.forEach((piece) => {
				if (piece.isDrag) {
					if (
						Math.abs(piece.offsetX) > piece.width / 2 ||
						Math.abs(piece.offsetY) > piece.height / 2
					) {
						if (piece.offsetX < 0) {
							const blank = pieces[piece.id - 1];
							replacePiece(piece, blank);
						} else if (piece.offsetX > 0) {
							const blank = pieces[piece.id + 1];
							replacePiece(piece, blank);
						} else if (piece.offsetY < 0) {
							const blank = pieces[piece.id - Math.sqrt(pieceLength)];
							replacePiece(piece, blank);
						} else if (piece.offsetY > 0) {
							const blank = pieces[piece.id + Math.sqrt(pieceLength)];
							replacePiece(piece, blank);
						}

						trouble = state.trouble + 1;
					} else {
						piece.offsetX = 0;
						piece.offsetY = 0;

						trouble = state.trouble;
					}
				}
			});

			return {
				...state,
				pieces: pieces.map((piece) =>
					piece.isDrag
						? {
								...piece,
								isDrag: false
						  }
						: piece
				),
				trouble
			};
		}

		case "CHANGE_SLIDE_PUZZLE_BG": {
			return {
				...state,
				bg: action.url
			};
		}

		case "CHANGE_PIECE_LENGTH": {
			return {
				...state,
				pieceLength: action.num
			};
		}

		case "INITIALIZE_SLIDE_PUZZLE": {
			const pieces = [...state.pieces];
			const pieceLength = action.pieceLength;
			const height = action.height;
			const width = action.width;
			const pieceImages = action.pieceImages;

			const rows = Math.sqrt(pieceLength);
			const cols = rows;

			let count = 0;

			for (let i = 0; i < cols; i++) {
				for (let j = 0; j < rows; j++) {
					const pieceHeight = height / cols;
					const pieceWidth = width / rows;

					const x = j * pieceHeight;
					const y = i * pieceHeight;

					pieces[count] = {
						...pieces[count],
						id: count,
						baseId: count,
						image: pieceImages[count],
						x,
						y,
						basePointX: x,
						basePointY: y,
						blank: count === pieceLength - 1 ? true : false,
						isDrag: false,
						isShuffle: false,
						height: pieceHeight,
						width: pieceWidth,
						offsetX: 0,
						offsetY: 0
					};

					count++;
				}
			}

			return {
				...state,
				pieces,
				pieceLength
			};
		}

		case "START_SHUFFLE_PIECES": {
			return {
				...state,
				isShuffle: true
			};
		}

		case "STOP_SHUFFLE_PIECES": {
			const pieces = [...state.pieces].map((piece) => ({
				...piece,
				isShuffle: false
			}));

			return {
				...state,
				pieces,
				isShuffle: false
			};
		}

		case "LOCK_SLIDE_PUZZLE": {
			return {
				...state,
				isLock: true
			};
		}

		case "UNLOCK_SLIDE_PUZZLE": {
			return {
				...state,
				isLock: false
			};
		}

		case "COMPLETE_SLIDE_PUZZLE": {
			return {
				...state,
				isComplete: true
			};
		}

		case "INCOMPLETE_SLIDE_PUZZLE": {
			return {
				...state,
				isComplete: false
			};
		}

		case "RESET_TROUBLE": {
			return {
				...state,
				trouble: 0
			};
		}

		default:
			throw new Error();
	}
};

const useDragAndDrop = () => {
	const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 });
	const [elementOffset, setElementOffset] = useState({ x: 0, y: 0 });

	const pointerStartPosition = useRef({ x: null, y: null });
	const pointerMovePosition = useRef({ x: null, y: null });

	const currentDragElement = useRef(null);

	const prevElementOffset = useRef({ x: 0, y: 0 });

	const prevElementOffsetX = useRef(0);
	const prevElementOffsetY = useRef(0);

	const getCurrentPosition = (elem) => {
		const { top, left } = elem.getBoundingClientRect();
		return { top, left };
	};

	const moveDistance = (distance) =>
		setElementOffset({
			x: prevElementOffset.current.x + distance.x,
			y: prevElementOffset.current.y + distance.y
		});

	const resetElementOffset = () => {
		setElementOffset({
			x: 0,
			y: 0
		});

		prevElementOffset.current = {
			x: 0,
			y: 0
		};
	};

	const resetPointerStartPosition = () => {
		if (
			pointerStartPosition.current.x === null ||
			pointerStartPosition.current.y === null
		)
			return;

		pointerStartPosition.current.x = null;
		pointerStartPosition.current.y = null;
	};

	const handleMouseDown = (e) => {
		e.preventDefault();

		pointerStartPosition.current.x = e.clientX;
		pointerStartPosition.current.y = e.clientY;

		currentDragElement.current = e.target;

		const elementCurrentPosition = getCurrentPosition(currentDragElement.current);

		setElementPosition({
			top: elementCurrentPosition.top,
			left: elementCurrentPosition.left
		});
	};

	const handleMouseMove = (e) => {
		e.preventDefault();

		if (!currentDragElement.current) return;
		console.log("suga move: ", currentDragElement.current);
		if (
			pointerStartPosition.current.x === null ||
			pointerStartPosition.current.y === null
		)
			return;

		pointerMovePosition.current.x = e.clientX;
		pointerMovePosition.current.y = e.clientY;

		const pointerMoveDistance = {
			x: pointerMovePosition.current.x - pointerStartPosition.current.x,
			y: pointerMovePosition.current.y - pointerStartPosition.current.y
		};

		moveDistance(pointerMoveDistance);
	};

	const handleMouseUp = (e) => {
		e.preventDefault();

		if (!currentDragElement.current) return;
		console.log("suga up: ", currentDragElement.current);
		resetPointerStartPosition();

		const elementCurrentPosition = getCurrentPosition(currentDragElement.current);

		setElementPosition({
			top: elementCurrentPosition.top,
			left: elementCurrentPosition.left
		});

		currentDragElement.current = null;
	};

	useEffect(() => {
		document.body.addEventListener("mousemove", handleMouseMove);
		document.body.addEventListener("mouseup", handleMouseUp);
		document.body.addEventListener("mouseleave", handleMouseUp);

		return () => {
			document.body.removeEventListener("mousemove", handleMouseMove);
			document.body.removeEventListener("mouseup", handleMouseUp);
			document.body.removeEventListener("mouseleave", handleMouseUp);
		};
	}, []);

	useEffect(() => {
		prevElementOffset.current = {
			x: elementOffset.x,
			y: elementOffset.y
		};
	}, [elementPosition.left, elementPosition.top]);

	return [
		{
			currentDragElement,
			elementPosition,
			elementOffset
		},
		{
			pointerStartPosition,
			pointerMovePosition
		},
		handleMouseDown,
		setElementOffset,
		resetElementOffset,
		setElementPosition
	];
};
const useResizeObserver = (elements, callback) => {
	useEffect(() => {
		console.log("mari");
		const resizeObserver = new ResizeObserver((entries) => callback(entries));

		for (const elem of elements) {
			//console.log(elem);
			elem.current && resizeObserver.observe(elem.current);
		}

		return () => resizeObserver.disconnect();
	}, []);
};
const useMutationObserver = (elements, callback, config) => {
	useEffect(() => {
		const mutationObserver = new MutationObserver((mutations) => {
			mutationObserver.disconnect();
			callback(mutations);
			for (const elem of elements) {
				elem.current && mutationObserver.observe(elem.current, config);
			}
		});

		for (const elem of elements) {
			elem.current && mutationObserver.observe(elem.current, config);
		}

		return () => mutationObserver.disconnect();
	}, []);
};
const useSlidePuzzle = (bg, pieceLength) => {
	const [
		element,
		pointer,
		handleMouseDown,
		setElementOffset,
		resetElementOffset,
		setElementPosition
	] = useDragAndDrop();

	const { currentDragElement, elementPosition, elementOffset } = element;

	const { pointerStartPosition, pointerMovePosition } = pointer;

	const [slidePuzzle, dispatch] = useReducer(slidePuzzleReducer, initialState);

	const slidePuzzleNode = useRef(null);
	const intervalId = useRef(null);

	const initializeSlidePuzzle = async () => {
		// https://shanabrian.com/web/javascript/element-client-width-height.php
		const width = slidePuzzleNode.current.clientWidth;
		const height = slidePuzzleNode.current.clientHeight;

		const rows = Math.sqrt(pieceLength);
		const cols = rows;
		const squareImageUrl = await getSquareImage(bg, width).then((r) => r);
		const pieceImages = divideImage(squareImageUrl, rows, cols);

		pieceImages.then((images) =>
			dispatch({
				type: "INITIALIZE_SLIDE_PUZZLE",
				pieceLength,
				pieceImages: images,
				height,
				width
			})
		);
	};
	/*const initializePieces = useCallback(() => {
		const rect = slidePuzzleNode.current;

		const pieceLength = slidePuzzle.pieceLength;
		const bg = slidePuzzle.bg;

		// https://shanabrian.com/web/javascript/element-client-width-height.php
		const width = rect.clientWidth; //width//puzzle.width
		const height = rect.clientHeight; //puzzle.height

		const rows = Math.sqrt(pieceLength);
		const cols = rows;

		const pieceImages = divideImage(bg, rows, cols);

		pieceImages.then((images) =>
			dispatch({
				type: "INITIALIZE_PIECES",
				//pieceLength,
				pieceImages: images,
				height,
				width
			})
		);
	}, [
		slidePuzzle.bg,
		slidePuzzle.pieceLength,
		slidePuzzle.width,
		slidePuzzle.height,
		slidePuzzle.pieceImages
	]);*/

	const changeSlidePuzzleBg = useCallback((url) =>
		dispatch({
			type: "CHANGE_SLIDE_PUZZLE_BG",
			url
		})
	);

	const changePieceLength = useCallback((num) =>
		dispatch({
			type: "CHANGE_PIECE_LENGTH",
			num
		})
	);

	const startShufflePieces = useCallback(() =>
		dispatch({
			type: "START_SHUFFLE_PIECES"
		})
	);

	const stopShufflePieces = useCallback(() =>
		//clearInterval(intervalId.current)
		dispatch({
			type: "STOP_SHUFFLE_PIECES"
		})
	);

	const lockSlidePuzzle = useCallback(() =>
		dispatch({
			type: "LOCK_SLIDE_PUZZLE"
		})
	);
	const unlockSlidePuzzle = useCallback(() =>
		dispatch({
			type: "UNLOCK_SLIDE_PUZZLE"
		})
	);

	const completeSlidePuzzle = useCallback(() =>
		dispatch({
			type: "COMPLETE_SLIDE_PUZZLE"
		})
	);
	const incompleteSlidePuzzle = useCallback(() =>
		dispatch({
			type: "INCOMPLETE_SLIDE_PUZZLE"
		})
	);

	const resetSlidePuzzle = useCallback(() =>
		dispatch({
			type: "RESET_SLIDE_PUZZLE"
		})
	);

	const resetTrouble = useCallback(() =>
		dispatch({
			type: "RESET_TROUBLE"
		})
	);

	useResizeObserver([slidePuzzleNode], (entries) => {
		const height = entries[0].contentRect.height;
		const width = entries[0].contentRect.width;

		if (height === 0 || width === 0) return;

		dispatch({
			type: "RESIZE_SLIDE_PUZZLE",
			width,
			height
		});
	});

	useEffect(() => {
		if (!slidePuzzle.isShuffle) return;

		let count = 0;

		intervalId.current = setInterval(() => {
			count++;

			dispatch({
				type: "SHUFFLE_PIECES"
			});

			if (count > MAX_SHUFFLE_COUNT) {
				clearInterval(intervalId.current);

				dispatch({
					type: "SORT_PIECES"
				});

				dispatch({
					type: "STOP_SHUFFLE_PIECES"
				});

				dispatch({
					type: "UNLOCK_SLIDE_PUZZLE"
				});
			}
		}, SHUFFLE_SPEED);
	}, [slidePuzzle.isShuffle]);

	/*useEffect(() => {
		if (slidePuzzle.isShuffle) return;
		clearInterval(intervalId.current);
	}, [slidePuzzle.isShuffle]);*/

	// mousedown
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (!currentDragElement.current) return;

		const num = parseFloat(
			currentDragElement.current.className.replace(/[^0-9]/g, "")
		);

		dispatch({
			type: "DRAG_START_PIECE",
			num
		});
	}, [elementPosition]);

	// mousemove
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (!currentDragElement.current) return;

		dispatch({
			type: "LIMIT_MOVEMENT_PIECE"
		});

		dispatch({
			type: "DRAG_MOVE_PIECE",
			elementOffsetX: elementOffset.x,
			elementOffsetY: elementOffset.y
		});
	}, [elementOffset.x, elementOffset.y]);
	//const [pieces, setPieces] = useState([])
	/*useMutationObserver(
    [slidePuzzleNode],
    (mutations) => {
      setPieces(pieces => {
        pieces.forEach(piece => {
        if(piece.isDrag){
          if(
          pieces[piece.id - 1] &&
          pieces[piece.id - 1].blank && 
          pieces[piece.id - 1].y === piece.y
          ) {
          console.log('is-left')
          pieces[piece.id] = {
            ...piece,
            offsetX: clamp(
            elementOffsetX,
            -piece.width,//pieces[piece.id - 1].x,
            0//pieces[piece.id - 1].x + piece.width
            )
          }
          }
          else if(
          pieces[piece.id + 1] &&
          pieces[piece.id + 1].blank &&
          pieces[piece.id + 1].y === piece.y//同じ行であれば。例えば2, 3は番号でいえば隣り合っているけどピースは1行ずれているからダメ
          ) {
          console.log('is-right')

          pieces[piece.id] = {
            ...piece,
            offsetX: clamp(
            elementOffsetX,
            0,//pieces[piece.id + 1].x - piece.width,
            piece.width//pieces[piece.id + 1].x
            )
          }
          }
          else if(
          pieces[piece.id - Math.sqrt(pieceLength)] &&
          pieces[piece.id - Math.sqrt(pieceLength)].blank &&
          pieces[piece.id - Math.sqrt(pieceLength)].x === piece.x
          ) {
          console.log('is-top')

           pieces[piece.id] = {
            ...piece,
            offsetY: clamp(
            elementOffsetY,
            -piece.height,//pieces[piece.id - Math.sqrt(pieceLength)].y,
            0//pieces[piece.id - Math.sqrt(pieceLength)].y + piece.height
            )
          }
          }
          else if(
          pieces[piece.id + Math.sqrt(pieceLength)] &&
          pieces[piece.id + Math.sqrt(pieceLength)].blank &&
          pieces[piece.id + Math.sqrt(pieceLength)].x === piece.x
          ) {
          console.log('is-bottom')

          pieces[piece.id] = {
            ...piece,
            offsetY: clamp(
            elementOffsetY,
            0,//pieces[piece.id + Math.sqrt(pieceLength)].y - piece.height,
            piece.height//pieces[piece.id + Math.sqrt(pieceLength)].y
            )
          }
          }
        }
        })
      })    
    },
    {
            attributes: true,
            subtree: false,
            childList: false,
            attributeFilter: ["class"]
        }
  )*/

	// mouseup
	useEffect(() => {
		if (slidePuzzle.isLock) return;

		if (currentDragElement.current) return;

		dispatch({
			type: "DRAG_END_PIECE"
		});

		dispatch({
			type: "SORT_PIECES"
		});

		resetElementOffset();
	}, [elementPosition]);

	//素早いmousemoveからのmouseup後にelementOffsetXが更新されてしまうため、ここでもリセット
	/*useEffect(() => {
		if (slidePuzzle.isLock) return;
		if (currentDragElement.current) return;
		console.log("oua: ", elementOffset.x);
		resetElementOffset();
	}, [elementOffset.x, elementOffset.y]);*/

	// mouseup後にpuzzleが変更されたら
	useEffect(() => {
		if (slidePuzzle.isLock) return;
		console.log(currentDragElement.current);
		if (currentDragElement.current) return;

		const matchPieces = slidePuzzle.pieces.filter(
			(piece) => piece.id === piece.baseId
		);
		//console.log(matchPieces)
		console.log(slidePuzzle);
		console.log(matchPieces.length);
		console.log(slidePuzzle.pieceLength);
		if (matchPieces.length === slidePuzzle.pieceLength) {
			console.log("clear");
			dispatch({
				type: "COMPLETE_SLIDE_PUZZLE"
			});
		} else {
			console.log("mada");
		}
	}, [slidePuzzle.pieces]);

	return [
		{
			pieces: slidePuzzle.pieces,
			slidePuzzleNode,
			handleMouseDown
		},
		initializeSlidePuzzle,
		//initializePieces,
		changeSlidePuzzleBg,
		changePieceLength,
		startShufflePieces,
		stopShufflePieces,
		lockSlidePuzzle,
		unlockSlidePuzzle,
		slidePuzzle.isComplete,
		completeSlidePuzzle,
		incompleteSlidePuzzle,
		resetSlidePuzzle,
		slidePuzzle.trouble,
		resetTrouble
	];
};

const StyledSlidePuzzle = styled.div`
	background-color: ${({ boardColor }) => boardColor}; //#997f5d;//#333;
	box-sizing: border-box;
	border: 1vmin solid ${({ boardColor }) => boardColor}; //#997f5d;//#333;
	border-radius: 1vmin;
	height: ${({ sideLength }) => sideLength}; //60vmin;//100%;
	position: relative;
	width: ${({ sideLength }) => sideLength}; //60vmin;//100%;
	z-index: 0;
`;

const Piece = styled.div`
	//background-color: ${({ boardColor }) => boardColor};//#997f5d;//#333;
	box-sizing: border-box;
	//border: .3vmin solid ${({ boardColor }) => boardColor};//#997f5d;//#333;
	height: ${({ piece }) => piece.height}px;
	padding: 0.3vmin;
	position: absolute;
	left: ${({ piece }) => piece.x}px;
	top: ${({ piece }) => piece.y}px;
	transition: ${({ piece }) => (piece.isShuffle ? `all linear 100ms` : "")};
	transform: translate3d(
		${({ piece }) => piece.offsetX}px,
		${({ piece }) => piece.offsetY}px,
		0
	);
	user-select: none;
	width: ${({ piece }) => piece.width}px;
	z-index: ${({ piece }) => (piece.blank ? 1 : 3)};
`;

const Img = styled.img`
  display: block
  height: auto;
  pointer-events: none;
  width: 100%;
`;

const Num = styled.span`
	color: white;
	font-size: 1vmin;
	position: absolute;
	top: 1.2vmin; //.6vmin;
	left: 1.2vmin;
`;

const SlidePuzzleWrapper = styled.div`
	//height: 60vmin;
	//width: 60vmin;
`;

const SlidePuzzle = ({ className, slidePuzzle, boardColor, sideLength }) => (
	<StyledSlidePuzzle
		className={className}
		ref={slidePuzzle.slidePuzzleNode}
		boardColor={boardColor}
		sideLength={sideLength}
	>
		{slidePuzzle.pieces.map((piece, i) => (
			<Piece
				className={`piece piece-${piece.baseId}`}
				piece={piece}
				onMouseDown={slidePuzzle.handleMouseDown}
				key={piece.baseId}
			>
				{!piece.blank && (
					<React.Fragment>
						<Num className="piece-num">{piece.baseId}</Num>
						<Img className="piece-image" src={piece.image}></Img>
					</React.Fragment>
				)}
			</Piece>
		))}
	</StyledSlidePuzzle>
);

const Margin = styled.div`
	margin-top: ${({ top }) => (top ? top : `0`)};
	margin-right: ${({ right }) => (right ? right : `0`)};
	margin-bottom: ${({ bottom }) => (bottom ? bottom : `0`)};
	margin-left: ${({ left }) => (left ? left : `0`)};
	width: 100%;
`;

const FlexRowSpaceBetween = styled.div`
	display: flex;
	justify-content: space-between;
`;

const FlexRowCenter = styled.div`
	display: flex;
	justify-content: center;
`;

const Button = styled.button`
	background-color: ${({ bgColor }) => (bgColor ? bgColor : "lightgray")};
	border: none;
	cursor: ${({ isDisabled }) => (isDisabled ? "cursor" : "pointer")};
	color: ${({ color }) => (color ? color : "#333333")};

	border-radius: 0.6vmin;
	font-size: 1.8vmin;
	//margin: 0vmin .6vmin;
	//min-width: 12vmin;
	padding: 1em 2em;
	margin-top: ${({ mTop }) => (mTop ? mTop : `0`)};
	margin-right: ${({ mRight }) => (mRight ? mRight : `0`)};
	margin-bottom: ${({ mBottom }) => (mBottom ? mBottom : `0`)};
	margin-left: ${({ mLeft }) => (mLeft ? mLeft : `0`)};
	outline: none;
	opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
	pointer-events: ${({ isDisabled }) => (isDisabled ? "none" : "auto")};
	user-select: none;
`;

const StyledDisplay = styled.div`
	color: ${({ color }) => (color ? color : "#333333")};
	font-size: ${({ fontSize }) => (fontSize ? fontSize : "1em")};
`;

const Display = ({ className, title, text, color, fontSize }) => (
	<StyledDisplay
		className={className}
		color={color}
		fontSize={fontSize}
	>{`${title}: ${text}`}</StyledDisplay>
);

const StyledModal = styled.div`
	${({ isClose, isShow }) =>
		isClose
			? `visibility: hidden;`
			: isShow
			? `visibility: visible;`
			: `visibility: hidden;`}
	z-index: 1000;
`;

const Overlay = styled.div`
	background-color: rgba(0, 0, 0, 0.6);
	position: fixed;
	top: -100vh;
	left: -100vw;
	bottom: -100vh;
	right: -100vw;
	z-index: 1;
`;

const ContentWrapper = styled.div`
	background-color: white;
	box-sizing: border-box;
	border-radius: 8vmin;
	color: #333;
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 60vmin;
	text-align: center;
	position: fixed;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	min-width: 80vmin;
	z-index: 2;
	> button {
		position: absolute;
		top: 4vmin;
		right: 4vmin;
	}
`;

const Content = styled.div`
	max-height: 80vmin;
	padding: 4vmin;
`;

const Title = styled.h1`
	font-size: 4vmin;
	margin: 3vmin auto 0;
`;

const Text = styled.p`
	color: #777;
	font-size: 2vmin;
	margin: 4vmin 0 0;
`;

const Position = styled.div`
	position: ${({ position }) => (position ? position : "static")};
	${({ top }) => top && `top: ${top}`}
	${({ left }) => left && `left: ${left}`}
	${({ bottom }) => bottom && `bottom: ${bottom}`}
	${({ right }) => right && `right: ${right}`}
`;

const Modal = ({ className, title, text, isShow, children }) => {
	const [isClose, setIsClose] = useState(false);
	return (
		<StyledModal className={className} isShow={isShow} isClose={isClose}>
			<Overlay />
			<ContentWrapper>
				<Button bgColor="transparent" onClick={() => setIsClose(true)}>
					✖
				</Button>
				<Content>
					<Title>{title}</Title>
					<Text>{text}</Text>
					{children}
				</Content>
			</ContentWrapper>
		</StyledModal>
	);
};

const StyledApp = styled.div`
	background-color: burlywood;
	display: flex;
	align-items: center;
	justify-content: center;
	height: 100vh;
	width: 100%;
`;

const App = () => {
	const [isStartGame, setIsStartGame] = useState(false);
	const [level, setLevel] = useState(LEVEL.easy);
	const [selectBg, setSelectBg] = useState(BG);
	const [
		slidePuzzle,
		initializeSlidePuzzle,
		//initializePieces,
		changeSlidePuzzleBg,
		changePieceLength,
		startShufflePieces,
		stopShufflePieces,
		lockSlidePuzzle,
		unlockSlidePuzzle,
		isComplete,
		completeSlidePuzzle,
		incompleteSlidePuzzle,
		resetSlidePuzzle,
		trouble,
		resetTrouble
	] = useSlidePuzzle(selectBg, level);

	useEffect(() => {
		initializeSlidePuzzle();
	}, []);

	const startGame = useCallback(() => {
		setIsStartGame(true);
		//lockSlidePuzzle();
		startShufflePieces();
	}, []);

	useEffect(() => {
		if (!isComplete) return;
		lockSlidePuzzle(); //resetSlidePuzzle()
		// alert('Game clear!')
	}, [isComplete]);

	const resetGame = useCallback(() => {
		setIsStartGame(false);
		resetSlidePuzzle();
		//stopShufflePieces()
		initializeSlidePuzzle();
	}, []);

	return (
		<StyledApp>
			<Modal
				title="Game clear!"
				text={`your trouble is ${trouble}.`}
				isShow={isComplete}
			/>
			<SlidePuzzleWrapper>
				<FlexRowSpaceBetween>
					<Display
						className="troble"
						title="Trouble"
						text={trouble}
						fontSize="2vmin"
					/>
					<Display
						className="level"
						title="Level"
						text={
							level === LEVEL.easy
								? "easy"
								: level === LEVEL.normal
								? "normal"
								: level === LEVEL.hard
								? "hard"
								: "easy"
						}
						fontSize="2vmin"
					/>
				</FlexRowSpaceBetween>
				<Margin bottom="1.2vmin" />
				<SlidePuzzle
					className="slide-puzzle"
					slidePuzzle={slidePuzzle}
					boardColor={BOARD_COLOR}
					sideLength="60vmin"
				/>
				<Margin top="3vmin">
					<FlexRowCenter>
						<Button
							bgColor="orange"
							color="#333"
							mRight=".5em"
							onClick={startGame}
							isDisabled={isStartGame ? true : false}
						>
							Start
						</Button>
						<Button
							className="dialog-button"
							color="#333"
							onClick={resetGame}
							mLeft=".5em"
							isDisabled={isStartGame ? false : true}
						>
							Reset
						</Button>
					</FlexRowCenter>
				</Margin>
			</SlidePuzzleWrapper>
		</StyledApp>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/styled-components/4.3.1/styled-components.min.js