パーティクルを作成する(HTML/JS)

前書き

パーティクルを作成する方法を掲載しています。

目次

  1. 下から飛び出す
  2. キラキラさせる
  3. 雪を降らせる
  4. 色々な形に整列させる
  5. 参考

下から飛び出す

Web Animations APIを使って動かしてみました。

概要

  • 横方向は一定の速度で動かす
  • 縦方向はイージングを調整して、放物線を描くような動きに見せる
  • 再生時間を縦方向の位置に合わせて調整する
  • 回転させる

見本


<div class="wrap js-wrap-popout">
  <div class="item js-item-popout">
    <div class="child">
      <div class="gchild"></div>
    </div>
  </div>
</div>

.wrap{
  position: relative; background: #f5edf3; padding: 50% 0 0; overflow: hidden;
}
.item{
  position: absolute; top: 0; left: 0;
}
.child{

}
.gchild{
  padding: 100% 0 0; border-radius: 50%;
}

const num = 50;//パーティクルの数
const wrap = document.querySelector(".js-wrap-popout");
const wrapHeight = wrap.offsetHeight;

//乱数を返す関数
const getRandom = ( min, max ) => { //min以上max未満
 return Math.floor( Math.random() * ( max - min ) + min );
}

//要素を複製
const target = document.querySelector(".js-item-popout");
for (let i = 0; i < num; i++){
  const targetClone = target.cloneNode(true);
  wrap.appendChild( targetClone );
}

//要素毎にアニメーションを設定する------------------
const arrFunc = [];
const colors = ['#e94d15', '#f18d1d', '#f8b633', '#a74535'];
const baseColor = 'transparent';

document.querySelectorAll(".js-item-popout").forEach((item , i) => {
  //要素
  const itemWidth = getRandom( 4, 8 ); //横幅
  item.style.width = itemWidth + '%';
  const itemHeight = item.offsetHeight; //高さ
  //子要素
  const child = item.firstElementChild;
  //孫要素
  const gChild = child.firstElementChild;
  gChild.style.backgroundImage = 'conic-gradient(from 112.5deg,' + colors[ i % 4 ] + ' 0%, ' + colors[ i % 4 ] + ' 85%, ' + baseColor + ' 85%, ' + baseColor + ' 100%)';

  //横
  const xRatio = 100 / itemWidth; //表示場所の幅 ÷ 要素の幅(パーセント)
  const xBegin = ( xRatio - 1 ) * 50; //中央
  const xRandom = getRandom( 0, xRatio - 1 ); //表示場所に収まる範囲で乱数を生成
  const xEnd = xRandom * 100;
  //縦
  const yRatio = wrapHeight / itemHeight; //表示場所の高さ ÷ 要素の高さ
  const yBottom = yRatio * 100; //一番下
  const yRandom = getRandom( 1, yRatio - 1 ); //表示場所に収まる範囲で乱数を生成
  const yTop = yRandom * 100; //値が大きいほど、低い位置
  //回転
  const rBegin = 0;
  const rEnd =  - ( xBegin - xEnd ) * 1; //横の移動方向と回転方向を合わせる
  //時間
  const durationTime = ( 1 - ( yTop / yBottom ) ** 2) * 3000; //縦の位置と合わせる。yTopが大きい(位置が低い)ほど、短い時間
  const delayTime =  - getRandom( 0, 3000 );

  //アニメーション(横)
  const func01 = item.animate(
    [
      { transform: 'translateX(' + xBegin + '%)'},
      { transform: 'translateX(' + xEnd + '%)'},
    ], {
      duration: durationTime, //ミリ秒
      iterations: Infinity, //繰り返し回数
      // iterationStart: getRandom( 0, 10 ) / 10 // ← safari 未対応
      delay: delayTime ,//iterationStartの代用
    }
  );

  //アニメーション(縦)
  const func02 = child.animate(
    [
      { transform: 'translateY(' + yBottom + '%)', easing: 'cubic-bezier(0.33, 1, 0.68, 1)' },
      { transform: 'translateY(' + yTop + '%)', easing: 'cubic-bezier(0.32, 0, 0.67, 0)' },
      { transform: 'translateY(' + yBottom + '%)'},
    ], {
      duration: durationTime, //ミリ秒
      iterations: Infinity, //繰り返し回数
      delay: delayTime,
    }
  );

  //アニメーション(回転)
  const func03 = gChild.animate(
    [
      { transform: 'rotate(' + rBegin + 'deg)'},
      { transform: 'rotate(' + rEnd + 'deg)'},
    ], {
      duration: durationTime, //ミリ秒
      iterations: Infinity, //繰り返し回数
      delay: delayTime ,
    }
  );

  arrFunc.push( func01, func02, func03);

});

