motionsites中文站 / Ancient Oath
Ancient Oath
aadmin2026-10-26 发布未分类1 阅读

Ancient Oath

类型: · 收录:2026-10-26 23:39:21 · 源码:会员可见

[hidecontent type="vip1"]
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>OBOL — the coin that came back</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/fonts/inter-tight.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/fonts/newsreader.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/fonts/pinyon-script.css">
  <style>
@font-face {
  font-family: 'Inter Tight Fallback';
  src: local('Arial');
  ascent-override: 96.88%;
  descent-override: 24.15%;
  line-gap-override: 0%;
  size-adjust: 100%;
}

:root {
  --bg: #000000; --ink: #ffffff;

  --font-text: 'Inter Tight', 'Inter Tight Fallback', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display: 'Newsreader', 'Times New Roman', serif;

  --fs-display:    3rem;
  --fs-statement:  1.875rem;
  --fs-sublead:    1.5rem;
  --fs-ui:         1rem;
  --fs-caption:    .875rem;
  --fs-meta:       .75rem;

  --lh-statement:  1;
  --lh-sublead:    1;
  --lh-ui:         1.5;
  --lh-caption:    1.43;
  --lh-meta:       1.33;

  --ls-statement:  -.04em;
  --ls-sublead:    -.01em;
  --ls-ui:         -.001em;

  --w-text: 400;
  --w-ui:   500;

  --ls-display-serif: -.02em;
  --lh-display-serif: .95;
  --w-display-serif: 300;

  --ease-out-expo: cubic-bezier(.19, 1, .22, 1);
  --dur-word: 1.6s;
  --step-word: 75ms;
  --split-mask-pad: .08em;
  --split-mask-bleed-y: .3em;

  --ease-quart: cubic-bezier(.41, .35, .2, 1);
}
@media (min-width: 48rem) {
  :root {
    --fs-display:   7rem;
    --fs-statement: 3rem;
    --fs-sublead:   1.875rem;
  }
}

html { font-size: 15px; }
@media (min-width: 640px)  { html { font-size: clamp(15px, 12.9574px + .3191vw, 18px); } }
@media (min-width: 1580px) { html { font-size: clamp(18px, 1.1765vw - .5882px, 22px); } }

* { box-sizing: border-box; margin: 0; }
html, body { background: var(--bg); }
html {
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
body {
  font-family: var(--font-text);
  font-weight: var(--w-text);
  color: var(--ink);
}

.t-statement { font-size: var(--fs-statement); line-height: var(--lh-statement); letter-spacing: var(--ls-statement); font-weight: var(--w-text); }
.t-sublead   { font-size: var(--fs-sublead);   line-height: var(--lh-sublead);   letter-spacing: var(--ls-sublead);   font-weight: var(--w-text); }
.t-ui        { font-size: var(--fs-ui);        line-height: var(--lh-ui);        letter-spacing: var(--ls-ui);        font-weight: var(--w-ui); }
.t-caption   { font-size: var(--fs-caption);   line-height: var(--lh-caption);   letter-spacing: 0;                   font-weight: var(--w-ui); }
.t-meta      { font-size: var(--fs-meta);      line-height: var(--lh-meta);      letter-spacing: 0;                   font-weight: var(--w-text); }

.t-display-serif {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: var(--w-display-serif);
  font-optical-sizing: auto;
  font-variation-settings: 'opsz' 72;
  line-height: var(--lh-display-serif);
  letter-spacing: var(--ls-display-serif);
}

.screen {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 0 4vw;
}

.title { text-align: center; }

.title--poster { font-size: clamp(6rem, 16vw, 15rem); line-height: .9; --split-mask-bleed-y: .5em; }
.line { display: block; position: relative; }
.line.front { z-index: 6; }
.line.back  { z-index: 4; }

.title:where(html.js *):not(:has(.word)) .line { opacity: 0; }
.word-mask, .char-mask {
  display: inline-block;
  padding-right: var(--split-mask-pad);
  margin-right: calc(var(--split-mask-pad) * -1);
  overflow: visible !important;
  clip-path: inset(calc(var(--split-mask-bleed-y) * -1) 0 calc(var(--split-mask-bleed-y) * -1) 0);
}
.word {
  display: inline-block;
  transform: translateY(130%);
  transition: transform var(--dur-word) var(--ease-out-expo);
  transition-delay: calc(var(--step-word) * var(--word, 0));
}
.title.is-inview .word { transform: translateY(0%); }
.title:not(.is-inview) .word { transition-duration: 0s; transition-delay: 0s; }
@media (prefers-reduced-motion: reduce) {
  .word { transition: none; transform: none; }
}

.header { position: fixed; inset-inline: 0; top: 0; z-index: 20; pointer-events: none; }
.header__bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 28px 4vw;
  pointer-events: auto;
}
.header__nav { display: flex; gap: 2.5rem; text-transform: uppercase; letter-spacing: .08em; }
.header__nav a { color: var(--ink); text-decoration: none; opacity: .85; }
.header__nav a:hover { opacity: 1; }
.header__nav--left  { justify-self: start; }
.header__nav--right { justify-self: end; }
.header__logo { justify-self: center; display: inline-flex; }
.header__logo img { height: 80px; width: auto; display: block; }
@media (max-width: 47.99rem) {
  .header__bar { padding: 20px 5vw; }
  .header__nav { gap: 1.25rem; }
  .header__logo img { height: 56px; }
}

.plane { position: fixed; inset: 0; pointer-events: none; will-change: transform, opacity; }
.plane img { width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; }
.plane--mid   { z-index: 3; }
.plane--front { z-index: 7; }

