2021年5月1日 星期六

p5.js 學習筆記 (九) : 動態圖形 (下)

前兩篇屬於互動類的動態圖形, 分別使用滑鼠與鍵盤控制圖形之繪製, 本篇則是要測試自動類的動態圖形, 主要是透過隨機 (亂數) 函式 random() 與雜訊函式 noise().


3. 利用隨機函式畫動態圖 : 

Javascript 的隨機函式 Math.random() 會傳回一個值域為 0~1 之間, 精度為小數點後 16 位之偽隨機數, p5.js 以將其包裝為更好用的內建函式 random(), 並提供其他相關的函式如下表 : 


 random([min=0, max=1]) 傳回介於 min (含, 預設 0) 與 max (不含) 之間的隨機浮點數
 random(arr) 從陣列 arr 的元素中隨機挑一個傳回
 randomSeed(seed) 設定隨機種子 seed 數值使 random() 傳回固定序列隨機數
 randomGaussian([mean, std]) 傳回平均值 mean 標準差 std 之常態或高斯分布隨機數


p5.js 的 random() 函式的參數是可有可無的, 用法如下 :
  • random() :
    不傳入任何參數時, 傳回值為 0~1 之間的浮點數亂數.
  • random(max) :
    只傳入一個參數時為 max, 傳回值 0~max 間之浮點亂數.  
  • random(min, max) :
    傳入兩個參數時, 前為 min 後為 max, 傳回值 min~max 間之浮點亂數.
也可以將一個陣列傳入 random(), 則傳回值是隨機從陣列中挑選的一個元素. 函式 randomGussian() 則是根據指定平均值與變異數之常態 (高斯) 機率分布來傳回隨機數, 其參數也是可有可無的, 用法如下 :
  • randomGaussian() :
    不傳入任何參數時, 表示使用平均值=0, 標準差=1 的常態分布密度函數.
  • randomGaussian(mean) :
    只傳入一個參數時, 表示使用平均值=mean, 標準差=1 的常態分布密度函數. 
  • randomGaussian(mean, std) :
    傳入兩個參數時, 表示使用平均值=mean, 標準差=std 的常態分布密度函數.
而 randomSeed(seed) 則是用來設定隨機種子, 有設定隨機種子為固定常數的話, 每次重新執行程式將傳回相同序列的偽隨機數; 反之沒設的話每次執行得到的偽隨機數序列會不同. 
 
例如 : 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(350, 120);
      background(1, 70, 100);
      fill('orange');
      frameRate(1);      //將圖框速率設為每秒 1 框
      textSize(16);
      }
    function draw() { 
      background(1, 70, 100);
      text('random():', 10, 20);
      text(random(), 90, 20);
      text('random(0,10):', 10, 40);
      var rnd1=random(0,10);     //傳回 0~10 間的隨機浮點數
      text(rnd1, 120, 40);
      text('parseInt(random(0,10)):', 10, 60);  //取整數部分
      text(parseInt(rnd1), 180, 60);
      text('random([0,1,...10]):', 10, 80); 
      text(random(arr), 150, 80);      //從 [0, 1, 2, ...10] 陣列中隨機挑一個元素傳回
      text('randomGaussian():', 10, 100);
      text(random(randomGaussian()), 150, 100);  //常態分佈隨機數
      }
  </script>
</body>
</html>

此例使用 text() 在畫布上顯示 random() 與 randomGaussian() 的亂數, 注意, 在 setup() 中刻意用 frameRate() 函式將圖框率設為每秒一框, 這樣才不會太快來不及觀察數字的跳動, 結果如下 : 




下面是將上面程式加上 randomSeed() 固定隨機種子的結果 :



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(350, 120);
      background(1, 70, 100);
      fill('orange');
      frameRate(1);
      textSize(16);
      randomSeed(2);      //設定隨機種子, 固定隨機序列
      }
    function draw() { 
      background(1, 70, 100);
      text('random():', 10, 20);
      text(random(), 90, 20);
      text('random(0,10):', 10, 40);
      var rnd1=random(0,10); 
      text(rnd1, 120, 40);
      text('parseInt(random(0,10)):', 10, 60);
      text(parseInt(rnd1), 180, 60);
      text('random([0,1,...10]):', 10, 80);
      text(random(arr), 150, 80);
      text('randomGaussian():', 10, 100);
      text(random(randomGaussian()), 150, 100);
      }
  </script>
</body>
</html>

