mcpField
G
globe-particles

globe-particles

github.com/mengtoデザイン・UI7.1k1.5k 回インストール3 日前に更新
リポジトリ

globe-particles について

globe-particles は、粒子による地球状ビジュアライゼーションを作るデザインスキルで、HTML、CSS、シェーダー、Three.js のレシピを提供します。球形の粒子コアと傾いた軌道リングを中心に、色・動き・性能の調整方法を扱います。

主な機能

  1. 01球形粒子コアと軌道リングの設計
  2. 02Three.js の実装レシピ
  3. 03粒子密度・サイズ・色の調整
  4. 04動き・発光・性能の調整
  5. 05レンダラーやリスナーの後処理

活用シーン

  1. 01惑星や軌道のビジュアル作成
  2. 02インフラや合成データの地球表現
  3. 03ブランドのメインカラーに合わせた軌道色の調整

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mengto/skills --skill globe-particles

インストール後、Claude Code で/globe-particlesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

demo/
index.html
PROMPT.md
source.json

詳細

グローブパーティクル

適用範囲

  • 球体のような3Dパーティクルビジュアライゼーションにのみ適用します。
  • ページ全体のレイアウト、コピー、無関係なモーションシステムは変更しないでください。
  • 惑星、軌道、インフラストラクチャ、または合成データのグローブエフェクトに使用します。
  • コアはニュートラルまたは白熱色に保ち、リングやグローのアクセントにはデザインのプライマリカラーを使用します。

ビジュアルの目標

  • 明るく輝く点で構成された、高密度の球状コア。
  • 球体を囲む、より細い外側の軌道リングまたは平坦なディスク。
  • 傾き、奥行き、層状のパーティクル密度によって、グローブのシルエットを明確にします。
  • 暗く大気感のある背景、控えめなグロー、すっきりした構造、繊細なSF的奥行き。
  • プレミアムでシネマティックな印象にし、遊び心やノイズの多い表現は避けます。

HTML と CSS

<div class="globe-particles-shell">
  <canvas class="globe-particles-canvas" data-globe-particles></canvas>
</div>
.globe-particles-shell {
  position: relative;
  width: min(100%, 760px);
  aspect-ratio: 1 / 1;
}

.globe-particles-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

パーティクルシェーダー

円形のシェーダーポイントを使用して、パーティクルのシャープさと輝きを保ちます。

const globeParticleVertex = `
attribute float a_size;
attribute float a_layer;

uniform float u_time;
uniform float u_pointSize;

varying float v_layer;
varying float v_depth;
varying float v_falloff;

void main() {
  vec3 pos = position;
  float breathe = 1.0 + sin(u_time * 0.65 + a_layer * 4.0) * 0.012;
  pos *= breathe;

  vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
  gl_PointSize = u_pointSize * a_size * (1.0 / max(0.18, -mvPosition.z));
  gl_Position = projectionMatrix * mvPosition;

  v_layer = a_layer;
  v_depth = smoothstep(-1.8, 1.8, pos.z);
  v_falloff = smoothstep(2.45, 0.25, length(pos));
}
`;

const globeParticleFragment = `
precision highp float;

uniform vec3 u_coreColor;
uniform vec3 u_accentColor;

varying float v_layer;
varying float v_depth;
varying float v_falloff;

void main() {
  vec2 uv = gl_PointCoord - 0.5;
  float d = length(uv);
  float alpha = smoothstep(0.5, 0.0, d);
  alpha *= alpha;

  vec3 color = mix(u_coreColor, u_accentColor, smoothstep(0.35, 1.0, v_layer));
  color += vec3(1.0) * v_depth * 0.08;
  color = mix(color * 0.42, color, clamp(v_falloff + v_layer * 0.28, 0.0, 1.0));
  alpha *= mix(0.52, 1.0, clamp(v_falloff + v_layer * 0.24, 0.0, 1.0));

  gl_FragColor = vec4(color, alpha);
}
`;

Three.js のレシピ

import * as THREE from "three";

function hexToRgb01(hex) {
  const clean = hex.replace("#", "").trim();
  const value = clean.length === 3
    ? clean.split("").map((char) => char + char).join("")
    : clean;

  return new THREE.Color(
    parseInt(value.slice(0, 2), 16) / 255,
    parseInt(value.slice(2, 4), 16) / 255,
    parseInt(value.slice(4, 6), 16) / 255
  );
}

