【CANVAS】うにょうにょ動いて色が変わる円を作ってみた

# まえがき
ある案件で動く円を背景に実装する機会があったので、
canvasでうにょうにょ動く円を作ってみることにしました。
円を動かしたり描画するだけならcssだけで実装も出来たのですが、
動かすと同時に円の色を変更したかったので、
下記コードを参考にごにょごにょして実装してみました。

# 準備
まず、基本的なうにょうにょ動く円のコードはこちらより拝借。
https://blog.nijibox.jp/article/canvas_wobbly_circle/

触ったところは基本設定をお好みに調整したぐらいで、
円の描画に関する処理はそのまま使わせて頂きました。
　
```javascript

// 基本設定
var MAX = 10;           //点の数
var RADIUS = 100;       //描画する円の半径
var FPS = 60;           //1秒あたり何回画面を描きなおすか
var CENTER = {x:200, y:200};     //描画位置
var canvas, ctx, timer, point;
var stats;
var mouseFlag = true;


// 円の描画基本設定
var Point = function(c, r, rota)
{
  this.x, this.y;
  this.centerX = c.x;
  this.centerY = c.y;
  this.radian = rota * (Math.PI / 180); 
  this.radius = r;
  
  this.speed = Math.random() * 10 + 5;   //うにょうにょ動くスピード
  this.r = Math.random() * 2 + 1;        //うにょうにょの柔らかさ
  this.rota = 0;

~ 省略

```

### 表示されたうにょうにょ動く円

拝借したコードを動かしてみると、うにょうにょ動くかわいい円が描画されました。(fillStyleで色を変更可能)

![スクリーンショット 2022-05-29 19.54.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533531/e73c94e5-7027-adb9-e363-d9acca733f0a.png)



# 色を変更する処理を追加

■仕様
指定の時間ごとに指定の３色にふわっとカラーチェンジする


### 色変更の基礎となるコード

```javascript
// 1.RGBのリスト
const colors = [
    {
        r: 20,
        g: 20,
        b: 20
    },
    {
        r: 40,
        g: 40,
        b: 40
    },
    {
        r: 50,
        g: 50,
        b: 50
    },
],
// 2.FPS設定
FPS = 20,

// 3.色が変化するまでのフレーム数 - 2秒ごと
duration = 2000 / FPS;


// 4.duration内の秒数カウンター、現在の目標の色の配列キー
let
    transition = [
      {
        r:0,
        g:0,
        b:0,
      }
    ], 
    color = [
      {
        r:0,
        g:0,
        b:0,
      }
    ],
    time_counter = 0,
    color_key = 0;


// 5.カラーチェンジの差分取得処理
function set_color_transition() {
    const target_color = colors[color_key];
    const basic_color = color_key !== 0 ? colors[ color_key - 1 ] : colors[colors.length - 1];
    transition = {
        r: (target_color.r - basic_color.r) / duration,
        g: (target_color.g - basic_color.g) / duration,
        b: (target_color.b - basic_color.b) / duration
    }
}

// 6.描画実行処理
function update() {
    time_counter++;

    // 色の変化が終わったら切り替える
    if ( duration === time_counter ) {
        color_key !== colors.length - 1 ? color_key++ : color_key = 0;
        set_color_transition();
        // Reset
        time_counter = 0;
    }

    color = {
        r: color.r + transition.r,
        g: color.g + transition.g,
        b: color.b + transition.b,
    }

    draw();
}
setInterval( update, ( 1000 / FPS ));



// 7.描画処理
function draw() {
    var canvas = new HTMLCanvasElement;
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = 'rgba('+ color.r +','+ color.g +','+ color.b +', 1)';

    ctx.fill();
}

```

## 順番に解説
**1. RGBのリスト**
　変更する3色のRBG値を設定

**2. FPS設定**
　setIntervalで描画する際に使用、お好みで。

**3.色が変化するまでのフレーム数を指定**
　どれくらいの時間をかけて色が変わるかをここで指定。お好みで。

**4.変数宣言**
　まず、letで使用する変数を宣言。
　transition・・・RBG値の変化量のための変数
　color・・・描画時の最終的なRBG値のための変数
　time_counter・・・色が変わるタイミングを検知するためのタイマーとしての変数
　color_key・・・RBGのリストを呼び出すためのキーとして使用する変数
　　※transitonとcolorはデフォルト値で0を入れておかないとエラー出る。

**5.カラーチェンジの差分取得処理**
　現在のカラーと次の目標のカラーを宣言し、
　transition変数のr,b,gそれぞれに色の変化量を入れる。

**6.描画実行処理**
　カウンターを呼び出し、色の変化が終わるたびに次の色に変更するため
　set_color_transition関数を再度実行&カウンターをリセットする。
　現在のRBG値にset_color_transition関数で計算した変化量を足してcolorに代入し、
　draw関数を呼び出し実際に描画。

**7.描画処理**
　canvasを指定、ctx.fillStyleにこれまでに計算したRBG値を入れ描画させる。

**8.完成**
　durationで設定した間隔で、円の色がRBG値が変わることによりじんわりと変更されました。



