Skip to content

カットイン演出

概要

Rift Survivors の最大の差別化要素。 Vibeverse ポータル経由でプレイヤーが入場する際に再生される 2.5秒 のカットイン演出。

インスピレーション元:

  • ペルソナ5 のオールアウトアタック / 覚醒カットイン(スタイリッシュな分割画面、キャラクター紹介)
  • Ratchet & Clank: Rift Apart の次元リフトオープンエフェクト(空間が裂けて異世界が覗く)

全体タイムライン

時間 (秒)
0.0s                0.5s          0.8s                    2.0s          2.5s
 │                   │             │                       │             │
 │   PHASE 1         │  PHASE 2    │      PHASE 3          │  PHASE 4    │
 │   リフトオープン   │  シルエット │  スプリットスクリーン  │  トランジション│
 │   (0.5s)          │  (0.3s)     │  (1.2s)               │  (0.5s)     │
 │                   │             │                       │             │
 ▼                   ▼             ▼                       ▼             ▼
 ┌─────────────┐ ┌─────────┐ ┌──────────────────────┐ ┌──────────────┐
 │ 画面中央に   │ │ 裂け目  │ │ 画面が斜めに分割    │ │ "LET'S VIBE" │
 │ 紫の裂け目   │ │ からキャ│ │ 左: キャラアート     │ │ テキスト     │
 │ が出現       │ │ ラの影  │ │ 右: ステータス       │ │ → フェード   │
 │ (R&C風)      │ │ が覗く  │ │ タイプライター表示   │ │ アウト       │
 └─────────────┘ └─────────┘ └──────────────────────┘ └──────────────┘

Phase 1: リフトオープン (0.0s - 0.5s)

Ratchet & Clank の次元リフトにインスパイアされたエフェクト。 画面中央に紫色の次元の裂け目が出現し、異世界のエネルギーが溢れ出す。

フレーム詳細

時間描画内容
0.00s画面全体がわずかに歪む(barrel distortion エフェクト開始)
0.05s画面中央に白い光点が出現(1px → 3px、brightness 500%)
0.10s光点から垂直に亀裂が走り始める。CRTスキャンラインノイズが画面全体に
0.15s亀裂が上下に伸長(高さ 5% → 20%)。亀裂の縁がネオンピンク #ff2d75 に発光
0.20s亀裂がさらに広がる(高さ 20% → 50%)。裂け目の中に紫のエネルギー渦が見える
0.25s亀裂が画面上端から下端まで到達。幅が広がり始める(幅 2px → 20px)
0.30s裂け目の幅が拡大(20px → 100px)。パーティクルが裂け目から両側に飛散
0.35s裂け目からエネルギー波が放出(リング状の衝撃波エフェクト)
0.40s裂け目の幅がさらに拡大(100px → 画面幅40%)。中にシルエットが見え始める
0.45sクロマティックアベレーション最大。裂け目の縁がRGB分離
0.50sPhase 2 へ遷移。裂け目が画面幅の50%まで開いた状態

裂け目のビジュアル

0.15s時点:          0.30s時点:          0.45s時点:
                                        
     │               ╱    ╲             ╱          ╲
     │              ╱  ::::  ╲         ╱  ::::::::  ╲
     │             ╱  ::::::  ╲       ╱  ::::::::::  ╲
     ┃            ╱  ::::::::  ╲     ╱  ::::▓▓:::::: ╲
     │             ╲  ::::::  ╱       ╲  ::::▓▓::::::╱
     │              ╲  ::::  ╱         ╲  ::::::::::╱
     │               ╲    ╱             ╲          ╱
                                        
  細い亀裂         紫エネルギー渦      シルエット出現

CSS実装アプローチ

css
/* Phase 1: リフトオープン */
.cutin-rift {
  position: fixed;
  inset: 0;
  z-index: 10000;
  overflow: hidden;
}