此例除了添加 randomSeed() 指另外, 程式碼與上面範例完全一樣, 但每次重新整理網頁時, 這些亂數出現的序列數值完全一樣, 通常用來作為示範隨機實驗之用, 學習者可以得到與示範者雷同的結果, 下面是此程式第二個隨機序列結果 :




下面是用亂數畫直線的例子 : 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(400, 200);
      strokeWeight(5);  //線條粗細=5px
      frameRate(1);  //圖框率每秒 5 框
      }
    function draw() { 
      background(1, 70, 100);
      for (var i=20; i < width-20; i += 5) {   //掃描 x 軸, 步階=5px
        var r=random(256);     //亂數取得 0~255 之紅色碼
        var g=random(256);    //亂數取得 0~255 之綠色碼
        var b=random(256);    //亂數取得 0~255 之藍色碼
        stroke(r, g, b);     //用亂數設定線條顏色
        line(i, 20, i, 180);    //沿 x 軸 y=180 畫直線
        }
      }
  </script>
</body>
</html>

此例使用 random(256) 產生 0~255 (不含 256) 的 R, G, B 三原色色碼用來設定畫筆的隨機顏色, 然後沿著 y=180 的 x 軸繪製高度 160 px 的直線, 同樣用 frameRate(1) 將圖框率降為每秒一框以免變化太快, 結果如下 : 




下面範例則是在畫布上繪製橢圓, 同樣利用 random() 產生隨機色碼與橢圓寬徑與高徑, 


測試 3-4 : 使用亂數畫橢圓 [看原始碼]  

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(400, 300);
      noStroke();
      frameRate(1);
      }
    function draw() { 
      background(1, 70, 100);
      for(var x=20; x<=width-20; x += 20){    //沿 x 軸遞增圓心位置
        for(var y=20; y<=height-20; y +=20){  //沿 y 軸遞增圓心位置
          var r=random(256);     //亂數取得 0~255 之紅色碼
          var g=random(256);    //亂數取得 0~255 之綠色碼
          var b=random(256);    //亂數取得 0~255 之藍色碼
          fill(r, g, b);   //填滿橢圓顏色
          var w=20 * random();    //亂數取得 0~1 隨機值計算橢圓寬徑 
          var h=20 * random();     //亂數取得 0~1 隨機值計算橢圓高徑
          ellipse(x, y, w, h);   //繪製橢圓
          }
        }
      }
  </script>
</body>
</html>

此例同樣用 random(256) 取得亂數 RGB 色碼, 用來傳給 fill() 填滿橢圓顏色; 另外橢圓之寬徑與高徑也各自用一個 random() 取得 0~1 的亂數乘以 20 作為該橢圓之寬徑與高徑, 結果如下 :  




可見每一個圖框中橢圓大小與顏色都不同, 圖框變換時也隨機變換造型. 

上面範例刻意將圖框率放慢以便觀察變化, 下面則以預設每秒 60 圖框來看看用亂數畫直線的跳動情形, 為了簡化起見, 此例只畫一條直線 : 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var x=0;
    function setup() {
      createCanvas(400, 300);
      stroke('yellow');
      strokeWeight(2);
      }
    function draw() { 
      background(1, 70, 100);
      x=random()*width;      //用亂數計算隨機 x 座標
      line(x, 0, x, height);     //繪製垂直線
      }
  </script>
</body>
</html>

結果如下 : 




可見每秒 60 幅的圖框率讓直線看起來像是隨機出現的直線, 變化非常快速且突兀. 

還有一種稱為雜訊或噪音 (noise) 的隨機序列, 其隨機特徵是體現於每次程序的重新執行與無限大的座標空間, 而非程序內的函式呼叫, 沿著座標軸變化所得到的噪音值可用來繪製平滑變化的動態圖.

下面這個是我從書上看到改寫的有趣的範例,  :



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(400, 300);
      }
    function draw() {
      background(1, 70, 100);
      stroke('orange');
      for (var x = 20; x < width; x += 20) {
        var mx = mouseX / 10;          //依據滑鼠 x 座標計算 x 軸偏移量
        var dx1=random(-mx, mx);   //利用正負偏移量取得 x 軸隨機偏移量 (起點)
        var dx2=random(-mx, mx);   //利用正負偏移量取得 x 軸隨機偏移量 (終點)
        line(x + dx1, 20, x - dx2, height - 20);    //利用 x 軸隨機偏移量繪製直線
        }
      }
  </script>
</body>
</html>

