Reactのinteractjsを使って3×3のスライドパズルを作った


完成形はこんな感じ

動かして中途半端な位置だと他のパズルが動かせないのでご注意ください。
完成すると、「完成!! Congratulations!!」と表示されます。
私のポートフォリオサイトでも試せます。

本記事内容

ソースコードは公開しているので(上記のcodesandbox内のものも含め)実装方法についてはあまり触れていません。(コメントは残したつもり)
参考情報とか、もしこのソースコードを利用する場合の注意点とか書いてます。
どっちかって言うと、「作ってみた」要素が強いです

作った経緯

Reactで「コンポーネントをドラッグで動かす」をやってみたかった。
動かせるUIって面白そう。

使った技術

土台はReact × TypeScriptです。
DOMをドラッグで動かすためにパッケージはInteractJSを使いました。

以下のブログを参考にさせてもらってます(感謝!)
【React】ドラッグでコンポーネントを動かせるようにするCustom Hookを作る
この記事読んで、(子供の頃遊んだ)「スライドパズル作れそう!」と思った次第です。(スライドパズルという名前だと今回作ってて知りました)
ソースコードもこちらのコードを改変していったものです。

使い方

ソースコードを改変せず、そのまま使う場合。
App.tsxplacement部分を編集してもらったら好きな絵でパズルにできます。

前提
パズルの各位置にアルファベットをふってます。(私がそう作っただけです)

alphabet-slide-puzzle

上記のアルファベットと以下のplacementのアルファベットが対応している形です。

~ 省略 ~
// アルファベットと配置の関係
// A B C
// D E F
// G H I J
// piece : 各ピースのinteractJSのCustom Hook部分
// image : パズルで表示する画像
// ans : 「アルファベットと配置の関係」に沿った答えの位置
  const placement = {
    a: {piece: useInteractJS(), image: PazzleC, ans: 'C'},
    b: {piece: useInteractJS(puzzlePositon.b), image: PazzleH, ans: 'H'},
    c: {piece: useInteractJS(puzzlePositon.c), image: PazzleE, ans: 'E'},
    d: {piece: useInteractJS(puzzlePositon.d), image: PazzleD, ans: 'D'},
    e: {piece: useInteractJS(puzzlePositon.e), image: PazzleA, ans: 'A'},
    f: {piece: useInteractJS(puzzlePositon.f), image: PazzleF, ans: 'F'},
    g: {piece: useInteractJS(puzzlePositon.g), image: PazzleB, ans: 'B'},
    h: {piece: useInteractJS(puzzlePositon.h), image: PazzleG, ans: 'G'},
    j: {piece: useInteractJS(puzzlePositon.j), image: PazzleI, ans: 'I'},
  };

~ 省略 ~

各アルファベット

パズルの初期値。「i」が存在しないのは、最初「空」になっている部分だからです。

piece

言葉で説明するのは少し難しいですが、interactJSを利用したCustom Hook部分です。
ここで、どのくらいコンポーネントを動かすか制御しています。
利用するだけなら、基本ここは触る必要ないです。

image

各ピースに表示させる画像を設定します。
「j」には「I」の位置に当てはまる画像を設定してください。
画像の9分割の仕方は後述します。

ans

imageで設定した画像が、実際は(正解では)どこの位置かアルファベットで設定します。

初期配置が私が設定している位置のままでいいなら、import部分で画像を差し替えるだけで完成します。

import PazzleA from './img/sakasakuma-a.png';
import PazzleB from './img/sakasakuma-b.png';
import PazzleC from './img/sakasakuma-c.png';
import PazzleD from './img/sakasakuma-d.png';
import PazzleE from './img/sakasakuma-e.png';
import PazzleF from './img/sakasakuma-f.png';
import PazzleG from './img/sakasakuma-g.png';
import PazzleH from './img/sakasakuma-h.png';
import PazzleI from './img/sakasakuma-i.png';

例えばPazzleAには「A」の位置の画像を指定してください。

画像の用意の仕方

正方形の絵か写真を用意したら、以下のサイトで9分割することができます。(感謝!)
Instagramのグリッド分割ジェネレーター

流れをまとめると、
画像の用意 → 画像の9分割 → 画像の差し替え(順番を先に変えたければお好きに)、、といった感じ。

補足情報

パズルが成り立っているか否かの判断は以下の証明が参考になります。
(この記事書くまでは知らなかったのですが、読んでいて面白かったです)
数学に抵抗感がない方は読んでみるといいかも。
8パズル,15パズルの不可能な配置と判定法

今回のソースコードでは空の位置が「I」なので、「空」の動かし方としてはゴールまで0回の置換で済みます。
よって、「空」が偶置換なら「その他」も偶置換となるように動かせばOKです。

言い換えると、「その他」の入れ替えが偶数回になるように動かせばいいはず。(間違っていたらごめんなさい)

その他の工夫点

工夫点というか、うまくいかなかったところの回避策というか……

動きの制御

「パズルを移動させている途中は、別のピースは動かさない」処理になっています。
なので、パズルが中途半端な位置にあると何も動かせなくなります。結構判定がシビアです。
(処理としてもあまり綺麗じゃないです……)

パズル1ピースの大きさ

80px×80pxにしています。
なぜかというと、スマホサイズにしても違和感の少ないサイズが80pxだったからです。
(正方形のサイズを変えたい場合はPuzzleHooks.tssideの値を変更すれば可能です)

逆にいうと、レスポンシブル対応(Web版とスマホ版でピースのサイズが変わるという意味で)はできていません。

touch-action: none;

パズルをスマホで動かす場合ですが、普通にやると「パズルを動かす操作」と「画面のスクロール」がバッティングします。
「画面のスクロール」が反応しないようにcssで「touch-action: none;」を指定してください。
サンプルコードではApp.cssの「App-puzzles」クラスに指定しています。

余談ですが、スマホの方が動かしやすくて楽しいです。

正解の判定

最初alertで出そうかと思ったのですが、alertが出た後もパズルを触った状態が継続していて、挙動が変になったのでやめました。

実装メモ

ソースコード自体を触る人もいるかもしれないので、心残り的なのも書いておくと、
今回の実装は、「パズルのピースを動かす度にグローバル変数の可動域を変更させる」というやり方で実装しています。
(グローバル変数はPuzzleHooks.tsの中にあります)

私はこの方法しか思いつかなかったのですが、処理の切り分けとしてはゴチャゴチャしていて微妙なんじゃないかと思います。
私の実装は参考程度にして新しい処理を書いてもらってもいいかもです。

おわりに

「作ってみた」で技術的新しい情報はなかったかもしれませんが、お読みいただきありがとうございました。
InteractJS、けっこう面白かったのでおすすめです。

以上になります!