Design Vault 用途・構成・配色・本文で検索 /
索引 カテゴリ タグ English

一式

画像と奥行き 30–34 — 面を切る、裏返す、飛び込む演出

ビフォーアフターからアイリスマスクまで 5 種。画像や面の見せ方を変える動きで、製品紹介の山場に向く

5 枚

持ち帰る JSON 何枚あっても 1 回で。順に束ねて写すので、貼った先でも並びが残る。 仕様の無い枠は、題名と JSON の場所だけを書く

読む順

  1. 01 動き ビフォーアフター ビフォーアフター
  2. 02 動き 3D カードフリップ 3D カードフリップ
  3. 03 動き パースペクティブ パースペクティブ
  4. 04 動き トンネル トンネル
  5. 05 動き アイリスマスク アイリスマスク

この一式について

持ち帰る

コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。

全部入り

scroll-motion-depth-everything.md

このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown

依頼文

6.3 KB
# ビフォーアフター
ビフォーアフター — Before/after wipe

画面に固定した 2 枚の図(CSS の図形)を重ね、「前」の図を下、「後」の図を上に置く。仕切り線とつまみは、区画の高さ 300vh のスクロールに合わせて左から右へ動く。
上の図は `clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0)` で仕切りの左側だけを見せ、--p が 0 のとき前、1 のとき後が全面に出る。
図を収める箱は 100vh の position: sticky にし、「後」の札は上の図の左端、「前」の札は下の図の右端に置く。
CSS と素の JavaScript だけで実装し、ライブラリは使わない。

外すと崩れる点:
- 上の図は仕切りの左側だけを見せ、切り抜きで入れ替える。拡大はしない
- 仕切りは区画の中で動き、図を収める箱は sticky で画面に留まる
- 動きを減らす設定では、後の図を全面に出して止める

動く実物(1 ファイルで完結する HTML): https://design-vault.pages.dev/designs/motion-compare-wipe/demo.html

---

# 3D カードフリップ
3D カードフリップ — 3D card flip on scroll

縦に並んだ 3 枚のカードが、画面の中央に来るたびに裏返る。カードは幅 520px 以下で、表の面は白、裏の面は強調色で、どちらも高さ 320px の角丸。
裏返しは `rotateY(0 → 180deg)` を 0.7 秒の緩急で行い、親要素に `perspective: 1000px`、裏返す箱に `transform-style: preserve-3d`、両面に `backface-visibility: hidden` を付ける。
カードは画面の上下 45% を除いた中央の帯に入った時点で一度だけ裏返り、戻らない。3 枚は別々のタイミングで順に裏返る。
動きを減らす設定では、裏返しを行わず裏面を見せた状態で止める。
CSS と IntersectionObserver だけで実装し、ライブラリは使わない。

外すと崩れる点:
- 裏返しは両面を `backface-visibility: hidden` で隠し、裏の面が表の面の裏側に回り込むように重ねる
- 一度裏返ったカードは、スクロールで戻っても表に戻さない
- 動きを減らす設定では、裏返さず裏面を見せた状態で止める

動く実物(1 ファイルで完結する HTML): https://design-vault.pages.dev/designs/motion-card-flip-3d/demo.html

---

# パースペクティブ
パースペクティブ — Perspective tilt reveal

画面ほどの大きさの UI 風の面(幅は最大 720px、縦横比 16:10)を、手前に倒れた状態から起き上がらせる。面は `perspective: 1200px` の親の中で、下端を回転の軸(`transform-origin: 50% 100%`)にして置く。
面が画面の下端から入るとき `rotateX(55deg)` で傾き、`translateZ(-160px)` だけ奥へ下がっている。スクロールに合わせて `rotateX` を 55deg から 0 へ、`translateZ` を -160px から 0 へ戻し、正面を向いたところで止める。
面の下に置いた影は、傾いている間は濃く(不透明度 1)、起き上がるにつれて薄く(0.35)し、横幅も 100% から 70% へ狭める。
面は 3 枚を 3 つの区画に置き、区画の高さは 70vh。進み具合は `--p` を 0.1〜0.6 の範囲で 0〜1 に写した値から作る。
CSS と素の JavaScript だけで実装し、ライブラリは使わない。

