AI先生のロボットキャラクター
第9章 - セクション2

白黒画像が拡大縮小するローディングアニメーション

白黒とカラーが切り替わりながらパルスし、最後に拡大する印象的なローディング演出。霧エフェクトでヒーローセクションへ自然に遷移します

男子生徒のアイコン

ローディング画面って、普通はプログレスバーとかスピナーを使いますよね。画像を使った演出ってどうなんですか?

AI先生のアイコン

画像を使ったローディングは、単なる待ち時間ではなく「体験の始まり」として演出できるんだ。特にヒーローセクションに使う画像でローディングすると、待ち時間そのものが印象的なオープニングになるよ。

女子生徒のアイコン

でも画像が大きいと読み込みに時間がかかりませんか?

AI先生のアイコン

それはいい視点だね。この手法では最初に小さく表示して、徐々に拡大していく。読み込み中も常に何かが動いているから、ユーザーは「処理が進んでいる」と感じられる。パルスアニメーションやカラー切り替えで、単調にならない工夫も入れられるよ。

作成した画像拡大ローディング

一般的なローディング画面は「待たせる」ものですが、このアプローチではローディングそのものがコンテンツの一部になります。画像のパルス、霧エフェクト、テキストの浮上という一連の演出により、待ち時間を印象的な体験に変換します。

HTML
<!-- ローディングオーバーレイ -->
<div class="loading-overlay">
    <div class="loading-image"></div>
    <div class="loading-dots">
        <span></span>
        <span></span>
        <span></span>
    </div>
</div>

<!-- 霧のオーバーレイ -->
<div class="fog-overlay"></div>

<!-- ヒーローセクション -->
<section class="hero-section">
    <div class="hero-content">
        <h1 class="hero-title">Nature Awaits</h1>
        <p class="hero-subtitle">Discover the Beauty Around You</p>
    </div>
</section>
CSS
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Georgia', serif;
    overflow: hidden;
    background: #0a0a0a;
}

/* ローディングオーバーレイ */
.loading-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #0a0a0a;
    z-index: 9999;
    overflow: hidden;
    opacity: 1;
    animation: fadeOutLoading 0.5s ease 8s forwards;
}

/* 画像本体 - パルス→拡大 */
.loading-image {
    width: 100vw;
    height: 100vh;
    background-image: url('/img/autumn-leaves.jpg');
    background-size: cover;
    background-position: center;
    transform: scale(0.1);
    border-radius: 20px;
    animation: pulseAndZoom 8s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

/* ローディングドット */
.loading-dots {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    gap: 12px;
    z-index: 10000;
    opacity: 1;
    animation: fadeOutDots 0.5s 6s forwards;
}

.loading-dots span {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.9);
    animation: dotPulse 1.5s ease-in-out infinite;
}

.loading-dots span:nth-child(2) {
    animation-delay: 0.2s;
}

.loading-dots span:nth-child(3) {
    animation-delay: 0.4s;
}

/* ドットのパルスアニメーション */
@keyframes dotPulse {
    0%, 100% {
        opacity: 0.3;
        transform: scale(0.8);
    }
    50% {
        opacity: 1;
        transform: scale(1.2);
    }
}

/* ドットのフェードアウト */
@keyframes fadeOutDots {
    to {
        opacity: 0;
    }
}

/* 霧のオーバーレイ */
.fog-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
    opacity: 0;
    z-index: 10000;
    pointer-events: none;
    animation: fogLifecycle 10s ease 7.5s forwards;
}

/* パルス→拡大アニメーション */
@keyframes pulseAndZoom {
    /* パルス1回目 */
    0% {
        transform: scale(0.1);
        opacity: 0.6;
        filter: grayscale(1);
    }
    12.5% {
        transform: scale(0.12);
        opacity: 0.7;
        filter: grayscale(1);
    }
    25% {
        transform: scale(0.1);
        opacity: 0.6;
        filter: grayscale(1);
    }
    /* パルス2回目 */
    37.5% {
        transform: scale(0.12);
        opacity: 0.7;
        filter: grayscale(1);
    }
    50% {
        transform: scale(0.1);
        opacity: 0.6;
        filter: grayscale(1);
    }
    /* パルス3回目 */
    62.5% {
        transform: scale(0.12);
        opacity: 0.7;
        filter: grayscale(1);
    }
    75% {
        transform: scale(0.1);
        opacity: 0.6;
        filter: grayscale(1);
    }
    /* カラーに移行 */
    87.5% {
        transform: scale(0.1);
        opacity: 0.8;
        filter: grayscale(0);
    }
    /* 目一杯拡大 */
    100% {
        transform: scale(4);
        opacity: 1;
        filter: grayscale(0);
    }
}

/* ローディングのフェードアウト */
@keyframes fadeOutLoading {
    to {
        opacity: 0;
        visibility: hidden;
    }
}

