DEMO
CODE
<div class="demo-stage mph-stage">
<svg class="mph-defs" aria-hidden="true" focusable="false"><defs><filter id="mph-threshold"><feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 255 -140"/></filter></defs></svg>
<span class="mph-sr">Design, Motion, Code, Ship</span>
<div class="mph-box" id="mph-box" aria-hidden="true">
<span class="mph-word" id="mph-a"></span>
<span class="mph-word" id="mph-b"></span>
</div>
</div>
.mph-stage { display:flex; align-items:center; justify-content:center; padding:2rem; background:#0B0B10; min-height:280px; }
.mph-defs { position:absolute; width:0; height:0; }
.mph-sr { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
.mph-box { position:relative; width:100%; max-width:420px; height:90px; filter:url(#mph-threshold) blur(.6px); }
.mph-word { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-family:'Oswald',sans-serif; font-size:3.4rem; font-weight:600; letter-spacing:.04em; color:#fff; }
@media (prefers-reduced-motion: reduce) {
.mph-box { filter:none; }
}
(function(){
var a = document.getElementById('mph-a');
var b = document.getElementById('mph-b');
if (!a || !b) return;
var WORDS = ['Design', 'Motion', 'Code', 'Ship'];
var MORPH = 1.0, HOLD = 1.4;
var idx = 0, t = 0, last = performance.now(), cool = HOLD;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function show(f){
b.style.filter = 'blur(' + Math.min(8 / f - 8, 100) + 'px)';
b.style.opacity = Math.pow(f, 0.4) * 100 + '%';
var g = 1 - f;
a.style.filter = 'blur(' + Math.min(8 / g - 8, 100) + 'px)';
a.style.opacity = Math.pow(g, 0.4) * 100 + '%';
}
function set(){ a.textContent = WORDS[idx % WORDS.length]; b.textContent = WORDS[(idx + 1) % WORDS.length]; }
set();
if (reduce) {
b.style.opacity = '0';
setInterval(function(){ idx++; set(); }, 2400);
return;
}
function frame(now){
var dt = (now - last) / 1000; last = now;
if (cool > 0) {
cool -= dt;
a.style.filter = ''; a.style.opacity = '100%'; b.style.filter = ''; b.style.opacity = '0%';
} else {
t += dt;
var f = Math.min(t / MORPH, 1);
show(Math.max(f, 0.0001));
if (f >= 1) { idx++; t = 0; cool = HOLD; set(); }
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
})();
AI PROMPT
表示中の単語がとろけるように崩れ、次の単語へ形を変えるモーフィングテキストを作って。同じ位置に2つの span(現在の単語 a と次の単語 b)を重ね、進行度 f(0→1)に応じて b は blur(8/f - 8 px) と opacity f^0.4、a は同じ式を 1-f で適用する(blur は100pxで頭打ち)。親要素に SVG の feColorMatrix フィルターをかけ、アルファ行を「0 0 0 255 -140」にすると、半透明のにじみが0か1に振り分けられて輪郭がくっつき、液体が変形するような見た目になる。1回の変形は1秒、変形後は1.4秒止めてから次へ進む。requestAnimationFrame で経過時間から進行度を計算すること(フレームレートが違っても速さが変わらない)。読み上げ用に全単語を並べた文を視覚的に隠した span に置き、変形する側の入れ物は aria-hidden="true" にする。prefers-reduced-motion: reduce の時はフィルターを外し、2.4秒ごとに単語を切り替えるだけにする。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
ある単語が崩れながら、別の単語の形へ連続的に変わっていく文字の演出です。Magic UI の同名コンポーネントで知られ、ヒーローのキャッチコピーで「〇〇を、もっと速く」の〇〇部分を入れ替える使い方が定番です。
当サイトの Rotating Words は単語を上下にスライドして入れ替える演出、Scramble Text は文字をランダムに乱してから確定させる演出です。本ページは2つの単語の形そのものが溶け合う別テーマで、切り替わりの瞬間に視線を集めたい場面に向きます。
仕組みの中心は、ぼかしで作ったにじみをSVGフィルターで二値化することです。フィルターは描画の負荷が高めなので、1ページに1か所、短い単語に絞って使うと、スマホでも滑らかに動きます。
feColorMatrix のしきい値は何をしているのですか?
アルファ(不透明度)の行を「0 0 0 255 -140」にすると、各画素の不透明度に255を掛けて140を引く計算になります。ぼかしで半透明になったにじみ部分が、ある濃さ以上は完全に不透明、それ未満は完全に透明へ振り分けられるため、2つの単語のにじみがくっついて液体のようにつながって見えます。
setInterval ではなく requestAnimationFrame を使う理由は?
requestAnimationFrame は画面の描き替えに合わせて呼ばれるため、ぼかしの変化が滑らかになり、タブが裏にある間は止まって電力を使いません。経過時間から進行度を計算しているので、60Hz と120Hz の画面で速さが変わることもありません。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。