//見えている時だけ動かす------------------
const switching = (e) => {
  if( e[0].isIntersecting ){ //見えてる時
    arrFunc.forEach((func) => {
      func.play();
    });
  }else{ //見えてない時
    arrFunc.forEach((func) => {
      func.pause();
    });
  }
}

//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
  let observer;
  const options = { root: null, rootMargin: "0%", threshold: 0 };
  observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
  observer.observe(wrap); //要素の監視を開始
}
createObserver();

キラキラさせる

Web Animations APIを使って動かしてみました。

概要

  • 横方向は無作為に配置する
  • 縦方向に移動させるアニメーションの開始点を無作為に設定する
  • 透明度を変化させる
  • 色相を変化させる

見本


<div class="wrap js-wrap-kirakira">
  <div class="item js-item-kirakira">
    <svg viewBox="0 0 82.17 82.18">
      <path d="M72.12,10.06c-29.72,26.95-32,27-61.67,0,26.92,29.75,27,31.94,0,61.66,29.72-26.95,32-26.95,61.66,0C45.14,42,45.17,39.77,72.12,10.06Z"/>
      <path d="M1.18,77.71c-1.45,1.46-1.47,2.9-.54,3.83S3,82.47,4.46,81,6.4,75.78,6.4,75.78,2.64,76.26,1.18,77.71Z"/>
      <path d="M81,4.46C82.47,3,82.44,1.54,81.53.63s-2.34-.92-3.82.55-1.94,5.21-1.94,5.21S79.54,5.92,81,4.46Z"/>
      <path d="M81,77.71c-1.46-1.45-5.22-1.93-5.22-1.93s.48,3.76,1.94,5.21,2.89,1.48,3.82.55S82.44,79.17,81,77.71Z"/>
      <path d="M.63.64c-.9.9-.93,2.35.55,3.82S6.39,6.4,6.39,6.4,5.91,2.64,4.46,1.18,1.54-.27.63.64Z"/>
    </svg>
  </div>
</div>

.wrap{
  position: relative; background: #000; padding: 50% 0 0; overflow: hidden;
}
.item{
  position: absolute; top: 0;
}

const num = 50;//パーティクルの数
const wrap = document.querySelector(".js-wrap-kirakira");
const wrapHeight = wrap.offsetHeight;

//乱数を返す関数
const getRandom = ( min, max ) => { //min以上max未満
 return Math.floor( Math.random() * ( max - min ) + min );
}

//要素を複製
const target = document.querySelector(".js-item-kirakira");
for (let i = 0; i < num; i++){
  const targetClone = target.cloneNode(true);
  wrap.appendChild( targetClone );
}

//要素毎にアニメーションを設定する------------------
const arrFunc = [];

document.querySelectorAll(".js-item-kirakira").forEach((item) => {

  item.style.left = getRandom( 0, 94 ) + "%"; //左端からの距離
  item.style.width = getRandom( 4, 8 ) + '%'; //横幅

  //アニメーション(移動)
  const func01 = item.animate(
    [
      { transform: 'translateY(' + wrapHeight / item.offsetHeight * 100 + '%)'},
      { transform: 'translateY(-100%)'},
    ], {
      duration: getRandom( 20000, 40000 ), //ミリ秒
      iterations: Infinity, //繰り返し回数
      // iterationStart: getRandom( 0, 10 ) / 10 // ← safari 未対応
      delay: - getRandom( 20000, 40000 ) //iterationStartの代用
    }
  );

  //アニメーション(透明度)
  const func02 = item.animate(
    [
      { opacity: 0 },
      { opacity: 1 },
      { opacity: 0 },
    ], {
      duration: getRandom( 5000, 7500 ), //ミリ秒
      iterations: Infinity, //繰り返し回数
      delay: - getRandom( 5000, 7500 ),
    }
  );

  //アニメーション(色)
  const svg = item.firstElementChild;
  const h1 = getRandom( 0, 180 ); //色相1
  const h2 = getRandom( 180, 360 ); //色相2
  const s = 100; //彩度
  const l = getRandom( 60, 80 ); //輝度
  const hsl1 = "hsl(" + h1 + "deg," + s + "%," + l + "%)";
  const hsl2 = "hsl(" + h2 + "deg," + s + "%," + l + "%)";
  const shadow1 = "drop-shadow(0px 0px 5px " + hsl1 + ")";
  const shadow2 = "drop-shadow(0px 0px 5px " + hsl2 + ")";

  const func03 = svg.animate(
    [
      { fill: hsl1, filter : shadow1 },
      { fill: hsl1, filter : shadow1, offset: 0.45 },
      { fill: hsl2, filter : shadow2, offset: 0.50 },
      { fill: hsl2, filter : shadow2, offset: 0.95 },
      { fill: hsl1, filter : shadow1 },
    ], {
      duration: 10000, //ミリ秒
      iterations: Infinity, //繰り返し回数
    }
  );

  arrFunc.push( func01, func02, func03);

});