外すと崩れる点:
- 面は下端を軸に起き上がり、箱からはみ出して大きくならない
- 動くのは transform と opacity だけで、影は不透明度で濃さを変える
- 動きを減らす設定では、面を正面に向けた状態で表示する

動く実物(1 ファイルで完結する HTML): https://design-vault.pages.dev/designs/motion-perspective-tilt/demo.html

---

# トンネル
トンネル — Z-axis tunnel fly-through

同心の枠を 6 枚重ね、画面の中心から奥へ飛び込むように手前へ通り過ぎさせる。
枠は 40vmax の正方形(画面の長い辺を基準にし、縦長の画面でも消える前に縁が画面の外へ出る)で、各枠の `scale` は 0.04 から 3.24 まで `--p` に合わせて広がり、枠ごとに 0.15 ずつ出番を遅らせる。
枠の `opacity` は出てくるときに 0 から 1 へ、通り過ぎるときに 1 から 0 へ戻す。奥行きの見え方は枠の大きさの差だけで作り、`translateZ` は使わない。
区画の高さは 300vh、枠を収める箱は 100vh の sticky にし、CSS と素の JavaScript だけで実装する。ライブラリは使わない。

外すと崩れる点:
- 枠は中心から広がり、画面の外へ出てから消える。途中で止めるように見せない
- 奥行きは scale の差で作り、translateZ は使わない(描画が安定する)
- 動きを減らす設定では枠を重ねた状態で止め、動かさない

動く実物(1 ファイルで完結する HTML): https://design-vault.pages.dev/designs/motion-tunnel-fly/demo.html

---

# アイリスマスク
アイリスマスク — Circular iris mask expand

画面を固定し、今の画面の上に次の画面を重ね、中央の一点から円形に開いて次の画面を見せる演出を作ってください。外側の区画は高さ 400vh とし、その中に position: sticky; top: 0 の 100vh の箱を置きます。箱の中では今の画面(下の面)の上に次の画面(上の面)を重ね、上の面だけを clip-path: circle(calc(var(--p) * 75vmax) at 50% 50%) で切ります。区画の上端が画面の上端に来たとき p=0、区画の下端が画面の下端に来たとき p=1 として進み具合 p(0〜1)を求めます。中心には直径 12px の点を置いて円の起点を示し、375px 幅でも両方の面が箱の中に収まって横スクロールが出ないようにします。CSS と素の JavaScript だけで実装し、ライブラリは使わないでください。

外すと崩れる点:
- 上の面だけを円で切り、下の面(今の画面)は動かさない
- 円の半径は進み具合 p から決め、時間では広げない
- 動きを減らす設定では円を使わず、次の画面だけを表示する

動く実物(1 ファイルで完結する HTML): https://design-vault.pages.dev/designs/motion-iris-mask/demo.html

仕様

472 B
# ビフォーアフター
https://design-vault.pages.dev/ai/assets/motion-compare-wipe.json

---

# 3D カードフリップ
https://design-vault.pages.dev/ai/assets/motion-card-flip-3d.json

---

# パースペクティブ
https://design-vault.pages.dev/ai/assets/motion-perspective-tilt.json

---

# トンネル
https://design-vault.pages.dev/ai/assets/motion-tunnel-fly.json

---

# アイリスマスク
https://design-vault.pages.dev/ai/assets/motion-iris-mask.json

ファイル

01 motion-compare-wipe/demo.html

10.3 KB

/designs/motion-compare-wipe/demo.html

中身を見る
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>30 ビフォーアフター — Before/after wipe</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.stack { display: grid; gap: var(--s6); }
.js [data-reveal] { opacity: 0; transform: translateY(24px); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }
.js [data-reveal].is-in { opacity: 1; transform: none; }

