{
  "id": "motion:motion-iris-mask",
  "slug": "motion-iris-mask",
  "kind": "motion",
  "title": {
    "ja": "アイリスマスク",
    "en": "Iris mask"
  },
  "aiName": "Circular iris mask expand",
  "summary": {
    "ja": "円形に開いて次の画面が出る",
    "en": "A circle opens from the centre to reveal the next screen"
  },
  "number": 34,
  "group": "media-depth",
  "techniques": [
    "position-sticky",
    "clip-path",
    "scroll-progress"
  ],
  "rules": [
    {
      "ja": "上の面だけを円で切り、下の面（今の画面）は動かさない",
      "en": "Clip only the upper face with the circle; the face beneath stays still"
    },
    {
      "ja": "円の半径は進み具合 p から決め、時間では広げない",
      "en": "Derive the circle radius from scroll progress p, never from a timer"
    },
    {
      "ja": "動きを減らす設定では円を使わず、次の画面だけを表示する",
      "en": "Under prefers-reduced-motion, drop the circle and show only the next screen"
    }
  ],
  "reproduction": {
    "prompt": {
      "ja": "アイリスマスク — Circular iris mask expand\n\n画面を固定し、今の画面の上に次の画面を重ね、中央の一点から円形に開いて次の画面を見せる演出を作ってください。外側の区画は高さ 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 だけで実装し、ライブラリは使わないでください。\n\n外すと崩れる点:\n- 上の面だけを円で切り、下の面（今の画面）は動かさない\n- 円の半径は進み具合 p から決め、時間では広げない\n- 動きを減らす設定では円を使わず、次の画面だけを表示する\n\n動く実物（1 ファイルで完結する HTML）: https://design-vault.pages.dev/designs/motion-iris-mask/demo.html",
      "en": "Iris mask — Circular iris mask expand\n\nBuild an effect that pins the screen and opens the next screen as a circle from one point at the centre, laid over the current screen. The outer section is 400vh tall and holds a 100vh box with position: sticky; top: 0. Inside the box, place the next screen over the current one (the lower face), and clip only the upper face with clip-path: circle(calc(var(--p) * 75vmax) at 50% 50%). Compute the progress p (0 to 1) as p=0 when the section's top reaches the top of the screen and p=1 when its bottom reaches the bottom. Put a 12px dot at the centre to mark where the circle starts, and keep both faces inside the box at 375px width so nothing scrolls sideways. Implement it with plain CSS and JavaScript only, with no library.\n\nDo not drop:\n- Clip only the upper face with the circle; the face beneath stays still\n- Derive the circle radius from scroll progress p, never from a timer\n- Under prefers-reduced-motion, drop the circle and show only the next screen\n\nA working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-iris-mask/demo.html"
    },
    "code": {
      "url": "https://design-vault.pages.dev/designs/motion-iris-mask/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>34 アイリスマスク — Circular iris mask expand</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.iris-zone { height: 400vh; --p: 0; }\n.iris-box { position: sticky; top: 0; height: 100vh; display: grid; overflow: hidden; }\n.iris-face { grid-area: 1 / 1; min-height: 0; display: grid; align-content: center; padding-block: var(--s6); }\n.iris-face .dv-wrap { display: grid; gap: var(--s3); }\n.iris-face--now { background: var(--surface); }\n.iris-face--next {\n  background: var(--ink);\n  color: var(--ground);\n  clip-path: circle(calc(var(--p) * 75vmax) at 50% 50%);\n}\n.iris-face--next .dv-eyebrow { color: var(--accent-soft); }\n.iris-face--next .dv-lead { color: var(--rule); }\n.iris-dot {\n  grid-area: 1 / 1;\n  place-self: center;\n  position: relative;\n  z-index: 1;\n  width: 12px;\n  height: 12px;\n  border-radius: 50%;\n  background: var(--accent);\n  box-shadow: 0 0 0 4px var(--ground);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .iris-zone { height: auto; }\n  .iris-box { position: static; height: auto; }\n  .iris-face { padding-block: var(--s7); }\n  .iris-face--now, .iris-dot { display: none; }\n  .iris-face--next { clip-path: none; }\n}\n</style>\n</head>\n<body data-motion-number=\"34\">\n<header class=\"dv-label\">\n  <div class=\"dv-wrap\">\n    <p class=\"dv-num\">34</p>\n    <h1 class=\"dv-name\">アイリスマスク</h1>\n    <p class=\"dv-ai\">Circular iris mask expand</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  <section class=\"iris-zone\" data-progress=\"pin\">\n    <div class=\"iris-box\">\n      <div class=\"iris-face iris-face--now\">\n        <div class=\"dv-wrap\">\n          <p class=\"dv-eyebrow\">Current</p>\n          <h2 class=\"dv-h2\">見出しが入ります</h2>\n          <p class=\"dv-lead\">本文のダミーです。中心の点から円が広がります。</p>\n          <div class=\"dv-ph dv-ph--alt\"></div>\n        </div>\n      </div>\n      <div class=\"iris-face iris-face--next\">\n        <div class=\"dv-wrap\">\n          <p class=\"dv-eyebrow\">Next</p>\n          <h2 class=\"dv-h2\">次の画面の見出し</h2>\n          <p class=\"dv-lead\">本文のダミーです。開き切ると、次の画面だけが残ります。</p>\n          <div class=\"dv-ph dv-ph--ink\"></div>\n        </div>\n      </div>\n      <span class=\"iris-dot\" aria-hidden=\"true\"></span>\n    </div>\n  </section>\n</main>\n\n<footer class=\"dv-end\">END — 34 アイリスマスク</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-iris-mask",
    "en": "https://design-vault.pages.dev/en/designs/motion-iris-mask",
    "json": "https://design-vault.pages.dev/ai/assets/motion-iris-mask.json",
    "designJson": null
  }
}