# 最終的なコード
https://awrd.com/sozo-yasashii/　
こちらの円のように、指定の色に変化したらその色を数秒間キープした後に、ふわっと色を変えたかったので、
上記のコードに加え、キープするためのカウンターをもう一つ追加し、実装しました。

また、ぬるぬる動かしたかったのでSetIntervalでの描画ではなく、
requestAnimationFrameを使って描画することにしました。

```javascript
// 円の設定
var MAX = 6; //頂点
var RADIUS = 150; //円の半径
var CENTER = {x:250, y:250}; //描画位置
var canvas, ctx, timer, point;


// 円の描画設定
var Point = function(c, r, rota)
{
  this.x, this.y;
  this.centerX = c.x;
  this.centerY = c.y;
  this.radian = rota * (Math.PI / 180);
  this.radius = r;  
  this.speed = Math.random() + .5; //うにょうにょスピード 
  this.r = Math.random() * .2; //やわらかさ  
  this.rota = 0;

  this.update = function(){
    var plus = Math.cos(this.rota * (Math.PI / 180)) * this.r;    
    this.radius += plus    
    var cos = Math.cos(this.radian) * this.radius;
    var sin = Math.sin(this.radian) * this.radius;

    this.x = cos + this.centerX;
    this.y = sin + this.centerY;
    this.rota += this.speed;    
    if(this.rota > 360){ this.rota -= 360; };
  }  
}

// 初期化
function initialize(){
  var rota = 360 / MAX;
  var i;
  for(i = 0; i < MAX; i++)
  {
    point[i] = new Point(CENTER, RADIUS, rota * i); //点を設定値だけ生成
  }
}


// RGBのリスト
const colors = [
  {
    r: 176,
    g: 230,
    b: 142
  },
  {
    r: 164,
    g: 222,
    b: 222
  },
  {
    r: 133,
    g: 185,
    b: 229
  },
];
// 色が変化するまでのフレーム数 - 何秒かけて色が変わるか(ベスト10)
const duration = 35;
// 色を止める時間
const stopTime = 460;


let
  color = {
    r: 176,
    g: 230,
    b: 142
  },
  transition = {
    r: 0,
    g: 0,
    b: 0
  },
  time_counter = 0,
  time_counter2 = 0,
  color_key = 0;  


// 次の色との差分ゲット
function set_color_transition() {
  var target_color = colors[color_key]; //変わる色
  var basic_color = color_key !== 0 ? colors[ color_key - 1 ] : colors[colors.length - 1]; //現在の色
  
  //現在の色と次に変化する色の値との差  
  transition = {
    r: (target_color.r - basic_color.r) / duration,
    g: (target_color.g - basic_color.g) / duration,
    b: (target_color.b - basic_color.b) / duration 
  }

}

// 描画処理
function update(){

  //全ての点に対して描画処理を走らせる
  for(var i = 0; i < MAX; i++)
  {
    point[i].update();     
  }
  
  // duration内の秒数カウンター、現在の目標の色の配列キー        
  time_counter++;  
  time_counter2++; 

  // タイマーが15になったら色止まる
  if ( duration < time_counter) {
    console.log('色停止中');
    color = {
      r:  color.r ,
      g:  color.g ,
      b:  color.b ,
    }    
  }else{
    color = {
      r:  color.r + transition.r,
      g:  color.g + transition.g,
      b:  color.b + transition.b,
    }
  }
  // タイマーが60になったら色が変わる
  if ( stopTime == time_counter2 ) {
    console.log('色変化中');
    color_key !== colors.length - 1 ? color_key++ : color_key = 0;
    set_color_transition();
  
    // Reset
    time_counter = 0;
    time_counter2 = 0;
  }


  
  //描画実行
  draw(); 

  requestAnimationFrame(update);
}


// 描画設定
function draw(){  
  ctx.clearRect(0, 0, canvas.width, canvas.height); //初期化    

  ctx.beginPath();
  var xc1 = (point[0].x + point[MAX - 1].x) / 2;
  var yc1 = (point[0].y + point[MAX - 1].y) / 2; 
  ctx.moveTo(xc1, yc1);  
  for(var i = 0; i < MAX - 1; i++){
    var xc = (point[i].x + point[i + 1].x) / 2;
    var yc = (point[i].y + point[i + 1].y) / 2;  
    ctx.quadraticCurveTo(point[i].x, point[i].y, xc, yc)
  }
  
  ctx.quadraticCurveTo(point[i].x, point[i].y, xc1, yc1);  
  ctx.closePath();  
    
  ctx.fillStyle = 'rgba('+ color.r +','+ color.g +','+ color.b +', 1)';
  
  ctx.fill();  
}

// 実際に動かす
window.onload = function(e){
  canvas = document.getElementById("canvas"); //canvasを指定
  ctx = canvas.getContext("2d"); //canvasの初期設定
  point = [];  //円の点の配列を用意
  initialize(); //点を初期化
  update();
}

```

# 完成形
完成したうにょうにょ動いて色が変わる円がこちら

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533531/955479b6-0a5e-2318-0ba4-8f1c68f5b4bf.gif)



# 参考
https://kkoo.hateblo.jp/entry/2015/02/18/192007
https://awrd.com/sozo-yasashii/
https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors
https://jablogs.com/detail/67575