Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ 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

Auto Save

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

              
                
              
            
!

JS

              
                const { useState, useRef, useCallback, useEffect, forwardRef } = React;
const { createGlobalStyle } = styled;

const BOARD_COLOR = "#997f5d";
const BOARD_SIZE = "60vmin";
const SLIDE_PUZZLE_IMAGE = "https://images.ctfassets.net/w6gireivdm0c/61UEuQEwHoSoz3GIpNkF9e/1d18ada4da6c6de4912efa71f57d44aa/NEKO9V9A9131_TP_V4.jpg";
const NUMBER_OF_PIECES = 16;

// 画像を読み込んだあとにその画像を返す関数
const loadImage = (url: string): Promise<HTMLImageElement> => {
	return new Promise((resolve) => {
		const image = new Image();
		image.src = url;
		image.crossOrigin = "Anonymous";
		image.addEventListener("load", (e) => {
			const target = e.target as HTMLImageElement;
			resolve(target);
		});
	});
};

// 画像を比率を維持したまま正方形に切り抜いてその画像を返す関数
// imageUrl=元画像のURL
// size=正方形の一辺の長さ
async function getSquareImage(imageUrl: string, size: number): Promise<string> {
	// 元画像を読み込んで取得する
	const image = await loadImage(imageUrl).then((r) => r);

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

	// canvasのサイズを生成する画像と同じサイズの正方形にする
	canvas.width = size;
	canvas.height = size;

	// 元画像の一辺の長さに対するこれから生成する画像の一辺の長さの比率
	// 元画像の高さと幅を比較して、小さいほうの一辺の長さを基にした比率を算出する(cssのvminと同じ考え方)
	// 元画像を縦横比を維持したまま正方形にするため、大きいほうを基準にしてしまうと生成した画像の両端に余白ができてしまう
	const scale =
		image.height >= image.width ? size / image.width : size / image.height;

	// 元画像の中央がcanvasの中央に来るように描画する
	// 元画像のサイズをscale倍にして小さいほうの一辺をcanvasにぴったり合わせて比率を維持したままサイズを変化させる
	// 両端のはみ出た部分は切り抜かれる
	ctx.drawImage(
		image,
		size / 2 - (image.width * scale) / 2,
		size / 2 - (image.height * scale) / 2,
		image.width * scale,
		image.height * scale
	);

	// canvasに描画されている内容を画像データURIとして取得する
	const squareImage = canvas.toDataURL("image/jpeg");

	return squareImage;
}

// 画像を格子状に分割してそれらの画像を返す関数
// imageUrl=元画像のURL
// numberOfDivisions=分割する数
async function divideImageEqually(
	imageUrl: string,
	numberOfDivisions: number
): Promise<string[]> {
	const canvas = document.createElement("canvas") as HTMLCanvasElement;

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

	const image = await loadImage(imageUrl).then((r) => r);

	// 行・列数
	const cols = Math.sqrt(numberOfDivisions);
	const rows = cols;

	// canvasのサイズを格子状の1マス分のサイズにする
	canvas.width = image.width / rows;
	canvas.height = image.height / cols;

	// 分割した画像のURLを格納する配列
	const dividedImageURLs: string[] = [];

	// ループ処理で左上から右へ1マスずつ元画像を切り抜いていく
	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
			);
			// canvasの内容を画像のデータURIとして取得する
			const dividedImageURL = canvas.toDataURL("image/jpeg");

			dividedImageURLs[count] = dividedImageURL;

			count++;
		}
	}

	return dividedImageURLs;
}

const clamp = (value: number, min: number, max: number): number => {
	if (value < min) return min;
	else if (value > max) return max;
	return value;
};

// pieceオブジェクトの型
type PieceData = {
	number: number;
	image: string;
	isBlank: boolean;
};

type DraggingDirection = {
	horizontal: 'left' | 'right' | null;
	vertical: 'up' | 'down' | null;
};

type MouseStatus = {
	isDown: boolean;
	isMove: boolean;
	isUp: boolean;
};

type DraggingElementStatus = {
	translate: Point;
	mouseStatus: MouseStatus;
	draggingElement: EventTarget & Element | null;
	draggingDirection: DraggingDirection;
};

type DraggingElementController = (e: React.MouseEvent<EventTarget & HTMLElement>) => void;

