OmochiXを検索

Esc で閉じる

背景・雰囲気 #020

パーティクル Particles

小さな粒がゆっくり漂う

上級 CSS JavaScript #背景 #Canvas #粒 #JavaScript

40個ほどの小さな粒が、画面の中をそれぞれの速さでゆっくり漂い、端まで行くと反対側から戻ってきます。

調整する

値を変えるとプレビューに反映され、「プロンプトをコピー」「コードをコピー」も調整後の内容になります。

AIへの頼み方

ヒーローの背景に、小さな粒がゆっくり上へ漂うパーティクルを足して。40個くらいで軽めに。

動きの仕様

  • <canvas> に 40個の点を描く。半径1〜3px、不透明度0.3〜0.8
  • 各点は上向きの速度(毎秒6〜16px)と小さな横揺れを持ち、上端を抜けたら下端から戻す
  • requestAnimationFrame で描画し、経過時間(dt)で動かす。タブが非表示のときは止める
  • 点の色はCSS変数(--primary)から取り、ダークモードでも読む
  • prefers-reduced-motion の環境では静止した1フレームだけ描く

使いどころ

ヒーロー背景、ローディング画面、祝福(完了)画面の演出。

コピーされるプロンプト全文を見る
「パーティクル(Particles)」の動きを実装してください。
OmochiX Learn の動く見本 #020(背景・雰囲気)です。

頼み方の例: ヒーローの背景に、小さな粒がゆっくり上へ漂うパーティクルを足して。40個くらいで軽めに。
どんな動きか: 小さな粒がゆっくり漂う
見本で起きること: 40個ほどの小さな粒が、画面の中をそれぞれの速さでゆっくり漂い、端まで行くと反対側から戻ってきます。
仕様:
- <canvas> に 40個の点を描く。半径1〜3px、不透明度0.3〜0.8
- 各点は上向きの速度(毎秒6〜16px)と小さな横揺れを持ち、上端を抜けたら下端から戻す
- requestAnimationFrame で描画し、経過時間(dt)で動かす。タブが非表示のときは止める
- 点の色はCSS変数(--primary)から取り、ダークモードでも読む
- prefers-reduced-motion の環境では静止した1フレームだけ描く
使いどころ: ヒーロー背景、ローディング画面、祝福(完了)画面の演出。
調整した値:
- 粒の数は40(既定)
- 速さは既定の1倍(既定)
- 粒の大きさは既定の1倍(既定)
- 粒の色: #6e4bff(既定)

実装の条件:
- HTML / CSS(必要なら素のJavaScript)で、外部ライブラリは使わない
- 見本の形・タイミング・イージングの数値はそのまま使い、文字・色・素材だけ今作っているものに合わせる
- prefers-reduced-motion: reduce の環境では動きを止めるか最小限にする
- 使った技術: CSS / JavaScript

見本(動くプレビューと参考コード): https://omochix.com/learn/motion/particles/

コード

プレビューで動いているものと同じコードです。HTMLに貼り付ければそのまま動きます。

HTML / CSS / JavaScript
<!-- パーティクル (Particles) | OmochiX Learn 動く見本 #020 | https://omochix.com/learn/motion/particles/ -->

<style>
.pt{display:block;width:320px;height:180px;color:var(--p-color,var(--primary))}
</style>

<canvas class="pt" width="320" height="180" aria-hidden="true"></canvas>

<script>
var omx = window.omx = window.omx || (function () {
  // OmochiX の見本用の最小ランタイム。animate() の処理をすべて 1 本の
  // requestAnimationFrame にまとめ、タブが隠れている間と
  // prefers-reduced-motion: reduce のときは止める。
  var fns = [], frame = 0, started = 0, stoppedAt = 0, lost = 0;
  var media = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
  function stopped() { return (media && media.matches) || document.hidden; }
  function tick(now) {
    frame = 0;
    if (stopped()) { stop(); return; }
    var t = Math.max(0, now - started - lost) / 1000; // rAF の時刻は要求時より前のことがある
    for (var i = 0; i < fns.length; i++) { try { fns[i](t); } catch (e) { /* 他の見本は止めない */ } }
    frame = window.requestAnimationFrame(tick);
  }
  function stop() {
    if (frame) { window.cancelAnimationFrame(frame); frame = 0; }
    if (started && !stoppedAt) stoppedAt = performance.now();
  }
  function sync() {
    api.reduced = !!(media && media.matches);
    if (stopped()) { stop(); return; }
    if (frame || !fns.length) return;
    var now = performance.now();
    if (!started) started = now; else if (stoppedAt) lost += now - stoppedAt;
    stoppedAt = 0;
    frame = window.requestAnimationFrame(tick);
  }
  var api = {
    root: document,
    reduced: !!(media && media.matches),
    animate: function (fn) { if (typeof fn === 'function') { fns.push(fn); sync(); } },
    param: function (key, fallback) { var p = window.omxParams || {}; return Object.prototype.hasOwnProperty.call(p, key) ? p[key] : fallback; },
    onParams: function () {}
  };
  document.addEventListener('visibilitychange', sync);
  if (media && media.addEventListener) media.addEventListener('change', sync);
  return api;
}());
var canvas=omx.root.querySelector('.pt');var ctx=canvas.getContext('2d');var W=canvas.width,H=canvas.height;var dots=[];function make(){dots=[];var n=omx.param('count',40);for(var i=0;i<n;i++){dots.push({x:Math.random()*W,y:Math.random()*H,r:1.2+Math.random()*2.2,vx:(Math.random()-.5)*14,vy:-6-Math.random()*10,a:.3+Math.random()*.5});}}make();omx.onParams(function(){if(dots.length!==omx.param('count',40))make();});var last=0;omx.animate(function(t){var dt=Math.min(t-last,.05);last=t;var speed=omx.param('speed',1),size=omx.param('size',1);var color=getComputedStyle(canvas).color||'#6e4bff';ctx.clearRect(0,0,W,H);ctx.fillStyle=color;for(var i=0;i<dots.length;i++){var d=dots[i];d.x+=d.vx*dt*speed;d.y+=d.vy*dt*speed;if(d.y<-4){d.y=H+4;d.x=Math.random()*W;}if(d.x<-4)d.x=W+4;if(d.x>W+4)d.x=-4;ctx.globalAlpha=d.a;ctx.beginPath();ctx.arc(d.x,d.y,d.r*size,0,Math.PI*2);ctx.fill();}ctx.globalAlpha=1;});
</script>