.cutin-rift__crack {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 2px;
  height: 100%;
  background: linear-gradient(
    180deg,
    transparent 0%,
    #ff2d75 20%,
    #a855f7 50%,
    #ff2d75 80%,
    transparent 100%
  );
  animation: crack-open 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  box-shadow:
    0 0 20px #ff2d75,
    0 0 60px #a855f7,
    0 0 100px rgba(168, 85, 247, 0.5);
}

@keyframes crack-open {
  0% {
    width: 2px;
    clip-path: inset(45% 0 45% 0);
    filter: brightness(5);
  }
  20% {
    width: 4px;
    clip-path: inset(20% 0 20% 0);
    filter: brightness(3);
  }
  50% {
    width: 100px;
    clip-path: inset(0 0 0 0);
    filter: brightness(2);
  }
  100% {
    width: 50vw;
    clip-path: inset(0 0 0 0);
    filter: brightness(1.5);
  }
}

/* 裂け目内部のエネルギー渦 */
.cutin-rift__vortex {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    ellipse at center,
    #a855f7 0%,
    #6b21a8 40%,
    #1a0533 80%,
    transparent 100%
  );
  animation: vortex-spin 2s linear infinite;
  mix-blend-mode: screen;
}

@keyframes vortex-spin {
  from { transform: rotate(0deg) scale(0.8); }
  to { transform: rotate(360deg) scale(1.2); }
}

/* パーティクル飛散 */
.cutin-rift__particle {
  position: absolute;
  width: 4px;
  height: 4px;
  background: #00f0ff;
  border-radius: 50%;
  box-shadow: 0 0 8px #00f0ff;
  animation: particle-burst 0.5s ease-out forwards;
}

@keyframes particle-burst {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(var(--dx), var(--dy)) scale(0);
    opacity: 0;
  }
}

Phase 2: シルエット (0.5s - 0.8s)

裂け目の中からキャラクターのシルエットが覗き、ペルソナ風のスタイリッシュな黒い影として描画される。

フレーム詳細

時間描画内容
0.50s裂け目の中央にキャラクターシルエットが出現(opacity 0 → 0.3)
0.55sシルエットが手前に迫ってくる(scale 0.5 → 0.7)。逆光エフェクト
0.60sシルエットのエッジがネオンカラーで縁取られる(outline glow)
0.65sシルエットがほぼ等身大に(scale 0.7 → 0.9)。ポーズが決まる
0.70s裂け目の背景が白くフラッシュ。シルエットが完全な黒に(opacity 1.0)
0.75sフラッシュが収束。シルエットのポーズが静止
0.80s画面が斜めに分割される遷移開始。Phase 3 へ

シルエット演出

0.55s:              0.65s:              0.75s:

  ╱          ╲      ╱          ╲       ╱          ╲
 ╱            ╲    ╱   ┌──┐    ╲     ╱   ┌──┐    ╲
╱     ┌─┐      ╲  ╱    │██│     ╲   ╱    │██│     ╲
╲     │░│      ╱  ╲   ┌┤██├┐    ╱   ╲   ┌┤██├┐    ╱
 ╲    └─┘     ╱    ╲  │└──┘│   ╱     ╲  │└──┘│   ╱
  ╲          ╱      ╲ │    │  ╱       ╲ │    │  ╱
                      ╲└──┘  ╱         ╲└──┘  ╱

  遠くに小さい影     近づいてくる       ポーズ決定

CSS実装アプローチ

css
/* Phase 2: シルエット */
.cutin-silhouette {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.5);
  width: 300px;
  height: 500px;
  background: #000;
  /* キャラクターシルエットのclip-path(実装時にキャラ形状に合わせる) */
  clip-path: polygon(/* キャラクター形状 */);
  animation: silhouette-approach 0.3s ease-out forwards;
  filter: drop-shadow(0 0 20px #ff2d75) drop-shadow(0 0 40px #a855f7);
}

