【JavaScript】パーティクルエフェクトデモ

パーティクル(光の粒)を使ったアニメーションエフェクトのデモと、ソースコードです。
アイデアのネタにしてくださ。
パーティクル爆発エフェクトデモ
マウスクリックでエフェクト
<style>
#containerA {
width: 100%;
height: 480px;
background:#000;
}
#canvasA {
display: block;
width: 100%;
height: 100%;
}
</style>
<div id="containerA">
<canvas id="canvasA"></canvas>
</div>
<script>
// キャンバスとコンテキストの取得
const canvasA = document.getElementById('canvasA');
const containerA = document.getElementById('containerA');
const ctxA = canvasA.getContext('2d');
canvasA.width = containerA.offsetWidth;
canvasA.height = containerA.offsetHeight;
// パーティクルを管理する配列
let particles = [];
// パーティクルクラスの定義(位置、速度、色、減衰など)
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = Math.random() * 3 + 2;
this.angle = Math.random() * Math.PI * 2;
this.speed = Math.random() * 6 + 2;
this.vx = Math.cos(this.angle) * this.speed;
this.vy = Math.sin(this.angle) * this.speed;
this.alpha = 1;
this.decay = Math.random() * 0.03 + 0.015;
this.hue = Math.floor(Math.random() * 360);
}
update() {
this.x += this.vx;
this.y += this.vy;
// 重力効果をシンプルに追加
this.vy += 0.05;
this.alpha -= this.decay;
}
draw() {
ctxA.save();
ctxA.globalAlpha = this.alpha;
ctxA.beginPath();
ctxA.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctxA.fillStyle = `hsl(${this.hue}, 100%, 50%)`;
ctxA.fill();
ctxA.restore();
}
}
// 爆発エフェクトを作成する関数
function createExplosion(x, y) {
canvasA.width = canvasA.offsetWidth;
canvasA.height = canvasA.offsetHeight;
const particleCount = 80;
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle(x, y));
}
}
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
// 半透明の黒で画面を塗り重ね、トレール効果を実現
ctxA.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctxA.fillRect(0, 0, canvasA.width, canvasA.height);
// 全パーティクルの更新&描画
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.update();
p.draw();
// パーティクルが見えなくなったら配列から削除
if (p.alpha <= 0) {
particles.splice(i, 1);
}
}
}
animate();
// クリックした位置にエフェクトを発生
canvasA.addEventListener('click', (e) => {
const x = e.clientX - canvasA.getBoundingClientRect().left;
const y = e.clientY - canvasA.getBoundingClientRect().top;
createExplosion(x, y);
});
// 一定間隔ごとにランダムな位置でエフェクト発生
setInterval(() => {
const x = Math.random() * canvasA.width;
const y = Math.random() * canvasA.height;
createExplosion(x, y);
}, 1000);
// ウィンドウサイズの変更に対応
window.addEventListener('resize', () => {
canvasA.width = containerA.offsetWidth;
canvasA.height = containerA.offsetHeight;
});
</script>マウストレイルエフェクトデモ
マウスカーソル移動でエフェクト
<style>
#containerB {
width: 100%;
height: 480px;
overflow: hidden;
background: #111;
}
#canvasB { display: block; }
</style>
<div id="containerB">
<canvas id="canvasB"></canvas>
</div>
<script>
const canvasB = document.getElementById('canvasB');
const containerB = document.getElementById('containerB');
const ctxB = canvasB.getContext('2d');
canvasB.width = containerB.offsetWidth;
canvasB.height = containerB.offsetHeight;
let trailParticles = [];
class TrailParticle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 5 + 2;
this.alpha = 1.0;
this.decay = 0.02;
this.hue = Math.floor(Math.random() * 360);
}
update() {
this.alpha -= this.decay;
}
draw() {
ctxB.save();
ctxB.globalAlpha = this.alpha;
ctxB.fillStyle = `hsl(${this.hue}, 100%, 50%)`;
ctxB.beginPath();
ctxB.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctxB.fill();
ctxB.restore();
}
}
canvasB.addEventListener("mousemove", e => {
const x = e.clientX - canvasB.getBoundingClientRect().left;
const y = e.clientY - canvasB.getBoundingClientRect().top;
trailParticles.push(new TrailParticle(x, y));
});
function animateB() {
ctxB.fillStyle = "rgba(17, 17, 17, 0.3)";
ctxB.fillRect(0, 0, canvasB.width, canvasB.height);
for (let i = trailParticles.length - 1; i >= 0; i--) {
const p = trailParticles[i];
p.update();
p.draw();
if (p.alpha <= 0) {
trailParticles.splice(i, 1);
}
}
requestAnimationFrame(animateB);
}
animateB();
window.addEventListener("resize", () => {
canvasB.width = containerB.offsetWidth;
canvasB.height = containerB.offsetHeight;
});
</script>
星空フライトエフェクトデモ
<style>
#containerC {
margin: 0;
overflow: hidden;
background: black;
width: 100%;
height: 480px;
}
#canvasC { display: block; }
</style>
<div id="containerC"></div>
<!-- Three.js がキャンバス要素を自動生成します -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.141.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.141.0/examples/jsm/"
}
}
</script>
<script type="module">
const containerC = document.getElementById('containerC');
// Three.js のモジュールをインポート
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// シーン、カメラ、レンダラー、星空用オブジェクトの宣言
let scene, camera, renderer, starField;
const starCount = 2000; // 星の密度はここで調整可能
// 初期化関数
function init() {
// シーンを作成
scene = new THREE.Scene();
// 透視投影カメラを作成 (視野角, アスペクト比, 近クリップ, 遠クリップ)
camera = new THREE.PerspectiveCamera(75, containerC.offsetWidth / containerC.offsetHeight, 0.1, 1000);
camera.position.z = 5;
// WebGLレンダラーを作成 (アンチエイリアス有効)
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(containerC.offsetWidth, containerC.offsetHeight);
containerC.appendChild(renderer.domElement);
// 星のジオメトリを作成
const geometry = new THREE.BufferGeometry();
const positions = [];
// 3D空間内に星を分散配置(範囲は必要に応じて調整)
const range = 100;
for (let i = 0; i < starCount; i++) {
const x = THREE.MathUtils.randFloatSpread(range);
const y = THREE.MathUtils.randFloatSpread(range);
// カメラ手前に見えるよう、z 値は負の値に設定
const z = -Math.random() * range;
positions.push(x, y, z);
}
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
// 円形の星を作成するために、テクスチャを読み込む
const textureLoader = new THREE.TextureLoader();
// ※以下の URL は Three.js の例で用いられている disc.png を参照しています
const circleTexture = textureLoader.load('https://threejs.org/examples/textures/sprites/disc.png');
// 星用のポイントマテリアルを作成(テクスチャを適用して丸型に)
const material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
map: circleTexture, // テクスチャとして円形の画像を設定
sizeAttenuation: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending,
depthWrite: false // 奥行きの書き込みを無効に(必要に応じて調整)
});
// ジオメトリとマテリアルを組み合わせて星空オブジェクトを生成
starField = new THREE.Points(geometry, material);
scene.add(starField);
// ウィンドウサイズ変更時のイベントリスナーを登録
window.addEventListener('resize', onWindowResize, false);
}
// ウィンドウサイズ変更時の処理
function onWindowResize() {
camera.aspect = containerC.offsetWidth / containerC.offsetHeight;
camera.updateProjectionMatrix();
renderer.setSize(containerC.offsetWidth, containerC.offsetHeight);
}
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
// 星の位置を更新して動きをシミュレート
// BufferAttributeから位置配列を取得
const positions = starField.geometry.attributes.position.array;
for (let i = 2; i < positions.length; i += 3) {
// 星をカメラに向かってゆっくり移動させる(速度はここで調整)
positions[i] += 0.2;
// 星がカメラを通過したら位置をリセット(z >= 0 になった場合)
if (positions[i] > 0) {
positions[i] = -100; // 遠方にリセット
}
}
// 位置情報が更新されたことをThree.jsに通知
starField.geometry.attributes.position.needsUpdate = true;
// シーンをレンダリング
renderer.render(scene, camera);
}
// 初期化とアニメーション開始
init();
animate();
</script>パーティクルアニメーションデモ
<style>
#containerD {
margin: 0;
overflow: hidden;
background: #000;
width: 100%;
height: 480px;
}
#canvasD { display: block; }
</style>
<div id="containerD">
<canvas id="canvasD"></canvas>
</div>
<script>
const canvasD = document.getElementById('canvasD');
const containerD = document.getElementById('containerD');
const ctxD = canvasD.getContext('2d');
canvasD.width = containerD.offsetWidth;
canvasD.height = containerD.offsetHeight;
const particlesD = [];
const particleCountD = 100;
class ParticleD {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 5 + 1;
this.speedX = Math.random() * 3 - 1.5;
this.speedY = Math.random() * 3 - 1.5;
this.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
if (this.size > 0.2) this.size -= 0.1;
}
draw() {
ctxD.fillStyle = this.color;
ctxD.beginPath();
ctxD.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctxD.closePath();
ctxD.fill();
}
}
function createParticlesD() {
for (let i = 0; i < particleCountD; i++) {
particlesD.push(new ParticleD(Math.random() * canvasD.width, Math.random() * canvasD.height));
}
}
function animateD() {
ctxD.clearRect(0, 0, canvasD.width, canvasD.height);
particlesD.forEach((particle, index) => {
particle.update();
particle.draw();
if (particle.size <= 0.2) {
particlesD.splice(index, 1);
particlesD.push(new ParticleD(Math.random() * canvasD.width, Math.random() * canvasD.height));
}
});
requestAnimationFrame(animateD);
}
createParticlesD();
animateD();
window.addEventListener('resize', () => {
canvasD.width = containerD.offsetWidth;
canvasD.height = containerD.offsetHeight;
});
</script>弾ける文字アニメーションデモ
<style>
#containerE {
margin: 0;
overflow: hidden;
width: 100%;
height: 480px;
background: #000;
}
#canvasE {
display: block;
width: 100%;
height: 100%;
}
#textE {
width: 200px;
}
</style>
<input type="text" id="textE" value="弾ける文字">
<button id="buttonE" type="button">実行</button>
<div id="containerE">
<canvas id="canvasE"></canvas>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const textE = document.getElementById('textE');
let str = textE.value;
const canvasE = document.getElementById('canvasE');
const containerE = document.getElementById('containerE');
const ctxE = canvasE.getContext('2d');
// キャンバスのサイズを設定
function setCanvasSize() {
canvasE.width = containerE.offsetWidth;
canvasE.height = containerE.offsetHeight;
}
setCanvasSize();
window.addEventListener('resize', () => {
setCanvasSize();
// リサイズ時は再描画
drawLetterA();
});
// 文字をキャンバス中央に描画する関数
function drawLetterA() {
ctxE.clearRect(0, 0, canvasE.width, canvasE.height);
ctxE.fillStyle = 'white';
const fontSize = Math.min(canvasE.width, canvasE.height) * 0.3;
ctxE.font = `${fontSize}px sans-serif`;
ctxE.textAlign = 'center';
ctxE.textBaseline = 'middle';
ctxE.fillText(str, canvasE.width / 2, canvasE.height / 2);
}
// パーティクルアニメーション開始関数
function startExplosion() {
// offscreen canvasに描画してパーティクルの元データを取得
const offCanvas = document.createElement('canvas');
offCanvas.width = canvasE.width;
offCanvas.height = canvasE.height;
const offCtx = offCanvas.getContext('2d');
offCtx.fillStyle = 'white';
const fontSize = Math.min(canvasE.width, canvasE.height) * 0.3;
offCtx.font = `${fontSize}px sans-serif`;
offCtx.textAlign = 'center';
offCtx.textBaseline = 'middle';
offCtx.fillText(str, canvasE.width / 2, canvasE.height / 2);
// 画像データを取得
const imageData = offCtx.getImageData(0, 0, canvasE.width, canvasE.height);
const data = imageData.data;
const particles = [];
const gap = 6; // サンプリング間隔
// パーティクル生成(アルファ値がしっかりしているピクセルのみ)
for (let y = 0; y < canvasE.height; y += gap) {
for (let x = 0; x < canvasE.width; x += gap) {
const index = (y * canvasE.width + x) * 4;
if (data[index + 3] > 128) {
const dx = x - canvasE.width / 2;
const dy = y - canvasE.height / 2;
const distance = Math.sqrt(dx * dx + dy * dy) || 1;
const angle = Math.atan2(dy, dx) + (Math.random() - 0.5) * 0.5;
const speed = (Math.random() * 2) + 2;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
alpha: 1
});
}
}
}
// パーティクルのアニメーションループ
function animateE() {
ctxE.clearRect(0, 0, canvasE.width, canvasE.height);
// パーティクルの更新
for (let i = 0; i < particles.length; i++) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.98;
p.vy *= 0.98;
p.alpha -= 0.01;
}
// 透明になったパーティクルを削除
for (let i = particles.length - 1; i >= 0; i--) {
if (particles[i].alpha <= 0) {
particles.splice(i, 1);
}
}
// パーティクルを描画
for (let i = 0; i < particles.length; i++) {
let p = particles[i];
ctxE.fillStyle = `rgba(255,255,255,${p.alpha})`;
ctxE.fillRect(p.x, p.y, gap, gap);
}
if (particles.length > 0) {
requestAnimationFrame(animateE);
}
}
animateE();
}
// 初期描画
drawLetterA();
// 一定時間後に爆発開始
setTimeout(startExplosion, 1000);
// ボタン押下で再実行
var buttonE = document.getElementById("buttonE");
buttonE.addEventListener("click", function() {
str = textE.value;
drawLetterA();
setTimeout(startExplosion, 1000);
});
});
</script>