/* 区画の高さ 300vh。中の箱は 100vh で画面に留まり、--p で仕切りを動かす */
.cmp { height: 300vh; }
.cmp-stick { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; overflow: hidden; }
.cmp-frame {
  position: relative;
  width: min(100vw - 2 * var(--s5), 960px);
  aspect-ratio: 16 / 9;
  box-shadow: 0 24px 60px -24px var(--shade);
  border-radius: var(--radius);
}
.cmp-fig { position: absolute; inset: 0; margin: 0; border-radius: var(--radius); overflow: hidden; }
.cmp-fig--before { background: var(--surface); }
.cmp-fig--after {
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  clip-path: inset(0 calc((1 - var(--p)) * 100%) 0 0);
}
.cmp-art { position: absolute; inset: 0; padding: var(--s6) var(--s5) var(--s5); display: grid; gap: var(--s3); align-content: start; }
.cmp-bar { height: 14px; border-radius: 999px; background: var(--rule); }
.cmp-fig--after .cmp-bar { background: color-mix(in srgb, var(--ground) 70%, transparent); }
.cmp-bar:nth-child(2) { width: 62%; }
.cmp-bar:nth-child(3) { width: 80%; }
.cmp-bar:nth-child(4) { width: 46%; }
.cmp-tag {
  position: absolute; top: var(--s3);
  padding: var(--s1) var(--s3);
  border-radius: 999px;
  font-size: var(--t-label);
  font-weight: 700;
}
.cmp-tag--before { right: var(--s3); background: var(--ground); color: var(--ink); }
.cmp-tag--after { left: var(--s3); background: var(--ink); color: var(--ground); }
.cmp-track { position: absolute; inset: 0; transform: translateX(calc(var(--p) * 100%)); pointer-events: none; }
.cmp-line { position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: var(--ground); }
.cmp-knob {
  position: absolute; top: 50%; left: 0;
  width: 48px; height: 48px; margin: -24px 0 0 -24px;
  border-radius: 50%;
  background: var(--ground) linear-gradient(var(--ink), var(--ink)) center / 2px 16px no-repeat;
  box-shadow: 0 6px 20px var(--shade);
}

@media (max-width: 600px) {
  .cmp-frame { aspect-ratio: 4 / 3; }
}

@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: 1; transform: none; transition: none; }
  .cmp-fig--after { clip-path: none; }
  .cmp-track { display: none; }
}
</style>
</head>
<body data-motion-number="30">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">30</p>
    <h1 class="dv-name">ビフォーアフター</h1>
    <p class="dv-ai">Before/after wipe</p>
    <p class="dv-desc">仕切りが動いて前後が入れ替わる</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <div class="dv-wrap stack">
    <section class="dv-card" data-reveal>
      <p class="dv-eyebrow">Section 1</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。スクロールすると仕切りが右へ動き、前の図が後の図に入れ替わります。</p>
    </section>
  </div>

  <section class="cmp" data-progress="pin">
    <div class="cmp-stick">
      <div class="cmp-frame">
        <figure class="cmp-fig cmp-fig--before">
          <span class="cmp-tag cmp-tag--before">前</span>
          <div class="cmp-art">
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
          </div>
        </figure>
        <figure class="cmp-fig cmp-fig--after">
          <span class="cmp-tag cmp-tag--after">後</span>
          <div class="cmp-art">
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
            <div class="cmp-bar"></div>
          </div>
        </figure>
        <div class="cmp-track" aria-hidden="true">
          <span class="cmp-line"></span>
          <span class="cmp-knob"></span>
        </div>
      </div>
    </div>
  </section>

  <div class="dv-wrap stack">
    <section class="dv-card" data-reveal>
      <p class="dv-eyebrow">Section 2</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。仕切りが端まで届いたあとに、この段落が続きます。</p>
    </section>
  </div>
</main>

<footer class="dv-end">END — 30 ビフォーアフター</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
dvReveal();
dvProgress();
</script>
</body>
</html>

02 motion-card-flip-3d/demo.html

9.8 KB

/designs/motion-card-flip-3d/demo.html

