CSS と JavaScript でカラオケのテロップを再現する

## 概要

カラオケでテロップの色が変わるあの感じを CSS で再現します。

![karaoke.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/486784/4423bd60-e3ff-f948-2fb9-5b2f4c4b7847.gif)

:::note warn
テロップをリズムに合わせて変えるのはやりません。気が向いたらやります。
:::

## 画面に表示する歌詞の要素

歌詞は行ごと、文字ごとにテロップのタイミングが決まっています。これを想定して、歌詞を一文字ずつ `span` タグで書きます。  
また、テロップはなめらかに変わるので、文字がどこまで塗られているかを表す変数を `style` 属性に用意しておきます。

```html
<div class="container">
    <div class="lyrics">
        <div class="lyrics--line">
            <span class="syllable" style="--cursor: 0%">ド</span>
            <span class="syllable" style="--cursor: 0%">リ</span>
            <span class="syllable" style="--cursor: 0%">ン</span>
            <span class="syllable" style="--cursor: 0%">ク</span>
            <span class="syllable" style="--cursor: 0%">バ</span>
            <span class="syllable" style="--cursor: 0%">ー</span>
            <span class="syllable" style="--cursor: 0%">は</span>
            <span class="syllable" style="--cursor: 0%">太</span>
            <span class="syllable" style="--cursor: 0%">る</span>
            <span>　</span>
            <span class="syllable" style="--cursor: 0%">太</span>
            <span class="syllable" style="--cursor: 0%">る</span>
            <span class="syllable" style="--cursor: 0%">よ</span>
            <span>　</span>
            <span class="syllable" style="--cursor: 0%">ba</span>
            <span class="syllable" style="--cursor: 0%">by</span>
        </div>
    </div>
</div>
```

## 歌詞の表示位置

歌詞は通常、画面下に表示するので、`align-items: flex-end` と `justify-content: center` で歌詞の行位置を調整します。また、歌詞の各文字は真ん中に並べて配置したいので、`justify-content: center` を指定します。

```css
html, body {
    height: 98vh;
}

.container {
    height: 100%;
    background-image: url("./karaoke_saiten.png");
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;

    display: flex;
    justify-content: center;
    align-items: flex-end;
}

.lyrics {
    font-size: 60px;
    background-color: rgb(0 140 90 / 20%);
    width: 100vw;
}

.lyrics--line {
    display: flex;
    justify-content: center;
    -webkit-text-stroke: 1px darkblue;
}
```

## 文字の一部分だけ色が変わるやつの表現

こちらを参考にしました。

https://creatornote.nakweb.com/half_css/

`background` に `linear-gradient` を指定することで、背景をグラデーションさせることができます。  
左から右方向の場合は 90 度回転させると良いらしいです。  
割合については、左端から 指定したパーセンテージまで青、それ以降右端まではグレーにしています。色がなめらかに変化する部分は無く、色の境界がはっきりしています。

`background-clip` を使うと、背景を特定の範囲にだけ表示させることができます。`text` を指定することで、文字部分にだけ背景が指定され、文字色が途中で切り替わっているように見せることができます。  
背景の色が見えるように、本来の文字色は透明 `transparent` にしておきます。

```css
.lyrics--line .syllable {
    background: linear-gradient(90deg, blue 0 var(--cursor, 0), darkgrey var(--cursor, 0) 100%);
    -webkit-background-clip: text;
    color: transparent;
}
```

各文字の cursor 変数の値を 0% から 100% の間で好きなように変更すると、指定した分だけ文字が塗られます。

## 文字色を徐々に変えていく

今回は画面のどこかをクリックすると文字色が変わっていくようにします。  
各音節ごとに 200 ms かけて文字を塗りつぶすよう、`--cursor` を変更していきます。

```js
const playLyrics = () => {
    const syllables = document.querySelectorAll('.syllable')
    let offset = 0
    for (const syllable of syllables) {
        for (let i = 0; i < 100; i++) {
            setTimeout(() => {
                syllable.style.setProperty('--cursor', `${i + 1}%`)
            }, offset)
            offset += 2
        }
    }
}
document.querySelector('html').addEventListener('click', playLyrics, {once: true})
```

## 感想

カラオケアプリ自体はたくさん出回っているし、何番煎じかよって感じですが、こうやって自分で作ってみると楽しいですね。でも曲は既存のやつを使うと権利まわりでお金がたくさんかかるので全部自作曲にしないといけません。これがとてもつらいです。

## コード全体

コード全体も載せておきますので、ご参考までに。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CSS でカラオケのテロップを再現する</title>
    <style>
        html, body {
            height: 98vh;
        }

        .container {
            height: 100%;
            background-image: url("./karaoke_saiten.png");
            background-size: contain;
            background-repeat: no-repeat;
            background-position: center;

            display: flex;
            justify-content: center;
            align-items: flex-end;
        }

        .lyrics {
            font-size: 60px;
            background-color: rgb(0 140 90 / 20%);
            width: 100vw;
        }

        .lyrics--line {
            display: flex;
            justify-content: center;
            -webkit-text-stroke: 1px darkblue;
        }

        .lyrics--line .syllable {
            background: linear-gradient(90deg, blue 0 var(--cursor, 0), darkgrey var(--cursor, 0) 100%);
            -webkit-background-clip: text;
            color: transparent;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="lyrics">
        <div class="lyrics--line">
            <span class="syllable" style="--cursor: 0%">ド</span>
            <span class="syllable" style="--cursor: 0%">リ</span>
            <span class="syllable" style="--cursor: 0%">ン</span>
            <span class="syllable" style="--cursor: 0%">ク</span>
            <span class="syllable" style="--cursor: 0%">バ</span>
            <span class="syllable" style="--cursor: 0%">ー</span>
            <span class="syllable" style="--cursor: 0%">は</span>
            <span class="syllable" style="--cursor: 0%">太</span>
            <span class="syllable" style="--cursor: 0%">る</span>
            <span>　</span>
            <span class="syllable" style="--cursor: 0%">太</span>
            <span class="syllable" style="--cursor: 0%">る</span>
            <span class="syllable" style="--cursor: 0%">よ</span>
            <span>　</span>
            <span class="syllable" style="--cursor: 0%">ba</span>
            <span class="syllable" style="--cursor: 0%">by</span>
        </div>
    </div>
</div>
</body>
<script>
    const playLyrics = () => {
        const syllables = document.querySelectorAll('.syllable')
        let offset = 0
        for (const syllable of syllables) {
            for (let i = 0; i < 100; i++) {
                setTimeout(() => {
                    syllable.style.setProperty('--cursor', `${i + 1}%`)
                }, offset)
                offset += 2
            }
        }
    }
    document.querySelector('html').addEventListener('click', playLyrics, {once: true})
</script>
</html>
```