此例以滑鼠 x 座標為計算基準, 除以 10 作為偏移量, 然後以正負偏移量用 random() 分別計算直線起訖點的隨機偏移量, 然後以此掃描 x 軸繪製直線, 因此滑鼠在最左邊 x 接近 0, 垂直線很穩定; 當滑鼠越向右, 偏移量的上下範圍越大, 使得直線因為圖框掃描看起來越斗越大, 結果如下 : 





下面則是沿 y 軸抖動的版本 :


 
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    function setup() {
      createCanvas(400, 300);
      }
    function draw() {
      background(1, 70, 100);
      stroke('orange');
      for (var y = 20; y < height; y += 20) {
        var my = mouseY / 10;           //依據滑鼠 y 座標計算 y 軸偏移量
        var dy1=random(-my, my);    //利用正負偏移量取得 y 軸隨機偏移量 (起點)
        var dy2=random(-my, my);    //利用正負偏移量取得 y 軸隨機偏移量 (終點)
        line(20, y + dy1, width - 20, y - dy2);     //利用 x 軸隨機偏移量繪製直線
        }
      }
  </script>
</body>
</html>

此例只是把上例的 x, y 對調而已, 結果如下 :





下面是從畫布中央開始隨機畫圓形的範例 :


測試 3-8 : 隨機堆疊的圓 [看原始碼] 


<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var speed=2;
    var x;
    var y;
    function setup() {
      createCanvas(400, 300);
      background(1, 70, 100);
      stroke('red');
      fill('cyan');
      x=width/2;
      y=height/2;
      }
    function draw() {
      if(x > width - 10) {
        xmin=5 * speed;
        xmax=speed;
        }
      else if(x < 10) {
        xmin=speed;
        xmax=5 * speed;
        }
      else {
        xmin=speed;
        xmax=speed;
        }
      if(y > height - 10) {
        ymin=5 * speed;
        ymax=speed;
        }
      else if(y < 10) {
        ymin=speed;
        ymax=5 * speed;
        }
      else {
        ymin=speed;
        ymax=speed;
        }
      x += random(-xmin, xmax);
      y += random(-ymin, ymax);
      d=random(20);
      circle(x, y, d);
      }
  </script>
</body>
</html>

此例原始構想來自書上, 但書上的範例有缺陷, 隨機圓可能衝出畫布而消失, 即使加上 constrain() 函式來限制也可能讓圓像鬼打牆一樣沿邊跑, 故此處加上 if else 敘述來限制, 當圓心 x 座標靠近畫布右邊界小於 10 px 時就將 random() 的左邊限擴大 5 倍; 反之靠近畫布左邊小於 10 px 時則將 random() 的右邊限擴大五倍, 對於 y 軸也是如此限制, 這樣就不會出界也不會沿邊打轉了, 結果如下 : 





4. 利用雜訊函式畫動態圖 : 

雜訊 (噪音) 的概念與亂數類似, 它是無限多個像波一樣平滑變化的隨機數, 其變化序列較自然和諧與平滑, 不像 randon() 的隨機序列轉變那麼生硬突兀, 其演算法由 Ken Perlin 於 1980 年代所發明, 稱為 Perlin 雜訊, 由於其變化特性較符合自然界的現象, 常用於計算機圖學或動畫製作中. 

p5.js 的雜訊相關函式如下表 : 


 noise(x, [y, z]) 傳回指定座標軸之 Perlin 噪音值 (0~1)
 noiseSeed(seed) 設定雜訊種子以固定傳回的雜訊序列


Perlin 雜訊在理論上是定義於無限的 n 維空間, 但 p5.js 僅提供 1D (x 軸), 2D (x, y 軸), 至多 3D (x, y, x 軸) 的雜訊序列. 

每次傳入相同的座標呼叫 noise() 時, 它將傳回相同的 0~1 之雜訊值; 但若重新執行程式則會得到不同的雜訊值. 如果想要在重新執行程式時得到相同的雜訊序列, 可先呼叫 noiseSeed() 設定雜訊種子. 

由於 noise() 傳回的值是 0~1 的浮點數, 故實務上需要將其放大, 通常會使用映射函式 map() 來達成此目的, 此函式參數如下 :

map(x, amin, amax, bmin, bmax)