.plane--mid img     { transform: translate(50px, 6%); }
.plane--front-l img { transform: translateX(-100px) scale(1.2); transform-origin: 0 50%; }
.plane--front-r img { transform: translateX(40px) scale(1.34) translateY(12%); transform-origin: 100% 40%; }

.stage {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  display: grid;
  place-items: center;
}
#coin {
  width: min(47.6vh, 47.6vw);
  height: min(47.6vh, 47.6vw);

  transform-origin: center;
}

.progress { position: absolute; left: 0; right: 0; bottom: 24px; text-align: center; text-transform: uppercase; letter-spacing: .12em; opacity: .5; }
.progress.done { display: none; }

.story { padding: 0 4vw 50vh; }
.story__col {
  display: flow-root;
  max-width: 56rem;
  margin-inline: auto;
}
.story__text {
  font-family: var(--font-display);
  font-weight: var(--w-text);
  line-height: 1.05;
  letter-spacing: -.01em;
  text-align: center;
}

.flow { position: relative; }
.flow__w {
  position: absolute;
  left: 0;
  top: 0;
  white-space: nowrap;
}

.script {
  font-family: 'Pinyon Script', 'Snell Roundhand', cursive;
  font-weight: 400;
  font-size: 1.15em;
  line-height: .9;
  letter-spacing: 0;
}

.chapters { padding: 0 4vw 35vh; }
.chapter { padding-block: 14vh 8vh; }
.chapter__word {
  position: relative;
  z-index: 2;
  font-size: clamp(6rem, 16vw, 15rem);
  line-height: .9;
  --split-mask-bleed-y: .5em;
  text-align: center;
  margin-bottom: -.28em;
}
.chapter__grid {
  display: grid;
  grid-template-columns: minmax(0, 38vw) 1fr;
  align-items: center;
  gap: 7vw;
  padding-inline: 6vw;
}
.chapter--flip .chapter__grid { grid-template-columns: 1fr minmax(0, 38vw); }
.chapter--flip .chapter__archwrap { order: 2; }
.chapter--flip .chapter__text { order: 1; justify-self: end; }

.chapter__archwrap { position: relative; aspect-ratio: 1 / 1.586; margin-top: -25.27%; }
.arch-clip {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: calc(100% * (4 / 3) / 1.586);
  overflow: hidden;
  border-radius: 50% 50% 0 0 / 37.5% 37.5% 0 0;
  --arch-top: 25.27%;
}
.arch-clip--bg { z-index: 1; }
.arch-clip--front { z-index: 3; }

.arch-clip img, .arch-mid {
  position: absolute; top: 0; height: auto; display: block;
  left: var(--l, 0%); width: var(--w, 100%);
  margin-top: calc(var(--t, 0%) - var(--arch-top, 0%));
}
.arch-mid { z-index: 2; pointer-events: none; }

