アナグラムの穴風を目指した文字列入れ替え

アナグラム関係無いものができあがる

# <i class="fa fa-play"></i> デモ 

https://jsfiddle.net/d9aLgc08/

FirefoxとChromeで確認

![ana.gif](https://qiita-image-store.s3.amazonaws.com/0/62354/f47d0609-f46b-904d-3e22-28b4b22f8505.gif)

# <i class="fa fa-code"></i> コード

```html

<!DOCTYPE html>

<html>
<head>
    <meta charset="UTF-8"> 
    <title>アナグラムの穴</title>
</head>
<body>
    <div><input id="anagram" type="text" placeholder="Completed text" required=""></div>
    <div><input id="mystic" type="text" placeholder="Init text ※optional"></div>
    <div><button id="start">start!</button></div>
    <div        id="display"> Here! </div>
</body>
<script>

const body = document.getElementsByTagName('body')[0];

const anagram   = document.getElementById('anagram');
const mystic    = document.getElementById('mystic');
const display   = document.getElementById('display');
const button    = document.getElementById('start');

var originalArray = [];
const initOriginalArray = () => {
    originalArray = anagram.value.split('');
}

const shuffle = (array) => {
    for(var i = array.length - 1; i > 0; i--){
        var r = Math.floor(Math.random() * (i + 1));
        var tmp = array[i];
        array[i] = array[r];
        array[r] = tmp;
    }
    return array;
}

const showInitText = () => {
    return new Promise((resolve, reject) => {
        let initeText = '';
        if (mystic.value) {
            initeText = mystic.value
        } else {
            initeText = shuffle(anagram.value.split('')).join('');
        }
        setTimeout(() => {
            display.innerText = initeText
            setTimeout(() => {
                resolve();
            }, 1000)
        }, 1000);
    });
}


const vanishText = () => {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            const vanishWord = '　';
            const duration = '800';
            const maxLoop = originalArray.length;

            let i = 0;
            let vanishOne = () => {
                let word;
                // だいぶ非効率
                while((word = shuffle(display.innerText.split(''))[0]) == vanishWord) {}
                display.innerText = display.innerText.replace(new RegExp(word), vanishWord);

                i++;
                if (i == maxLoop) {
                    clearInterval(timer);
                    resolve();
                }
            }
            const timer = setInterval(vanishOne,duration);
        }, 2400);
    });
}

const showAnagram = () => {
    return new Promise((resolve, reject) => {
        const duration = '800';
        const maxLoop = originalArray.length;
        // Array.keys()ではイテレータになるので
        const randomOrder = shuffle(Object.keys(originalArray))

        let i = 0;
        let showOne = () => {
            let reg;
            if (randomOrder[i] == 0 ) {
                // $1に空文字を入れるためのダミー()
                reg = new RegExp('^().');
            } else {
                // jsでは肯定戻り読みできない
                // http://stackoverflow.com/questions/4200157/javascript-regular-expression-exception-invalid-group
                reg = new RegExp('(^.{' + randomOrder[i] + '}).');
            }
            display.innerText = display.innerText.replace(reg, '$1' + originalArray[randomOrder[i]]);
            i++;
            if (i == maxLoop) {
                    clearInterval(timer);
                    resolve();
            }
        }
        const timer = setInterval(showOne,duration);
    });
}


button.addEventListener('click',()=>{
    if (!anagram.validity.valid) {
        return;
    }
    body.className = 'change';
    display.className = 'change';

    initOriginalArray();
    showInitText().then(vanishText).then(showAnagram).then(()=>{console.log('done')});
});

</script>
<style type="text/css">

@keyframes main-bg {
    0% { background: white;}
    100% { background: black; }
}

@keyframes main-txt {
    0% { color: black;}
    45% {font-size: 0}
    75% {font-size: 38px}
    90% {font-size: 20px}
    100% { color: white; 
        font-size: 32px;}
}

body.change {
    animation-name:main-bg;
    animation-duration: 2s;
    animation-timing-function:linear;
    animation-fill-mode: forwards;
}

#display {
    font-size: 32px;
    color: black;
}

#display.change {
    animation-name:main-txt;
    animation-duration: 2s;
    animation-timing-function:linear;
    animation-fill-mode: forwards;
}

</style>
</html>

```

# <i class="fa fa-cube"></i> アナグラムとは

>アナグラム（anagram）とは、言葉遊びの一つで、単語または文の中の文字をいくつか入れ替えることによって、全く別の意味にさせる遊びである。

>例えば「アナグラム」から「グアムなら」などのアナグラムを作ることができる。

[アナグラム - Wikipedia]

# アナグラムの穴とは

ラーメンズの小林賢太郎のソロライブ、POTSUNENの演目。
文字のカードを使い、アナグラム前後や全体でシュールなストーリーを展開する。
アナグラム前のカードの集め方とアナグラム後のカードの置き方がランダム(かのように)に行われ、徐々に完成するアナグラムが途中で分かる点が面白い。

# <i class="fa fa-cube"></i> 流れ

* 最終型(アナグラム後)の文字列を入力する
* (最初に表示される(アナグラム前)文字列を入力する)
* アナグラム前の文字を表示する。入力が無い場合はアナグラム後の文字をシャッフルして表示する  
(パッと切り替わるとしまらないのでついでにCSSアニメーションを調べてfont-size 0pxのときに切り換える)
* 最終型の文字列を配列にして、各文字と位置の対応を記憶する  
* ランダムに一文字ずつ全角スペースに置換して、文字が消える風を演出する
  * 文字列配列の添字配列が0~length-1までなので、これをシャッフルしてlength回i++ループさせることでランダムな順番を作る
* 全て消したら、文字列配列の添字配列をもう一度シャッフルして戻す順番を決めて、完成形の文字列を一文字ずつ全角スペースと置換していく

正しくはアナグラム前の文字列を並び替えるわけではないので、文字数だけあっていたらただの文字の変化。

目的の文字列へのランダムな入れ替えソートでできると一層それらしい演出になるのだが、実装方法や入れ替えアニメーションなどを考え一旦全削除にしてしまった。
アナグラムの穴で言うとカードを入れ替えるか、一旦手元にまとめてランダムに置いていくかでいうと後者


# <i class="fa fa-cube"></i> その他

やって見たいことが出来ても検索に困る。
文字列のシャッフルは見つかるが、それ以上なにを検索すればいいのかが難しい。

アナグラムは最初、自動生成しようと思って調べた。
「豚辞書」というものが配布されてそれを利用したページが幾つか見つかったが、本元は404になってしまっていた。

***

という記事をおそらく15,6年ごろに書いていました。
今見返すと、スタート前の文字列をfont-sizeが0のときに書き換えるのはなかなかの力技で、JS側の書き換えのshowInitTextのsetTimeoutの時間がCSS側のアニメーション時間の、それも全体の中央値を現すマジックナンバーになっているのが分かりにくいです。

今書くなら、
始めの文字を消すアニメーション、
アナグラム前の文字を出すアニメーション
の二つの要素、アニメに分けて、それぞれを`animationend`イベントでつなげる気がします。
アニメーション間のディレイはJS側で付けるとしても、アニメーションの全体時間を考慮する必要はなくなる…かな？

ただ、当時は調査能力不足か終了検知方法がわからなかった記憶があります。
あるいはPromiseをチェーンしたかっただけかもです。


<!-- ------------------------------->

[アナグラム - Wikipedia]:https://ja.wikipedia.org/wiki/アナグラム