更新内容

動画にシンクロしてとかそういうものではありません。オリジナルを鳴らしながら弾けるように動画プレイヤーを埋め込んだだけです。

スピードとかイジれると良いのですが、ここには葛藤があってゆっくり弾くことにはあまり意味はないと考えているフシもありまして、できるだけオリジナルと同じように弾くことこそが練習だと思っています。

編集したギターコードや歌詞はボタン一つでクリップボードにコピーできるようにもしました。もし手元にテキストエディターがあってそこに歌詞とギターコードを書き込んで保存していた場合、次からファイルをアップロードすればすぐに見れて反映できるので、おや?と思うコードやその位置を変更した場合に保存できた方が良いわけですが、Ctrl+Aの後Ctrl+Cでできるものの、コピーボタンがあれば1回で済むわけで、タブレットやスマホなどは最初から想定はしてないもののそれらキーボードの操作ができない物にとってはこの方が比較的ラクだろうと思ったので実装してみました。

これら変更に伴って、Youtubeのアドレスがある場合は、output欄のレイアウトも変更しました。

Youtube動画を読み込む方法

1つは、歌詞などと一緒にテキストエリアに動画のアドレスを書き込んでおけばそれらを読み込みます。じゃまにならないように最後尾に忍ばせておくのが良いかと思います。
複数アドレスの書き込みがある場合は最初のものがヒットします。またそれとは別にYoutubeの動画アドレスを入れられるinput要素も追加しました。

このテキストエリアかinput欄いずれかに動画のアドレスがあったらoutput欄付近にその動画が表示されるようになります。

もしテキストエリアに動画アドレスがあり、input欄いずれにも動画アドレスがあった場合、input欄の方を優先します。

  • Youtubeの動画アドレスはブラウザでアクセスしたページのURL欄にあるもの

  • 共有ボタンからアドレスをコピーしたhttps://youtu.be/ID形式もそのままでいけます

    • そのままで…というのはyoutu.be/ID?si=...とあるものをそのままでもOKです
  • 他の動画サイトのアドレスには対応してないのでYoutubeの動画だけの機能です

  • input欄にアドレスが残ったまま新しいテキストファイルがドロップされてCheckボタンが押されるとテキストファイルの内容と異なった動画が表示されるので、ファイルがドロップされた場合はinput欄の値も消去するようにしました。
    テキストエリアの編集時には自動で消えませんのでClearボタンなどで対応してください

コピーボタンの表示方法・その他

コピーボタンはテキストエリアで何か入力があれば表示されます。

Youtubeの動画プレイヤーも、コピーボタンもデザイン的には小さすぎるとか諸々があるので、変更する可能性は高いです。
また縦に伸びすぎているのですぐにテキストエリアとoutput欄を移動できるように位置の移動用のボタンを設置するかもしれません。自動スクロールで移動できるようにしました。

checkボタンを押してoutput欄に出力がされると、トランスポーズボタンが画面上部の位置まで自動でスクロールされるようにしました。

中身の技術的なこと

要素内のテキストを一度でコピー


copyButton.addEventListener('click', copyText);

function copyText() {
    const text = area.value;
    navigator.clipboard.writeText(text)
        .then(() => {
            copiedMessage.innerText = 'Copied!';

            setTimeout(() => {
                copyButton.classList.remove('active');
                copiedMessage.innerText = 'Copy all';
            }, 2000);
        })
        .catch(error => {
            console.error('Failed to copy text:', error);
        });
};

const text= area.valueは、テキストエリアのテキストです。navigator.clipboard.writeText(text)はクリップボードにテキストエリアの値を書き込むもので、成功した場合はthen(()=>{...ここ...})が処理され、失敗したらcatchが実行されコンソールログに出力されますがたいていは成功します。

コピーボタンは、テキストエリアが空でない場合に表示されるようになっています。何かを入力してバックスペースなどで値が無くなったらやはり空になるのでコピーボタンは隠れます。

area.addEventListener('input', () => {
    if (area.value !== "") {
        copyButton.classList.add('active');
    } else {
        copyButton.classList.remove('active');
    }
});

それはこのように書いてあります。テキストエリアに何かしら入力されたら、空でない場合はコピーボタンに.activeをつけ空だったらとるということをしています。この状態から上のcopyText()の処理になります。