中身を見る
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>31 3D カードフリップ — 3D card flip on scroll</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.stack { display: grid; gap: var(--s6); }
.flip-stack { display: grid; gap: 60vh; justify-items: center; padding-block: 30vh; }
.flip { width: min(100%, 520px); perspective: 1000px; }
.flip-inner { display: grid; gap: var(--s3); }
.flip-face {
  min-height: 320px;
  display: grid;
  align-content: center;
  gap: var(--s2);
  padding: var(--s4);
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  box-shadow: 0 16px 40px var(--shade);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.flip-face--back {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--ground);
  transform: rotateY(180deg);
}
.flip-no { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; }
.flip-face--back .flip-no { color: var(--ground); }
.flip-back-name { font-size: var(--t-h2); font-weight: 700; }
.js .flip-inner { position: relative; display: block; transform-style: preserve-3d; transition: transform var(--dur) var(--ease); }
.js .flip-face { position: absolute; inset: 0; }
.js .flip-face--front { position: relative; }
.js .flip.is-in .flip-inner { transform: rotateY(180deg); }

@media (prefers-reduced-motion: reduce) {
  .js .flip-inner { transition: none; }
}
</style>
</head>
<body data-motion-number="31">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">31</p>
    <h1 class="dv-name">3D カードフリップ</h1>
    <p class="dv-ai">3D card flip on scroll</p>
    <p class="dv-desc">カードが裏返って裏面が出る</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 1</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。中央に来たカードが一枚ずつ裏返ります。</p>
    </section>
  </div>

  <div class="dv-wrap flip-stack">
    <div class="flip" data-flip>
      <div class="flip-inner">
        <div class="flip-face flip-face--front">
          <p class="flip-no">CARD 01</p>
          <h2 class="dv-h2">見出しが入ります</h2>
          <p class="dv-lead">表の本文のダミーです。</p>
        </div>
        <div class="flip-face flip-face--back">
          <p class="flip-no">BACK 01</p>
          <p class="flip-back-name">裏面が入ります</p>
        </div>
      </div>
    </div>

    <div class="flip" data-flip>
      <div class="flip-inner">
        <div class="flip-face flip-face--front">
          <p class="flip-no">CARD 02</p>
          <h2 class="dv-h2">見出しが入ります</h2>
          <p class="dv-lead">表の本文のダミーです。</p>
        </div>
        <div class="flip-face flip-face--back">
          <p class="flip-no">BACK 02</p>
          <p class="flip-back-name">裏面が入ります</p>
        </div>
      </div>
    </div>

    <div class="flip" data-flip>
      <div class="flip-inner">
        <div class="flip-face flip-face--front">
          <p class="flip-no">CARD 03</p>
          <h2 class="dv-h2">見出しが入ります</h2>
          <p class="dv-lead">表の本文のダミーです。</p>
        </div>
        <div class="flip-face flip-face--back">
          <p class="flip-no">BACK 03</p>
          <p class="flip-back-name">裏面が入ります</p>
        </div>
      </div>
    </div>
  </div>

  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 2</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。ここまで読み進めると、三枚とも裏返った状態になります。</p>
    </section>
  </div>
</main>

<footer class="dv-end">END — 31 3D カードフリップ</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
// 中央の帯(画面の上下 45% を除いた部分)に入ったカードへ .is-in を付け、一度だけ裏返す。
dvReveal('[data-flip]', { threshold: 0, rootMargin: '-45% 0px -45% 0px' });
</script>
</body>
</html>

03 motion-perspective-tilt/demo.html

11.1 KB

/designs/motion-perspective-tilt/demo.html

