カットイン演出
概要
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.50s | Phase 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.00s | Na → Nam → Name → Name: → Name: D ... |
| 1.10s | 名前表示完了: NAME: Drifter。区切り線がスライドイン |
| 1.15s | FROM: のタイプライター開始 |
| 1.25s | FROM表示完了。HPバーがアニメーション開始(左から伸びる) |
| 1.35s | HPバー完了。ATKバー開始 |
| 1.45s | ATKバー完了。DEFバー開始 |
| 1.55s | DEFバー完了。SPDバー開始 |
| 1.65s | SPDバー完了 |
| 1.70s | カラー表示(■ カラーコード) |
| 1.80s | "Ready to Vibe..." がフェードイン |
| 2.00s | Phase 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.10s | LET'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.3s | Phase 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 = [];
}
}