//見えている時だけ動かす------------------
const switching = (e) => {
  if( e[0].isIntersecting ){ //見えてる時
    arrFunc.forEach((func) => {
      func.play();
    });
  }else{ //見えてない時
    arrFunc.forEach((func) => {
      func.pause();
    });
  }
}

//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
  let observer;
  const options = { root: null, rootMargin: "0%", threshold: 0 };
  observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
  observer.observe(wrap); //要素の監視を開始
}
createObserver();

雪を降らせる

three.jsを使って作成しています。

概要

  • 雪の画像を貼り付けて、ランダムに配置
  • x,y,z方向に移動させる

見本


<div class="wrap js-wrap-snow">
  <canvas class="canvas js-canvas-snow"></canvas>
</div>

.wrap{
  position: relative; padding: 50% 0 0; overflow: hidden;
}
.canvas{
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}

import * as THREE from './three_r127/build/three.module.js';
import { OrbitControls } from './three_r127/examples/jsm/controls/OrbitControls.js';

// サイズ------------------
const wrap = document.querySelector(".js-wrap-snow");
let wrapWidth = wrap.clientWidth;
let wrapHeight = wrap.clientHeight;

// レンダラー------------------
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector(".js-canvas-snow"),
  antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(wrapWidth, wrapHeight);

// シーン------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0x000000 ); //背景色

// カメラ------------------
const camera = new THREE.PerspectiveCamera( 60, wrapWidth / wrapHeight );
camera.position.set( 0, 0, 100 );

// カメラコントローラーを作成
const controls = new OrbitControls( camera, renderer.domElement );

// 物体 ------------------
// テクスチャ
const texture = new THREE.TextureLoader().load( './snow.svg' );
const material = new THREE.SpriteMaterial({
  map: texture,
  color: 0xffffff,
});
// グループ
const group = new THREE.Group();
scene.add( group );
// パーティクル
const num = 1000; // パーティクルの数
const range = 1000; // 配置する範囲
const rangeHalf = range / 2;

for ( let i = 0; i < num; i ++ ) {

  const sprite = new THREE.Sprite( material ); //常に正面を向く3Dオブジェクト
  sprite.position.x = range * (Math.random() - 0.5);
  sprite.position.y = range * (Math.random() - 0.5);
  sprite.position.z = range * (Math.random() - 0.5);
  sprite.scale.x = sprite.scale.y = sprite.scale.z = Math.random() * 10 + 5;
  sprite.matrixAutoUpdate = false;
  sprite.updateMatrix();
  group.add( sprite );

}

//位置を変える------------------
const windX = 0.25; //x方向の速度(共通)
const variationX = 0.25; //x方向の速度にバラつきを加える
const gravityY = 0.5 //y方向の速度(共通)
const variationY = 0.25;//y方向の速度にバラつきを加える
const windZ = 0; //z方向の速度(共通)
const variationZ = 0.5;//z方向の速度にバラつきを加える

const positionUpdate = () =>{

  for ( let i = 0; i < num; i ++ ) {

    const obj = group.children[ i ];
    //x
    if( obj.position.x < - rangeHalf){
      obj.position.x = rangeHalf;
    }else if ( obj.position.x > rangeHalf){
      obj.position.x = - rangeHalf;
    }else{
      obj.position.x +=  windX + variationX * ( i / num - 0.5  ) ;
    }
    //y
    if( obj.position.y < - rangeHalf){
      obj.position.y = rangeHalf;
    }else{
      obj.position.y -= gravityY + variationY * i / num ;
    }
    //z
    if( obj.position.z < - rangeHalf){
      obj.position.z = rangeHalf;
    }else if ( obj.position.z > rangeHalf){
      obj.position.z = - rangeHalf;
    }else{
      obj.position.z +=  windZ + variationZ * ( i / num - 0.5  ) ;
    }
    obj.matrixAutoUpdate = false;
    obj.updateMatrix();
  }

}

