Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div id="root"></div>
              
            
!

CSS

              
                @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;
}
              
            
!

JS

              
                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"));

              
            
!
999px

Console