type Direction = 'left' | 'right' | 'up' | 'down' ;
type LineDirection = 'row' | 'col'

const useDraggableElements = (isStyleTransform: boolean = true): [
	DraggingElementStatus,
	DraggingElementController
] => {
	const [translate, setTranslate] = useState<Point>({
		x: 0,
		y: 0
	});
	
	const [mouseStatus, setMouseStatus] = useState<MouseStatus>({
		isDown: false,
		isMove: false,
		isUp: false
	});
	
	const startPoint = useRef<Point>({ x: 0, y: 0 });

	const prevTranslate = useRef<Point>({ x: 0, y: 0 });
	
	const prevDifference = useRef<Point>({ x: 0, y: 0 });
	
	const draggingElement = useRef<EventTarget & HTMLElement | null>(null);

	const draggingDirection = useRef<DraggingDirection>({
		horizontal: null,
		vertical: null,
	});
	
	const isDraggable = (): boolean => draggingElement.current ? draggingElement.current.classList.contains('draggable') : false;
	
	const handleDown = useCallback((e: React.MouseEvent<EventTarget & HTMLElement>): void => {
		draggingElement.current = e.currentTarget;
		if(!isDraggable()) return;
		
		const matrix = new DOMMatrix(getComputedStyle(draggingElement.current).transform);
		
		prevTranslate.current = {
			x: matrix.translateSelf().e,
			y: matrix.translateSelf().f
		};
	
		const draggableElements = document.getElementsByClassName("draggable") as HTMLCollectionOf<HTMLElement>;
		
		for(let i = 0; i < draggableElements.length; i++) {
			draggableElements[i].style.zIndex = `1000`;
		}
		
		draggingElement.current.style.position = 'relative';
		draggingElement.current.style.zIndex = `1001`;

		const x = e.pageX;
		const y = e.pageY;

		startPoint.current = { x, y };
		
		setMouseStatus(prevMouseStatus => ({
			...prevMouseStatus,
			isUp: false,
			isDown: true
		}));
	}, []);
	
	const handleMove = (e: MouseEvent): void => {
		e.preventDefault();
		if(!draggingElement.current) return;
		if(!isDraggable()) return;
		
		console.log('mousemove');
		const differenceX = e.pageX - startPoint.current.x;
		const differenceY = e.pageY - startPoint.current.y;
		
		if (differenceX > prevDifference.current.x) {
			draggingDirection.current.horizontal = "right";
		}
		else if (differenceX < prevDifference.current.x) {
			draggingDirection.current.horizontal = "left";
		}

		if (differenceY > prevDifference.current.y) {
			draggingDirection.current.vertical = "down";
		}
		else if (differenceY < prevDifference.current.y) {
			draggingDirection.current.vertical = "up";
		}
		
		console.log('directionX: ', draggingDirection.current.horizontal);
		console.log('directionY: ', draggingDirection.current.vertical);

		setTranslate({
			x: prevTranslate.current.x + differenceX,
			y: prevTranslate.current.y + differenceY
		});
		
		prevDifference.current = {
			x: differenceX,
			y: differenceY
		};
		
		setMouseStatus(prevMouseStatus => ({
			...prevMouseStatus,
			isMove: true
		}));
	};

	const handleUp = (e: MouseEvent): void => {
		if(!draggingElement.current) return;
		if(!isDraggable()) return;
		console.log('mouseup');
		
		draggingElement.current = null;
		
		setMouseStatus(prevMouseStatus => ({
			...prevMouseStatus,
			isDown: false,
			isMove: false,
			isUp: true
		}));
	};
	
	useEffect(() => {
		if(!isStyleTransform) return;
		if(!draggingElement.current) return
		
		draggingElement.current.style.transform = `translate3d(${translate.x}px, ${translate.y}px, 0)`;
	}, [translate]);

	// 初回のレンダー後に一度だけ実行
	useEffect(() => {
		document.body.addEventListener("mousemove", handleMove);
		document.body.addEventListener("mouseup", handleUp);
		document.body.addEventListener("mouseleave", handleUp);
		
		return () => {
			document.body.removeEventListener("mousemove", handleMove);
			document.body.removeEventListener("mouseup", handleUp);
			document.body.removeEventListener("mouseleave", handleUp);
		};
	}, []);

	return [
		{
			translate,
			mouseStatus,
			draggingElement: draggingElement.current,
			draggingDirection: draggingDirection.current
		},
		handleDown
	];
};