@keyframes silhouette-approach {
  0% {
    transform: translate(-50%, -50%) scale(0.5);
    opacity: 0.3;
    filter: drop-shadow(0 0 10px #ff2d75);
  }
  70% {
    transform: translate(-50%, -50%) scale(0.95);
    opacity: 0.8;
    filter: drop-shadow(0 0 30px #ff2d75) drop-shadow(0 0 60px #a855f7);
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
    filter: drop-shadow(0 0 15px #ff2d75);
  }
}

/* 逆光フラッシュ */
.cutin-silhouette__flash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%, #fff, transparent 60%);
  animation: backlight-flash 0.15s ease-out forwards;
  animation-delay: 0.2s;
  opacity: 0;
}

@keyframes backlight-flash {
  0% { opacity: 0; }
  30% { opacity: 1; }
  100% { opacity: 0; }
}

Phase 3: スプリットスクリーン (0.8s - 2.0s)

ペルソナ風の核心部分。画面が斜め(対角線)に2分割され、左側にキャラクターアート、右側にステータス情報がタイプライター風に表示される。

画面構成

┌──────────────────────────────────────────────────────────────┐
│ ╲                                                           │
│   ╲                                                         │
│     ╲                           NAME: Drifter               │
│       ╲                         ───────────────             │
│  ┌──────╲──────────┐            FROM: Neon City             │
│  │      │╲         │                                        │
│  │      │  ╲       │            HP ████████████ 100         │
│  │  CHAR│    ╲     │            ATK ██████░░░░░  34         │
│  │  ART │      ╲   │            DEF ████░░░░░░░  22         │
│  │      │        ╲ │            SPD ████████░░░  8          │
│  │  ┌───┤          ╲                                        │
│  │  │   │           ╲╲          COLOR: ■ #00f0ff            │
│  │  └───┤             ╲                                     │
│  └──────┘               ╲      "Ready to Vibe..."          │
│                           ╲                                 │
│                             ╲                               │
└──────────────────────────────────────────────────────────────┘
  ← キャラクターアート →     ← ステータス(タイプライター)→
     背景: キャラカラーの          背景: ダークグレー
     グラデーション                パターン: 斜線ハッチング

フレーム詳細

時間描画内容
0.80s画面が斜めに分割開始。対角線がシャープに引かれる
0.85s分割完了。左側にキャラクターアート(カラーで塗りつぶし)がスライドイン
0.90s右側の背景にハッチングパターンが表示される
0.95s名前がタイプライターエフェクトで表示開始: N
1.00sNaNamNameName:Name: D ...
1.10s名前表示完了: NAME: Drifter。区切り線がスライドイン
1.15sFROM: のタイプライター開始
1.25sFROM表示完了。HPバーがアニメーション開始(左から伸びる)
1.35sHPバー完了。ATKバー開始
1.45sATKバー完了。DEFバー開始
1.55sDEFバー完了。SPDバー開始
1.65sSPDバー完了
1.70sカラー表示(■ カラーコード)
1.80s"Ready to Vibe..." がフェードイン
2.00sPhase 4 へ遷移

タイプライターエフェクト

1文字ずつ表示されるタイプライターエフェクト。モノスペースフォントで、カーソル(_)が点滅する。

css
/* タイプライターエフェクト */
.cutin-typewriter {
  font-family: 'JetBrains Mono', 'Fira Code', monospace;
  font-size: 24px;
  color: #00f0ff;
  white-space: nowrap;
  overflow: hidden;
  border-right: 3px solid #00f0ff;
  animation:
    typing 0.4s steps(var(--char-count)) forwards,
    cursor-blink 0.5s step-end infinite;
}

@keyframes typing {
  from { width: 0; }
  to { width: 100%; }
}

@keyframes cursor-blink {
  50% { border-color: transparent; }
}

/* ステータスバーのアニメーション */
.cutin-stat-bar {
  height: 12px;
  background: linear-gradient(90deg, #00f0ff, #a855f7);
  border-radius: 2px;
  transform-origin: left;
  animation: stat-fill 0.2s ease-out forwards;
}

@keyframes stat-fill {
  from { transform: scaleX(0); }
  to { transform: scaleX(var(--stat-ratio)); }
}

斜め分割のCSS

css
/* スプリットスクリーン */
.cutin-split {
  position: fixed;
  inset: 0;
  z-index: 10000;
}

.cutin-split__left {
  position: absolute;
  inset: 0;
  /* 斜め切り抜き: 左下三角 */
  clip-path: polygon(0 0, 65% 0, 35% 100%, 0 100%);
  background: linear-gradient(135deg, var(--char-color), rgba(0,0,0,0.8));
  animation: split-left-enter 0.15s ease-out forwards;
}

.cutin-split__right {
  position: absolute;
  inset: 0;
  /* 斜め切り抜き: 右上三角 */
  clip-path: polygon(65% 0, 100% 0, 100% 100%, 35% 100%);
  background:
    repeating-linear-gradient(
      45deg,
      transparent,
      transparent 10px,
      rgba(255,255,255,0.03) 10px,
      rgba(255,255,255,0.03) 20px
    ),
    #0a0a0f;
  animation: split-right-enter 0.15s ease-out forwards;
}

@keyframes split-left-enter {
  from { transform: translateX(-100%); }
  to { transform: translateX(0); }
}

@keyframes split-right-enter {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

/* 分割線のグロウ */
.cutin-split__divider {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: linear-gradient(
    to bottom right,
    transparent calc(50% - 2px),
    #ff2d75 calc(50% - 1px),
    #fff 50%,
    #ff2d75 calc(50% + 1px),
    transparent calc(50% + 2px)
  );
  filter: drop-shadow(0 0 10px #ff2d75);
  animation: divider-glow 1.2s ease-in-out infinite alternate;
}

@keyframes divider-glow {
  from { filter: drop-shadow(0 0 10px #ff2d75); }
  to { filter: drop-shadow(0 0 25px #ff2d75) drop-shadow(0 0 50px #a855f7); }
}

Phase 4: "LET'S VIBE" トランジション (2.0s - 2.5s)

スプリットスクリーンが閉じ、"LET'S VIBE" のテキストが画面中央に大きく表示されてからロビーに遷移する。

フレーム詳細

時間描画内容
2.00sスプリットスクリーンの両側が画面外にスライドアウト開始
2.05s背景が黒にフェード
2.10sLET'S VIBE テキストが画面中央に出現。scale 3.0 → 1.0 のズームイン
2.15sテキストに激しいグロウエフェクト。ネオンピンク + シアンの二重グロウ
2.25sテキストが0.1s静止
2.30sテキストがグリッチエフェクトで揺れる(RGB分離 + 位置ずれ)
2.35s画面全体がホワイトフラッシュ
2.40sフラッシュが収束。ロビー画面がフェードイン開始
2.50sロビー画面の表示完了。カットイン終了

"LET'S VIBE" テキスト演出

2.10s:                    2.25s:                    2.35s:

                                                    L̶E̸T̷'̵S̸
  LET'S VIBE              LET'S VIBE                V̵I̸B̶E̷
  (巨大→通常)            (グロウ最大)              (グリッチ)
  scale: 3→1              静止                      RGB分離

CSS実装アプローチ

css
/* Phase 4: LET'S VIBE */
.cutin-letsvibe {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  z-index: 10001;
}

.cutin-letsvibe__text {
  font-family: 'JetBrains Mono', monospace;
  font-size: 72px;
  font-weight: 900;
  letter-spacing: 0.2em;
  color: #fff;
  text-shadow:
    0 0 20px #ff2d75,
    0 0 40px #ff2d75,
    0 0 80px #a855f7,
    0 0 120px #a855f7;
  animation:
    letsvibe-zoom 0.15s cubic-bezier(0.16, 1, 0.3, 1) forwards,
    letsvibe-glow 0.3s ease-in-out forwards,
    letsvibe-glitch 0.1s ease-in-out 0.2s forwards;
}

@keyframes letsvibe-zoom {
  from {
    transform: scale(3);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes letsvibe-glow {
  0% {
    text-shadow: 0 0 20px #ff2d75;
  }
  50% {
    text-shadow:
      0 0 30px #ff2d75,
      0 0 60px #ff2d75,
      0 0 100px #a855f7,
      0 0 150px #a855f7;
  }
  100% {
    text-shadow:
      0 0 20px #ff2d75,
      0 0 40px #a855f7;
  }
}

@keyframes letsvibe-glitch {
  0% { transform: translate(0, 0); }
  20% { transform: translate(-5px, 3px); filter: hue-rotate(90deg); }
  40% { transform: translate(5px, -3px); filter: hue-rotate(-90deg); }
  60% { transform: translate(-3px, -2px); }
  80% { transform: translate(3px, 2px); filter: hue-rotate(45deg); }
  100% { transform: translate(0, 0); filter: none; }
}

/* ホワイトフラッシュ */
.cutin-flash {
  position: fixed;
  inset: 0;
  background: #fff;
  z-index: 10002;
  animation: flash-out 0.15s ease-out forwards;
  animation-delay: 0.25s;
  opacity: 0;
}

@keyframes flash-out {
  0% { opacity: 1; }
  100% { opacity: 0; }
}

スキップ機能

操作効果
画面タップ / クリック演出を即座にスキップ
ESCキー演出を即座にスキップ
Spaceキー演出を即座にスキップ

スキップ時は全アニメーションを即座に終了し、0.3sのフェードトランジションでロビーに遷移する。

短縮カットイン(ダンジョン出撃時)

ミッション出撃時には、フルカットインの代わりに短縮版(1.0秒)を使用する。

時間: 0.0s ──── 0.3s ──── 0.6s ──── 1.0s
       │          │          │          │
       │ リフト   │ ミッション│ フェード  │
       │ オープン │ 名表示   │ アウト    │
       │ (簡易版) │          │          │
Phase時間内容
裂け目0.0s - 0.3sPhase 1 の簡略版(パーティクル少なめ)
ミッション名0.3s - 0.6s画面中央にミッション名がズームイン表示
遷移0.6s - 1.0sフェードアウト → ダンジョン画面表示

パフォーマンス考慮

項目対応
CSS vs Canvas基本はCSS animation。パーティクルのみ Canvas 2D
パーティクル数PC: 50個、モバイル: 20個
prefers-reduced-motion検出時はフェードイン/アウトのみの簡易版に差し替え
GPU負荷will-change: transform, opacity で合成レイヤー化。filter の多用を避ける
メモリカットイン完了後、全DOM要素を即座に破棄

実装メモ

Web Animations API での制御

CSS animation ではなく Web Animations API を使うことで、スキップ時のアニメーション制御が容易になる。

javascript
// Web Animations API を使ったカットイン制御の概要
class CutinDirector {
  private animations: Animation[] = [];
  private timeline: AnimationTimeline;

  async play(params: PortalParams): Promise<void> {
    // Phase 1: リフトオープン
    const riftAnim = this.playRiftOpen();
    this.animations.push(riftAnim);
    await riftAnim.finished;

    // Phase 2: シルエット
    const silhouetteAnim = this.playSilhouette();
    this.animations.push(silhouetteAnim);
    await silhouetteAnim.finished;

    // Phase 3: スプリットスクリーン
    const splitAnim = this.playSplitScreen(params);
    this.animations.push(splitAnim);
    await splitAnim.finished;

    // Phase 4: LET'S VIBE
    const vibeAnim = this.playLetsVibe();
    this.animations.push(vibeAnim);
    await vibeAnim.finished;

    this.cleanup();
  }

  skip(): void {
    // 全アニメーションを即座に終了
    this.animations.forEach(a => a.finish());
    this.cleanup();
  }

  private cleanup(): void {
    // DOM要素を破棄してメモリ解放
    document.getElementById('cutin-container')?.remove();
    this.animations = [];
  }
}