Kevin's Data Analytics Blog

データサイエンティスト、AIエンジニアを目指す方に向けて情報発信していきます。

スクリーンセーバー的なアニメーション動画の作成|Processing

Processingについての投稿3回目です。今回は、ずっと見ていられるスクリーンセーバーのようなものを作りたいと思い、2つ実装してみました。どちらも、ぼーっと見ていられるアニメーションになっていると思います。

アニメーション

1つ目は、複数のボールが動きながら色を塗りつぶしていく動画です。ボールが跳ね返る度に、少しずつボールの色が変化していきます。スマートフォン向けに縦動画です。
youtube.com

2つ目は、ボール同士が衝突しながら合体し、大きくなっていく動画です。全てのボールが合体するとまたバラバラになります。PC向けに横動画です。
youtu.be

アニメーションのコード

カラフルなボールが平面を塗りつぶしていく動画

// 変数宣言
int ballnum = 60;  // 玉の数(初期値)
Point[] points = new Point[ballnum];

void setup() {
  size(540, 980);
  
  background(255);
  // カラーモード指定
  colorMode( HSB, 360, 100, 100, 100 );
  
  // 初期化
  for (int i=0; i<points.length; i++) {
    points[i] = new Point(random(width), random(height), random(-5, 5), random(-5, 5), random(20,100), random(0, 360), random(50, 60), random(90, 100));
    // x方向,y方向の移動速度を制御する変数は、√(x^2+y^2)で標準化した上で、サイズに応じた値を掛け算する
    points[i].speedX = 4 * 30/points[i].ballsize * points[i].speedX / sqrt(sq(points[i].speedX)+sq(points[i].speedY));
    points[i].speedY = 4 * 30/points[i].ballsize * points[i].speedY / sqrt(sq(points[i].speedX)+sq(points[i].speedY));
  }
}

void draw() {
  
  frameRate(60);
  //background(0);

  // 移動と描画
  for (int i=0; i<points.length; i++) {
    points[i].move();
    points[i].drawEllipse();
  }
   
  // 動画作成用にpngファイルを保存 ※保存時にコメントインする
  //saveFrame("frames/######.png");

}

class Point {
  // 変数を宣言
  float x;  //
  float y;
  float speedX;
  float speedY;
  float ballsize;
  float col1; 
  float col2;
  float col3;

  // constractorを初期化
  Point(float _x, float _y, float _speedX, float _speedY, float _ballsize,
        float _col1, float _col2, float _col3) {
    x = _x;
    y = _y;
    speedX = _speedX;
    speedY = _speedY;
    ballsize = _ballsize;
    col1 = _col1;
    col2 = _col2;
    col3 = _col3;
  }

  // method 関数
  void move() {
    // x,y方向にそれぞれ移動
    x += speedX;
    y += speedY;
    
    // 玉が左端に到達したらx軸方向を逆向きにする
    // ※半径の幅を考慮してballsize/2としている
    if (x < ballsize/2){
      x = ballsize/2;
      speedX = -speedX;
      col1 += 10;
    }
    // 玉が右端に到達したらx軸方向を逆向きにする
    else if (x > width-ballsize/2) {
      x = width-ballsize/2;
      speedX = -speedX;
      col1 += 10;
    }
    // 玉が上端に到達したらy軸方向を逆向きにする
    if (y < ballsize/2){
      y = ballsize/2;
      speedY = -speedY;
      col1 += 10;
    }
    // 玉が下端に到達したらy軸方向を逆向きにする
    else if (y > height-ballsize/2){
      y = height-ballsize/2;
      speedY = -speedY;
      col1 += 10;
    }
    
    // 色相が360を超えたら360引く 
    if (col1 > 360){
      col1 -= 360;
    }
  }
  
  // 描画 
  void drawEllipse() {
    // 縁なし
    noStroke();
    
    // 玉の色を指定
    fill(col1, col2, col3, 50);
    
    // 位置とサイズを指定して、玉を表示
    ellipse(x, y, ballsize, ballsize);
  }
}

玉が衝突しながら大きくなっていく動画

// 変数宣言
int ballnum = 100;  // 玉の数(初期値)
int default_ballsize = 40;  // 玉のサイズ(初期値)
int base_speed=12; // 速度の基準値
int counter = 0; // リセット処理用カウンタ
boolean reset_flag = false;  // リセットフラグ
Point[] points = new Point[ballnum];

void setup() {
  
  // フルスクリーン表示
  fullScreen();

  // カラーモード指定
  colorMode( HSB, 360, 100, 100, 100 );
  
  // 初期化
  for (int i=0; i<points.length; i++) {
    points[i] = new Point(random(width), random(height), random(-5, 5), random(-5, 5), default_ballsize, random(0, 360), random(90, 100), random(90, 100));
    
    // x方向,y方向の移動速度を制御する変数は、2次元上での移動速度が全ての玉で一緒になるように、base_speed/√(x^2+y^2)で標準化する
    points[i].speedX = base_speed*points[i].speedX/sqrt(sq(points[i].speedX)+sq(points[i].speedY));
    points[i].speedY = base_speed*points[i].speedY/sqrt(sq(points[i].speedX)+sq(points[i].speedY));
  }
}

