デモ
デモを読み込めませんでした。
仕様
- 用途
- カーソル
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
グレージュの円を mix-blend-mode の difference で重ね、ポインタを少し遅れて追わせます。円の色との差を取るので、暗い地は明るいグレージュに、明るい文字は暗い青みのスレートに抜けます。円そのものは 1 色で、反転は色の合成が作ります。カーソルの点も同じ色の合成にしているので、円の中の地では点が元の暗い色に戻って見えます。
円の直径は 260px で、quickTo を使って 0.35 秒の power3 でポインタへ追いつかせます。見出しは反転を見せる舞台なので、144px まで大きくしています。字が太く大きいほど、円が文字と地にまたがる所が増え、反転がよく読めます。和文は 1 字がほぼ字の大きさの幅になるので、144px なら 1 行 8 字(行末の読点を含む)までにし、行は文の切れ目で分けます。箱に入ったときは円を 0.3 秒で大きくして出し、出るときは小さくして消します。
地の色を body に塗ると、合成の下地にならないことがあり、円が色のまま出ます。円と文字を包む箱に地の色を塗り、その箱に isolation の isolate を付けて、合成をその中に閉じてください。円は画面に固定して置くので、箱の overflow では切れません。箱に clip-path を付けておくと、はみ出した円が下の節に残りません。
キーボードで箱の中のリンクへ移ったときは、円をそのリンクの上に置いて反転させます。指で触る端末では円を出さず、本物のカーソルのままにします。動きを減らす設定では、遅れなしでポインタに付けます。
つまみは、円の直径、追いつく時間、円の色です。地と文字は、それぞれ円の色との差の色に抜けます。白にすると明暗だけが入れ替わり、淡い色にすると、暗い地はその色に、明るい文字はその補色寄りの暗い色になります。見出しの大きいヒーローや、スタジオのトップページ、キャンペーンページに向いています。
コード
<!-- 円と文字を包む箱。地の色はこの箱に塗り、色の合成もこの中に閉じる -->
<section class="blend" aria-labelledby="blend-title">
<h2 class="blend__title" id="blend-title">伝えたいことを、<br>まっすぐに。</h2>
<p class="blend__lead">ここに説明文が入ります。</p>
<a class="blend__link" href="/studio/">スタジオについて <span aria-hidden="true">→</span></a>
<!-- 反転させる円と、カーソルの点。飾りなので読み上げから外す -->
<span class="blend__circle" aria-hidden="true"></span>
<span class="blend__dot" aria-hidden="true"></span>
</section>
.blend {
--blend-bg: #111111; /* 地。円の中では明るく抜ける */
--blend-ink: #eeece6; /* 文字。円の中では暗く抜ける */
--blend-color: #d2b999; /* 円と点の色。地との差・文字との差の色に抜ける(白なら明暗だけが入れ替わる) */
--blend-circle: 260px; /* 円の直径 */
--blend-dot: 12px; /* カーソルの点の直径 */
/* 地の色は body ではなく、この箱に塗る(body の背景は合成の下地にならないことがあり、円が色のまま出る) */
background: var(--blend-bg);
color: var(--blend-ink);
/* 合成をこの箱の中に閉じる */
isolation: isolate;
/* 画面に固定した円は overflow では切れないので、箱の外へはみ出す分は clip-path で切る */
clip-path: inset(0);
padding: 144px clamp(24px, 7vw, 96px) 96px;
}
.blend__title {
margin: 0;
font-size: clamp(34px, 10vw, 144px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.02em;
}
.blend__lead { margin: 48px 0 24px; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }
.blend__link {
color: inherit;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-underline-offset: 0.4em;
}
.blend__link:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
.blend__circle,
.blend__dot {
position: fixed;
left: 0;
top: 0;
border-radius: 50%;
/* 円の色との差を取るので、暗い地は円の色に近く、明るい文字は補色寄りの暗い色に抜ける */
background: var(--blend-color);
mix-blend-mode: difference;
/* 下のリンクを押せるようにする */
pointer-events: none;
/* JS がつないだときだけ出す。指で触る端末やスクリプトが動かないときは、本物のカーソルのまま */
display: none;
}
.blend__circle { width: var(--blend-circle); height: var(--blend-circle); }
.blend__dot { width: var(--blend-dot); height: var(--blend-dot); }
.blend.has-blend-cursor .blend__circle,
.blend.has-blend-cursor .blend__dot {
display: block;
}
/* 本物のカーソルは、箱の中で円を出したときだけ隠す(点がカーソルの代わりになる) */
.blend.has-blend-cursor,
.blend.has-blend-cursor a {
cursor: none;
}
import { gsap } from "gsap";
// 指で触る端末にはポインタが無いので、円は出さず本物のカーソルのままにする
const canHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const CHASE = 0.35; // 円がポインタに追いつく時間(秒)
const GROW = reduceMotion ? 0 : 0.3; // 箱に出入りしたとき、円が現れる・消える時間(秒)
document.querySelectorAll(".blend").forEach((section) => {
const circle = section.querySelector(".blend__circle");
const dot = section.querySelector(".blend__dot");
if (!canHover || !circle || !dot) return;
// 中心をポインタに合わせる。箱に入るまでは大きさ 0 で隠しておく
gsap.set([circle, dot], { xPercent: -50, yPercent: -50, scale: 0 });
section.classList.add("has-blend-cursor");
// 動きを減らす設定では遅れなし(quickTo は duration が 0 だと値が動かない)
const follow = (prop) => (reduceMotion ? (value) => gsap.set(circle, { [prop]: value }) : gsap.quickTo(circle, prop, { duration: CHASE, ease: "power3" }));
const moveX = follow("x");
const moveY = follow("y");
const place = (x, y) => {
gsap.set(dot, { x, y }); // 点は遅れなし
moveX(x);
moveY(y);
};
const show = (on) => {
gsap.to([circle, dot], { scale: on ? 1 : 0, duration: GROW, ease: on ? "power3.out" : "power2.in", overwrite: "auto" });
};
section.addEventListener("pointerenter", (event) => {
// 入った所から出す。前に出ていった位置から飛んでこないよう、円も先に置いておく
gsap.set(circle, { x: event.clientX, y: event.clientY });
place(event.clientX, event.clientY);
show(true);
});
section.addEventListener("pointermove", (event) => place(event.clientX, event.clientY));
section.addEventListener("pointerleave", () => show(false));
// キーボードで中のリンクへ移ったときは、円をそのリンクの上に置いて反転させる(マウスで押したときの focus は除く)
section.addEventListener("focusin", (event) => {
if (!event.target.matches(":focus-visible")) return;
const box = event.target.getBoundingClientRect();
place(box.left + box.width / 2, box.top + box.height / 2);
show(true);
});
section.addEventListener("focusout", () => {
if (!section.matches(":hover")) show(false);
});
});