中身を見る
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>32 パースペクティブ — Perspective tilt reveal</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.stack { display: grid; gap: var(--s6); }
/* 区画の中で面が立ち上がる。--q は --p を 0.1〜0.6 の範囲で 0〜1 に写したもの */
.tilt {
  --q: clamp(0, calc((var(--p, 0) - .1) * 2), 1);
  perspective: 1200px;
  min-height: 70vh;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--s3);
}
.tilt-face {
  position: relative;
  width: min(100%, 720px);
  aspect-ratio: 16 / 10;
  display: grid;
  grid-template-rows: auto 1fr;
  background: var(--ground);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  transform-origin: 50% 100%;
}
.tilt-bar { display: flex; gap: var(--s2); align-items: center; padding: var(--s2) var(--s3); background: var(--surface); border-bottom: 1px solid var(--rule); }
.tilt-bar i { width: 10px; height: 10px; border-radius: 50%; background: var(--muted); opacity: .4; }
.tilt-body { display: grid; grid-template-columns: 28% 1fr; min-height: 0; }
.tilt-side { display: grid; align-content: start; gap: var(--s2); padding: var(--s3); background: var(--surface); border-right: 1px solid var(--rule); }
.tilt-side b { display: block; height: 10px; border-radius: 4px; background: color-mix(in srgb, var(--ink) 14%, var(--surface)); }
.tilt-side b:first-child { background: var(--accent-soft); }
.tilt-main { display: flex; flex-direction: column; gap: var(--s3); padding: var(--s3); min-width: 0; }
.tilt-hero { flex: 1; border-radius: var(--s1); background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid)); }
.tilt-hero--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 60%, var(--accent-mid)); }
.tilt-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.tilt-row span { height: 40px; border-radius: var(--s1); background: var(--surface); border: 1px solid var(--rule); }
.tilt-row span:first-child { background: var(--accent-soft); border-color: transparent; }
.tilt-floor {
  width: 80%;
  height: var(--s4);
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--shade), transparent);
}

.js .tilt-face { transform: translateZ(calc((1 - var(--q)) * -160px)) rotateX(calc((1 - var(--q)) * 55deg)); }
.js .tilt-floor { opacity: calc(.35 + (1 - var(--q)) * .65); transform: scaleX(calc(.7 + (1 - var(--q)) * .3)); }

@media (prefers-reduced-motion: reduce) {
  .js .tilt-face { transform: none; }
  .js .tilt-floor { opacity: .35; transform: scaleX(.7); }
}
</style>
</head>
<body data-motion-number="32">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">32</p>
    <h1 class="dv-name">パースペクティブ</h1>
    <p class="dv-ai">Perspective tilt reveal</p>
    <p class="dv-desc">傾いた面が起き上がって正面を向く</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 1</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。下の面が、手前へ倒れた状態から起き上がります。</p>
    </section>
  </div>

  <div class="dv-wrap">
    <section class="tilt" data-progress="through">
      <div class="tilt-face">
        <div class="tilt-bar"><i></i><i></i><i></i></div>
        <div class="tilt-body">
          <div class="tilt-side"><b></b><b></b><b></b><b></b></div>
          <div class="tilt-main">
            <div class="tilt-hero"></div>
            <div class="tilt-row"><span></span><span></span></div>
          </div>
        </div>
      </div>
      <div class="tilt-floor"></div>
    </section>
  </div>

  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 2</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。次の面は、別の段の中で同じように起き上がります。</p>
    </section>
  </div>

  <div class="dv-wrap">
    <section class="tilt" data-progress="through">
      <div class="tilt-face">
        <div class="tilt-bar"><i></i><i></i><i></i></div>
        <div class="tilt-body">
          <div class="tilt-side"><b></b><b></b><b></b><b></b></div>
          <div class="tilt-main">
            <div class="tilt-row"><span></span><span></span><span></span></div>
            <div class="tilt-hero tilt-hero--alt"></div>
          </div>
        </div>
      </div>
      <div class="tilt-floor"></div>
    </section>
  </div>

  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 3</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。最後の面が起き上がると、この段落が続きます。</p>
    </section>
  </div>

  <div class="dv-wrap">
    <section class="tilt" data-progress="through">
      <div class="tilt-face">
        <div class="tilt-bar"><i></i><i></i><i></i></div>
        <div class="tilt-body">
          <div class="tilt-side"><b></b><b></b><b></b><b></b></div>
          <div class="tilt-main">
            <div class="tilt-hero"></div>
            <div class="tilt-row"><span></span><span></span></div>
          </div>
        </div>
      </div>
      <div class="tilt-floor"></div>
    </section>
  </div>
</main>