//リサイズ------------------
const wrapResize = () =>{
  wrapWidth = wrap.clientWidth;
  wrapHeight = wrap.clientHeight;
  renderer.setSize(wrapWidth, wrapHeight);
  // camera.aspect = wrapWidth / wrapHeight;
  // camera.updateProjectionMatrix();
}

//一定時間毎に処理------------------
let tick;
const switching = (e) => {
  if( e[0].isIntersecting ){ //見えてる時
    tick = () => {
      wrapResize(); //リサイズ
      positionUpdate(); //位置を変える
      renderer.render(scene, camera); //レンダリング
      requestAnimationFrame( tick ); //繰り返し
    }
    requestAnimationFrame( tick );
  }else{ //見えてない時
    tick = () => {
      cancelAnimationFrame( tick )
    }
  }
}

//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
  let observer;
  const options = { root: null, rootMargin: "0%", threshold: 0 };
  observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
  observer.observe(wrap); //要素の監視を開始
}
createObserver();

色々な形に整列させる

three.jsを使って作成しています。

概要

  • 形の座標を準備
  • 適用する座標を時間毎に切り替える

見本


<div class="wrap js-wrap-seiretu">
  <canvas class="canvas js-canvas-seiretu"></canvas>
</div>

.wrap{
  position: relative; padding: 50% 0 0; overflow: hidden;
}
.canvas{
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}

import * as THREE from './three_r127/build/three.module.js';
import { OrbitControls } from './three_r127/examples/jsm/controls/OrbitControls.js';
import { gsap } from './gsap_3.6.1/esm/gsap-core.js';

const wrap = document.querySelector(".js-wrap-seiretu");
let wrapWidth = wrap.clientWidth;
let wrapHeight = wrap.clientHeight;

// レンダラー------------------
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector(".js-canvas-seiretu"),
  antialias: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(wrapWidth, wrapHeight);

// シーン------------------
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xf5edf3 ); //背景色

// カメラ------------------
const camera = new THREE.PerspectiveCamera( 45, wrapWidth / wrapHeight );
camera.position.set( 250, 250, 250 );

// カメラコントローラーを作成
const controls = new OrbitControls( camera, renderer.domElement );
controls.autoRotate = true; // 自動回転をONにする
controls.autoRotateSpeed = 0.25; // 自動回転の速度
controls.enableDamping = true; // 視点操作のイージングをONにする
controls.dampingFactor = 0.2; // 視点操作のイージングの値
controls.rotateSpeed = 0.5; // 視点変更の速さ

// 光源------------------
// 環境光源
const aLight = new THREE.AmbientLight(0xffffff, 0.75);
scene.add(aLight);
// 平行光源
const dLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(dLight);

// 物体 ------------------
// グループ
const group = new THREE.Group();
scene.add( group );

const geometryRadius = 10
const geometry = new THREE.IcosahedronGeometry( geometryRadius, 15 );
const baseNum = 7;
const num = baseNum**3; // パーティクルの数
const baseRange = 150;


// 位置、色 ------------------
const colors = [];
const positions = {  rd: [], sp: [], cb: [], hx: [] };

//色
for ( let i = 0; i <= num; i ++ ) {
  const ipn = ( num - i ) / num;
  colors.push(
    Math.round( ipn * 90 + 305 ) / 360, // 色相
    Math.round( ipn * 20 + 40 ) / 100, // 彩度
    Math.round( ipn * 20 + 40 ) / 100, // 輝度
  );
}

//ラムダムな位置
const rdRange = baseRange * 4 ;
for ( let i = 0; i < num; i ++ ) {
  positions.rd.push(
    rdRange * (Math.random() - 0.5),
    rdRange * (Math.random() - 0.5),
    rdRange * (Math.random() - 0.5)
  );
}

//球の位置
//各層にパーティクルをいくつずつ分配するか
const spLayers = 3; // 何層にするか
const spNums = [];
const spNumsCalc = () =>{
  //配分の調整
  const spRate = 2;
  //分母
  let spTotal = 0;
  for ( let i = 1; i <= spLayers; i ++ ) {
    spTotal += i**spRate; //べき乗
  }
  //各層に配置するパーティクルの個数
  for ( let i = 1; i <= spLayers; i ++ ) {
    const spNum = Math.round ( num * i**spRate / spTotal ); //個数 * 分子/分母
    spNums.push( spNum );
  }
}
spNumsCalc();