其中 x 是要映射的變數, amin 與 amax 是原來的大小範圍, bmin 與 bmax 則是映射後的範圍. 對於 noise() 函式來說就是 map(noise(), 0, 1, bmin, bmax), 例如要將雜訊映射至色碼 (0~255) 的話, 就是 
map(noise(), 0, 1, 0, 255). 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(350, 120);
      background(1, 70, 100);
      fill('orange');
      frameRate(1);
      textSize(16);
      }
    function draw() { 
      background(1, 70, 100);
      text('noise(0.1):', 10, 20);
      text(noise(0.1), 90, 20);                         //1D 雜訊 (x 軸)
      text('noise(0.1):', 10, 40);
      text(noise(0.1), 90, 40);
      text('noise(100, 200):', 10, 60);             //2D 雜訊 (x, y 軸)
      text(noise(100, 200), 130, 60);
      text('noise(1, 2, 3):', 10, 80);    
      text(noise(1, 2, 3), 110, 80);                 //3D 雜訊 (x, y, z 軸)
      text('noise(100, 200, 300):', 10, 100);
      text(noise(100, 200, 300), 170, 100);
      }
  </script>
</body>
</html>

此例測試了 1D/2D/3D 的雜訊值, 結果如下 :




連續呼叫 noise(0.1) 都傳回同樣的值, 且 draw() 迴圈不斷執行這些值都不會變, 可見在同一個程序中傳入相同參數時 noise() 函式會傳回固定的值, 不像 random() 每次呼叫都會傳回不同的值, 但如果重新載入網頁執行新的程序, 則這些值就會改變, 雜訊的隨機性在此. 

如果用 noiseSeed() 設定雜訊種子, 則隨機序列會被固定, 只要除入 noise() 的參數不變, 即使重整網頁啟動新的程序也會得到一樣序列的值, 例如 :



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var arr=[0,1,2,3,4,5,6,7,8,9];
    function setup() {
      createCanvas(350, 120);
      background(1, 70, 100);
      fill('orange');
      frameRate(1);
      textSize(16);
      noiseSeed(1);    //設定雜訊種子
      }
    function draw() { 
      background(1, 70, 100);
      text('noise(0.1):', 10, 20);
      text(noise(0.1), 90, 20);
      text('noise(0.1):', 10, 40);
      text(noise(0.1), 90, 40);
      text('noise(100, 200):', 10, 60);
      text(noise(100, 200), 130, 60);
      text('noise(1, 2, 3):', 10, 80);
      text(noise(1, 2, 3), 110, 80);
      text('noise(100, 200, 300):', 10, 100);
      text(noise(100, 200, 300), 170, 100);
      }
  </script>
</body>
</html>

此例與前一個範例的差別是在 setup() 中多了一個 noiseSeed() 設定雜訊種子, 所以重載網頁還是會得到相同的值 (因為傳入參數相同), 關掉網頁重新開啟也是不變, 結果如下 :




接著來看看 noise() 的自然噪音效果與 random() 的隨機效果有何差別. 下面範例改用 noise() 來繪製上面測試 3-4 以 random() 畫的亂數 x 座標直線 : 



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var dx=0;
    function setup() {
      createCanvas(400, 300);
      stroke('yellow');
      strokeWeight(2);
      }
    function draw() { 
      background(1, 70, 100);
      x=noise(dx)*width;
      line(x, 0, x, height);
      dx += 0.01;
      }
  </script>
</body>
</html>

此例設定了一個沿 x 軸增量的變數 dx, 傳入 noise() 取得 x 軸的雜訊值 (0~1), 將其乘以 width 後得到直線的 x 座標用來繪製垂直線, 然後在每個 draw() 迴圈中將 dx 增量 0.01, 因此傳入 noise() 的值會是 0, 0.01, 0.02, 0.03, .... 這樣的序列, 這些值會傳回不同的雜訊值, 從而得到不同的 x 座標, 結果如下 : 




可見 noise() 的變化較平滑, 不像 random() 那樣突兀, 適合用來在動畫中模擬動作的推移. 

下面這個測試改編自官網範例, 利用滑鼠座標位置與雜訊繪製隨滑鼠移動而變化之圖形 :



<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>p5.js test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.min.js"></script>
</head>
<body>
  <script>
    var r=0.02;   //坐標軸縮放比率
    function setup() {
      createCanvas(400, 300);
      }
    function draw() { 
      background('black');
      for (var x=0; x < width; x++) {    //走訪 x 軸
        var n=noise((mouseX + x) * r, mouseY * r);    //計算 2D 雜訊
        stroke(n*255);
        line(x, mouseY + n * 80, x, height);  //畫直線
        }
      }
  </script>
</body>
</html>

此例設定了一個全域變數 r 來調整座標軸縮放比率, 以計算 2D 雜訊值 n, 並利用 n 來設定畫筆灰階顏色, 由於 noise() 會對相同輸入座標傳回同樣的值, 因此滑鼠不動, 圖形也不會動, 結果如下 :




沒有留言 :