
Webサイトで、画像やテキストがマスクからスッと現れる「リビールアニメーション」。
シンプルな動きながら、ページに動きやインパクトを与えられるため、Web制作でもよく使われる表現のひとつです。
しかし、実際に実装しようとすると「どのような仕組みで動いているの?」と疑問に思う方も多いのではないでしょうか。
今回は、ホバーによるリビールアニメーションの基本的な仕組みから、実装方法までを初心者の方にも分かりやすく解説します。
リビールアニメーションとは?
リービルアニメーションは簡単に説明すると隠れていた要素が、何かのきっかけで「現れる」アニメーションです。
何かしらのきっかけとは、スクロールやクリックなどのイベントを発火させる動作を指します。

本記事ではホバーを使用したリービルアニメーションを紹介します。
今回作成するリビールアニメーション
今回は、ホバーしたら花火が現れるリービルアニメーションを作成します。
デモはこちらです。
ホバーリービルアニメーション
作成の手順
最終的なコードはこちらです。
1 2 3 4 5 | <div class="mv-img"> <canvas id="cv"></canvas> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .mv-img { border-radius: 20px; z-index: 900; overflow: hidden; margin: auto; max-width: 1000px; } canvas { display: block; width: 100%; cursor: none; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | const cv = document.getElementById("cv"); const ctx = cv.getContext("2d"); let W = 0, H = 0; let mx = -999, my = -999; let animMx = 0, animMy = 0; const EASE = 0.1; const ASPECT = 620 / 1000; const RADIUS = 180; const FEATHER = 30; const IMAGES = ["../img/img_before.jpg.webp", "../img/img_after.jpg.webp"]; // オフスクリーンcanvas(画像用・マスク用・合成用)を最初に1度だけ作る const offscreens = [ document.createElement("canvas"), document.createElement("canvas"), ]; const maskCanvas = document.createElement("canvas"); const tmpCanvas = document.createElement("canvas"); const rawImgs = []; let loaded = 0; function setSize() { const w = cv.parentElement.clientWidth; const h = Math.round(w * ASPECT); cv.width = W = w; cv.height = H = h; offscreens.forEach((c) => { c.width = W; c.height = H; }); maskCanvas.width = W; maskCanvas.height = H; tmpCanvas.width = W; tmpCanvas.height = H; redrawOffscreens(); } function redrawOffscreens() { rawImgs.forEach((img, i) => { if (!img) return; const c = offscreens[i].getContext("2d"); c.clearRect(0, 0, W, H); const imgAspect = img.naturalWidth / img.naturalHeight; const canvasAspect = W / H; let sw, sh, sx, sy; if (imgAspect > canvasAspect) { sh = img.naturalHeight; sw = sh * canvasAspect; sx = (img.naturalWidth - sw) / 2; sy = 0; } else { sw = img.naturalWidth; sh = sw / canvasAspect; sx = 0; sy = (img.naturalHeight - sh) / 2; } c.drawImage(img, sx, sy, sw, sh, 0, 0, W, H); }); } IMAGES.forEach((url, i) => { const img = new Image(); img.onload = () => { rawImgs[i] = img; loaded++; redrawOffscreens(); if (loaded === 2) requestAnimationFrame(draw); }; img.onerror = () => console.error("画像読み込み失敗:", url); img.src = url; }); function updateMask(px, py) { const mc = maskCanvas.getContext("2d"); mc.clearRect(0, 0, W, H); const outer = RADIUS + FEATHER; const inner = Math.max(0, RADIUS - FEATHER); const grd = mc.createRadialGradient(px, py, inner, px, py, outer); grd.addColorStop(0, "rgba(0,0,0,1)"); grd.addColorStop(1, "rgba(0,0,0,0)"); mc.fillStyle = grd; mc.beginPath(); mc.arc(px, py, outer, 0, Math.PI * 2); mc.fill(); } function draw() { animMx += (mx - animMx) * EASE; animMy += (my - animMy) * EASE; // 1. ベース画像 ctx.clearRect(0, 0, W, H); ctx.drawImage(offscreens[0], 0, 0); // 2. マスクを更新 updateMask(animMx, animMy); // 3. 合成用canvasにオーバーレイ画像を描き、マスクで切り抜く const tx = tmpCanvas.getContext("2d"); tx.clearRect(0, 0, W, H); tx.drawImage(offscreens[1], 0, 0); tx.globalCompositeOperation = "destination-in"; tx.drawImage(maskCanvas, 0, 0); tx.globalCompositeOperation = "source-over"; // 次フレームのためにリセット // 4. 切り抜いた結果をメインcanvasに重ねる ctx.drawImage(tmpCanvas, 0, 0); // 5. カーソル描画 if (mx > 0 && my > 0) { ctx.save(); ctx.beginPath(); ctx.arc(animMx, animMy, RADIUS, 0, Math.PI * 2); ctx.strokeStyle = "rgba(255,255,255,0.8)"; ctx.lineWidth = 4; ctx.stroke(); ctx.restore(); } requestAnimationFrame(draw); } cv.addEventListener("mousemove", (e) => { const rect = cv.getBoundingClientRect(); mx = (e.clientX - rect.left) * (W / rect.width); my = (e.clientY - rect.top) * (H / rect.height); }); cv.addEventListener("mouseleave", () => { mx = -999; my = -999; }); cv.addEventListener( "touchmove", (e) => { e.preventDefault(); const rect = cv.getBoundingClientRect(); mx = (e.touches[0].clientX - rect.left) * (W / rect.width); my = (e.touches[0].clientY - rect.top) * (H / rect.height); }, { passive: false }, ); let resizeTimer = null; new ResizeObserver(() => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => setSize(), 100); // 100ms 止まったら実行 }).observe(cv.parentElement); setSize(); |
HTML
HTMLはシンプルです。
1 2 3 4 5 | <div class="mv-img"> <canvas id="cv"></canvas> </div> |
jQueryで出力できるように、表示用のcanvasを置くだけです。
CSS
CSSもシンプルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | .mv-img { border-radius: 20px; z-index: 900; overflow: hidden; margin: auto; max-width: 1000px; } canvas { display: block; width: 100%; cursor: none; } |
デザインに合わせてサイズの調整等を適宜行ってください。
jQuery
jQueryでは、「1枚目の画像を背景として表示し、マウスカーソルの周辺だけ2枚目の画像を円形に見せる」という仕組みを作成します。
大きく8つの項目に分けると以下です。
Canvasの準備
2枚の画像を用意
オフスクリーンCanvasとマスクを準備する
画像をCanvasに描画する
マウスの位置に円形マスクを作る
画像を合成する
マウス・タッチ操作に対応する
画面サイズが変わったときの対応
Canvasの準備
まずは、アニメーションを描画するためのCanvasを用意します。
1 2 3 4 | const cv = document.getElementById("cv"); const ctx = cv.getContext("2d"); |
HTMLに用意したCanvasを取得し、jQueryから画像や図形を描画できるようにしています。
アニメーションに必要な設定
リービルアニメーションの動きを決める設定を用意します。
1 2 3 4 5 6 7 8 9 | let W = 0, H = 0; let mx = -999, my = -999; let animMx = 0, animMy = 0; const EASE = 0.1; const ASPECT = 620 / 1000; const RADIUS = 180; const FEATHER = 30; |
役割は以下の通りです。
| W | Canvasの幅 |
|---|---|
| H | Canvasの高さ |
| mx | 実際のマウスのX座標 |
| my | 実際のマウスのY座標 |
| animMx | アニメーション用のX座標 |
| animMy | アニメーション用のY座標 |
| EASE | マウスへの追従速度 |
| ASPECT | Canvasの縦横比 |
| RADIUS | リビールする円の半径 |
| FEATHER | 円形マスクのぼかし幅 |
mxとmyで-999を設定しているのは、「まだマウスがCanvas上にいない」という状態を表すためです。
2枚の画像を用意
今回のリビールアニメーションでは、2枚の画像を重ね合わせて使用します。
1 2 3 | const IMAGES = ["../img/img_before.jpg.webp", "../img/img_after.jpg.webp"]; |
役割としては以下の通りです。
img_before.jpg.webp =「最初から表示されている画像」
img_after.jpg.webp =「ホバーした際に表示される画像」
オフスクリーンCanvasとマスクを準備する
画面に表示するCanvasとは別に、いくつかのCanvasを用意しています。
1 2 3 4 5 6 7 8 | const offscreens = [ document.createElement("canvas"), document.createElement("canvas"), ]; const maskCanvas = document.createElement("canvas"); const tmpCanvas = document.createElement("canvas"); |
それぞれの役割は以下の通りです。
| offscreens[0] | 画像「img_before.jpg.webp」を描画 |
|---|---|
| offscreens[1] | 画像「img_after.jpg.webp」を描画 |
| maskCanvas | 画像「img_after.jpg.webp」を見せる場所を作る |
| tmpCanvas | 画像「img_after.jpg.webp」とマスクを合成する |
最終的に、「画像「img_after.jpg.webp」+マスクで切り抜いた画像「img_after.jpg.webp」⇒メインCanvasに表示」という流れです。
画像をCanvasに描画する
画像を読み込んだあと、redrawOffscreens()でオフスクリーンCanvasに画像を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function redrawOffscreens() { rawImgs.forEach((img, i) => { if (!img) return; const c = offscreens[i].getContext("2d"); c.clearRect(0, 0, W, H); const imgAspect = img.naturalWidth / img.naturalHeight; const canvasAspect = W / H; let sw, sh, sx, sy; if (imgAspect > canvasAspect) { sh = img.naturalHeight; sw = sh * canvasAspect; sx = (img.naturalWidth - sw) / 2; sy = 0; } else { sw = img.naturalWidth; sh = sw / canvasAspect; sx = 0; sy = (img.naturalHeight - sh) / 2; } c.drawImage(img, sx, sy, sw, sh, 0, 0, W, H); }); } |
画像の縦横比とCanvasの縦横比を比較し、画像がCanvasからはみ出す部分を切り取っています。
CSSでいうと、『object-fit: cover;』『object-position: center;』に近い処理です。
そのため、画像のサイズがCanvasと違っていても、Canvasいっぱいに画像を表示できます。
マウスの位置に円形マスクを作る
リビールアニメーションの中心となるのが、この部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function updateMask(px, py) { const mc = maskCanvas.getContext("2d"); mc.clearRect(0, 0, W, H); const outer = RADIUS + FEATHER; const inner = Math.max(0, RADIUS - FEATHER); const grd = mc.createRadialGradient(px, py, inner, px, py, outer); grd.addColorStop(0, "rgba(0,0,0,1)"); grd.addColorStop(1, "rgba(0,0,0,0)"); mc.fillStyle = grd; mc.beginPath(); mc.arc(px, py, outer, 0, Math.PI * 2); mc.fill(); } |
マウスの位置を中心に円形のグラデーションを作っています。
中心は不透明、外側に行くほど透明になるので画像「img_after.jpg.webp」が自然に現れるようにできます。
画像を合成する
最後に今までの処理を組み合わせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | function draw() { animMx += (mx - animMx) * EASE; animMy += (my - animMy) * EASE; // 1. ベース画像 ctx.clearRect(0, 0, W, H); ctx.drawImage(offscreens[0], 0, 0); // 2. マスクを更新 updateMask(animMx, animMy); // 3. 合成用canvasにオーバーレイ画像を描き、マスクで切り抜く const tx = tmpCanvas.getContext("2d"); tx.clearRect(0, 0, W, H); tx.drawImage(offscreens[1], 0, 0); tx.globalCompositeOperation = "destination-in"; tx.drawImage(maskCanvas, 0, 0); tx.globalCompositeOperation = "source-over"; // 次フレームのためにリセット // 4. 切り抜いた結果をメインcanvasに重ねる ctx.drawImage(tmpCanvas, 0, 0); // 5. カーソル描画 if (mx > 0 && my > 0) { ctx.save(); ctx.beginPath(); ctx.arc(animMx, animMy, RADIUS, 0, Math.PI * 2); ctx.strokeStyle = "rgba(255,255,255,0.8)"; ctx.lineWidth = 4; ctx.stroke(); ctx.restore(); } requestAnimationFrame(draw); } |
この処理を簡単に説明すると以下です。
① 画像「img_before.jpg.webp」を表示
② マウス位置を取得
③ マウス位置に円形マスクを作る
④ 画像「img_after.jpg.webp」をマスクで切り抜く
⑤ 画像「img_before.jpg.webp」の上に画像「img_after.jpg.webp」を重ねる
⑥ 次のフレームで繰り返す
マウス・タッチ操作に対応する
マウスやスマートフォンのタッチ位置を取得しています。
▼PC
1 2 3 4 5 6 7 | cv.addEventListener("mousemove", (e) => { const rect = cv.getBoundingClientRect(); mx = (e.clientX - rect.left) * (W / rect.width); my = (e.clientY - rect.top) * (H / rect.height); }); |
マウスが動くたびに、Canvas内でのマウス座標を取得します。
▼スマートフォン
1 2 3 4 5 6 7 | cv.addEventListener("mousemove", (e) => { const rect = cv.getBoundingClientRect(); mx = (e.clientX - rect.left) * (W / rect.width); my = (e.clientY - rect.top) * (H / rect.height); }); |
指の位置を取得しています。
画面サイズが変わったときの対応
最後にレスポンシブ対応のための処理です。
画面幅が変わったときにCanvasのサイズも変更し、画像を描き直します。
1 2 3 4 5 6 | new ResizeObserver(() => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => setSize(), 100); // 100ms 止まったら実行 }).observe(cv.parentElement); |
完成
リービルアニメーションの完成です。
ホバーリービルアニメーション
まとめ
今回は、CanvasとJavaScriptを使ったホバーリビールアニメーションの仕組みと実装方法をご紹介しました。
2枚の画像と円形のマスクを組み合わせることで、マウスの動きに合わせて画像を部分的に見せることができます。
一見複雑に見える処理も、「画像を用意する」「マスクを作る」「画像を合成する」という流れで考えると理解しやすくなります。
Canvasを使うことで、CSSだけでは表現しにくい自由度の高いアニメーションも実装できます。
ぜひ今回のコードを参考に、サイズや動き、マスクの形などを変えてオリジナルの表現にも挑戦してみてください。





