デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
画面を上下 2 枚の幕で覆い、どちらにも同じ数字を置いて、境目からはみ出す分を隠します。上の幕には数字の上半分、下の幕には下半分だけが見えるので、読み込みのあいだは 1 つの数字に見えます。数え終わって幕を上下に送ると、数字ごと真っ二つに割れて、下のページが出てきます。
数字は、行の箱の中央ではなく、字の上下の中央を境目に合わせます。欧文の書体は数字の中央が箱の中央より少し上にあるので、箱で合わせると上半分だけが小さくなります。ここでは行の上端から字の大きさの 0.463 倍の位置で、書体を替えたら測り直す値です。
割れる前に、中央へ細い線を 0.35 秒で走らせます。何の前触れもなく割れるより、合図が 1 つあるほうが目が付いていけます。幕は 0.9 秒の power4.inOut で抜けます。前半がゆっくりなので、開き始めの一瞬だけ下のページがのぞき、そこから一気に開きます。
幕を送る量は、画面の高さに対する割合(yPercent)で書きます。px で書くと、窓の高さが変わったときや、スマートフォンでアドレスバーが隠れたときに、開いたはずの幕が帯になって残ります。
進み具合は 0〜1 で渡し、読み込みが終わったときだけ幕を開きます。開く動きを 1 本のタイムラインとして返しておくと、ページ自身の見出しのせり上がりを、幕が開き始める時刻につなげられます。最初の 1 ページだけの演出に向いていて、ページを移るたびに出すと、待ち時間が増えたように感じます。
コード
<div class="split-loader" role="progressbar" aria-label="読み込みの進み具合"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="split-loader__half split-loader__half--top">
<p class="split-loader__number" aria-hidden="true">0</p>
<p class="split-loader__note">読み込み中</p>
</div>
<!-- 下の幕にも同じ数字を入れる。境目で切り抜かれ、下半分だけが見える -->
<div class="split-loader__half split-loader__half--bottom">
<p class="split-loader__number" aria-hidden="true">0</p>
<p class="split-loader__note">少々お待ちください</p>
</div>
<span class="split-loader__line"></span>
</div>
.split-loader {
--number: clamp(120px, 26vw, 300px); /* 数字の大きさ */
--center: 0.463; /* 字の上下の中央(行の上端からの比)。書体を替えたら測り直す */
--surface: #262626; /* 幕の色 */
--ink: #eeece6; /* 数字と線の色 */
--mute: #a8a6a0; /* 待ち時間の文言の色 */
position: fixed;
inset: 0;
z-index: 10;
color: var(--ink);
}
/* 上下 2 枚の幕。はみ出しを隠すので、境目で数字が切り抜かれる */
.split-loader__half {
position: absolute;
left: 0;
right: 0;
height: 50%;
overflow: hidden;
background: var(--surface);
}
.split-loader__half--top { top: 0; }
.split-loader__half--bottom { bottom: 0; }
/* 行の箱の中央ではなく、字の上下の中央を境目に合わせる */
.split-loader__number {
position: absolute;
left: 0;
right: 0;
margin: calc(var(--number) * var(--center) * -1) 0 0;
font-size: var(--number);
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
text-align: center;
}
.split-loader__half--top .split-loader__number { top: 100%; }
.split-loader__half--bottom .split-loader__number { top: 0; }
.split-loader__note {
position: absolute;
left: 32px;
right: 32px;
margin: 0;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--mute);
}
.split-loader__half--top .split-loader__note { top: 32px; }
.split-loader__half--bottom .split-loader__note { bottom: 32px; text-align: right; }
.split-loader__line {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
margin-top: -1px;
background: var(--ink);
transform: scaleX(0);
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 12 : 1; // 動きを減らす設定では、ほぼ一瞬で開く
function createLoader(root) {
const halves = root.querySelectorAll(".split-loader__half");
const numbers = root.querySelectorAll(".split-loader__number");
const line = root.querySelector(".split-loader__line");
const state = { value: 0 };
const draw = () => {
const text = String(Math.round(state.value));
numbers.forEach((number) => (number.textContent = text));
root.setAttribute("aria-valuenow", text);
};
return {
// 進み具合を 0〜1 で渡す
set(progress) {
const to = Math.min(1, Math.max(0, progress)) * 100;
gsap.to(state, { value: to, duration: 0.5 / SPEED, ease: "power2.out", onUpdate: draw });
},
// 読み込みが終わったら呼ぶ。返ってくるタイムラインに、ページ自身の出現をつなげられる
// 例: loader.done().add(heroIn, "-=0.75")
done() {
return gsap
.timeline({ onUpdate: draw, onComplete: () => root.remove() })
// 100 まで数え、中央に線を走らせてから割る
.to(state, { value: 100, duration: 0.3, ease: "power2.in" })
.to(line, { scaleX: 1, duration: 0.35, ease: "power3.inOut" })
// 幕は画面の高さぶん送る。px で送ると、高さが変わったときに幕が残る
.to(halves[0], { yPercent: -100, duration: 0.9, ease: "power4.inOut" })
.to(halves[1], { yPercent: 100, duration: 0.9, ease: "power4.inOut" }, "<")
.to(line, { autoAlpha: 0, duration: 0.25, ease: "power1.out" }, "<0.1")
.timeScale(SPEED);
},
};
}
export const loader = createLoader(document.querySelector(".split-loader"));