サイトロゴ

JavaScriptでお絵描き p5.js

著者画像
Toshihiko Arai

フィボナッチスパイラル

ひまわりの種の螺旋配置です。黄金角と呼ばれる円を黄金比で分割した数「137.5度」ずつ回しながら、外向きに点を打つだけで自然なヒマワリの模様が現れます。黄金角以外で試すと、タネ同士に隙間ができて不恰好になります。黄金角が一番一様にタネが配置されるので不思議です。

let n = 0;
let c = 5; // 間隔を調整する係数

function setup() {
  createCanvas(600, 600);
  angleMode(DEGREES);
  background(255);
}

function draw() {
  translate(width / 2, height / 2);
  
  let a = n * 137.5; // 黄金角
  let r = c * sqrt(n);

  let x = r * cos(a);
  let y = r * sin(a);

  fill(100, 200, 255, 150);
  noStroke();
  ellipse(x, y, 8, 8);

  n++;
  
  if (n > 1000) {
    noLoop();
  }
}

ローレンツアトラクタ(カオスの軌跡)

p5.jsでローレンツ方程式のカオス軌道を描くこともできます:

let x = 0.01;
let y = 0;
let z = 0;

let sigma = 10;
let rho = 28;
let beta = 8.0 / 3.0;

let trail = [];

function setup() {
    createCanvas(800, 600, WEBGL);
    background(0);
    stroke(255);
    strokeWeight(1);
    noFill();
}

function draw() {
    // 軌道の更新
    for (let i = 0; i < 5; i++) { // 1フレームで複数点進めて滑らかに
        let dt = 0.005;
        let dx = sigma * (y - x) * dt;
        let dy = (x * (rho - z) - y) * dt;
        let dz = (x * y - beta * z) * dt;

        let px = x;
        let py = y;
        let pz = z;

        x += dx;
        y += dy;
        z += dz;

        trail.push({ x1: px, y1: py, z1: pz, x2: x, y2: y, z2: z });
    }

    // 描画
    background(0); // ← 毎フレーム消して再描画
    rotateY(frameCount * 0.005);
    scale(5);

    for (let segment of trail) {
        let brightness = map(segment.z2, -30, 30, 100, 255);
        stroke(brightness, 100, 255, 180); // 色の変化で奥行きを出す
        line(segment.x1, segment.y1, segment.z1, segment.x2, segment.y2, segment.z2);
    }

    // trail 長さ制限
    if (trail.length > 3000) {
        trail.splice(0, trail.length - 3000);
    }
}

はじめに

久しぶりにプログラミングでお絵描きをしたいと思った。 以前にProcessingやPython、OpenCVなどでお絵描きというかアニメーションのテストをしたことがあるが、今回はJavaScriptで実現してみたいと思った。


JavaScriptでお絵描きができる「p5.js」

そこで方法を探していたところ「p5.js」が良さそう。 「p5.js」はProcessingの概念をJavaScriptに移植する形で誕生。HTML5のCanvasを使って描画される。

「p5.js」をはじめる

「p5.js」を手っ取り早くはじめるには、CDNで提供されている「p5.js」ライブラリをHTMLに読み込むだけ。

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>

あとは <script></script> 内に、Processingのようなプログラミング形式でスクリプトを記述していく。 setupは最初に一度だけ呼ばれる関数なので、キャンバスの作成や初期設定を記述する。drawは毎フレーム呼ばれるループ関数である。 Arduinoをやったことのある人なら、同じ感覚でプログラミングできるため理解も簡単だろう。

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(220);
    ellipse(200, 200, 50, 50);
}

キャンバスを画像で保存

描画したキャンバスをjpgファイルとして保存したい場合があるだろう。その場合はmousePressed関数にsaveCanvasを実装すれば良い。

function mousePressed() {
    saveCanvas('test1', 'jpg');
}

キャンバスをクリックすると画像として保存できる。とても便利だ。

アニメーション、ボールを動かす

次に、ボールを動かしてみよう。さらに壁に当たったら、反射するようになっている。

let x = 250, y = 200;
let xSpeed = 10, ySpeed = 7;

function setup() {
    createCanvas(400, 400);
}

function draw() {
    background(220);
    ellipse(x, y, 50, 50);
    x += xSpeed;
    y += ySpeed;

    if(x > width - 25 || x < 25) xSpeed *= -1;
    if(y > height - 25 || y < 25) ySpeed *= -1;
}

アニメーション、ボールを動かす応用

さらに発展させて、クリックするたびにボールが追加してランダムな色・スピード・方向へ動かしてみよう。 オブジェクト指向なプログラミングアプローチとして、Ballクラスを定義すると扱いやすくなる。

let balls = [];

function setup() {
    createCanvas(400, 400);
    // 初期状態で5個のボールを生成
    for (let i = 0; i < 5; i++) {
        balls.push(new Ball(random(25, width - 25), random(25, height - 25), random(-5, 5), random(-5, 5)));
    }
}

function draw() {
    // 半透明の背景でトレイル効果を出す(alpha値を50に設定)
    background(30, 30, 30, 80);

    // 各ボールの動きと描画
    for (let ball of balls) {
        ball.move();
        ball.display();
    }
}

// クリックで新しいボールを追加
function mousePressed() {
    balls.push(new Ball(mouseX, mouseY, random(-5, 5), random(-5, 5)));
}

// ボールのクラス定義
class Ball {
    constructor(x, y, xSpeed, ySpeed) {
        this.x = x;
        this.y = y;
        this.xSpeed = xSpeed;
        this.ySpeed = ySpeed;
        this.size = random(20, 40);
        this.r = random(100, 255);
        this.g = random(100, 255);
        this.b = random(100, 255);
    }

    move() {
        this.x += this.xSpeed;
        this.y += this.ySpeed;

        // 壁に当たったら跳ね返る処理
        if (this.x > width - this.size/2 || this.x < this.size/2) {
            this.xSpeed *= -1;
        }
        if (this.y > height - this.size/2 || this.y < this.size/2) {
            this.ySpeed *= -1;
        }
    }

    display() {
        noStroke();
        fill(this.r, this.g, this.b);
        ellipse(this.x, this.y, this.size);
    }
}

点と線を結ぶジェネラティブアート風アニメーション

▼ キャンバスをクリックで開始/一時停止
let points = [];
let velocities = [];
const num = 64;
const radius = 2;
const maxSpeed = 3;
const connectionDistSq = 6000;

function setup() {
    createCanvas(600, 400);
    stroke(222);
    fill(222);
    for (let i = 0; i < num; i++) {
        points.push(createVector(random(width), random(height)));
        velocities.push(p5.Vector.random2D().mult(random(maxSpeed)));
    }
    frameRate(30)
}

function draw() {
    background(0);

    // 点の移動と描画
    for (let i = 0; i < num; i++) {
        let pt = points[i];
        let v = velocities[i];
        pt.add(v);

        // 反射
        if (pt.x < 0 || pt.x > width) v.x *= -1;
        if (pt.y < 0 || pt.y > height) v.y *= -1;

        circle(pt.x, pt.y, radius * 2);
    }

    // 線の描画
    for (let i = 0; i < num; i++) {
        for (let j = i + 1; j < num; j++) {
            let d = distSq(points[i], points[j]);
            if (d < connectionDistSq) {
                line(points[i].x, points[i].y, points[j].x, points[j].y);
            }
        }
    }
}

function distSq(p1, p2) {
    let dx = p1.x - p2.x;
    let dy = p1.y - p2.y;
    return dx * dx + dy * dy;
}

関連記事