前回の記事では、ページ上にスライドパズルを表示しました。
今回はこのスライドパズルにピースをドラッグ&ドロップ可能にする機能を追加します。
連載目次
- ページ上にスライドパズルを表示する
- ピースをドラッグで動かす(この記事)
- ピースの移動範囲を制限する
- ピースをシャッフルする
- クリア判定を行う
- 制限時間を設ける
- 難易度を設ける
- 好きな画像を選べるようにする
- リサイズ
開発環境
開発環境はViteで構築しました。
ディレクトリ構成
現在のsrc配下は下記の通りです。
react-slide-puzzle
└── src
├── assets
├── components
| ├── Blank.tsx
| ├── SlidePuzzle.tsx
| └── SlidePuzzlePiece.tsx
├── constants
| └── slidePuzzle.ts
├── hooks
| └── useSlidePuzzle.ts
├── types
| ├── SlidePuzzle.ts
| └── SlidePuzzlePiece.ts
├── utils
| ├── clamp.ts
| └── images.ts
├── App.css
├── App.tsx
├── index.css
├── main.tsx
└── vite-env.d.ts
ピースの要素をドラッグ可能にする
はじめに、ピースの要素(コンポーネント)をドラッグ可能なものにします。
まずuseSlidePuzzle
フックを以下のように変更します。
/* 省略 */
const useSlidePuzzle = (
numberOfPieces: number,
imageUrl: string
): [PuzzleStatus, PiecesHandler] => {
const [
{
translate,
mouseStatus,
draggingElement,
draggingDirection
},
handleDown
] = useDraggableElements(false);
/* 省略 */
return [
{ pieces, puzzleElementRef: elementRef },
{ handleDown }
];
};
export default useSlidePuzzle;
変更点は、useDraggableElements
フックを呼び出し、戻り値として展開されたhandleDown
関数をuseSlidePuzzle
フックの戻り値に加えている点です。
useDraggingElements
フックは、指定した要素をドラッグ&ドロップ可能にするカスタムフックであり、handleDown
関数はドラッグ&ドロップのきっかけとなる関数です。これらの詳細については【React + TypeScript】要素をドラッグ&ドロップ可能にするをご覧ください。
次に、SlidePuzzlePiece
コンポーネントを以下のように変更します。
/* 省略 */
type SlidePuzzlePieceProps = {
/* 省略 */
handleDown: (e: React.MouseEvent<HTMLElement>) => void;
};
/* 省略 */
const SlidePuzzlePiece = ({ className, piece, handleDown }: SlidePuzzlePieceProps) => {
return (
<StyledSlidePuzzlePiece
// 省略
onMouseDown={handleDown}
>
{/* 省略 */}
</StyledSlidePuzzlePiece>
);
};
export default SlidePuzzlePiece;
SlidePuzzlePiece
コンポーネントの変更点は、propsとしてhandleDown
関数を受け取った後、それをStyledSlidePuzzlePiece
のonMouseDown
属性に指定している点です。
これにより、ピースの要素を押し込んだ時にhandleDown
関数が実行されるようになります。
次に、SlidePuzzle
コンポーネントを以下のように変更します。
/* 省略 */
type SlidePuzzleProps = {
/* 省略 */
handleDown: (e: React.MouseEvent<HTMLElement>) => void;
};
const SlidePuzzle = forwardRef<
HTMLDivElement,
Omit<SlidePuzzleProps, "pieceQuantity">
>(({ className, pieces, size, handleDown }, ref: React.ForwardedRef<HTMLDivElement>) => {
return (
<StyledSlidePuzzle
// 省略
>
{pieces.map((piece, index) =>
!piece.isBlank ? (
<SlidePuzzlePiece
// 省略
className="draggable"
handleDown={handleDown}
/>
) : (
<Blank key={piece.number} />
)
)}
</StyledSlidePuzzle>
);
});
export default SlidePuzzle;
SlidePuzzle
コンポーネントの変更点は次の通りです。
SlidePuzzlePiece
コンポーネントにhandleDown
関数を受け渡すSlidePuzzlePiece
コンポーネントのclassName
属性に.draggable
クラスを指定
1つ目の変更点については、propsとしてhandleDown
関数を受け取るようにしたあと、それをSlidePuzzlePiece
コンポーネントのhandleDown
属性に指定しています。バケツリレーということです。
2つ目の変更点についてですが、なぜ.draggable
クラスを指定しているのかというと、useDraggableElements
フックは.draggable
クラスを指定した要素がドラッグ可能となるような実装をしているためです。
pieceの座標とドラッグを連動させる
ピースの要素をドラッグした距離=piece
のx
, y
の値となるようにします。
はじめにSlidePuzzlePiece
コンポーネントを以下のように変更します。
/* 省略 */
const StyledSlidePuzzlePiece = styled.div`
// 省略
transform: translate3d(${({ piece }) => piece.x}px, ${({ piece }) => piece.y}px, 0);
`;
const SlidePuzzlePiece = ({ className, piece, handleDown }: SlidePuzzlePieceProps) => {
return (
<StyledSlidePuzzlePiece
// 省略
id={`piece-${piece.number}`}
piece={piece}
>
{/* 省略 */}
</StyledSlidePuzzlePiece>
);
};
export default SlidePuzzlePiece;
変更点は以下の通りです。
- IDを指定
piece.x
,piece.y
をtransform: translate3d()
の値に指定
IDを指定
StyledSlidePuzzlePiece
コンポーネントにIDを指定しています。このID名はドラッグしたピースを判定する際に用います。
piece.x, yをtransform: translate3d()の値に指定
StyledSlidePuzzlePiece
コンポーネントにpropsとしてpiece
を渡したあと、そのx
, y
プロパティ値をtransform: translate3d()
のx
, y
値に指定しています。そうです。ピースはtranslate3d()
で動かすのです。
次に、useSlidePuzzle
フックを以下のように変更します。
/* 省略 */
const useSlidePuzzle = (
numberOfPieces: number,
imageUrl: string
): [PuzzleStatus, PiecesHandler] => {
/* 省略 */
// ピースのデータを初期化する関数
const initializePieces = async () => {
/* 省略 */
pieceImageUrls.then((imageUrls) => {
const pieces = [];
for (let i = 0; i < numberOfPieces; i++) {
const piece = {
/* 省略 */
x: 0,// ピースの横の移動距離
y: 0,// ピースの縦の移動距離
};
pieces[i] = piece;
}
setPieces(pieces);
});
};
/* 省略 */
// ドラッグでピースを移動させる
useEffect(() => {
if(!draggingElement) return;// nullチェック
setPieces(prevPieces =>
prevPieces.map((piece, i) => {
if(`piece-${piece.number}` === draggingElement.id){
piece.x = translate.x;
piece.y = translate.y;
return {
...piece
}
}else{
return piece;
}
})
);
}, [translate]);
return [
{ pieces, puzzleElementRef: elementRef },
{ handleDown }
];
};
export default useSlidePuzzle;
変更点は次の通りです。
piece
にx
,y
座標を追加- ドラッグしたピースに紐づく
piece
のx
,y
プロパティにtranslate.x, yを代入
pieceにx, y座標を追加
initializePieces
関数による初期化の際、piece
にx
, y
プロパティを新たに追加しています。
ドラッグしたピースに紐づくpieceのx, yプロパティにtranslate.x, yを代入
useEffect
をピースがドラッグされたときに実行されるようにしています。その後、先ほど要素に指定したID名と文字列piece-${piece.number}
を比較し、pieces
内からドラッグしたピースに紐づくpiece
を見つけ出しています。で、そのpiece
のx
, y
プロパティにtranslate
のx
, y
をそれぞれ代入しています。
ここまでのコード
これまでに書いたコードや実際の動作は下記のCodePenで確認できます。
次回
「ピースをドラッグで動かす」については以上です。次回はピースの移動範囲を制限します。