複数のキャンバス(canvas要素)に対してアニメーション効果を適用し、画面内に表示されているときは画像が歪んだ状態を維持し、表示されていないときは元に戻す処理を行います。
一応ドラクエを意識しました。おかしな部分もあるのですがおおむねうまくいってるので公開します。
ただ処理が重いので複数に適応は厳しい。実用的じゃない(-_-;)
document.addEventListener('DOMContentLoaded', () => { const canvases = document.querySelectorAll('.c-canvas'); canvases.forEach((canvas) => { const img = canvas.querySelector('img'); // 各キャンバス内のimg要素を取得 const ctx = canvas.getContext('2d'); // 各キャンバスのコンテキスト let originalImageData; // 元の画像データ let distortedImageData; // 歪んだ画像データ let distortionFrame = 0; // 歪みのアニメーションフレーム let maxDistortionFrame = 60; // 歪みの最大フレーム数 let distortionStrength = 0; // 歪みの強さ(初期値は0) let maxDistortionStrength = 60; // 歪みの最大強さ let restorationStartTime = 0; // 元に戻る処理の開始時刻 let restorationDuration = 5000; // 元に戻る処理の所要時間 let restorationProgress = 0; // 元に戻る進行状況 let isImageLoaded = false; // 画像がロードされたかどうかのフラグ let wasVisiblePreviously = false; // 以前、範囲内にあったかどうか // 歪みが最大値に達するのに5秒かかるように設定 const maxDistortionSpeed = maxDistortionStrength / 300; // 画像のロードが完了した時の処理 const loadImage = () => { // キャンバスのサイズを親要素に合わせる const canvasContent = canvas.parentElement; canvas.width = canvasContent.clientWidth; // 親要素の幅に合わせる canvas.height = canvasContent.clientHeight; // 親要素の高さに合わせる // キャンバスに画像を描画 ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 元データを取得して保存 originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); distortedImageData = new Uint8ClampedArray(originalImageData.data); // 歪みデータを複製 isImageLoaded = true; // フラグを更新 requestAnimationFrame(() => animate(canvas)); // アニメーション開始 }; // 画像がすでに読み込まれている場合の処理 if (img.complete) { loadImage(); } else { img.onload = loadImage; } // 歪みを加える関数 function distortImage() { if (!isImageLoaded) return; const originalData = originalImageData.data; const distortedData = distortedImageData; // 画像全体に歪みを適用 for (let y = 0; y < canvas.height; y++) { for (let x = 0; x < canvas.width; x++) { // 歪み強さを動的に変化させる const offset = Math.sin((y / 10) + distortionFrame / 10) * distortionStrength; const srcX = x + offset; // offsetを加える // 範囲外の場合でも処理を続ける const correctedSrcX = Math.min(Math.max(srcX, 0), canvas.width - 1); // 範囲を補正 const correctedSrcY = Math.min(Math.max(y, 0), canvas.height - 1); // 範囲を補正 // 歪んだ座標に対する元データの取得 const srcIndex = (Math.floor(correctedSrcX) + Math.floor(correctedSrcY) * canvas.width) * 4; const destIndex = (x + y * canvas.width) * 4; distortedData[destIndex] = originalData[srcIndex]; distortedData[destIndex + 1] = originalData[srcIndex + 1]; distortedData[destIndex + 2] = originalData[srcIndex + 2]; distortedData[destIndex + 3] = originalData[srcIndex + 3]; } } // 歪みを反映 ctx.putImageData(new ImageData(distortedData, canvas.width, canvas.height), 0, 0); } // 元に戻す処理を進行させる関数 function restoreImage() { if (!isImageLoaded) return; const elapsedTime = Date.now() - restorationStartTime; restorationProgress = Math.min(elapsedTime / restorationDuration, 1); // 進行状況を計算 // 歪み強さを進行状況に応じて減少させる distortionStrength = distortionStrength * (1 - restorationProgress); // 歪みが無くなっていく const originalData = originalImageData.data; const distortedData = distortedImageData; for (let y = 0; y < canvas.height; y++) { for (let x = 0; x < canvas.width; x++) { // 歪みを減らす const offset = Math.sin((y / 10) + distortionFrame / 10) * distortionStrength; const srcX = x + offset; // offsetを加える // 範囲外の場合でも処理を続ける const correctedSrcX = Math.min(Math.max(srcX, 0), canvas.width - 1); // 範囲を補正 const correctedSrcY = Math.min(Math.max(y, 0), canvas.height - 1); // 範囲を補正 const srcIndex = (Math.floor(correctedSrcX) + Math.floor(correctedSrcY) * canvas.width) * 4; const destIndex = (x + y * canvas.width) * 4; // 元画像のデータに戻るように補正 distortedData[destIndex] = originalData[srcIndex] + (distortedData[destIndex] - originalData[srcIndex]) * restorationProgress; distortedData[destIndex + 1] = originalData[srcIndex + 1] + (distortedData[destIndex + 1] - originalData[srcIndex + 1]) * restorationProgress; distortedData[destIndex + 2] = originalData[srcIndex + 2] + (distortedData[destIndex + 2] - originalData[srcIndex + 2]) * restorationProgress; distortedData[destIndex + 3] = originalData[srcIndex + 3] + (distortedData[destIndex + 3] - originalData[srcIndex + 3]) * restorationProgress; } } // 歪みを反映 ctx.putImageData(new ImageData(distortedData, canvas.width, canvas.height), 0, 0); } // 歪みと元に戻す処理を連続的に行う関数 function animate() { // 歪み強さを少しずつ増やす if (distortionStrength < maxDistortionStrength) { distortionStrength = Math.min(maxDistortionStrength, distortionStrength + maxDistortionSpeed); } distortionFrame++; if (distortionFrame > maxDistortionFrame) { distortionFrame = 0; } const rect = canvas.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; // キャンバスが画面に表示されていない場合、歪みを加える if (!isVisible) { restorationStartTime = Date.now(); // 元に戻す処理を開始 requestAnimationFrame(() => animate()); distortImage(); // 歪みを元に戻す } // 画面に表示されている間はアニメーションを続ける if (isVisible) { requestAnimationFrame(() => animate()); restoreImage(); } wasVisiblePreviously = isVisible; } }); });
<div class="canvas-content"> <canvas class="c-canvas"> <img src="https://mitumine.stars.ne.jp/kopipesityainayo/wp-content/uploads/2025/01/yuu0024-009.jpg" alt="Image to distort"> </canvas> </div>
.canvas-content { width: 100%; /* 親要素の横幅に合わせる */ height: 0; padding-bottom: 56.25%; /* アスペクト比(16:9)の場合の設定。別の比率を使用する場合は、この値を変更 */ position: relative; } .c-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .c-canvas img { display: block; max-width: 100%; height: auto; object-fit: contain; /* アスペクト比を維持したまま、キャンバス内に画像をフィットさせる */ }