【ウマ娘】エイプリルフール2025特設サイトの構造を調べる【BoC'z we＼n】

ウマ娘プロジェクトでは毎年4月1日に特設Webサイトが期間限定で公開されている

その特徴のひとつに比較的モダンな技術スタックで制作されていることが挙げられる

2024年にはNuxt.jsが採用されていたが今回はAstroが採用されている

https://x.com/uma_musu/status/1906723255874105600

リリース4周年時にはDMMGAMESの特設サイトもAstroでリプレイスされている

https://dmg.umamusume.jp/

Webフロントエンド部門のtechブログや講演が無いか探したが2025年4月時点では発見できなかった

そこで筆者の理解できる範囲で本サイトの構造を調査した


# Astro活用例

Astro自体の説明は割愛する

https://docs.astro.build/ja/getting-started/

https://qiita.com/udayaan/items/24ecb2f5f4608fc1df4c


## スコープ付きスタイル

https://docs.astro.build/ja/guides/styling/#%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%95%E3%82%8C%E3%81%9F%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB

Astroコンポーネントでstyleタグを記載するとそのコンポーネント専用のスタイルとなる

スコープされたコンポーネントには`data-astro-cid-xxx`属性が付与される


## Astro ImageTools

https://github.com/RafidMuhymin/astro-imagetools

https://qiita.com/heeroo_ymsw/items/07a4dbeac9515559694f

画像アセットを様々なサイズ・拡張子で出力して表示の出し分けが可能

該当要素にはクラス名に`astro-imagetools-picture`が付与される


# マークアップ大観

ここから主にHTML/CSSの構造に注目する

本サイトはおよそ5つのブロックから構成されていた
（本記事では上から2つまでに絞って記載する）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/5ce30d56-90a6-4118-a162-bae142d9ea65.png)

下記classがsection要素に追加される
（参考：hoisted.*.js）
- ページローディング完了 → `scene-sound`
- 音声ON/OFF選択完了 → `scene-kv`
- kv(キービジュアル)アニメ終了 → `scene-end`


## attentionコンポーネント
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/3639050f-ee88-4924-8265-0e77bf307092.png)

横幅900px以下の横向き時のみ表示される（CSSで制御）

また端末の向きを変えたり一定の画面サイズに変化することで再読み込みが走る


## introコンポーネント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/c2fef6f3-f815-404e-9727-f7d2886c5d91.png)

ローディング→音声選択→アニメーション実行 までの表示を担う

## loadingコンポーネント

![ezgif-1aed16bba8692c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/acfaf226-a642-4db4-8393-a48fc1f95ba1.gif)

ページ読み込み中に表示されるCSSアニメーション

`animation-delay`で周期ズレの実現


## sound-selectコンポーネント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/f49ecb1e-90fc-4887-85fb-ccb2222577f7.png)

`transform`でコンポーネント自身を上下左右中央揃え

```css
{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate3d(-50%, -50%, 0);
}
```

## videoコンポーネント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/ca60e988-acff-4903-ab24-326b2fea61a2.png)

シンプルなvideo要素

黒背景の上から`opacity`20%の被せで控えめな表示

## kvコンポーネント

音声ON/OFFが選択されるまでは`opacity`と`visibility`で非表示
section要素に`scene-kv`クラスが付与されてからアニメーションタイムラインが開始される