function buildGlobeParticleGeometry(options = {}) {
  const sphereCount = options.sphereCount || 2600;
  const ringCount = options.ringCount || 1300;
  const radius = options.radius || 1.35;
  const ringRadius = options.ringRadius || 2.05;
  const ringThickness = options.ringThickness || 0.12;
  const total = sphereCount + ringCount;

  const positions = new Float32Array(total * 3);
  const sizes = new Float32Array(total);
  const layers = new Float32Array(total);

  for (let i = 0; i < sphereCount; i++) {
    const z = Math.random() * 2 - 1;
    const theta = Math.random() * Math.PI * 2;
    const r = radius * (0.58 + Math.pow(Math.random(), 0.42) * 0.42);
    const root = Math.sqrt(1 - z * z);
    const index = i * 3;

    positions[index] = Math.cos(theta) * root * r;
    positions[index + 1] = Math.sin(theta) * root * r;
    positions[index + 2] = z * r;
    sizes[i] = 0.72 + Math.random() * 0.72;
    layers[i] = Math.random() * 0.28;
  }

  for (let i = 0; i < ringCount; i++) {
    const pointIndex = sphereCount + i;
    const angle = Math.random() * Math.PI * 2;
    const r = ringRadius + (Math.random() - 0.5) * ringThickness;
    const y = (Math.random() - 0.5) * ringThickness * 0.58;
    const index = pointIndex * 3;

    positions[index] = Math.cos(angle) * r;
    positions[index + 1] = y;
    positions[index + 2] = Math.sin(angle) * r;
    sizes[pointIndex] = 0.62 + Math.random() * 0.58;
    layers[pointIndex] = 0.72 + Math.random() * 0.28;
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("a_size", new THREE.BufferAttribute(sizes, 1));
  geometry.setAttribute("a_layer", new THREE.BufferAttribute(layers, 1));
  return geometry;
}

function initGlobeParticles(canvas, options = {}) {
  if (!canvas) return () => {};

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: true,
  });
  renderer.setClearColor(0x000000, 0);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));
  renderer.outputColorSpace = THREE.SRGBColorSpace;

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(38, 1, 0.1, 100);
  camera.position.set(0, 0, options.cameraDistance || 5.6);

  const accent = options.accentColor
    ? new THREE.Color(options.accentColor)
    : hexToRgb01(getComputedStyle(document.documentElement).getPropertyValue("--brand-accent").trim() || "#8b5cf6");

  const geometry = buildGlobeParticleGeometry(options);
  const material = new THREE.ShaderMaterial({
    vertexShader: globeParticleVertex,
    fragmentShader: globeParticleFragment,
    transparent: true,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    uniforms: {
      u_time: { value: 0 },
      u_pointSize: { value: options.pointSize || 18 },
      u_coreColor: { value: new THREE.Color(options.coreColor || 0xf8fafc) },
      u_accentColor: { value: accent },
    },
  });

  const particles = new THREE.Points(geometry, material);
  particles.rotation.x = options.tiltX ?? -0.42;
  particles.rotation.z = options.tiltZ ?? 0.22;
  scene.add(particles);

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  const pointer = new THREE.Vector2(0, 0);
  let rafId = 0;

  function resize() {
    const width = Math.max(1, canvas.clientWidth);
    const height = Math.max(1, canvas.clientHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, options.maxDpr || 1.6));
    renderer.setSize(width, height, false);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
  }

  function handlePointerMove(event) {
    const rect = canvas.getBoundingClientRect();
    pointer.x = ((event.clientX - rect.left) / rect.width - 0.5) * 2;
    pointer.y = ((event.clientY - rect.top) / rect.height - 0.5) * 2;
  }

  function render(time = 0) {
    const t = time * 0.001;
    material.uniforms.u_time.value = t;

    const mouseStrength = options.mouseStrength ?? 0.08;
    const breath = reduceMotion ? 0 : Math.sin(t * 0.55) * 0.045;
    particles.rotation.y = t * (options.rotationSpeed || 0.12);
    particles.rotation.x = (options.tiltX ?? -0.42) + pointer.y * mouseStrength;
    particles.rotation.z = (options.tiltZ ?? 0.22) + pointer.x * mouseStrength;
    particles.scale.setScalar(1 + breath);

    renderer.render(scene, camera);
    if (!reduceMotion) rafId = requestAnimationFrame(render);
  }

  function handleResize() {
    cancelAnimationFrame(rafId);
    resize();
    render();
  }

  resize();
  render();
  window.addEventListener("resize", handleResize);
  window.addEventListener("pointermove", handlePointerMove);

  return () => {
    cancelAnimationFrame(rafId);
    window.removeEventListener("resize", handleResize);
    window.removeEventListener("pointermove", handlePointerMove);
    geometry.dispose();
    material.dispose();
    renderer.dispose();
  };
}

const cleanupGlobe = initGlobeParticles(document.querySelector("[data-globe-particles]"), {
  sphereCount: 2600,
  ringCount: 1300,
  accentColor: "#8b5cf6",
  radius: 1.35,
  ringRadius: 2.05,
  rotationSpeed: 0.12,
  mouseStrength: 0.08,
});

調整項目

  • 密度:sphereCount と ringCount を個別に調整します。
  • スケール:radius、ringRadius、ringThickness、cameraDistance を調整します。
  • 色:coreColor はニュートラルに保ち、accentColor はブランドのプライマリカラーをもとに設定します。
  • モーション:rotationSpeed、tiltX、tiltZ、mouseStrength、呼吸の振幅を調整します。
  • グロー:pointSize、加算合成、パーティクル数を調整し、形状のシャープさを保ちます。
  • パフォーマンス:ビジュアル構造を変更する前に、パーティクル数を減らすか maxDpr に上限を設定します。

表現のルール

  • シルエットは、散漫な星空ではなくグローブとして認識できる必要があります。
  • リングは軌道を感じさせる傾きを持たせ、平坦な装飾用アンダーラインのように見せないでください。
  • グローは控えめにし、密度と奥行きでプレミアムな印象を演出します。
  • マウスへの反応は穏やかにし、オブジェクトが大きく振れるのではなく、ゆっくり漂うようにします。
  • グローブは暗い背景の上に配置するか、暗く大気感のあるシェルの内側に配置します。

避けること

  • 球状の構造がない、ありきたりな星空のノイズ。
  • パーティクルが大きすぎる、またはグローブのシルエットを損なうブルーム。
  • デザインのプライマリカラーが明確なのに、アクセントカラーをハードコードすること。
  • カーソルへの過剰な反応や高速回転。
  • オブジェクトがぼやけた塊になるほど濃いフォグ。

簡易チェック

  • 球体とリングが異なるパーティクル群として表現されています。
  • コアは主にニュートラルまたは白熱色に見えます。
  • リング、ハイライト、またはグローにアクセントカラーが使われています。
  • 傾きによってリングとグローブの奥行きが見えます。
  • モーション低減時には、静止またはほぼ静止したオブジェクトが表示されます。
  • ジオメトリ、マテリアル、レンダラー、リスナー、RAF がクリーンアップされます。

コメント