<footer class="dv-end">END — 32 パースペクティブ</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
dvProgress();
</script>
</body>
</html>

04 motion-tunnel-fly/demo.html

8.0 KB

/designs/motion-tunnel-fly/demo.html

中身を見る
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>33 トンネル — Z-axis tunnel fly-through</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.stack { display: grid; gap: var(--s6); }
.tunnel { height: 300vh; }
.tunnel-stick { position: sticky; top: 0; height: 100vh; overflow: hidden; background: var(--surface); }
.tunnel-frame {
  --q: clamp(0, calc(var(--p) * 1.8 - var(--i) * 0.15), 1);
  position: absolute; left: 50%; top: 50%;
  width: 40vmax; height: 40vmax;
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  transform: translate(-50%, -50%) scale(calc(0.04 + var(--q) * 3.2));
  opacity: min(1, calc(var(--q) * 6), calc((0.9 - var(--q)) * 8));
}

@media (prefers-reduced-motion: reduce) {
  .tunnel-frame { transform: translate(-50%, -50%) scale(calc(0.3 + var(--i) * 0.14)); opacity: 1; }
}

</style>
</head>
<body data-motion-number="33">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">33</p>
    <h1 class="dv-name">トンネル</h1>
    <p class="dv-ai">Z-axis tunnel fly-through</p>
    <p class="dv-desc">奥へ飛び込むように層が迫る</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 1</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。スクロールすると、枠が奥から手前へ通り過ぎます。</p>
    </section>
  </div>

  <section class="tunnel" data-progress="pin">
    <div class="tunnel-stick">
      <div class="tunnel-frame" style="--i: 0"></div>
      <div class="tunnel-frame" style="--i: 1"></div>
      <div class="tunnel-frame" style="--i: 2"></div>
      <div class="tunnel-frame" style="--i: 3"></div>
      <div class="tunnel-frame" style="--i: 4"></div>
      <div class="tunnel-frame" style="--i: 5"></div>
    </div>
  </section>

  <div class="dv-wrap stack">
    <section class="dv-card">
      <p class="dv-eyebrow">Section 2</p>
      <h2 class="dv-h2">見出しが入ります</h2>
      <p class="dv-lead">本文のダミーです。枠が通り過ぎたあとに、この段落が続きます。</p>
    </section>
  </div>
</main>

<footer class="dv-end">END — 33 トンネル</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
dvProgress();
</script>
</body>
</html>

05 motion-iris-mask/demo.html

8.2 KB

/designs/motion-iris-mask/demo.html

中身を見る
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>34 アイリスマスク — Circular iris mask expand</title>
<style>
/* dv-motion-base:start */
:root {
  --ground: #FFFFFF;
  --surface: #F3F5F8;
  --ink: #14181F;
  --muted: #5A6472;
  --accent: #1F5FA8;
  --rule: #D8DDE4;
  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
  --shade: color-mix(in srgb, var(--ink) 14%, transparent);

  --font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --t-label: 13px;
  --t-body: 16px;
  --t-lead: 20px;
  --t-h2: 32px;
  --t-h1: clamp(40px, 7vw, 72px);

  --s1: 4px;
  --s2: 8px;
  --s3: 16px;
  --s4: 24px;
  --s5: 40px;
  --s6: 64px;
  --s7: 104px;

  --radius: 12px;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --dur: .7s;
  --measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--t-body);
  line-height: 1.7;
  font-feature-settings: "palt";
  overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
  min-height: 72vh;
  display: grid;
  align-content: center;
  gap: var(--s3);
  padding-block: var(--s6);
  border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
  border-radius: var(--radius);
  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
  aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
  min-height: 60vh;
  display: grid;
  place-items: center;
  border-top: 1px solid var(--rule);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .12em;
}
/* dv-motion-base:end */