![boczwen.umamusume.jp_(iPhone 14 Pro Max) (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/c4426fe3-50c9-4b8c-810d-26f88dd7a6d2.png)

最初に[ぼかし背景]と[中央ロゴ]を表示

![boczwen.umamusume.jp_(iPhone 14 Pro Max) (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/0f677da5-0b04-4cf9-9790-5d033bb46f70.png)

そのあと[通常背景]と[斜めロゴ]、[キャラ画像]を表示という流れ

横幅がおよそ768px以上の場合フェードインとスケールダウンアニメが入るが、
768px未満の場合は左から右へスライド移動するアニメが入る

:::note
sound-selectコンポーネントが表示されている状態でintroコンポーネントのclassに`scene-kv`を手動で追加するとkvコンポーネントのアニメーションが表示される

また `Ctrl + Z` および `Ctrl + Y` で繰り返し再生することも可能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/25069ae3-6957-44ea-8547-ed1103a1b72b.png)
:::

また数秒経過するかSKIPボタンを押下するとkvコンポーネントがフェードアウトする

その後introコンポーネント自体が消滅する


# アニメーション

画像のアニメーションは基本`transition`で制御されている

```html:画像アニメーションのための基本マークアップ構造
<div>
  <figure>
  
    <!-- ここからAstro ImageTools -->
    <picture>
      <source/>
      <source/>
      ...
      <img/>
    </picture>
    <!-- ここまでAstro ImageTools -->
    
  </figure>
</div>
```

## 共通セットアップ処理

本サイトの画像要素のほとんどに仕込まれているアニメ

### 画像読み込み中

```css:picture要素 - 初期状態
figure picture {
    opacity: calc(1 - var(--opacity, 1));
    transition: opacity .3s cubic-bezier(.165,.84,.44,1);
}
```

`var(--opacity, 1)`とあるが画像読み込み完了前は変数`--opacity`が与えられていないため、第二引数の`1`が選ばれる

よってこの時点では`opacity`は`0`

### 画像読み込み完了後

```html:img要素
<img
    src="(略)"
    onload="parentElement.style.setProperty('--z-index', 1);
    parentElement.style.setProperty('--opacity', 0);"
/>
```

画像読み込みが完了すると`onload`が発火して親picture要素にスタイル情報が付与される

※ `onload`の値もAstro ImageToolsによって自動生成されている


```css:picture要素 - フェードイン
figure picture {
    opacity: calc(1 - var(--opacity, 1));
    transition: opacity .3s cubic-bezier(.165,.84,.44,1);
}
```

初期状態とスタイルは同一だが、
CSS変数が与えられた瞬間から0.3秒かけて`opacity`が`1`になる

これにて画像の準備が完了する


## 中央ロゴのアニメーション

![ezgif-8b4224eb6200bc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/e53af663-6973-4f35-b9ed-caece553ec93.gif)


### フェードイン開始前

```css:フェードイン前
img {
    opacity: 0;
}
```

`scene-kv`クラスが与えられる前の状態

### フェードイン＋スケールアップ

```css:img要素 - フェードイン
.scene-kv img {
    animation: anime-logo 4s .3s both;
}

@keyframes anime-logo {
    0% {
        opacity: 0;
        transform: translateZ(0) scale(.5);
        animation-timing-function: cubic-bezier(.895,.03,.685,.22)
    }

    12% {
        opacity: 1;
        transform: translateZ(0) scale(.9);
        animation-timing-function: cubic-bezier(.25,.25,.75,.75)
    }

    to {
        opacity: 1;
        transform: translateZ(0) scale(1.2)
    }
}
```

`scene-kv`クラスが付与されてから0.3秒待機してフェードイン＋スケールアップアニメーションを4秒間実行

0.3秒待つことで必ず親picture要素のセットアップを終えてからアニメを実行する


### フェードアウト

```css:div要素 - フェードアウト
.scene-kv div {
    opacity: 0;
    transition: opacity .5s 2.2s cubic-bezier(.19,1,.22,1);
}
```

`scene-kv`クラスが付与されて2.2秒経過後に親div要素にてフェードアウトアニメを0.5秒間実行

:::note 
フェードインアニメを仕掛ける先は 子要素（img/picture要素）
フェードアウトアニメを仕掛ける先は 親要素（ラッパー/コンテナ要素）
:::

## キャラ画像のアニメーション（PC版）

![ezgif-8c2236b1c3f551.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3194558/5205d7f9-ae99-4973-948b-7259a086cd32.gif)


```css:div要素 - 初期状態
div {
    opacity: 0;
    transform: translateZ(0) scale(1.15);
}
```

若干拡大させた状態で待機

```css:div要素 - フェードイン＋スケールダウン
.scene-kv div {
    opacity: 1;
    transform: translateZ(0) scale(1);
    transition: opacity 1.8s 2.2s cubic-bezier(.165,.84,.44,1), transform 2s 2.2s cubic-bezier(.77,0,.175,1);
}
```

`scene-kv`クラスが付与されてから2.2秒待機してフェードイン＋スケールダウンアニメーションを1.8秒間実行

このように待機秒数を設定することにより[キャラ画像]が[中央ロゴ]と入れ替わる形で出現する


:::note 
キャラ画像にフェードアウトアニメが存在しないためか
このフェードインアニメは 親要素（ラッパー/コンテナ要素） に仕掛けられている
:::


# まとめ

導入部分を軽く見ただけでも新しい技術スタックや個人的に参考になる点が多く見受けられ、最高のコンテンツを作る会社というビジョンに嘘偽りは無いと実感した

本記事で記載した内容は一部にすぎないがアニメーションの実装内容については多少追跡しやすくなったのではないだろうか

今後の特設サイトにもぜひ注目したい