黒魔法【フレア】
2025年2月5日
みなさんこんにちは。
ケミストのWeb担当みやのです。
「Web」記事では、これまでに得たWebに関する知識を記録として残していきたいと思います。
今回は、ファイ〇ルファ〇タジーシリーズでおなじみの黒魔法「フレア」のエフェクトに挑戦してみました。画面のどこかをクリック(タッチ)するとフレアが撃てます!
なんでこんなものを作ろうと思ったかというと、この前のCanvas ConfettiのStarsを見て、フレアの終わり際に似てるな、と思ったからです。暇人乙!
効果音は脳内で再生してください。
F〇Ⅵのフレアをイメージしています(〇FⅥには黒魔法白魔法という分類はありませんが)
① ヒュンヒュンヒュンヒュンヒュンヒュン
② ボボボボン ボボボボン ボボボボン
③ コポポポポポポポ
という感じです(私は何を言っているんだろう)
CSS
収束する円のスタイルを設定します。
body {
margin: 0;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
position: relative;
}
.circle {
position: absolute;
border-radius: 50%;
background: #ff8300;
}
① 収束
今回は、いつぞやのマウスストーカーの時に出てきた「GSAP」を使います。
まずはGSAPを読み込んでおきます。
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.11.5/gsap.min.js"></script>
100個の200pxの円をランダムな位置に生成し、その中からクリックした位置に近い12個を選び、0.2秒差で小さくなりながらクリックした位置に収束していく、という感じにしました。
const totalCircles = 100;
const circleCount = 12;
const delayIncrement = 0.2;
function handleClick(event) {
const scrollY = window.scrollY;
const scrollX = window.scrollX;
const centerX = event.clientX - 100 + scrollX;
const centerY = event.clientY - 100 + scrollY;
const circles = [];
for (let i = 0; i < totalCircles; i++) {
const randomX = Math.random() * (window.innerWidth - 200);
const randomY = Math.random() * (document.body.scrollHeight - 200);
const circle = document.createElement('div');
circle.className = 'circle';
circle.style.width = '200px';
circle.style.height = '200px';
circle.style.left = `${randomX}px`;
circle.style.top = `${randomY}px`;
circle.style.opacity = 0;
document.body.appendChild(circle);
circles.push({ element: circle, x: randomX, y: randomY });
}
circles.sort((a, b) => {
const distanceA = Math.hypot(a.x - centerX, a.y - centerY);
const distanceB = Math.hypot(b.x - centerX, b.y - centerY);
return distanceA - distanceB;
});
const closestCircles = circles.slice(0, circleCount);
closestCircles.forEach((circleObj, index) => {
gsap.to(circleObj.element, {
duration: 0.05,
opacity: 1,
delay: index * delayIncrement,
onComplete: () => {
gsap.to(circleObj.element, {
duration: 1,
scale: 0,
x: centerX - circleObj.x,
y: centerY - circleObj.y,
onComplete: () => {
document.body.removeChild(circleObj.element);
if (index === circleCount - 1) {
triggerExplosion(centerX, centerY, event);
}
}
});
}
});
});
}
document.body.addEventListener('click', handleClick);
② 爆発
収束アニメーション終了後、16回爆発を起こします。
function triggerExplosion(centerX, centerY, event) {
const isDarkTheme = document.body.classList.contains('dark-theme');
const circleColor = isDarkTheme ? 'gray' : '#ff8300';
for (let j = 0; j < 16; j++) {
const angle = Math.random() * 2 * Math.PI;
const radius = 50 + Math.random() * 100;
const randomOffsetX = Math.cos(angle) * radius + 75;
const randomOffsetY = Math.sin(angle) * radius + 75;
const maxDiameter = 100 + Math.random() * 150;
const largeCircle = document.createElement('div');
largeCircle.className = 'circle large-circle';
largeCircle.style.width = `${maxDiameter}px`;
largeCircle.style.height = `${maxDiameter}px`;
largeCircle.style.left = `${centerX + randomOffsetX - maxDiameter / 2}px`;
largeCircle.style.top = `${centerY + randomOffsetY - maxDiameter / 2}px`;
largeCircle.style.borderRadius = '50%';
largeCircle.style.opacity = 0;
largeCircle.style.transform = 'scale(0)';
largeCircle.style.background = `radial-gradient(circle, transparent 40%, ${circleColor} 60%)`;
document.body.appendChild(largeCircle);
gsap.to(largeCircle, {
duration: 0.6,
opacity: 1,
scale: 1,
delay: j * 0.175,
onComplete: () => {
gsap.to(largeCircle, {
duration: 0.6,
opacity: 0,
background: `radial-gradient(circle, transparent 50%, ${circleColor} 70%)`,
ease: "power2.out",
onComplete: () => {
document.body.removeChild(largeCircle);
}
});
}
});
}
setTimeout(() => {
const x = event.clientX / window.innerWidth;
const y = event.clientY / window.innerHeight;
shoot(x, y);
}, 3600);
}
ダークモードだと灰色になるようにしてみました(Ⅴの調合「シャドーフレア」ですね)
③ 放出
最後の放出アニメーションは①の収束アニメーションを逆再生した感じにしたかったのですが、何度やってもうまくいかなかったのでここでCanvas Confetiを使います。
function shoot(x = 0.5, y = 0.5) {
const defaults = {
spread: 360,
ticks: 80,
gravity: 0,
decay: 1,
flat: true,
startVelocity: 40,
colors: ['#ff8300'],
origin: { x: x, y: y },
};
for (let i = 0; i < 12; i++) {
setTimeout(() => {
confetti({
...defaults,
particleCount: 1,
scalar: 4,
shapes: ['circle'],
});
confetti({
...defaults,
particleCount: 1,
scalar: 8,
shapes: ['circle'],
});
}, i * 150);
}
}
flat:trueとすると、紙吹雪が回転しないようになります。
decayは速度の減衰を表し(初期値は0.9)この値を大きくすると、速く消えていくようになります。
今後の課題
・球体に赤~オレンジ~黄~白のグラデーションをかけたい
→Canvas Confettiには、グラデーションを効かせることができませんでした。
・放出する球体を徐々に大きくしたい
→Canvas Confettiのscalarは、途中で値を変えることはできないようです。
・爆発するアニメーションを、もっと爆発している感じにしたい
→もっと滑らかにして、ゲー〇ボ〇イのサ〇2のフレアのような感じを出したいです。
・アニメーションしている間、画面全体に赤い半透明のオーバーレイをかけたい
→赤い半透明のオーバーレイをopacity:0で設置し、アニメーションしている間だけopacity:1→アニメーション終了後opacity:0に戻す、としてみたのですが、オーバーレイをposition:fixedにするとCanvas Confettiの発動位置がクリックした場所とずれるという問題がどうしても解決できなくて、今回は実装を見送りました。