void draw() {
  
  //frameRate(20);
  background(0,0,0);

  // 移動と描画
  for (int i=0; i<points.length; i++) {
    points[i].move();
    points[i].drawEllipse();
  }
  
  // 玉同士の接触判定
  for (int i=0; i<points.length; i++) {
    for (int j=i+1; j<points.length; j++) {
      // どちらかの玉が既に接触済で、サイズ0になっていればスキップ
      if ((points[i].ballsize == 0) || (points[j].ballsize == 0)) {continue;}
      
      // 接触範囲 = 2つの玉の半径を足し算
      float range = points[i].ballsize/2 + points[j].ballsize/2;
      
      // 玉の中心同士の距離 = √((x1-x2)^2+(y1-y2)^2)
      float dist = sqrt(sq(points[i].x - points[j].x) + sq(points[i].y - points[j].y));
      
      // 距離が半径の和よりも近い場合は接触
      if (dist <= range) {
        
        // 2玉のうち、サイズが大きい方が残る
        if (points[i].ballsize >= points[j].ballsize){
          
          // 新しい玉の面積が、2つの玉の面積の足し算になるように調整
          points[i].ballsize = sqrt(sq(points[i].ballsize)+sq(points[j].ballsize));
          
          // 玉の大きさに応じて移動速度を設定
          points[i].speedX = base_speed*points[i].speedX / sqrt(sq(points[i].speedX)+sq(points[i].speedY)) * sqrt(default_ballsize / points[i].ballsize);
          points[i].speedY = base_speed*points[i].speedY / sqrt(sq(points[i].speedX)+sq(points[i].speedY)) * sqrt(default_ballsize / points[i].ballsize);
          
          // 小さい方の玉のサイズを0にして表示から消す
          points[j].ballsize = 0;
        }
        else {
          //jの方がサイズが大きい場合の処理。内容は上と同じ
          points[j].ballsize = sqrt(sq(points[i].ballsize)+sq(points[j].ballsize));
          points[j].speedX = base_speed*points[j].speedX / sqrt(sq(points[j].speedX)+sq(points[j].speedY)) * sqrt(default_ballsize / points[j].ballsize);
          points[j].speedY = base_speed*points[j].speedY / sqrt(sq(points[j].speedX)+sq(points[j].speedY)) * sqrt(default_ballsize / points[j].ballsize);
          points[i].ballsize = 0;          
        }
      }
    }
  }
  
  // リセット判定
  int del_count = 0;
  // サイズが0になっている玉の数を確認
  for (int i=0; i<points.length; i++) {
    if (points[i].ballsize == 0){
      del_count += 1;
    }
  }
  // 全数-1がサイズ0になっていたらreset_flagをtrueにする
  if (del_count >= ballnum-1){
    reset_flag = true;
  }  
  
  // リセット処理
  if (reset_flag){
    // 回数カウンタを1追加
    counter += 1;
    
    // ボールの位置などを全て初期化
    for (int i=0; i<ballnum; i++) {
      points[i].x = random(width);
      points[i].y = random(height);
      points[i].speedX = random(-5, 5);
      points[i].speedY = random(-5, 5);
      points[i].speedX = base_speed*points[i].speedX/sqrt(sq(points[i].speedX)+sq(points[i].speedY));
      points[i].speedY = base_speed*points[i].speedY/sqrt(sq(points[i].speedX)+sq(points[i].speedY));
      points[i].ballsize = default_ballsize;
      points[i].col1 = random(0, 360);
      points[i].col2 = random(90, 100);
      points[i].col3 = random(90, 100);
    }
    // 演出のため、ボールの位置の初期化を50回繰り返した後でreset_flagをfasleに戻す
    if (counter >= 50){
      counter = 0;
      reset_flag = false;
    }
  }
  
  // 動画作成用にpngファイルを保存 ※保存時にコメントインする
  //saveFrame("frames1/######.png");
}

class Point {
  // 変数を宣言
  float x;  //
  float y;
  float speedX;
  float speedY;
  float ballsize;
  float col1; 
  float col2;
  float col3;

  // constractorを初期化
  Point(float _x, float _y, float _speedX, float _speedY, float _ballsize,
        float _col1, float _col2, float _col3) {
    x = _x;
    y = _y;
    speedX = _speedX;
    speedY = _speedY;
    ballsize = _ballsize;
    col1 = _col1;
    col2 = _col2;
    col3 = _col3;
  }

  // method 関数
  void move() {
    // x,y方向にそれぞれ移動
    x += speedX;
    y += speedY;
    
    // 玉が左端に到達したらx軸方向を逆向きにする
    // ※半径の幅を考慮してballsize/2としている
    if (x < ballsize/2){
      x = ballsize/2;
      speedX = -speedX;
    }
    // 玉が右端に到達したらx軸方向を逆向きにする
    else if (x > width-ballsize/2) {
      x = width-ballsize/2;
      speedX = -speedX;
    }
    // 玉が上端に到達したらy軸方向を逆向きにする
    if (y < ballsize/2){
      y = ballsize/2;
      speedY = -speedY;
    }
    // 玉が下端に到達したらy軸方向を逆向きにする
    else if (y > height-ballsize/2){
      y = height-ballsize/2;
      speedY = -speedY;
    }
  }
  
  // 描画 
  void drawEllipse() {
    // 縁なし
    noStroke();
    
    // 玉の色を指定
    fill(col1, col2, col3, 90);
    
    // 位置とサイズを指定して、玉を表示
    ellipse(x, y, ballsize, ballsize);
  }
}

だいぶやりたいことが表現できるようになってきました。今回で、一旦、Processingの実践は終わりにしますが、また作りたいアニメーションができたら紹介します。
最後まで読んでいただきありがとうございました。