ツクログネット

【React Hooks + TypeScript】ドラッグ&ドロップで遊べるスライドパズルを作る その2「ピースをドラッグ&ドロップで動かす」

eye catch

前回の記事では、ページ上にスライドパズルを表示しました。

今回はこのスライドパズルにピースをドラッグ&ドロップ可能にする機能を追加します。

連載目次

  1. ページ上にスライドパズルを表示する
  2. ピースをドラッグで動かす(この記事)
  3. ピースの移動範囲を制限する
  4. ピースをシャッフルする
  5. クリア判定を行う
  6. 制限時間を設ける
  7. 難易度を設ける
  8. 好きな画像を選べるようにする
  9. リサイズ

開発環境

開発環境は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関数を受け取った後、それをStyledSlidePuzzlePieceonMouseDown属性に指定している点です。 これにより、ピースの要素を押し込んだ時に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の座標とドラッグを連動させる

ピースの要素をドラッグした距離=piecex, 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.ytransform: 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;

変更点は次の通りです。

  • piecex, y座標を追加
  • ドラッグしたピースに紐づくpiecex, yプロパティにtranslate.x, yを代入

pieceにx, y座標を追加

initializePieces関数による初期化の際、piecex, yプロパティを新たに追加しています。

ドラッグしたピースに紐づくpieceのx, yプロパティにtranslate.x, yを代入

useEffectをピースがドラッグされたときに実行されるようにしています。その後、先ほど要素に指定したID名と文字列piece-${piece.number}を比較し、pieces内からドラッグしたピースに紐づくpieceを見つけ出しています。で、そのpiecex, yプロパティにtranslatex, yをそれぞれ代入しています。

ここまでのコード

これまでに書いたコードや実際の動作は下記のCodePenで確認できます。

次回

「ピースをドラッグで動かす」については以上です。次回はピースの移動範囲を制限します。