昔YouTubeで流行った、横から流れるランキング動画風ページを生成するクソアプリをつくった

こんにちは、毎年恒例 [クソアプリアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/kuso-app) 18日目担当の黒神 (@kokushin) です。

今回で5回目の参加になります、今年もよろしくお願いします！

## 前置き

みなさん一昔前に流行った「ランキング動画」というものをご存知でしょうか。

こういう動画です。

https://youtu.be/cQrOmZQ4EzQ

**ランキングや比較の情報が、右から左に流れていくだけの~~しょーもない~~動画です。**

最近はあまり見かけなくなりましたが、1〜2年前は本当によく見かけました。  
(そしてBGMはなぜか魔王魂さんのシャイニングスターがよく使われていました)

おそらく手軽に作れてそこそこ再生数を稼げるからでしょうね。

今回は、そんな **ランキング動画をWebページとして再現しつつ、簡単に生成できるクソアプリを作ってしまおう！** といった内容です。


## 成果物

その名も **ランキングメーカー** です。

https://rankingmaker.vercel.app/

わりとガチ目なアプリになってしまいました。
でも大丈夫です。きっと世の中の役には立ちません。

こんな感じのページが作成できます。
**※音が流れますのでご注意ください**
**※Chrome最新版、iOS16以降しか対応していません**


https://rankingmaker.vercel.app/eAwz2aFIWWhxdk1lox3n?v=1

サムネである程度ネタバレしているところがクソっぽさあって良い感じです。

## 使い方

[ランキング作成ページ](https://rankingmaker.vercel.app/edit)へ移動して、下記の必要な情報を入力してください。

- ランキングタイトル
- BGM
  - シャイニングスター or Burning Heart の2曲が選べます

入力したら、画面下半分の項目パネルを追加してください。
**「項目を増やす」** を押すと項目を追加することができます。

また、パネルは無駄にドラッグ&ドロップで入れ替えることができます。

![スクリーンショット 2022-12-18 5.47.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57867/d6f84e33-8bb7-98c0-156f-db408d112baa.png)

なお、画像アップロード機能は予算と実装期間の都合で用意できませんでした。
**クソアプリなので許してください。**

一旦は[imgur.com](https://imgur.com/upload)など画像アップローダの利用を推奨しています。
※直リンクが禁止されている場合の画像URL引用はお控えください

左上のプレビューは編集を行うとリアルタイムで反映されます。
公開前に念入りに確認してください。
(シークできないのはお察しください)

![スクリーンショット 2022-12-18 4.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57867/b1ad1f8b-7025-e4bc-74b0-2e1fcc3e8c1f.png)

入力できたら、**「ランキングを生成」** ボタンを押しましょう。
ランキング動画風ページがインターネット上に生成されます。

ぜひ作ったランキングを SNS でシェアして見せびらかしてください。

## 技術的なお話

構成はこんな感じです。

- フロントエンド: Next.js
- DB: Firebase
- インフラ: Vercel

一応後から編集機能を追加しようと思っていたので、匿名認証を Firebase で実装しています。
Firebaseは本当にサクッとCRUD周りを実装できるので、こういう簡単なアプリ開発と相性が良いですね。

今回、クソアプリ開発を通じて特に勉強になった箇所は下記の2点です。

### vercel/ogを使った動的なOG画像生成

今回はシェア前提のアプリということもあり、og画像の生成を動的に行ってみました。

こんな感じでAPIに対してパラメータを投げてあげると、サーバ側で画像を生成して返してくれます。

```
/api/og?title=ほげほげ
```

内部的にはvercel/satoriというライブラリが動いていて、HTMLタグ/Style属性を用いてog画像をマークアップすることができます。

https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation

ただしすべてのStyle属性を使えるわけではなく、下記のような制約もあります。

- 要素の中に子要素が存在する場合は、必ず親要素に `display: flex` を指定しなければならない
- text-shadow や background-color などには単一の値しか指定できない
- background-size が contain や cover といった値をサポートしていない

各種プロパティの対応状況は下記のREADME.mdを確認してください。

https://github.com/vercel/satori#css

### Web Animation API の利用

Web Animation APIは、CSSアニメーションをJavaScriptで操作することができるAPIです。
今回は一時停止や再生再開、再生時間の取得などの実装が必要だったため利用しました。

使い方はわりとシンプルで、まずは要素に対してアニメーションの設定を行います。

```tsx
const animation = elementRef?.current?.animate(
  [
    { transform: `translate3D(0,0,0)` }, // 再生位置 0%~50%
    { transform: `translate3D(-100%,0,0)` }, // 再生位置 51%~100%
  ],
  {
    fill: "forwards", // アニメーション効果反映のタイミング
    easing: "linear", // イージングの種類
    duration: 3000, // 再生時間(ms)
    delay: 500, // 再生までの遅延時間(ms)
  }
);
```

あとはオブジェクトを色々操作することができます。

```tsx
animation?.pause(); // 一時停止
animation?.play(); // 再生
animation?.cancel(); // 停止
```

簡単ですね。
CSSアニメーションよりも複雑な処理や情報を取得することができるので、役立つシーンは多そうです。

### コンテナクエリの利用

今回、編集画面のプレビュー箇所でコンテナクエリを利用してみました。
コンテナクエリとは、親要素をターゲットにして子要素のスタイルを定義できる、比較的新しいCSSの機能です。

例えばウィンドウの横幅を基準にする単位 `vw` がありますが、親要素の横幅を基準にしたい場合には利用できません。

そんなときはまず基準にしたい親要素に対して `container` を定義します。

```css
.parent {
  /* container-name は任意の値 */
  container: container-name / inline-size;
}
```

そうすると親要素に内包されている子要素では、`cqw` といった親要素の横幅を基準とした値を扱うことができます。
※100cqw = 親要素の横幅100%分に相当

```css
.child {
  width: 100cqw;
}
```

ランキングメーカーのプレイヤー箇所はすべてcqwで実装しているため、どのコンポーネントに適当に突っ込んでも良い感じに伸縮してくれます。
これは本当に革命的だなぁと思いました。

## あとがき

今回は仕事の合間で2週間くらいかけて実装していましたが、わりと良い感じのクソアプリを作ることができたんじゃないでしょうか。
新しい技術にも触れることができたので、実装していてすごく楽しかったです。

余談ですが最近、「ノベルゲームメーカー」というブラウザでノベルゲーが作れちゃうツールものんびり開発してたりします。やっぱりツール系の開発は楽しいですね。

そちらも完成したらTwitterで発信すると思うので、気になる方はフォローしてみてください。

ランキングメーカーも遊んでもらえると嬉しいです。
今年も一年、お疲れさまでした！


## 過去作

https://qiita.com/kokushin/items/a2b00a8ecf1edccb8199

https://qiita.com/kokushin/items/d413464b1d98162d07ad

https://qiita.com/kokushin/items/253ef1d5bd90d4c68d14

https://qiita.com/kokushin/items/54894b6ff9a4b24c0855