/* 霧のライフサイクル */
@keyframes fogLifecycle {
    0% {
        opacity: 0;
    }
    15% {
        opacity: 1;
    }
    50% {
        opacity: 1;
    }
    70% {
        opacity: 0.25;
    }
    100% {
        opacity: 0;
    }
}

/* ヒーローセクション */
.hero-section {
    position: relative;
    width: 100%;
    height: 100vh;
    background-image: url('/img/autumn-leaves.jpg');
    background-size: cover;
    background-position: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    opacity: 0;
    animation: fadeInHero 1s ease 8s forwards;
}

@keyframes fadeInHero {
    to {
        opacity: 1;
    }
}

.hero-section::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(135deg, rgba(10, 10, 10, 0.6) 0%, rgba(10, 10, 10, 0.3) 100%);
}

.hero-content {
    position: relative;
    z-index: 1;
    text-align: center;
    color: white;
}

.hero-title {
    font-size: clamp(2.5rem, 8vw, 5rem);
    font-weight: 200;
    letter-spacing: 0.15em;
    margin-bottom: 1.5rem;
    opacity: 0;
    transform: translateY(40px) scale(0.95);
    animation: revealTitle 1.2s cubic-bezier(0.16, 1, 0.3, 1) 9.5s forwards;
    text-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
}

.hero-subtitle {
    font-size: clamp(1rem, 3vw, 1.8rem);
    font-weight: 300;
    letter-spacing: 0.08em;
    opacity: 0;
    transform: translateY(30px);
    animation: revealSubtitle 1s cubic-bezier(0.16, 1, 0.3, 1) 10.2s forwards;
    text-shadow: 0 2px 15px rgba(0, 0, 0, 0.4);
}