コピーボタンをクリックすると、copyText()が処理されて、クリップボードにテキストがコピーされたりします。この時にコピーボタンはテキストエリアが空であると隠れてますし、テキストをコピーされてもボタン自体に何の変化もないので、setTimeoutを使って、copiedとメッセージが出た後、2秒後に元のテキストであるcopy allを入れて、.activeを取り除きます。するとまたボタンは隠れます。

この状態でまた何かテキストエリアが編集されたらボタンが表示されます。

いったんターゲットまで上スクロールさせて数px下スクロールさせる

なんでこんな面倒くさいことをしているかと言うと、当サイトでは、下向きのスクロールの時はヘッダはposition: relativeでいつもの位置にあり、上向きにスクロールした場合はヘッダにメニューがあることもありヘッダがposition: fixed等で画面上部に表示されます。

下向きに移動する場合にはこれら理由でヘッダが表示されませんからCheckボタンを押してoutput欄が表示されるのには問題ありませんが、今度は下にある要素からDropエリアを表示するには必ず上向きスクロールが発生します。するとおのずとヘッダが表示されますからDropエリアをターゲットにした場合、スクロール終了時にはターゲットの要素にヘッダが重なるということになります。

そこで重なっていたら消すということもできるのでしょうが、後々面倒なので、一旦通り過ぎて通り過ぎた分だけ戻すという事をしています。

色々と方法はありそうですがpromiseを使用しています。

const backtoDropArea = document.querySelector(".backtoDropArea");

async function scrollToDropArea() {
    const dropAreaRect = dropArea.getBoundingClientRect();
    const scrollTop = dropAreaRect.top + window.scrollY - 5;
    await new Promise(resolve => {
        window.scrollTo({
            top: scrollTop,
            behavior: 'smooth'
        });
        setTimeout(resolve, 500); // スクロールの完了まで少し待機する
    });

    const scrollTopAfter = scrollTop + 5;
    window.scrollTo({
        top: scrollTopAfter,
        behavior: 'smooth'
    });
}

backtoDropArea.addEventListener("click", () => {
    scrollToDropArea();
});

なぜこんな事をしているかですが、実は、

TARGET.scrollIntoView();
window.scrollBy(0,1)

たったこれだけのことなのです。TARGET要素がscrollIntoViewに入るまで、オプションを何も書かなければTARGET.topまでスクロールします。その後、scrollByで1pxだけ下にwindowをスクロールさせます。

しかし、これは動作しません。個別にscroll-behavior: smoothが指定してあるならまだしもcssでガッツリ書いてある当サイトのような設定ではピクリともしないのではないかと。scrollIntoViewは動作するかもしれませんが、これはスムーススクロールされているビヘイビアが設定されているのでスクロール完了までに時間がかかります。その間にscrollByが動作しようとして、何してくれてますの?となるわけです。

では、promiseを使用して、TARGET.scrollIntoViewの後、window.scrollByを実行したらどうでしょうか?これもうまくいきませんでした。もしかするとやり方によっては動作するのかもしれませんが上手く行かないので↑で書いたように、scrollToを使用して書くことにしました。

scrollIntoViewでは勝手に算出してくれているターゲット要素のトップ位置ですが、これはscrollToを使用する場合には算出しないといけません。そこでconst dropAreaRect = dropArea.getBoundingClientRect();でドロップエリアの位置を算出します。

次にどこまでスクロールさせるかをconst scrollTopで求めています。dropエリアの上辺(.top)までのスクロール量を求めてそこまでスクロールさせるわけですが、いったん行き過ぎて戻ってくるのが目的なので、- 5(px)しています。

次にawaitを使用するのでasyncが入っていますが、ここでpromiseを作りスクロールさせます。スクロールが終わるまでawaitして、途中でresolve(解決)されると問題なのでsetTimeout5msだけ待機しています。これがscroll-behavior: smoothが設定されていないなら待機する必要はないかもしれません。その場合はスクロールというよりジャンプして移動しているように見えるだろうと思います。

で、上向きのスクロールが(待機時間も含めて)完了したらconst scrollTopAfter部分が実行されます。5px分行き過ぎているので、その分下向きに5px分スクロールさせます。これによってヘッダがドップエリアに重なって表示されないようにしました。

このような感じで見た目は地味なものですが色々と手を加えています。