.arch-clip--bg { background: #000; }
.arch-clip--bg img { opacity: .5; }

.chapter__text { max-width: 30rem; position: relative; z-index: 2; }
.chapter__meta { margin-top: 2em; letter-spacing: .04em; opacity: .55; }
.chapter__name { font-size: var(--fs-sublead); margin-top: .8em; }

.chapter__text .chapter__lead, .chapter__text .chapter__meta, .chapter__text .chapter__name {
  opacity: 0;
  transition: opacity .9s var(--ease-quart);
}
.chapter__text.is-inview .chapter__lead { opacity: 1; transition-delay: .35s; }
.chapter__text.is-inview .chapter__meta { opacity: .55; transition-delay: .5s; }
.chapter__text.is-inview .chapter__name { opacity: 1; transition-delay: .6s; }

.chapter__word:where(html.js *):not(:has(.char)) { opacity: 0; }
.chapter__word .char {
  display: inline-block;
  transform: translateY(130%);
  transition: transform var(--dur-word) var(--ease-out-expo);
  transition-delay: calc(35ms * var(--char, 0));
}
.chapter__word.is-inview .char { transform: translateY(0%); }
.chapter__word:not(.is-inview) .char { transition-duration: 0s; transition-delay: 0s; }

.homage { height: 380vh; }

.homage-word {
  position: fixed; inset: 0; pointer-events: none;
  display: grid; align-content: center; justify-items: center;
  visibility: hidden;
}
.homage-word--back  { z-index: 4; }
.homage-word--front { z-index: 6; }
.homage-word__line--ghost { visibility: hidden; }
.homage-word.is-active { visibility: visible; }
.homage-word__line { display: block; text-align: center; font-size: clamp(6rem, 17vw, 17rem); line-height: .9; --split-mask-bleed-y: .5em; }
.homage-word__line--2 { --char-offset: 7; }
.homage-word .char {
  display: inline-block;
  transform: translateY(130%);
  transition: transform var(--dur-word) var(--ease-out-expo);
  transition-delay: calc(35ms * (var(--char, 0) + var(--char-offset, 0)));
}
.homage-word.is-active .char { transform: translateY(0%); }
.homage-word:not(.is-active) .char { transition-duration: 0s; transition-delay: 0s; }
.homage-panel {
  position: fixed; inset: 0; z-index: 6; pointer-events: none;
  display: grid; place-items: center;
  visibility: hidden; opacity: 0;
}
.homage-panel__box {
  position: relative;
  width: 76vw;
  aspect-ratio: 2.1 / 1;
  display: flex;
  will-change: transform;
}
.homage-panel__half { width: 50%; height: 100%; overflow: hidden; position: relative; will-change: transform, opacity; }
.homage-panel__half--r { margin-left: -1px; width: calc(50% + 1px); }
.homage-panel__half img { position: absolute; top: 0; height: 100%; width: 200%; object-fit: cover; }
.homage-panel__half--l img { left: 0; }
.homage-panel__half--r img { right: 0; }

.homage-arms {
  position: fixed; inset: 0; z-index: 6; pointer-events: none;
  visibility: hidden; opacity: 0;
}
.homage-arms__l, .homage-arms__r { position: absolute; will-change: transform; }
.homage-arms__l { left: -2vw; top: 34%; width: 54vw; }
.homage-arms__r { right: -2vw; top: 31%; width: 52vw; }
.homage-caps { position: fixed; inset: 0; z-index: 7; pointer-events: none; visibility: hidden; opacity: 0; letter-spacing: .04em; }
.homage-caps__c { position: absolute; opacity: .6; }
.homage-caps__c--bl { bottom: 40px; left: 4vw; }
.homage-caps__c--br { bottom: 40px; right: 4vw; }

.reveal-image { clip-path: inset(-15% -5% 115% -25%); }
.reveal-image.is-inview.is-loaded { animation: reveal-wipe .9s var(--ease-quart) both; }
.reveal-image.is-inview.is-loaded .arch-clip--bg img { animation: reveal-settle .9s var(--ease-quart) both; }
@keyframes reveal-wipe {
  0%  { clip-path: inset(-15% -5% 115% -25%); }
  to  { clip-path: inset(-15% -5% -5% -25%); }
}
@keyframes reveal-settle {
  0%  { transform: scale(1.08) translateY(-3%); }
  to  { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .reveal-image { clip-path: none; animation: none !important; }
  .reveal-image .arch-clip--bg img { animation: none !important; }
  .chapter__text .chapter__lead, .chapter__text .chapter__meta, .chapter__text .chapter__name { transition: none; opacity: 1; }
  .chapter__text .chapter__meta { opacity: .55; }
  .chapter__word .char, .homage-word .char { transition: none; transform: none; }
  .homage { height: 120vh; }
}

@media (max-width: 47.99rem) {
  .chapter__grid, .chapter--flip .chapter__grid { grid-template-columns: 1fr; gap: 8vw; padding-inline: 0; }
  .chapter__archwrap, .chapter--flip .chapter__archwrap { order: 1; width: 86vw; margin-inline: auto; }
  .chapter__text, .chapter--flip .chapter__text { order: 2; justify-self: start; }
  .title--poster, .chapter__word { font-size: clamp(4rem, 19vw, 6rem); }
  .chapter__word { margin-bottom: -.1em; }
  .homage-panel__box { width: 92vw; }
}
  </style>
  <script>document.documentElement.classList.add('js')</script>
</head>
<body>
<header class="header">
    <div class="header__bar">
      <nav class="header__nav header__nav--left t-ui" aria-label="Primary">
        <a href="#">About</a>
        <a href="#">Story</a>
      </nav>
      <a class="header__logo" href="#" aria-label="Home">
        <img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/logo-lion.png" alt="" width="88" height="80">
      </a>
      <nav class="header__nav header__nav--right t-ui" aria-label="Secondary">
        <a href="#">Journal</a>
        <a href="#">Contact</a>
      </nav>
    </div>
  </header>

  <div class="plane plane--mid" aria-hidden="true"><img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/hero/mid.webp" alt="" fetchpriority="high"></div>
  <div class="plane plane--front plane--front-l" aria-hidden="true"><img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/hero/front-l.webp" alt="" fetchpriority="high"></div>
  <div class="plane plane--front plane--front-r" aria-hidden="true"><img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/hero/front-r.webp" alt="" fetchpriority="high"></div>

  <div class="stage" aria-hidden="true">
    <canvas id="coin"></canvas>
    <div class="progress t-meta" id="progress">0 / 120</div>
  </div>

  <section class="screen">

    <h1 class="title title--poster t-display-serif" data-split="words" data-split-mask="words" data-scroll data-scroll-offset="30%">
      <span class="line back">Still</span>
      <span class="line front">unspent.</span>
    </h1>
  </section>

  <section class="screen">
    <h2 class="title t-display-serif" data-split="words" data-split-mask="words" data-scroll data-scroll-offset="30%" data-scroll-repeat>
      <span class="line back">Turned by time</span>
      <span class="line front">still turning</span>
    </h2>
  </section>

  <section class="story" data-story>
    <div class="story__col" data-story-col>
      <p class="story__text t-statement" data-story-text>
        This coin was struck by hand in the workshop of <span class="script">Alexandros of Miletus</span>, somewhere in the fourth century before our era, from a blank of copper that had already lived one life as a temple nail. The lion on its face was cut into the die by an apprentice whose name nobody wrote down, and the strike landed a little off centre, as they often did. It passed through the purse of <span class="script">Theron the ferryman</span>, paid for a night's lodging in <span class="script">Halikarnassos</span>, and was lost in the mud of a riverbank for eleven hundred years. A farmer's daughter, <span class="script">Eleni Vassilaki</span>, found it in the spring of <span class="script">1911</span> and kept it in a matchbox under her pillow. The high points wore smooth against her thumb. Every scratch on it is a hand it has known, and every hand is a story that nobody finished telling.
      </p>
    </div>
  </section>

  <section class="chapters" data-chapters>
    <article class="chapter" data-block>
      <h3 class="chapter__word t-display-serif" data-word data-split="chars" data-split-mask="chars" data-scroll data-scroll-offset="10%" data-scroll-repeat>Struck</h3>
      <div class="chapter__grid">

        <div class="chapter__archwrap reveal-image" data-scroll data-scroll-offset="12%" data-scroll-repeat>
          <div class="arch-clip arch-clip--bg" data-plate>
            <img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/mint-bg.webp" style="--l:0%;--t:0%;--w:100%" alt="The mint workshop" decoding="async">
          </div>
          <img class="arch-mid" src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/mint-mid.webp" style="--l:-21.75%;--t:0.07%;--w:101.92%" alt="The apprentice swings the hammer" decoding="async" data-scroll data-scroll-speed="0.12">
          <div class="arch-clip arch-clip--front">
            <img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/mint-front.webp" style="--l:-5.85%;--t:-39.16%;--w:149.63%" alt="The old master steadies the die on the table" decoding="async" data-scroll data-scroll-speed="0.2">
          </div>
        </div>
        <div class="chapter__text" data-scroll data-scroll-offset="25%" data-scroll-repeat data-scroll-speed="0.05">
          <p class="chapter__lead t-sublead">No blade had ever marked the lion. An apprentice's burin did — and the strike landed a little off centre.</p>
          <p class="chapter__meta t-caption">miletus · c. 350 bce</p>
          <p class="chapter__name script">Alexandros of Miletus</p>
        </div>
      </div>
    </article>

    <article class="chapter chapter--flip" data-block>
      <h3 class="chapter__word t-display-serif" data-word data-split="chars" data-split-mask="chars" data-scroll data-scroll-offset="10%" data-scroll-repeat>Refused</h3>
      <div class="chapter__grid">
        <div class="chapter__archwrap reveal-image" data-scroll data-scroll-offset="12%" data-scroll-repeat>
          <div class="arch-clip arch-clip--bg" data-plate>
            <img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/ferry-bg.webp" style="--l:0%;--t:0%;--w:100%" alt="The crossing at dawn" decoding="async">
          </div>
          <img class="arch-mid" src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/ferry-mid.webp" style="--l:-2.96%;--t:-10.13%;--w:115.73%" alt="The ferryman in his boat offers the coin back" decoding="async" data-scroll data-scroll-speed="0.12">
          <div class="arch-clip arch-clip--front">
            <img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/arches/ferry-front.webp" style="--l:-78.24%;--t:19.56%;--w:141.89%" alt="The veiled traveller reaches for the coin" decoding="async" data-scroll data-scroll-speed="0.2">
          </div>
        </div>
        <div class="chapter__text" data-scroll data-scroll-offset="25%" data-scroll-repeat data-scroll-speed="0.05">
          <p class="chapter__lead t-sublead">The ferryman weighs every obol in the dark. Short weight does not cross — it goes back to the shore of the living.</p>
          <p class="chapter__meta t-caption">the crossing · refused</p>
          <p class="chapter__name script">Charon</p>
        </div>
      </div>
    </article>
  </section>

  <section class="homage" data-homage aria-label="It came back"></section>

  <div class="homage-word homage-word--back" data-hword aria-hidden="true">
    <span class="homage-word__line t-display-serif" data-split="chars" data-split-mask="chars">It came</span>
    <span class="homage-word__line homage-word__line--ghost t-display-serif" data-split="chars" data-split-mask="chars">back.</span>
  </div>
  <div class="homage-panel" aria-hidden="true">
    <div class="homage-panel__box">
      <div class="homage-panel__half homage-panel__half--l"><img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/chapters/homage-bg.webp" alt=""></div>
      <div class="homage-panel__half homage-panel__half--r"><img src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/chapters/homage-bg.webp" alt=""></div>
    </div>
  </div>

  <div class="homage-arms" aria-hidden="true">
    <img class="homage-arms__l" src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/chapters/homage-arm-l.webp" alt="">
    <img class="homage-arms__r" src="https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/assets/img/chapters/homage-arm-r.webp" alt="">
  </div>
  <div class="homage-word homage-word--front" data-hword aria-hidden="true">
    <span class="homage-word__line homage-word__line--ghost t-display-serif" data-split="chars" data-split-mask="chars">It came</span>
    <span class="homage-word__line homage-word__line--2 t-display-serif" data-split="chars" data-split-mask="chars">back.</span>
  </div>
  <div class="homage-caps t-caption" aria-hidden="true">
    <span class="homage-caps__c homage-caps__c--bl">worn smooth against a thumb</span>
    <span class="homage-caps__c homage-caps__c--br">three letters · unread</span>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/locomotive-scroll@5.0.0/bundled/locomotive-scroll.min.js"></script>
  <script>
(function () {
const { gsap, ScrollTrigger, SplitText } = window;
if (!gsap) throw new Error('gsap missing');
gsap.registerPlugin(ScrollTrigger, SplitText);
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;

let refreshQueued = false;
const queueRefresh = () => {
  if (refreshQueued) return;
  refreshQueued = true;
  requestAnimationFrame(() => { refreshQueued = false; ScrollTrigger.refresh(); });
};

function initSplit(root = document) {
  const splits = [];
  let cancelled = false;
  document.fonts.ready.then(() => requestAnimationFrame(() => {
    if (cancelled) return;
    gsap.utils.toArray('[data-split]', root).forEach((el) => {
      const { split, splitMask } = el.dataset;
      splits.push(new SplitText(el, {
        tag: 'span',
        type: split?.toLowerCase() || 'lines',
        mask: splitMask?.toLowerCase() || 'lines',
        charsClass: 'char',
        wordsClass: 'word',
        linesClass: 'line',
        propIndex: true,
        autoSplit: true,
        onSplit: queueRefresh,
      }));
    });
  }));
  return () => { cancelled = true; splits.forEach((s) => s.revert()); };
}

let instance = null;
const listeners = new Set();

function initScroll() {
  if (instance) return instance;
  instance = new window.LocomotiveScroll({
    lenisOptions: { lerp: 0.07, wheelMultiplier: 0.85, touchMultiplier: 1.3, syncTouch: false },
    initCustomTicker: (render) => gsap.ticker.add(render),
    destroyCustomTicker: (render) => gsap.ticker.remove(render),
    scrollCallback: (e) => listeners.forEach((fn) => fn(e)),
  });
  if (history.scrollRestoration) {
    history.scrollRestoration = 'manual';
    window.scrollTo(0, 0);
  }
  instance.lenisInstance?.on('scroll', ScrollTrigger.update);
  gsap.ticker.lagSmoothing(0);
  return instance;
}

const getScroll = () => instance;
const onScroll = (fn) => { listeners.add(fn); return () => listeners.delete(fn); };

function initImageLoad(root = document) {
  root.querySelectorAll('.reveal-image:not([data-load-init])').forEach((box) => {
    box.setAttribute('data-load-init', '');
    const imgs = [...box.querySelectorAll('img')];
    let pending = imgs.length;
    const one = () => { pending -= 1; if (pending <= 0) box.classList.add('is-loaded'); };
    if (!pending) return box.classList.add('is-loaded');
    imgs.forEach((img) => {
      if (img.complete && img.naturalWidth > 0) one();
      else {
        img.addEventListener('load', one, { once: true });
        img.addEventListener('error', one, { once: true });
      }
    });
  });
}

function createFlow({ el, points, centre, margin = 16 }) {

  const words = [];
  el.normalize();
  const walk = (node, script) => {
    if (node.nodeType === Node.TEXT_NODE) {
      const raw = node.textContent;

      const glue = words.length > 0 && !/^s/.test(raw);
      raw.split(/s+/).filter(Boolean).forEach((t, k) => words.push({ text: t, script, glue: glue && k === 0 }));
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      const glue = words.length > 0 && node.previousSibling && node.previousSibling.nodeType === Node.TEXT_NODE && !/s$/.test(node.previousSibling.textContent);
      const before = words.length;
      node.childNodes.forEach((c) => walk(c, script || node.classList.contains('script')));
      if (glue && words[before]) words[before].glue = true;
    }
  };
  [...el.childNodes].forEach((n) => walk(n, false));
  el.textContent = '';
  el.classList.add('flow');
  words.forEach((w) => {
    const s = document.createElement('span');
    s.className = w.script ? 'flow__w script' : 'flow__w';
    s.textContent = w.text;
    el.appendChild(s);
    w.el = s;
    w.x = 0; w.y = 0; w.shift = null;
  });

  const state = { W: 0, L: 0, space: 0, height: 0 };
  let lines = [];

  function measure() {
    const cs = getComputedStyle(el);
    state.W = el.clientWidth;
    state.L = parseFloat(cs.lineHeight);
    const probe = document.createElement('span');
    probe.className = 'flow__w';
    probe.textContent = ' ';
    el.appendChild(probe);
    state.space = probe.getBoundingClientRect().width;
    probe.remove();
    words.forEach((w) => { w.width = w.el.getBoundingClientRect().width; });
    wrap();
  }

  function wrap() {
    const { W, L, space } = state;
    lines = [];
    let i = 0;
    while (i < words.length) {
      const start = i;
      let used = 0;
      while (i < words.length) {
        const w = words[i];
        const next = used + (used === 0 ? 0 : (w.glue ? 0 : space)) + w.width;
        if (next > W && used > 0) break;
        used = next;
        i += 1;
      }
      const y = lines.length * L;
      let x = (W - used) / 2;
      const lineWords = words.slice(start, i);
      lineWords.forEach((w, k) => {
        if (k > 0) x += w.glue ? 0 : space;
        w.x = x; w.y = y; w.shift = null;
        w.el.style.transform = `translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
        x += w.width;
      });

      const mid = W / 2;
      let split = 0;
      let best = Infinity;
      for (let k = 0; k <= lineWords.length; k += 1) {
        const gx = k === 0 ? lineWords[0].x : k === lineWords.length ? lineWords[k - 1].x + lineWords[k - 1].width
          : (lineWords[k - 1].x + lineWords[k - 1].width + lineWords[k].x) / 2;
        const d = Math.abs(gx - mid);
        if (d < best) { best = d; split = k; }
      }
      const leftEnd = split > 0 ? lineWords[split - 1].x + lineWords[split - 1].width : -Infinity;
      const rightStart = split < lineWords.length ? lineWords[split].x : Infinity;
      const gap = split === 0 ? rightStart : split === lineWords.length ? leftEnd : (leftEnd + rightStart) / 2;
      lines.push({ y, words: lineWords, split, leftEnd, rightStart, gap });
    }
    state.height = lines.length * L;
    el.style.height = `${state.height}px`;
  }

  function holeAt(ya, yb, cx, cy, size, anchor) {
    const ramp = margin * 2;
    let lo = Infinity;
    let hi = -Infinity;
    for (const [px, py] of points) {
      const x = cx + (px - centre[0]) * size;
      const y = cy + (py - centre[1]) * size;
      const depth = Math.min(y - (ya - ramp), (yb + ramp) - y);
      if (depth <= 0) continue;
      const wgt = Math.min(1, depth / ramp);
      const xe = anchor + (x - anchor) * wgt;
      lo = Math.min(lo, xe - margin * wgt);
      hi = Math.max(hi, xe + margin * wgt);
    }
    return lo === Infinity ? null : [lo, hi];
  }

  const setShift = (w, s) => {
    if (w.shift === s) return;
    w.shift = s;
    w.el.style.transform = `translate(${(w.x + s).toFixed(1)}px, ${w.y.toFixed(1)}px)`;
  };

  function update(cx, cy, size) {
    if (!state.W) measure();
    const { L } = state;
    for (const ln of lines) {
      const hole = size > 0 ? holeAt(ln.y, ln.y + L, cx, cy, size, ln.gap) : null;
      let left = 0;
      let right = 0;
      if (hole) {
        left = Math.max(0, ln.leftEnd - hole[0]);
        right = Math.max(0, hole[1] - ln.rightStart);
      }
      ln.words.forEach((w, k) => setShift(w, k < ln.split ? -left : right));
    }
  }

  return { measure, update, get height() { return state.height; }, get lines() { return lines; }, words };
}

const N = 120;
const useCdn = true;
const BASE = 'https://cdn.jsdelivr.net/gh/roninsegun/new-15@1ddbe25a76966693874e2d8045480c7b25b3f9e4/test-sandwich/frames/';
const FRAMES_V = '2';

const TURN_END = 1.5;
const THROW_PEAK = 0.45;
const THROW_END = 1.0;
const SCALE_END = 0.5;    // the coin's size for the rest of the page (the hero starts at it too)

const canvas = document.getElementById('coin');
const ctx = canvas.getContext('2d');
const progress = document.getElementById('progress');
const [mid, frontL, frontR] = ['mid', 'front-l', 'front-r'].map((n) => document.querySelector(`.plane--${n}`));
const frames = new Array(N);
let loaded = 0;
let current = -1;

const pad = (i) => String(i).padStart(3, '0');

function drawFrame(i) {
  if (i === current) return;
  const img = frames[i];
  if (!img || !img.complete || !img.naturalWidth) return;
  current = i;
  const { width, height } = canvas;
  ctx.clearRect(0, 0, width, height);
  ctx.drawImage(img, 0, 0, width, height);
}

function sizeCanvas() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  const box = canvas.clientWidth;
  canvas.width = Math.round(box * dpr);
  canvas.height = Math.round(box * dpr);
  current = -1;
  drawFrame(targetFrame());
}

const IN_FLIGHT = 8;
let next = 0;
function load(i) {
  const img = new Image();
  if (useCdn) img.crossOrigin = 'anonymous';
  img.decoding = 'async';
  if (i === 0) img.fetchPriority = 'high';
  img.onload = img.onerror = () => {
    loaded += 1;
    progress.textContent = `${loaded} / ${N}`;
    if (loaded === N) progress.classList.add('done');
    if (i === targetFrame()) { current = -1; drawFrame(i); }
    if (next < N) load(next++);
  };
  img.src = `${BASE}${pad(i)}.webp?v=${FRAMES_V}`;
  frames[i] = img;
}
while (next < IN_FLIGHT) load(next++);

const state = { frame: 0 };
const targetFrame = () => Math.round(state.frame) % N;
const vh = (k) => () => innerHeight * k;

if (reduced) document.querySelectorAll('[data-scroll-speed]').forEach((el) => el.removeAttribute('data-scroll-speed'));
initScroll();
initSplit();
initImageLoad();

document.fonts.ready.then(() => requestAnimationFrame(() => requestAnimationFrame(() => {
  document.querySelectorAll('[data-scroll]').forEach((el) => {
    const r = el.getBoundingClientRect();
    if (r.top < innerHeight * 0.7 && r.bottom > 0) el.classList.add('is-inview');
  });
})));

if (reduced) {
  state.frame = 0;
  gsap.set(canvas, { scale: SCALE_END });

  gsap.fromTo([mid, frontL, frontR], { opacity: 1 }, { opacity: 0, ease: 'none', scrollTrigger: { start: 0, end: () => innerHeight * 0.5, scrub: true, invalidateOnRefresh: true } });
} else {
  const tl = gsap.timeline({
    defaults: { ease: 'none' },
    scrollTrigger: { start: 0, end: () => innerHeight * TURN_END, scrub: true, invalidateOnRefresh: true },
    onUpdate: () => drawFrame(targetFrame()),
  });

  tl.fromTo(state, { frame: 0 }, { frame: N - 1, duration: TURN_END }, 0)

    .fromTo(canvas, { scale: SCALE_END }, { scale: 1, duration: THROW_PEAK, ease: 'sine.out' }, 0)
    .fromTo(canvas, { scale: 1 }, { scale: SCALE_END, duration: THROW_END - THROW_PEAK, ease: 'sine.inOut', immediateRender: false }, THROW_PEAK)

    .fromTo(mid, { x: 0, y: 0, scale: 1 }, { x: vh(0.35), y: vh(-0.07), scale: 0.94, duration: 1 }, 0)
    .fromTo(mid, { opacity: 1 }, { opacity: 0, duration: 0.4, immediateRender: false }, 0.5)
    .fromTo(frontL, { x: 0, y: 0 }, { x: vh(-1), y: vh(1), duration: 1 }, 0)
    .fromTo(frontR, { x: 0, y: 0 }, { x: vh(1), y: vh(1), duration: 1 }, 0);
}

const ARCH_SCALE = SCALE_END * 0.5;
const TURN_VH = 1.5;
const LIFT_VH = 0.04;
const SPRING_PERIOD = 1.3;
const MAX_VH_PER_S = 1.2;
const MAX_LAG_VH = 0.6;
const plates = gsap.utils.toArray('[data-plate]');
const chapters = document.querySelector('[data-chapters]');
const homage = document.querySelector('[data-homage]');
let walk = null;
let devFollow = null; let devSpring = null;
if (!reduced && plates.length) {
  const FACE = N - 1;
  const docTop = (el) => el.getBoundingClientRect().top + scrollY;
  const centreX = (el) => {
    if (innerWidth < 768) return 0;
    const r = el.getBoundingClientRect();
    return (r.left + r.right) / 2 - innerWidth / 2;
  };
  const range = { s0: 0, d: 1 };
  const spring = { cur: 0, vel: 0, target: 0 };
  let shown = -1;
  const render = (force) => {
    if (!force && spring.cur === shown) return;
    shown = spring.cur;
    if (scrollY < innerHeight * TURN_END) {

      gsap.set(canvas, { x: 0, y: 0 });
      return;
    }
    walk.progress(spring.cur);
    drawFrame(targetFrame());
  };
  const buildWalk = () => {
    if (walk) walk.kill();
    const vh = innerHeight;
    const S0 = docTop(chapters) - 0.5 * vh;
    const S1 = docTop(homage) + homage.offsetHeight - vh;
    const D = S1 - S0;
    range.s0 = S0; range.d = D;
    const at = (docY, frac) => docY - frac * vh - S0;
    walk = gsap.timeline({ paused: true, defaults: { ease: 'none', immediateRender: false } });
    const move = (t0, t1, x0, x1) => {
      const d = t1 - t0;
      walk.fromTo(canvas, { x: x0 }, { x: x1, duration: d, ease: 'power2.inOut' }, t0)
        .fromTo(canvas, { y: 0 }, { y: -LIFT_VH * vh, duration: d / 2, ease: 'sine.out' }, t0)
        .fromTo(canvas, { y: -LIFT_VH * vh }, { y: 0, duration: d / 2, ease: 'sine.in' }, t0 + d / 2);
    };
    let x = 0;
    let prevLeaveEnd = -Infinity;
    const holds = [];
    plates.forEach((plate) => {
      const wrap = plate.closest('.chapter__archwrap');
      const wt = docTop(wrap);
      const wb = wt + wrap.offsetHeight;
      const cx = centreX(plate);

      const a0 = Math.max(at(wt, 0.75), prevLeaveEnd + 0.02 * vh, 0); const a1 = at(wt, 0.08);
      move(a0, a1, x, cx);
      walk.fromTo(canvas, { scale: SCALE_END }, { scale: ARCH_SCALE, duration: a1 - a0, ease: 'power2.inOut' }, a0);

      const l0 = at(wb, 0.72); const l1 = at(wb, 0.4);
      walk.fromTo(canvas, { scale: ARCH_SCALE }, { scale: SCALE_END, duration: l1 - l0, ease: 'power2.inOut' }, l0);
      holds.push([a1, l0]);
      prevLeaveEnd = l1;
      x = cx;
    });

    let t = 0;
    holds.forEach(([docked, leaving]) => {
      walk.fromTo(state, { frame: FACE }, { frame: FACE + N, duration: Math.max(1, docked - t) }, t);
      t = leaving;
    });
    const rest = Math.max(1, D - t);
    const turns = Math.max(1, Math.round(rest / (vh * TURN_VH)));
    walk.fromTo(state, { frame: FACE }, { frame: FACE + turns * N, duration: rest }, t);

    const ht = docTop(homage);
    move(at(ht, 0.98), at(ht, 0.8), x, 0);

    spring.target = spring.cur = gsap.utils.clamp(0, 1, (scrollY - S0) / D);
    spring.vel = 0;
    render(true);
  };
  buildWalk();
  ScrollTrigger.addEventListener('refreshInit', buildWalk);

  ScrollTrigger.addEventListener('refresh', () => { if (spring.cur > 0) { walk.render(walk.totalTime(), true, true); drawFrame(targetFrame()); } });

  const omega = (2 * Math.PI) / SPRING_PERIOD;
  const follow = (time, deltaMs) => {
    spring.target = gsap.utils.clamp(0, 1, (scrollY - range.s0) / range.d);
    const far = scrollY < range.s0 - innerHeight;
    const maxLag = far ? 0 : (MAX_LAG_VH * innerHeight) / range.d;
    const lag = spring.target - spring.cur;
    if (Math.abs(lag) > maxLag) { spring.cur = spring.target - Math.sign(lag) * maxLag; if (far) spring.vel = 0; }
    const gap = spring.target - spring.cur;
    if (Math.abs(gap) >= 1e-6 || Math.abs(spring.vel) >= 1e-6) {
      const dt = Math.min(deltaMs / 1000, 1 / 30);
      const acc = omega * omega * gap - 2 * omega * spring.vel;
      const vmax = (MAX_VH_PER_S * innerHeight) / range.d;
      spring.vel = gsap.utils.clamp(-vmax, vmax, spring.vel + acc * dt);
      spring.cur += spring.vel * dt;
      if (Math.abs(spring.target - spring.cur) < 1e-6) { spring.cur = spring.target; spring.vel = 0; }
    }
    render();
  };
  gsap.ticker.add(follow);
  devFollow = follow; devSpring = spring;

  const box = document.querySelector('.homage-panel__box');
  const halves = gsap.utils.toArray('.homage-panel__half');
  const arms = document.querySelector('.homage-arms');
  const wordH = gsap.utils.toArray('[data-hword]');
  const caps = document.querySelector('.homage-caps');
  const cover = () => 1.04 * Math.max(innerWidth / box.offsetWidth, innerHeight / box.offsetHeight);
  gsap.timeline({
    defaults: { ease: 'none' },
    scrollTrigger: {
      trigger: homage, start: 'top 80%', end: 'bottom bottom', scrub: true, invalidateOnRefresh: true,
      onUpdate: (self) => {
        wordH.forEach((el) => el.classList.toggle('is-active', self.progress > 0.7));
      },
    },
  })
    .fromTo(['.homage-panel', '.homage-arms'], { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.25 }, 0)
    .fromTo(box, { y: vh(1), scale: cover }, { y: 0, duration: 1.8 }, 0)
    .fromTo(arms, { y: vh(1) }, { y: 0, duration: 1.8 }, 0)
    .to({}, { duration: 0.8 }, 1.8)
    .to(box, { scale: 1, y: vh(0.07), duration: 1.8 }, 2.6)

    .to('.homage-arms__l', { x: () => -innerWidth * 0.05, duration: 1, ease: 'power2.out' }, 3)
    .to('.homage-arms__r', { x: () => innerWidth * 0.05, duration: 1, ease: 'power2.out' }, 3)
    .to('.homage-arms__l', { x: () => -innerWidth * 0.10, duration: 2.4 }, 4)
    .to('.homage-arms__r', { x: () => innerWidth * 0.10, duration: 2.4 }, 4)
    .to('.homage-arms__l', { x: () => -innerWidth * 0.45, duration: 1.8, ease: 'power1.in' }, 6.4)
    .to('.homage-arms__r', { x: () => innerWidth * 0.45, duration: 1.8, ease: 'power1.in' }, 6.4)

    .to(halves[0], { xPercent: -20, autoAlpha: 0, filter: 'blur(20px)', duration: 1.4 }, 6.6)
    .to(halves[1], { xPercent: 20, autoAlpha: 0, filter: 'blur(20px)', duration: 1.4 }, 6.6)
    .to(arms, { autoAlpha: 0, duration: 0.8 }, 7.8)
    .set('.homage-panel', { autoAlpha: 0 }, 8)
    .fromTo(caps, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.6 }, 8.6)
    .fromTo('.homage-caps__c', { y: 14, opacity: 0 }, { y: 0, opacity: 0.6, duration: 1, stagger: 0.2 }, 8.6)
    .to({}, { duration: 0.4 }, 9.6);
} else if (reduced) {

  document.querySelectorAll('[data-hword]').forEach((el) => el.classList.add('is-active'));
  gsap.set('.homage-caps', { autoAlpha: 1 });
  gsap.set('.homage-caps__c', { opacity: 0.6 });
}

const text = document.querySelector('[data-story-text]');
let flow = null;

function updateFlow() {
  if (!flow) return;
  const box = text.getBoundingClientRect();
  const coin = canvas.getBoundingClientRect();
  const size = coin.width;
  const cx = coin.left + coin.width / 2 - box.left;
  const cy = coin.top + coin.height / 2 - box.top;
  const near = cy > -size && cy < box.height + size;
  flow.update(cx, cy, near ? size : 0);
}

const shape = {"centre":[0.4963,0.497],"points":[[0.4963,0.038],[0.5363,0.0398],[0.576,0.045],[0.6146,0.0555],[0.6516,0.0703],[0.689,0.0837],[0.7228,0.1046],[0.7539,0.129],[0.7838,0.1544],[0.8119,0.1814],[0.8359,0.212],[0.8635,0.2399],[0.887,0.2714],[0.9061,0.3059],[0.923,0.3417],[0.9368,0.379],[0.9464,0.4176],[0.9535,0.457],[0.9592,0.497],[0.9457,0.5363],[0.9541,0.5777],[0.9425,0.6166],[0.9285,0.6543],[0.9132,0.6914],[0.8938,0.7265],[0.8715,0.7597],[0.8456,0.7901],[0.8188,0.8195],[0.7888,0.8456],[0.7573,0.8698],[0.7248,0.8928],[0.6886,0.9094],[0.6526,0.9265],[0.6153,0.9413],[0.576,0.949],[0.5365,0.9562],[0.4963,0.9579],[0.4559,0.9581],[0.4166,0.949],[0.3775,0.9403],[0.3393,0.9283],[0.3023,0.913],[0.2668,0.8945],[0.2353,0.8698],[0.205,0.8441],[0.1759,0.8174],[0.1492,0.7883],[0.1251,0.7569],[0.1064,0.7221],[0.0954,0.684],[0.0751,0.6503],[0.0633,0.613],[0.0529,0.5752],[0.0459,0.5364],[0.0412,0.497],[0.041,0.4572],[0.0452,0.4175],[0.0539,0.3785],[0.065,0.34],[0.0812,0.3034],[0.0971,0.2665],[0.1187,0.2326],[0.1432,0.2007],[0.171,0.1718],[0.2006,0.1447],[0.2325,0.1202],[0.2668,0.0995],[0.3019,0.0801],[0.3386,0.0639],[0.3772,0.0527],[0.4166,0.045],[0.4561,0.0378]]};
document.fonts.ready.then(() => {
  const gap = parseFloat(getComputedStyle(text).fontSize) * 0.4;
  flow = createFlow({ el: text, points: shape.points, centre: shape.centre, margin: gap });
  flow.measure();
  updateFlow();
  ScrollTrigger.refresh();
});

onScroll(updateFlow);

ScrollTrigger.config({ ignoreMobileResize: true });
ScrollTrigger.addEventListener('refresh', () => {
  if (flow) flow.measure();
  sizeCanvas();
  updateFlow();
});
sizeCanvas();

window.__test = {
  frames, state, updateFlow, get flow() { return flow; },
  get loaded() { return loaded; },
  get current() { return current; },
  get scale() { return gsap.getProperty(canvas, 'scale'); },
  planes: { mid, frontL, frontR },
  get walk() { return walk; },
  follow: (dtMs) => devFollow && devFollow(0, dtMs), get spring() { return devSpring; },
  get scroll() { return getScroll(); },
  base: BASE,
};
})();
  </script>
</body>
</html>
[/hidecontent]
← 返回首页