@keyframes revealTitle {
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

@keyframes revealSubtitle {
    to {
        opacity: 0.95;
        transform: translateY(0);
    }
}

AIへのプロンプト例

以下のようなプロンプトをAIに送信します:

秋の紅葉の画像を使った、印象的なローディング画面からヒーローセクションへの遷移を作成してください。

### 初期状態
- 画像を小さく丸く表示
- 画像の中央に3つの白いドットを配置

### パルス動作(約6秒間、3回繰り返し)
- 約2秒間ごとに画像が少し拡大・縮小
- パルス中は常に白黒で表示
- ドットは順番に明滅してローディング中を表現

### 拡大フェーズ(6-8秒)
- ドットを消す
- 画像をカラーに戻す
- 画面いっぱいに拡大

### 霧エフェクト(7.5秒以降)
- 白い霧のようなエフェクトが出現
- しばらく画面全体を真っ白に
- 霧がモヤのように薄くなる
- 最後に完全に晴れる

### ヒーローセクション(8秒以降)
- 同じ画像を背景に表示
- 「Nature Awaits」というタイトルを底から浮かび上がらせる
- 「Discover the Beauty Around You」というサブタイトルを時間差で表示

このパーツの特徴

  • 白黒パルスからカラーへの遷移 パルス時は白黒で表示し、パルス終了後にカラーへ移行します。準備中から本番へのコントラストが印象的なローディング表現です
  • ローディングドット 画像中央に3つのドットがアニメーション。「処理中」であることを明確に伝えます
  • 霧エフェクトによる遷移 パルス終了後、白い霧が現れてゆっくりと晴れていくことで、ローディングからヒーローセクションへ自然に移行します
  • 純粋なCSS制御 JavaScriptを使わず、すべてCSSの@keyframesで制御。タイミングが一元管理され、メンテナンスしやすい設計です
画像は以下のものを使用しています:

UnsplashBernd Schulzが撮影した写真

コードのポイント

キーフレームによる複雑なアニメーション制御:

8秒間のアニメーションを@keyframesで細かく制御しています。この手法の利点は、すべてのタイミングを一箇所で管理できることです。

@keyframes pulseAndZoom {
    /* パルス1回目 (0-2秒 = 0-25%) */
    0% {
        transform: scale(0.1);
        opacity: 0.6;
        filter: grayscale(1); /* パルス時=白黒 */
    }
    12.5% {
        transform: scale(0.12);
        opacity: 0.7;
        filter: grayscale(1); /* パルス時=白黒 */
    }
    /* ... */
}
パーセンテージによる正確な制御

アニメーション全体を8秒とした場合、パーセンテージで以下のように計算できます。

  • 12.5% = 8秒 × 0.125 = 1秒 パルスの拡大ピーク
  • 25% = 8秒 × 0.25 = 2秒 1回目のパルス終了
  • 75% = 8秒 × 0.75 = 6秒 3回目のパルス終了

この計算により、各段階のタイミングを正確にコントロールできます。

複数プロパティの同時変化

transformopacityfilterを同時に変化させることで、複雑で印象的な演出を実現しています。

  • transform: scale() サイズの拡大・縮小でパルスの動きを表現
  • opacity 透明度の変化で「フォーカスが合う」感覚を演出
  • filter: grayscale() パルス時は白黒、パルス終了後にカラーへ移行し、準備完了を表現

これらを組み合わせることで、単なる拡大縮小ではなく、「生命感のある」パルスアニメーションになります。

レイヤー構造とz-index管理

複数のオーバーレイを重ねて段階的な演出を実現しています。Web開発では、要素の重なり順を正しく管理することが重要です。

.loading-overlay {
    z-index: 9999;
}

.loading-dots {
    z-index: 10000;
}

.fog-overlay {
    z-index: 10000;
    pointer-events: none;
}

レイヤーの役割分担

それぞれのレイヤーには明確な役割があります。

  • ローディングオーバーレイ(z-index: 9999) 背景の黒い画面と画像を表示する基底レイヤー。全体の土台となる
  • ローディングドット(z-index: 10000) 画像の上に表示し、ローディング中であることを視覚的に伝える
  • 霧オーバーレイ(z-index: 10000) 最前面で白い霧エフェクトを表示。pointer-events: noneでクリックをブロックしない

pointer-events: noneの重要性

霧のオーバーレイにはpointer-events: noneを指定しています。これにより、霧が表示されている間でも、その下のヒーローセクションにマウスイベントが届きます。霧が完全に消える前にユーザーがクリックしても、正しく動作するための配慮です。

複数アニメーションの協調動作

各要素のアニメーションタイミングを調整し、全体で一つの演出を構成しています。CSSアニメーションの強みは、このように複数の要素を連携させて動かせることです。

.loading-image {
    animation: pulseAndZoom 8s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}

.loading-dots {
    animation: fadeOutDots 0.5s 6s forwards;
}

.fog-overlay {
    animation: fogLifecycle 10s ease 7.5s forwards;
}

.hero-section {
    animation: fadeInHero 1s ease 8s forwards;
}

タイムライン全体の設計

アニメーション全体のタイムラインを可視化すると、以下のようになります。

  • 0-8秒
    • パルスアニメーション実行。画像が3回パルスし、最後に拡大
  • 6-6.5秒
    • ドットがフェードアウト。カラー移行の直前に消えることで、移行をスムーズに
  • 7.5-17.5秒
    • 霧のライフサイクル。出現→真っ白維持→モヤ→消滅という段階的変化
  • 8秒~
    • ヒーローセクションが表示開始。霧の演出と並行して進行
  • 9.5秒~
    • タイトルテキストが浮かび上がる。霧が薄くなった頃に登場
  • 10.2秒~
    • サブタイトルが表示。タイトルの後を追うように時間差で

animation-delayによる時間差制御

animationプロパティの3番目のパラメータ(例: 6s7.5s)はanimation-delayを指定しています。これにより、各アニメーションの開始タイミングを正確にコントロールできます。

例えば、ドットは6秒待機してからフェードアウトを開始し、霧は7.5秒待機してから出現を開始します。この時間差によって、パルス→拡大→霧という自然な流れが生まれるのです。

forwardsキーワードの役割

すべてのアニメーションにforwardsを指定しています。これは「アニメーション終了後、最終状態を維持する」という意味です。これがないと、アニメーション終了後に元の状態に戻ってしまいます。ローディング画面が再表示されるような不自然な動きを防ぐため、forwardsは必須です。

まとめ

男子生徒のアイコン

CSSだけでこんなに複雑なアニメーションができるんですね。

AI先生のアイコン

そうだね。すべてCSSで管理することで、後から調整しやすくなるんだ。例えば全体を速くしたければ、各アニメーションの秒数を比例的に変えるだけでいい。JavaScriptだと複数のタイマーを調整する必要があって、もっと複雑になるよ。

女子生徒のアイコン

霧エフェクトがいい感じですよね。ローディングからコンテンツへの移行が自然で。

AI先生のアイコン

そう。単にフェードインするよりも、霧を使うことで「何かが明らかになっていく」という物語性が生まれる。待ち時間も体験の一部として楽しめるようになるんだ。

画像拡大ローディングの重要ポイント
  • @keyframesでの精密制御 パーセンテージを使って複数のプロパティを同時にアニメーション。タイミングを一元管理し、保守性を向上
  • filter: grayscale()の活用 拡大時カラー、縮小時白黒の切り替えで視覚的なリズムを創出。単調な動きに変化を加える
  • レイヤー構造の設計 z-indexで複数のオーバーレイを管理し、段階的な演出を実現。pointer-events制御でユーザビリティを確保
  • アニメーションの協調 animation-delayとforwardsを組み合わせて、各要素が連携した一つの流れを作る
  • cubic-bezier()イージング Material Designの標準イージング関数(0.4, 0, 0.2, 1)で滑らかで自然な動きを実現

このパーツは、ポートフォリオサイトやランディングページの印象的なオープニングとして活用できます。画像を変えたり、パルスの回数やタイミングを調整したりして、プロジェクトに合わせてカスタマイズしてみましょう。

学習チェック

このレッスンを理解できたら「完了」をクリックしてください。
後で見直したい場合は「未完了に戻す」で進捗をリセットできます。

レッスン完了!🎉

お疲れさまでした!