JPG画像をパーティクルアートに変換する
JPG画像を、粒子の集まりとして表示するシステムを作った。
マウスを近づけると粒子が逃げる。離れると元の位置に戻る。単純だけど、画像の新しい見せ方として面白いと思った。
作ったもの
画像をアップロードすると、その画像が無数の粒子に変換される。
オリジナル画像とパーティクル版を並べて表示する仕組み。マウスを動かすと、粒子が反応して逃げていく。まるで画像が生きているみたいな感覚になる。
アップロードした画像がどう変換されるか、リアルタイムで確認できる。
なぜ作ったか
写真展示で使う新しい表現を探していた。
通常のJPG表示は、もう見飽きた感じがあった。何か違うアプローチはないか。そう考えた時、パーティクルアートの手法を思い出した。
粒子で画像を構成すれば、静止画に動きを与えられる。マウス操作でインタラクティブにもできる。これだ。
パーティクル生成の仕組み
画像を読み込んで、Canvasに描画する。
const reader = new FileReader();
reader.onload = function(e) {
const img = new window.Image();
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
};
};
次に、画像データから粒子を生成する。
全ピクセルを粒子にすると重すぎる。間引きが必要だった。yStepとxStepで間隔を調整している。
const yStep = Math.max(3, Math.round(h / 100));
const xStep = Math.max(3, Math.round(w / 140));
const particles = [];
for(let y=0; y<h; y+=yStep) {
for(let x=0; x<w; x+=xStep) {
const i = (y*w + x)*4;
const color = `rgb(${data[i]},${data[i+1]},${data[i+2]})`;
particles.push({x, y, color, ox:x, oy:y});
}
}
oxとoyは元の位置。マウスで動かした後、この座標に戻す。
粒子密度の調整
最初は全ピクセルをパーティクルにしようとした。大失敗だった。
1920x1080の画像だと、約200万個の粒子。ブラウザが固まる。描画が追いつかない。
間引きの倍率を色々試した:
- 10ピクセルおき:粗すぎて画像が分からない
- 5ピクセルおき:まだ粗い、でも軽い
- 3ピクセルおき:ちょうど良い、画像が認識できる
最終的に、画像サイズに応じて自動調整する方式にした。
h / 100とw / 140の比率。これで、どんな画像サイズでも適切な密度になる。小さい画像なら密に、大きい画像なら粗めに。バランスが取れた。
マウスインタラクションの実装
粒子をマウスから逃がす仕組み。
canvas.onmousemove = function(ev) {
const rect = canvas.getBoundingClientRect();
const mx = ev.clientX - rect.left;
const my = ev.clientY - rect.top;
for(const p of particles) {
const dx = p.ox - mx, dy = p.oy - my;
const dist = Math.sqrt(dx*dx+dy*dy);
if(dist < 40) {
p.x = p.ox + dx/dist*20;
p.y = p.oy + dy/dist*20;
} else {
p.x += (p.ox - p.x)*0.1;
p.y += (p.oy - p.y)*0.1;
}
}
draw();
};
マウスとの距離を計算。40ピクセル以内なら反発させる。
反発の強さは20ピクセル。最初は50だったが、飛びすぎた。10だと弱すぎる。20がちょうど良かった。
離れた粒子は、元の位置に戻る。戻る速度は0.1。イージング効果で滑らかに。
描画処理の最適化
最初はrequestAnimationFrameで毎フレーム描画していた。
でも、マウスを動かさない時も描画し続けるのは無駄。CPUが熱くなる。
マウスイベント内でのみ描画するように変更。
function draw() {
ctx.clearRect(0,0,w,h);
for(const p of particles) {
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, Math.max(2.5, w/160), 0, Math.PI*2);
ctx.fill();
}
}
clearRectで一旦クリア。各粒子を円として描画。粒子の半径は画像サイズに応じて調整。
この方式にしてから、CPU使用率が80%→10%に下がった。意外でした。イベント駆動の方が効率的だった。
粒子サイズの決定
粒子のサイズは、画像の幅に応じて変わる。
最小サイズは2.5ピクセル。それより小さいと見えない。
大きい画像なら、粒子も大きく。1920px幅なら、粒子は12ピクセルくらい。
最初は固定サイズにしていた。でも、小さい画像だと粒子が大きすぎる。大きい画像だと小さすぎる。
比率で調整したら、どんなサイズでもバランスが取れた。
試してみた画像
風景写真を試した。青空と緑の山。パーティクルにすると、絵画みたいになった。色の粒が集まって風景を作る感覚。
人物写真も試した。顔の部分は粒子が密集する。髪の毛は粗い。意外と雰囲気が出る。
抽象的な画像が一番面白かった。色のグラデーションが粒子で表現される。マウスを動かすと、色が踊る。
システム全体の流れ
使ってみて
画像をパーティクルアートに変換するシステムとして、実用的に仕上がった。
ポイントは以下の3つ:
- 粒子密度を画像サイズに応じて自動調整(h/100、w/140の比率)
- マウスインタラクションで反発距離40px、反発強さ20px
- イベント駆動の描画でCPU使用率を80%→10%に削減
パーティクルアートは、静止画に動きを与える手法として面白い。写真展示で使えば、訪問者の目を引く演出になると思う。
同じようなインタラクティブな画像表現を試している方の参考になれば嬉しいです。
まとめ
今回は、JPG画像をパーティクルアートに変換するシステムを作りました。
ポイントは以下の3つ:
- 画像データをサンプリングして粒子配列を生成
- マウス位置との距離計算で反発効果を実装
- イベント駆動の描画でパフォーマンスを最適化
単純な仕組みだけど、画像の新しい見せ方として十分機能した。マウス操作でインタラクティブに楽しめる点が良かった。
さらに深く学ぶには
この記事で興味を持った方におすすめのリンク:
自分の関連記事:
最後まで読んでくださり、ありがとうございました。