/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.iris-zone { height: 400vh; --p: 0; }
.iris-box { position: sticky; top: 0; height: 100vh; display: grid; overflow: hidden; }
.iris-face { grid-area: 1 / 1; min-height: 0; display: grid; align-content: center; padding-block: var(--s6); }
.iris-face .dv-wrap { display: grid; gap: var(--s3); }
.iris-face--now { background: var(--surface); }
.iris-face--next {
  background: var(--ink);
  color: var(--ground);
  clip-path: circle(calc(var(--p) * 75vmax) at 50% 50%);
}
.iris-face--next .dv-eyebrow { color: var(--accent-soft); }
.iris-face--next .dv-lead { color: var(--rule); }
.iris-dot {
  grid-area: 1 / 1;
  place-self: center;
  position: relative;
  z-index: 1;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--ground);
}

@media (prefers-reduced-motion: reduce) {
  .iris-zone { height: auto; }
  .iris-box { position: static; height: auto; }
  .iris-face { padding-block: var(--s7); }
  .iris-face--now, .iris-dot { display: none; }
  .iris-face--next { clip-path: none; }
}
</style>
</head>
<body data-motion-number="34">
<header class="dv-label">
  <div class="dv-wrap">
    <p class="dv-num">34</p>
    <h1 class="dv-name">アイリスマスク</h1>
    <p class="dv-ai">Circular iris mask expand</p>
    <p class="dv-desc">円形に開いて次の画面が出る</p>
    <p class="dv-hint">SCROLL ↓</p>
  </div>
</header>

<main class="dv-stage">
  <section class="iris-zone" data-progress="pin">
    <div class="iris-box">
      <div class="iris-face iris-face--now">
        <div class="dv-wrap">
          <p class="dv-eyebrow">Current</p>
          <h2 class="dv-h2">見出しが入ります</h2>
          <p class="dv-lead">本文のダミーです。中心の点から円が広がります。</p>
          <div class="dv-ph dv-ph--alt"></div>
        </div>
      </div>
      <div class="iris-face iris-face--next">
        <div class="dv-wrap">
          <p class="dv-eyebrow">Next</p>
          <h2 class="dv-h2">次の画面の見出し</h2>
          <p class="dv-lead">本文のダミーです。開き切ると、次の画面だけが残ります。</p>
          <div class="dv-ph dv-ph--ink"></div>
        </div>
      </div>
      <span class="iris-dot" aria-hidden="true"></span>
    </div>
  </section>
</main>

<footer class="dv-end">END — 34 アイリスマスク</footer>

<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');

/**
 * 画面に入った要素へ .is-in を付ける。
 * data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
 */
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
  const els = [...document.querySelectorAll(selector)];
  if (dvReduce || !('IntersectionObserver' in window)) {
    els.forEach((el) => el.classList.add('is-in'));
    return;
  }
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('is-in');
        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
      } else if (e.target.dataset.reveal === 'repeat') {
        e.target.classList.remove('is-in');
      }
    }
  }, { threshold, rootMargin });
  els.forEach((el) => io.observe(el));
}

/**
 * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
 *   data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
 *   data-progress="pin"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
 *                             (中に position: sticky の箱を持つ、背の高い区画用)
 *   data-progress="page"    : ページ全体のスクロール量
 * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
 */
function dvProgress(onUpdate) {
  const els = [...document.querySelectorAll('[data-progress]')];
  const clamp = (n) => Math.min(1, Math.max(0, n));
  let queued = false;
  const measure = () => {
    queued = false;
    const vh = window.innerHeight;
    for (const el of els) {
      const r = el.getBoundingClientRect();
      let p;
      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
      else if (el.dataset.progress === 'page') {
        const max = document.documentElement.scrollHeight - vh;
        p = max > 0 ? window.scrollY / max : 0;
      } else p = (vh - r.top) / (vh + r.height);
      p = clamp(p);
      el.style.setProperty('--p', p.toFixed(4));
      if (onUpdate) onUpdate(el, p);
    }
  };
  const request = () => {
    if (!queued) { queued = true; requestAnimationFrame(measure); }
  };
  addEventListener('scroll', request, { passive: true });
  addEventListener('resize', request);
  measure();
}
/* dv-motion-helpers:end */

/* ---- この演出だけの JS ---- */
dvProgress();
</script>
</body>
</html>