type PuzzleStatus = {
	pieces: PieceData[];
	puzzleElementRef: React.RefObject<HTMLDivElement>;
};

type PiecesHandler = {
	handleDown: (e: React.MouseEvent<HTMLElement>) => void;
};


const useSlidePuzzle = (
	pieceCount: number,
	imageUrl: string
): [PuzzleStatus, PiecesHandler] => {
	const [
		{
			translate,
			mouseStatus,
			draggingElement,
			draggingDirection
		},
		handleDown
	] = useDraggableElements(false);
	
	// ピースのデータを管理するステート
	const [pieces, setPieces] = useState<PieceData[]>([]);

	// DOM 要素を保持するための ref を作成します。
	const elementRef = useRef<HTMLDivElement>(null);

	// 移動距離を制限したtranslate
	/*const restrictedTranslate = useRef({
		x: 0,
		y: 0
	});*/

	// 現在のピース数
	//const currentNumberOfPieces = useRef(numberOfPieces);

	// 一辺のピース数を取得する関数
	const getOneSidePieceCount = (): number => Math.sqrt(pieceCount);
	
	// 空白を取得する関数
	const getBlank = (pieces: PieceData[]): PieceData => {
		return pieces.filter((piece) => piece.isBlank)[0];
	};

	// 指定した番号に位置するピースを取得する関数
	const getPiece = (pieces: PieceData[], index: number): PieceData => {
		return pieces.filter((piece) => piece.index === index)[0];
	};

	// ドラッグ中のピースを取得する関数
	/*const getDraggingPiece = (pieces: PieceData[]): PieceData => {
		return pieces.filter((piece) => piece.isDragging)[0];
	};*/

	// targetがotherのdirection側に隣接しているか否かを判定する関数
	const isNextToOther = (
		target: PieceData,
		other: PieceData,
		direction: Direction
	): boolean => {
		if(!target || !other) return;
		
		const oneSidePieceCount = getOneSidePieceCount();

		if(direction === 'left'){
			return target.rowNumber === other.rowNumber &&
			target.index === other.index - 1
		}
		else if(direction === 'right'){
			return target.rowNumber === other.rowNumber &&
			target.index === other.index + 1;			
		}else if(direction === 'up') {
			return target.colNumber === other.colNumber &&
			target.index === other.index - oneSidePieceCount;
		}else if(direction === 'down'){
			return target.colNumber === other.colNumber &&
			target.index === other.index + oneSidePieceCount;
		}

		return false;
	};

	// 対象のピースtargetがいずれかの方向で空白blankと隣接しているか否かを判定する関数
	const isNextToBlank = (pieces: PieceData[], index: number): boolean => {
		const target = pieces[index];
		const blank = getBlank(pieces);
		
		return (
			isNextToOther(target, blank, 'left') ||
			isNextToOther(target, blank, 'right') ||
			isNextToOther(target, blank, 'up') ||
			isNextToOther(target, blank, 'down')
		);
	};
	
	// 空白に隣接しているすべてのピースを取得する関数
	const getSlidablePieces = (pieces: PieceData[]): PieceData[] => {
		const blank = getBlank(pieces);
		return pieces.filter((piece, i) => isNextToBlank(pieces, piece.index));
	};

	// 空白のdirection側に隣接しているピースを取得する関数
	const getPieceNextToBlank = (
		pieces: PieceData[],
		direction: Direction
	): PieceData => {
		const blank = getBlank(pieces);
		const slidablePieces = getSlidablePieces(pieces);
		
		const target = slidablePieces.filter(
			(piece) => isNextToOther(piece, blank, direction)
		)[0];

		return target;
	};
	
	// 移動中のピースと異なる軸の方向で空白と隣接しているピースが移動中であるか否かを判定する関数
	/*const otherIsSlidingDifferentDirections = (pieces: PieceData[]): boolean => {
		const piece = getDraggingPiece(pieces);
		const blank = getBlank(pieces);

		const leftPiece = getPieceNextToBlank(pieces, 'left');
		const rightPiece = getPieceNextToBlank(pieces, 'right');
		const upperPiece = getPieceNextToBlank(pieces, 'up');
		const lowerPiece = getPieceNextToBlank(pieces, 'down');

		if(
			isNextToOther(piece, blank, 'left') ||
			isNextToOther(piece, blank, 'right')
		) {
			if(
				upperPiece && Math.abs(upperPiece.y) > 0 ||
				lowerPiece && Math.abs(lowerPiece.y) > 0
			) {
				return true;
			}
		}else if(
			isNextToOther(piece, blank, 'up') ||
			isNextToOther(piece, blank, 'down')
		) {
			if(
				leftPiece && Math.abs(leftPiece.x) > 0 ||
				rightPiece && Math.abs(rightPiece.x) > 0
			) {
				return true;
			}
		}

		return false;
	};*/
	
	/*const isSlidingPiece = (piece: PieceData, slidingDirection: LineDirection): boolean => {
		
	}*/
	
	// isIntersectingWithOther
	const isSlidingPiece = (pieces: PieceData[], slidingDirection: LineDirection): boolean => {
        const leftPiece = getPieceNextToBlank(pieces, 'left');
        const rightPiece = getPieceNextToBlank(pieces, 'right');
        const upperPiece = getPieceNextToBlank(pieces, 'up');
        const lowerPiece = getPieceNextToBlank(pieces, 'down');

        if(slidingDirection === 'col') {
            if(
                upperPiece && Math.abs(upperPiece.y) > 0 ||
                lowerPiece && Math.abs(lowerPiece.y) > 0
            ) {
                return true;
            }
        }else{
            if(
                leftPiece && Math.abs(leftPiece.x) > 0 ||
                rightPiece && Math.abs(rightPiece.x) > 0
            ) {
                return true;
            }
        }
		
		return false;
    };

	// ピースのデータを初期化する関数
	const initializePieces = async () => {
		//if(!puzzleRef.current) return;

		// パズルのサイズ
		const puzzleSize = elementRef.current.clientWidth;
		
	console.log('elementRef.current: ', elementRef.current.children);
		// bgをパズルと同じサイズに切り抜く
		const squareImageUrl = await getSquareImage(imageUrl, puzzleSize).then(
			(r) => r
		);

		//squareImageUrlをcols×rowsに分割する
		const pieceImageUrls = divideImageEqually(squareImageUrl, pieceCount);
		
		pieceImageUrls.then((imageUrls) => {
			const pieces = [];
			
			// ピースの行・列数
			const cols = getOneSidePieceCount();
			const rows = cols;
			
			let currentRowNumber = 1;
			
			for (let i = 0; i < pieceCount; i++) {
				const piece = {
					number: i + 1, // ピースの番号
					isBlank: i === pieceCount - 1 ? true : false, // 初めは最後の番号を空白にする
					image: imageUrls[i], // ピースの画像
					index: i,// pieces内におけるpieceの番号
					colNumber: (i % cols) + 1,// 列番号
					rowNumber: currentRowNumber,// 行番号
					isDragging: false,// 移動中であればtrue
					x: 0,// ピースの横の移動距離
					y: 0,// ピースの縦の移動距離
					height: puzzleSize / cols,// ピースの高さ
					width: puzzleSize / rows// ピースの高さ				
				};

				pieces[i] = piece;
				
				// i + 1がrowsの倍数になる毎に行番号を1増やす
				if((i + 1) % rows === 0) {
					currentRowNumber++;
				}
			}

			setPieces(pieces);
		});
	};

	// ピースの数が変わるたびに初期化する
	useEffect(() => {
		initializePieces();
	}, [pieceCount]);

	// ピースをドラッグしたときの処理
	// ピースの移動範囲を様々な状況に応じて制限する
	useEffect(() => {
		if(!draggingElement) return;// nullチェック

		// 前回ドラッグしたピースの座標が入っているためリセットする
		/*restrictedTranslate.current.x = 0;
		restrictedTranslate.current.y = 0;*/
		
		const restrictedTranslate = {
			x: 0,
			y: 0
		};

		setPieces(prevPieces => 
			prevPieces.map((piece, i) => {		
				// ドラッグしたピースに紐づくpieceを判定する
				if(`piece-${piece.number}` === draggingElement.id){	
					const blank = getBlank(prevPieces);
					const oneSidePieceCount = getOneSidePieceCount();

					piece.isDragging = true;
					
					// 上下左右いずれかの方向で空白に隣接しているとき
					if(isNextToBlank(prevPieces, piece.index)) {
						console.log('隣接!');
						
						// t | b
						if(isNextToOther(piece, blank, 'left')) {
							console.log('| t | b |');
							
							// ドラッグするピースと異なる軸で空白と隣接しているピースが中途半端な位置で止まっている(交差している)場合は動かないようにする
							if(isSlidingPiece(prevPieces, 'col')){
								return piece;
							}
							
							// 二つ後のpiece//
							const nextNextPiece = prevPieces[i + 2];
							// ひとつ前のpiece
							const prevPiece = prevPieces[i - 1];	
							
							// p | t | b | p
							if(
								isNextToOther(prevPiece, piece, 'left') &&
								isNextToOther(nextNextPiece, blank, 'right')
							){				
								restrictedTranslate.x = clamp(
									translate.x,
									prevPiece.x,
									piece.width + nextNextPiece.x//piece.width - Math.abs(nextNextPiece.x)
								);
							}
							
							// p | t | b |
							else if(isNextToOther(prevPiece, piece, 'left')){
								restrictedTranslate.x = clamp(
									translate.x,
									prevPiece.x,
									piece.width
								);
							}
							
							// | t | b | p
							else if(isNextToOther(nextNextPiece, blank, 'right')){
								restrictedTranslate.x = clamp(
									translate.x,
									0,
									piece.width + nextNextPiece.x//piece.width - Math.abs(nextNextPiece.x)
								);
							}					
						}
						
						// b | t 
						else if(isNextToOther(piece, blank, 'right')) {
							console.log(`空白は左にある`);
							
							if(isSlidingPiece(prevPieces, 'col')){
								return piece;
							}
							
							const prevPrevPiece = prevPieces[i - 2];
							const nextPiece = prevPieces[i + 1];

							// p | b | t | p
							if(
								isNextToOther(prevPrevPiece, blank, 'left') &&
								isNextToOther(nextPiece, piece, 'right')
							){
								restrictedTranslate.x = clamp(
									translate.x,
									-piece.width + prevPrevPiece.x,
									nextPiece.x
								);
							}
							
							// | b | t | p
							else if(isNextToOther(nextPiece, piece, 'right')){
								restrictedTranslate.x = clamp(
									translate.x,
									-piece.width,
									nextPiece.x
								);
							}
							
							// p | b | t |
							else if(isNextToOther(prevPrevPiece, blank, 'left')){
								restrictedTranslate.x = clamp(
									translate.x,
									-piece.width + prevPrevPiece.x,
									0
								);
							}
						}
						
						/*
							b
							-
							t
						*/
						else if(isNextToOther(piece, blank, 'down')){			
							console.log(`空白は↑にある`);

							if(isSlidingPiece(prevPieces, 'row')){
								return piece;
							}
							
							const prevPrevPiece = prevPieces[i - oneSidePieceCount * 2];
							const nextPiece = prevPieces[i + oneSidePieceCount];

							/*
								p
								-
								b
								-
								t
								-
								p
							*/			
							if(
								isNextToOther(prevPrevPiece, blank, 'up') &&
								isNextToOther(nextPiece, piece, 'down')
							){
								restrictedTranslate.y = clamp(
									translate.y,
									-piece.height + prevPrevPiece.y,
									nextPiece.y
								);
							}
							
							/*
								p
								-
								b
								-
								t
								-
							*/
							else if(isNextToOther(prevPrevPiece, blank, 'up')){
								restrictedTranslate.y = clamp(
									translate.y,
									-piece.height + prevPrevPiece.y,
									0
								);
							}
							
							/*
								-
								b
								-
								t
								-
								p
							*/
							else if(isNextToOther(nextPiece, piece, 'down')){
								restrictedTranslate.y = clamp(
									translate.y,
									-piece.height,
									nextPiece.y
								);
							}
						}
						
						/*
							t
							-
							b
						*/
						else if(isNextToOther(piece, blank,  'up')){	
							console.log(`空白は↓にある`);

							if(isSlidingPiece(prevPieces, 'row')){
								return piece;
							}
							
							const nextNextPiece = prevPieces[i + oneSidePieceCount * 2];
							const prevPiece = prevPieces[i - oneSidePieceCount];
	
							/*
								p
								-
								t
								-
								b
								-
								p
							*/
							if(
								isNextToOther(nextNextPiece, blank, 'down') &&
								isNextToOther(prevPiece, piece, 'up')
							){
								restrictedTranslate.y = clamp(
									translate.y,
									prevPiece.y,
									piece.height - Math.abs(nextNextPiece.y)
								);
							}
							
							/*
								-
								t
								-
								b
								-
								p
							*/
							else if(isNextToOther(nextNextPiece, blank, 'down')){
								restrictedTranslate.y = clamp(
									translate.y,
									0,
									piece.height - Math.abs(nextNextPiece.y),
								);
							}
							
							/*
								p
								-
								t
								-
								b
								-
							*/
							else if(isNextToOther(prevPiece, piece, 'up')){
								restrictedTranslate.y = clamp(
									translate.y,
									prevPiece.y,
									piece.height
								);
							}
						}
					}
					
					// 空白に隣接していない
					else{
					//else if(
					//	piece.row === blank.row || 
					//	piece.col === blank.col
					//){
						console.log('空白はない');
						
						if(piece.rowNumber === blank.rowNumber) {
							const prevPiece = prevPieces[i - 1];
							const nextPiece = prevPieces[i + 1];
							
							// p | t | p
							if(
								isNextToOther(prevPiece, piece, 'left') &&
								isNextToOther(nextPiece, piece, 'right')
							){							
								restrictedTranslate.x = clamp(
									translate.x,
									prevPiece.x,
									nextPiece.x
								);
							}

							// p | t |
							else if(isNextToOther(prevPiece, piece, 'left')){						
								restrictedTranslate.x = clamp(
									translate.x,
									prevPiece.x,
									0
								);
							}

							// | t | p
							else if(isNextToOther(nextPiece, piece, 'right')){
								restrictedTranslate.x = clamp(
									translate.x,
									0,
									nextPiece.x
								);
							}
						}else if(piece.colNumber === blank.colNumber){
							const prevPiece = prevPieces[i - oneSidePieceCount];
							const nextPiece = prevPieces[i + oneSidePieceCount];
							
							/*
							p
							-
							t
							-
							p
							*/
							if(
								isNextToOther(prevPiece, piece, 'up') &&
								isNextToOther(nextPiece, piece, 'down')
							){
								restrictedTranslate.y = clamp(
									translate.y,
									prevPiece.y,
									nextPiece.y
								);
							}

							/*
								p
								-
								t
								-
							*/
							else if(isNextToOther(prevPiece, piece, 'up')){
								restrictedTranslate.y = clamp(
									translate.y,
									prevPiece.y,
									0
								);
							}

							/*
								-
								t
								-
								p
							*/
							else if(isNextToOther(nextPiece, piece, 'down')){						console.log('sita')
								restrictedTranslate.y = clamp(
									translate.y,
									0,
									nextPiece.y
								);
							}						
						}
					}

					piece.x = restrictedTranslate.x;
					piece.y = restrictedTranslate.y;
					
					return {
						...piece
					}		
				}else{
					return piece;
				}
			})
		);
	}, [translate]);

	// ドロップ後の処理
	// 空白の上でドロップしたときにピースと空白を入れ替える
	useEffect(() => {
		if(!mouseStatus.isUp) return;
		
		setPieces(prevPieces => {
			prevPieces.forEach((piece, i) => {
				if(piece.isDragging) {
					piece.isDragging = false;

					if(
						Math.abs(piece.x) >= piece.width ||
						Math.abs(piece.y) >= piece.height
					){
						const pieceColNumber = piece.colNumber;
						const pieceRowNumber = piece.rowNumber;
						const pieceIndex = piece.index;

						const blank = getBlank(prevPieces);
						const blankColNumber = blank.colNumber;
						const blankRowNumber = blank.rowNumber;		
						const blankIndex= blank.index;
						const unidirectionalPieceNum = getOneSidePieceCount();
						
						piece.index = blankIndex;
						blank.index = pieceIndex;

						// →へ着地
						if(
							isNextToOther(piece, blank, 'left') ||
							isNextToOther(piece, blank, 'right')
						){
							piece.x = 0;
							piece.colNumber = blankColNumber;
							blank.colNumber = pieceColNumber;
						}
						
						// ↓へ着地
						else if(
							isNextToOther(piece, blank, 'up') ||
							isNextToOther(piece, blank, 'down')
						){
							piece.y = 0;
							piece.rowNumber = blankRowNumber;
							blank.rowNumber = pieceRowNumber;
						}
						
						prevPieces[i] = {...blank};
						prevPieces[blankIndex] = {...piece};
					}
				}
			});
			
			return [...prevPieces];
		});
	}, [
		mouseStatus.isUp
	]);
	
	return [
		{ pieces, puzzleElementRef: elementRef },
		{ handleDown }
	];
};

