デモ
デモを読み込めませんでした。
仕様
- 用途
- カーソル
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
暗い背景の上をグレージュの円がポインタを少し遅れて追いかけ、円が重なった所だけ、背景は明るいグレージュに、明るい文字は暗いスレートカラーに変わります。円そのものは単色で、色を入れ替えているのは mix-blend-mode の difference です。下にある色と円の色との差が表示されるので、暗い所は明るく、明るい所は暗く反転して見えます。
カーソル代わりの小さな点も、同じ合成で重ねてあります。円の外ではグレージュに見える点が、円の中では差をもう一度取ることになり、元の暗い色に戻ります。円の直径は260pxで、quickTo で0.35秒かけてポインタに追いつきます。
背景色は body に塗りたくなりますが、そうすると背景が円との合成に加わらない場合があり、円が反転せずにただのグレージュの円として表示されてしまいます。背景色は円と文字を包むセクションの要素に塗り、その要素に isolation の isolate を指定して、合成をその中に閉じることにしました。円は画面に固定して置いているので、セクションの overflow では切れません。はみ出した円が下のセクションに残らないよう、セクションには clip-path を指定しています。
見出しは反転を見せるための舞台なので、最大144pxまで大きくしました。文字が太く大きいほど、円が文字と背景の両方にまたがる部分が増え、反転がよく読み取れます。和文の字幅はほぼ文字サイズと同じなので、この大きさなら1行8文字(行末の読点を含む)までに収め、行は文の切れ目で分けています。
円の色を替えると、反転の色合いも一緒に変わります。白にすると明暗だけが入れ替わり、淡い色にすると、暗い背景はその色に、明るい文字はその補色に近い暗い色になります。
コード
<!-- 円と文字を包むセクション。背景色はこのセクションに塗り、色の合成もこの中に閉じる -->
<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);
});
});