パーティクルが集まって、画像のシルエットになる(しょぼい)

ThreeJS

シルエット画像のダウロード

https://www.cleanpng.com/free/silhouette-car.html

CDN

<script type="importmap">
{
    "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.175.0/build/three.module.js"
    }
}
</script>
<script type="module">
import * as THREE from "three";

Scene / PerspectiveCamera / WebGLRenderer

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 500;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

ブラウザ幅のリサイズ対応

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

Canvasで画像を読み取って粒子配置のポイントを取得します

const image = new Image();
image.src = 'https://8ch.tech/wp-content/uploads/2025/04/car.png';
image.onload = () => {
  const canvas = document.createElement('canvas');
  canvas.width = image.width;
  canvas.height = image.height;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(image, 0, 0);

  const imageData = ctx.getImageData(0, 0, image.width, image.height).data;
  const points = [];

  for (let y = 0; y < image.height; y += 2) {
    for (let x = 0; x < image.width; x += 2) {
      const i = (y * image.width + x) * 4;
      const alpha = imageData[i + 3];

      if (alpha > 128) {
        points.push({
          x: x - image.width / 2,
          y: -(y - image.height / 2),
          z: Math.random() * 20 - 10, // ランダムな奥行き
        });
      }
    }
  }

  createParticles(points);

パーティクルの作成

function createParticles(points) {
  const geometry = new THREE.BufferGeometry();
  const particleCount = points.length;

  const positions = new Float32Array(particleCount * 3);
  const targets = new Float32Array(particleCount * 3);

  for (let i = 0; i < particleCount; i++) {
    // 初期位置はランダム
    positions[i * 3] = (Math.random() - 0.5) * 800;
    positions[i * 3 + 1] = (Math.random() - 0.5) * 800;
    positions[i * 3 + 2] = (Math.random() - 0.5) * 800;

    // 目標位置(動物の形)
    targets[i * 3] = points[i].x;
    targets[i * 3 + 1] = points[i].y;
    targets[i * 3 + 2] = points[i].z;
  }

  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute('target', new THREE.BufferAttribute(targets, 3));

  const material = new THREE.PointsMaterial({
    size: 2,
    color: 0xffffff
  });

  const particles = new THREE.Points(geometry, material);
  scene.add(particles);

  animateParticles(particles);
}

パーティクルが目標地点に近づくアニメーション

function animateParticles(particles) {
  const pos = particles.geometry.attributes.position;
  const tgt = particles.geometry.attributes.target;

  function animate() {
    requestAnimationFrame(animate);

    for (let i = 0; i < pos.count; i++) {
      const ix = i * 3;
      for (let j = 0; j < 3; j++) {
        const delta = tgt.array[ix + j] - pos.array[ix + j];
        pos.array[ix + j] += delta * 0.05; // だんだん近づける
      }
    }

    pos.needsUpdate = true;
    renderer.render(scene, camera);
  }

  animate();
}

};  <--- image.onload = () => {} の }
BACK