type SlidePuzzlePieceProps = {
	className?: string;
	piece: PieceData;
	handleDown: (e: React.MouseEvent<HTMLElement>) => void;
};

const StyledSlidePuzzlePiece = styled.div`
	padding: 0.3vmin;
	position: relative;
	transform: translate3d(${({ piece }) => piece.x}px, ${({ piece }) => piece.y}px, 0);
`;

const PieceImage = styled.img`
	display: block;
	height: auto;
	pointer-events: none; // 画像がドラッグされないようにする
	width: 100%;
`;

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

const SlidePuzzlePiece = ({ className, piece, handleDown }: SlidePuzzlePieceProps) => {
	return (
		<StyledSlidePuzzlePiece
			id={`piece-${piece.number}`}
			className={className}
			piece={piece}
			onMouseDown={handleDown}
		>
			<>
				<PieceNumber className="piece-number">{piece.number}</PieceNumber>
				<PieceImage className="piece-image" src={piece.image}></PieceImage>
			</>
		</StyledSlidePuzzlePiece>
	);
};

const StyledBlank = styled.div`
	padding: 0.3vmin;
`;

const Blank = ({ className }: Pick<SlidePuzzlePieceProps, "className">) => {
	return <StyledBlank className={className} />;
};

type SlidePuzzleProps = {
	className?: string;
	size: string;
	pieces: PieceData[];
	pieceQuantity: number;
	handleDown: (e: React.MouseEvent<HTMLElement>) => void;
};

