{
  "id": "motion:motion-scroll-zoom",
  "slug": "motion-scroll-zoom",
  "kind": "motion",
  "title": {
    "ja": "スクロールズーム",
    "en": "Scale on scroll"
  },
  "aiName": "Scale on scroll",
  "summary": {
    "ja": "画像が拡大して全画面に",
    "en": "An image scales up until it fills the whole screen"
  },
  "number": 11,
  "group": "scroll-linked",
  "techniques": [
    "scroll-progress",
    "position-sticky"
  ],
  "rules": [
    {
      "ja": "拡大は区画の中で完結し、箱は sticky で画面に留まる",
      "en": "The zoom completes inside the pinned section; the box stays fixed on screen"
    },
    {
      "ja": "拡大率は 0.5 から 1 の範囲に収める",
      "en": "Scale stays between 0.5 and 1"
    },
    {
      "ja": "動きを減らす設定では拡大せず、全画面の状態で表示する",
      "en": "With reduced motion, show the full-screen end state without scaling"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "スクロールズーム — Scale on scroll\n\n画像が画面の半分の大きさで現れ、スクロールに合わせて全画面まで広がる。区画の高さは 300vh、画像を収める箱は 100vh の position: sticky にする。\n拡大率は `scale(calc(0.5 + var(--p) * 0.5))` とし、角丸は `calc((1 - var(--p)) * 32px)` で、--p が 1 になったところで角丸を 0 にする。\n画像の前後には本文を 1 段ずつ置き、拡大が終わってから次の本文が見えるようにする。\nCSS と素の JavaScript だけで実装し、ライブラリは使わない。\n\n外すと崩れる点:\n- 拡大は区画の中で完結し、箱は sticky で画面に留まる\n- 拡大率は 0.5 から 1 の範囲に収める\n- 動きを減らす設定では拡大せず、全画面の状態で表示する\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-scroll-zoom/demo.html",
      "en": "Scale on scroll — Scale on scroll\n\nAn image appears at half the screen size and expands to full screen as the user scrolls. Make the section 300vh tall and the box that holds the image a 100vh sticky element.\nUse `scale(calc(0.5 + var(--p) * 0.5))` for the scale, and `calc((1 - var(--p)) * 32px)` for the corner radius so the corners reach 0 when --p is 1.\nPlace one paragraph of text before and after the image, so the next paragraph appears only after the zoom has finished.\nImplement with CSS and plain JavaScript only; do not use a library.\n\nDo not drop:\n- The zoom completes inside the pinned section; the box stays fixed on screen\n- Scale stays between 0.5 and 1\n- With reduced motion, show the full-screen end state without scaling\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-scroll-zoom/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-scroll-zoom/demo.html",
      "html": "<!doctype html>\n<html lang=\"ja\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>11 スクロールズーム — Scale on scroll</title>\n<style>\n/* dv-motion-base:start */\n:root {\n  --ground: #FFFFFF;\n  --surface: #F3F5F8;\n  --ink: #14181F;\n  --muted: #5A6472;\n  --accent: #1F5FA8;\n  --rule: #D8DDE4;\n  --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));\n  --accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));\n  --shade: color-mix(in srgb, var(--ink) 14%, transparent);\n\n  --font: \"Zen Kaku Gothic New\", \"Hiragino Sans\", \"Noto Sans JP\", \"Yu Gothic\", system-ui, sans-serif;\n  --mono: ui-monospace, \"SF Mono\", Menlo, Consolas, monospace;\n\n  --t-label: 13px;\n  --t-body: 16px;\n  --t-lead: 20px;\n  --t-h2: 32px;\n  --t-h1: clamp(40px, 7vw, 72px);\n\n  --s1: 4px;\n  --s2: 8px;\n  --s3: 16px;\n  --s4: 24px;\n  --s5: 40px;\n  --s6: 64px;\n  --s7: 104px;\n\n  --radius: 12px;\n  --ease: cubic-bezier(.2, .7, .2, 1);\n  --dur: .7s;\n  --measure: 1080px;\n}\n*, *::before, *::after { box-sizing: border-box; }\nhtml { -webkit-text-size-adjust: 100%; }\nbody {\n  margin: 0;\n  background: var(--ground);\n  color: var(--ink);\n  font-family: var(--font);\n  font-size: var(--t-body);\n  line-height: 1.7;\n  font-feature-settings: \"palt\";\n  overflow-x: hidden;\n}\nh1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }\nul, ol { list-style: none; }\n.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }\n.dv-label {\n  min-height: 72vh;\n  display: grid;\n  align-content: center;\n  gap: var(--s3);\n  padding-block: var(--s6);\n  border-bottom: 1px solid var(--rule);\n}\n.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }\n.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }\n.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }\n.dv-desc { font-size: var(--t-lead); color: var(--ink); }\n.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }\n.dv-stage { padding-block: var(--s7); }\n.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }\n.dv-lead { font-size: var(--t-lead); color: var(--muted); }\n.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }\n.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }\n.dv-ph {\n  border-radius: var(--radius);\n  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));\n  aspect-ratio: 16 / 9;\n}\n.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }\n.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }\n.dv-end {\n  min-height: 60vh;\n  display: grid;\n  place-items: center;\n  border-top: 1px solid var(--rule);\n  color: var(--muted);\n  font-family: var(--mono);\n  font-size: var(--t-label);\n  letter-spacing: .12em;\n}\n/* dv-motion-base:end */\n\n/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */\n.stack { display: grid; gap: var(--s6); }\n.zoom { height: 300vh; }\n.zoom-stick { position: sticky; top: 0; height: 100vh; overflow: hidden; }\n.zoom-img {\n  position: absolute; inset: 0;\n  display: grid; place-items: center;\n  background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));\n  transform: scale(calc(0.5 + var(--p) * 0.5));\n  border-radius: calc((1 - var(--p)) * 32px);\n}\n.zoom-cap { font-size: var(--t-h2); font-weight: 700; color: var(--ground); }\n\n@media (prefers-reduced-motion: reduce) {\n  .zoom-img { transform: none; border-radius: 0; }\n}\n</style>\n</head>\n<body data-motion-number=\"11\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">11</p>\n    <h1 class=\"dv-name\">スクロールズーム</h1>\n    <p class=\"dv-ai\">Scale on scroll</p>\n    <p class=\"dv-desc\">画像が拡大して全画面に</p>\n    <p class=\"dv-hint\">SCROLL ↓</p>\n  </div>\n</header>\n\n<main class=\"dv-stage\">\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\">\n      <p class=\"dv-eyebrow\">Section 1</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。スクロールすると、下の画像が画面の半分の大きさから広がります。</p>\n    </section>\n  </div>\n\n  <section class=\"zoom\" data-progress=\"pin\">\n    <div class=\"zoom-stick\">\n      <div class=\"zoom-img\"><p class=\"zoom-cap\">見出しが入ります</p></div>\n    </div>\n  </section>\n\n  <div class=\"dv-wrap stack\">\n    <section class=\"dv-card\">\n      <p class=\"dv-eyebrow\">Section 2</p>\n      <h2 class=\"dv-h2\">見出しが入ります</h2>\n      <p class=\"dv-lead\">本文のダミーです。拡大が終わったあとに、この段落が続きます。</p>\n    </section>\n  </div>\n</main>\n\n<footer class=\"dv-end\">END — 11 スクロールズーム</footer>\n\n<script>\n/* dv-motion-helpers:start */\nconst dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;\ndocument.documentElement.classList.add('js');\n\n/**\n * 画面に入った要素へ .is-in を付ける。\n * data-reveal=\"repeat\" の要素は画面を出たら外す（何度でも再生）。\n */\nfunction dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {\n  const els = [...document.querySelectorAll(selector)];\n  if (dvReduce || !('IntersectionObserver' in window)) {\n    els.forEach((el) => el.classList.add('is-in'));\n    return;\n  }\n  const io = new IntersectionObserver((entries) => {\n    for (const e of entries) {\n      if (e.isIntersecting) {\n        e.target.classList.add('is-in');\n        if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);\n      } else if (e.target.dataset.reveal === 'repeat') {\n        e.target.classList.remove('is-in');\n      }\n    }\n  }, { threshold, rootMargin });\n  els.forEach((el) => io.observe(el));\n}\n\n/**\n * スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。\n *   data-progress=\"through\" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1\n *   data-progress=\"pin\"     : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1\n *                             （中に position: sticky の箱を持つ、背の高い区画用）\n *   data-progress=\"page\"    : ページ全体のスクロール量\n * onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。\n */\nfunction dvProgress(onUpdate) {\n  const els = [...document.querySelectorAll('[data-progress]')];\n  const clamp = (n) => Math.min(1, Math.max(0, n));\n  let queued = false;\n  const measure = () => {\n    queued = false;\n    const vh = window.innerHeight;\n    for (const el of els) {\n      const r = el.getBoundingClientRect();\n      let p;\n      if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;\n      else if (el.dataset.progress === 'page') {\n        const max = document.documentElement.scrollHeight - vh;\n        p = max > 0 ? window.scrollY / max : 0;\n      } else p = (vh - r.top) / (vh + r.height);\n      p = clamp(p);\n      el.style.setProperty('--p', p.toFixed(4));\n      if (onUpdate) onUpdate(el, p);\n    }\n  };\n  const request = () => {\n    if (!queued) { queued = true; requestAnimationFrame(measure); }\n  };\n  addEventListener('scroll', request, { passive: true });\n  addEventListener('resize', request);\n  measure();\n}\n/* dv-motion-helpers:end */\n\n/* ---- この演出だけの JS ---- */\ndvProgress();\n</script>\n</body>\n</html>\n"
    }
  },
  "license": "Original",
  "catalogVersion": 1,
  "designSchemaVersion": 2,
  "capabilities": [
    "live-preview",
    "copy-prompt",
    "get-code"
  ],
  "reusePolicy": "construction-reuse",
  "urls": {
    "ja": "https://design-vault.pages.dev/designs/motion-scroll-zoom",
    "en": "https://design-vault.pages.dev/en/designs/motion-scroll-zoom",
    "json": "https://design-vault.pages.dev/ai/assets/motion-scroll-zoom.json",
    "designJson": null
  }
}