//各層毎に処理
for ( let i = 1; i <= spLayers; i ++ ) {

  const eachNum = spNums[ i - 1 ]; //パーティクルの個数
  const eachRange = baseRange / spLayers * i; //中心からの距離

  for ( let j = 0; j < eachNum; j ++ ) {
    const phi = Math.acos( j / eachNum * 2 - 1 ); // アークコサイン(-1~1)→ ラジアン(0~180 * Math.PI/180)
    const theta = Math.sqrt( eachNum * Math.PI ) * phi;
    positions.sp.push(
    	eachRange * Math.cos( theta ) * Math.sin( phi ),
    	eachRange * Math.sin( theta ) * Math.sin( phi ),
    	eachRange * Math.cos( phi )
    );
  }
}

//立方体の位置
const cbRange = baseRange * 0.25;
for ( let x = 0; x < baseNum; x ++ ) {
  for ( let y = 0; y < baseNum; y ++ ) {
    for ( let z = 0; z < baseNum; z ++ ) {
      positions.cb.push(
        cbRange * ( x - baseNum / 2 ),
        cbRange * ( y - baseNum / 2 ),
        cbRange * ( z - baseNum / 2 )
      );
    }
  }
}

//螺旋の位置
const hxNum = 45; // 一周あたりの個数
const hxRange = baseRange * 1; // 螺旋の半径
const hxRize = geometryRadius / hxNum * 2.5; // y方向にずらす距離
for ( let i = 0; i < num; i ++ ) {
  const phi = 360 / hxNum * i * Math.PI / 180;
  positions.hx.push(
    hxRange * Math.sin( phi ),
    hxRize * ( i - num / 2 ),
    hxRange * Math.cos( phi )
  );
}

// パーティクル
let position = positions.rd;
for ( let i = 0; i < num; i ++ ) {

  const sphere = new THREE.Mesh( geometry, new THREE.MeshToonMaterial() );
  sphere.position.set(
    position[ 3 * i ],
    position[ 3 * i + 1 ],
    position[ 3 * i + 2 ]
  );
  sphere.material.color.setHSL(
    colors[ 3 * i ],
    colors[ 3 * i + 1 ],
    colors[ 3 * i + 2 ]
  );

  group.add( sphere );
}

//位置を変える------------------
let count = 0;
const positionUpdate = () =>{

  let update = false;

  if(count == 200 ){
    position = positions.sp; //球
    update = true;
  } else if( count == 400 ){
    position = positions.hx; //螺旋
    update = true;
  } else if( count == 600 ){
    position = positions.cb; // 立方体
    update = true;
  } else if( count == 800 ){
    position = positions.rd; // ランダム
    update = true;
    count = 0;
  }

  if( update ){
    for ( let i = 0; i < num; i ++ ) {
      gsap.to( group.children[ i ].position, {
        x:position[ 3 * i ],
        y:position[ 3 * i + 1 ],
        z:position[ 3 * i + 2 ],
        duration: 1
      });
    }
    update = false;
  }
  count++;
}

//リサイズ------------------
const wrapResize = () =>{
  wrapWidth = wrap.clientWidth;
  wrapHeight = wrap.clientHeight;
  renderer.setSize(wrapWidth, wrapHeight);
  // camera.aspect = wrapWidth / wrapHeight;
  // camera.updateProjectionMatrix();
}

//一定時間毎に処理------------------
let tick;
const switching = (e) => {
  if( e[0].isIntersecting ){ //見えてる時
    tick = () => {
      wrapResize(); //リサイズ
      positionUpdate(); //位置を更新
      controls.update(); //カメラコントローラー
      renderer.render(scene, camera); //レンダリング
      requestAnimationFrame( tick ); //繰り返し
    }
    requestAnimationFrame( tick );
  }else{ //見えてない時
    tick = () => {
      cancelAnimationFrame( tick )
    }
  }
}

//見えているかどうか(Intersection Observer API)------------------
const createObserver = () => {
  let observer;
  const options = { root: null, rootMargin: "0%", threshold: 0 };
  observer = new IntersectionObserver(switching, options); //コールバック関数とオプションを渡す
  observer.observe(wrap); //要素の監視を開始
}
createObserver();

参考

Three.jsのスプライト

threejs examples #css3d_sprites

Three.jsで螺旋アニメーション

素のJavaScriptだけでアニメーションを実装するWeb Animations API

Web Animations APIを使う

イージング関数チートシート

関連記事

簡単な地図を自作する(HTML/CSS/JS)

文字を立体で表示する(HTML/CSS/JS)

360度画像を表示する(HTML/JS)

うにょうにょ動かす(HTML/CSS/JS)

円を描くように要素を動かす(HTML/CSS/JS)

マウスの位置に合わせて要素を動かす(HTML/CSS/JS)

先頭に戻る
ページの先頭に戻る