const StyledSlidePuzzle = styled.div<
	Pick<SlidePuzzleProps, "pieceQuantity" | "size">
>`
	background-color: #997f5d;
	border: 1vmin solid #997f5d;
	border-radius: 1vmin;
	box-sizing: content-box;// border, padding分を除いたサイズを取得するため
	display: grid;
	grid-template-rows: repeat(
		${({ pieceQuantity }) => Math.sqrt(pieceQuantity)},
		auto
	);
	grid-template-columns: repeat(
		${({ pieceQuantity }) => Math.sqrt(pieceQuantity)},
		auto
	);
	height: ${({ size }) => size};
	width: ${({ size }) => size};
`;

const SlidePuzzle = forwardRef<
	HTMLDivElement,
	Omit<SlidePuzzleProps, "pieceQuantity">
>(({ className, pieces, size, handleDown }, ref: React.ForwardedRef<HTMLDivElement>) => {
	return (
		<StyledSlidePuzzle
			className={className}
			ref={ref}
			size={size}
			pieceQuantity={pieces.length}
		>
			{pieces.map((piece, index) =>
				!piece.isBlank ? (
					<SlidePuzzlePiece
						className="draggable"
						key={piece.number}
						piece={piece}
						handleDown={handleDown}
					/>
				) : (
					<Blank key={piece.number} />
				)
			)}
		</StyledSlidePuzzle>
	);
});

const GlobalStyle = createGlobalStyle`
    :root {
    --bg-color: burlywood;
    --accent-color: darkorange;
    --text-color: rgb(40, 40, 40);
  }

    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }

    body {
        background-color: var(--bg-color);
        color: var(--text-color);
    }
`;

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

const App = () => {
	const [
		{ pieces, puzzleElementRef },
		{ handleDown }
	] = useSlidePuzzle(NUMBER_OF_PIECES, SLIDE_PUZZLE_IMAGE);

	return (
		<>
			<GlobalStyle />
			<StyledApp>
				<SlidePuzzle
					className="slide-puzzle"
					pieces={pieces}
					size={BOARD_SIZE}
					ref={puzzleElementRef}
					handleDown={handleDown}
				/>
			</StyledApp>
		</>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));
              
            
!
999px

Console