探索顏色漸變繪製演算法(基於Processing語言) 第一部分

EYE發表於2021-07-06

突然間意識到連續變化的顏色在程式中是如何實現的這一問題。沒錯,就想有事找事,我會分好幾部分慢慢探尋,其實筆者也不會,我們一起研究。ok,我們開始!?

第一部分

初始部分就從官方案例來入手學習。官方給了三個相似問題的解決方案:

image

其中LinearGradient是線性漸變,即兩點漸變,RadialGradient是基於圓心漸變,WaveGradient是基於sin函式來繪製漸變色。我們從第一個入手,從兩點開始【拉漸變】。

開始

官方示例很明確是採用繪製多條Line來達成效果,即每根線都緊挨著,在巨集觀上看呈現連續的色塊,即:

/**
 * Simple Linear Gradient 
 * 
 * The lerpColor() function is useful for interpolating
 * between two colors.
 */

// Constants
int Y_AXIS = 1;
int X_AXIS = 2;		//設立橫縱兩軸拉漸變的方法
color b1, b2, c1, c2;

void setup() {
  size(640, 360);

  // Define colors
  b1 = color(255);
  b2 = color(0);
  c1 = color(204, 102, 0);
  c2 = color(0, 102, 153);

  noLoop();
}

void draw() {
  // Background
  setGradient(0, 0, width/2, height, b1, b2, X_AXIS);
  setGradient(width/2, 0, width/2, height, b2, b1, X_AXIS);
  // Foreground
  setGradient(50, 90, 540, 80, c1, c2, Y_AXIS);
  setGradient(50, 190, 540, 80, c2, c1, X_AXIS);
}

void setGradient(int x, int y, float w, float h, color c1, color c2, int axis ) {

  noFill();

  if (axis == Y_AXIS) {  // Top to bottom gradient
    for (int i = y; i <= y+h; i++) {
      float inter = map(i, y, y+h, 0, 1);
      color c = lerpColor(c1, c2, inter);
      stroke(c);
      line(x, i, x+w, i);
    }
  }  
  else if (axis == X_AXIS) {  // Left to right gradient
    for (int i = x; i <= x+w; i++) {
      float inter = map(i, x, x+w, 0, 1);
      color c = lerpColor(c1, c2, inter);   //取兩色之間的差值
      stroke(c);        			//每次劃線都採取相鄰的顏色值
      line(i, y, i, y+h);			//繪製連續的直線
    }
  }
}

程式碼中設定了橫縱兩軸方向性,然後新建了自己的函式setGradient()。引數有起始位置以及寬高數值,還有兩個顏色極值參考,使用lerpColor()算出介於兩顏色間的中間值並定義劃線顏色,然後統一在for迴圈中畫出:

image

那麼我們可以借它的思想來修改。setGradient()重新編寫:

void setGradient(int x, int y, float w, float h, color c1, color c2) {   //方向性選擇去掉
  noFill();
  for (int i = y; i <= y+h; i++) {
    float inter = map(i, y, y+h, 0, 1);
    color c = lerpColor(c1, c2, inter);
    stroke(c);
    line(x, i, x+w, i);
  }
}

然後可以用該方法繪製出特定方向[橫縱兩方向]的漸變色,並且可以實時繪製。如:

setGradient(50, 0, width, mouseY, c1, c2);

接著

如果想不定方向地繪製漸變呢?現在的思路是,隨意的拖拽滑鼠,記錄兩點,一點為起始點選位置,一點為終點拖拽位置,基於這兩點的長度和方向來繪製line線,其中線的顏色基於兩個顏色值進行lerpColor()計算得來。先上程式碼:

PVector p1;
PVector p2;
PVector p3;
PVector p4;
PVector p5, p6;

float len;
color  c1, c2;
int index = 0;
boolean showUI = true;

void setup()
{
  size(800, 600);
  //fullScreen();
  c1 = color(204, 102, 0);
  c2 = color(0, 102, 153);
}

void draw()
{
  background(0);
  //setGradient(50, 0, width, mouseY, c1, c2);

  if (showUI)
  {
    push();
    noFill();
    stroke(250);
    if (p1 != null)
      circle(p1.x, p1.y, 30);
    if (p2 != null)
      circle(p2.x, p2.y, 30);
    if (p2 != null && p1 != null)
    {
      line(p2.x, p2.y, p1.x, p1.y);

      p3 = PVector.sub(p2, p1).normalize().rotate(HALF_PI);
      p3.mult(60).add(p1);
      p4 = PVector.sub(p2, p1).normalize().rotate(-HALF_PI);
      p4.mult(60).add(p1);

      line(p4.x, p4.y, p3.x, p3.y);

      p5 = PVector.sub(p2, p1).normalize().rotate(HALF_PI);
      p5.mult(60).add(p2);
      p6 = PVector.sub(p2, p1).normalize().rotate(-HALF_PI);
      p6.mult(60).add(p2);

      line(p6.x, p6.y, p5.x, p5.y);

      len = PVector.sub(p1,p2).mag();

      for (float i = 0; i <= len; i+=1.0) {

        float x = lerp(p3.x, p5.x, i/len);   //使用lerp函式求得兩點之間的中間差值點位置,下同
        float y = lerp(p3.y, p5.y, i/len);
        point(x, y);

        float x2 = lerp(p4.x, p6.x, i/len);
        float y2 = lerp(p4.y, p6.y, i/len);
        point(x2, y2);

          float inter = map(i, 0, len, 0.0, 1.0);
          color c = lerpColor(c1, c2, inter);
          stroke(c);
          line(x, y, x2, y2);
      }
    }

    pop();
  }
}

void setGradient(int x, int y, float w, float h, color c1, color c2) {
  noFill();
  for (int i = y; i <= y+h; i++) {
    float inter = map(i, y, y+h, 0, 1);
    color c = lerpColor(c1, c2, inter);
    stroke(c);
    line(x, i, x+w, i);
  }
}

void mousePressed() {
    p1 = null;    //復位
    p2 = null;
  
    p1 = new PVector(mouseX, mouseY);
}

void mouseDragged(){

    p2 = new PVector(mouseX, mouseY);      //實時更新第二個點位置
}

void mouseReleased(){

    //p2 = new PVector(mouseX, mouseY);

    println(len);    //將兩點距離列印出來
}

void keyPressed() {
  showUI = !showUI;
}

其中滑鼠的操作通過mousePressed() mouseDragged() mouseReleased()等事件達成。至於漸變方塊的方向計算,具體大小確定,都基於基本的向量運算得來,詳情請參考原始碼。效果如下:

image

image

說一下不足。很明顯,這樣拉出來的漸變帶有空隙,不能完美的填充所有畫素點,和理想狀態差很多,但至少已經達成了初步的想法,在Processing中【拉漸變】!?

改進

我們能不能沿用這個思路來改進一下?借用討巧的方法---矩陣變換。我們先拉出橫平豎直的漸變,然後旋轉它,最後呈現出來。在P5中預設是畫在了一個PGraphics g的圖層上,所以漸變讓其繪製在單獨的一層上方便旋轉等變換操作,修改上文程式碼:

PVector p1;
PVector p2;
PVector p3;
PVector p4;
PVector p5, p6;
PGraphics pg;
float len;
color  c1, c2;
int index = 0;
boolean showUI = true;

void setup()
{
  size(800, 600);
  //fullScreen();
  c1 = color(204, 102, 0);
  c2 = color(0, 102, 153);

  float pgsize = sqrt(sq(width)+sq(height));
  pg = createGraphics(120, (int)pgsize);
}

void draw()
{
  background(0);

  if (showUI)
  {
    push();
    noFill();
    stroke(250);
    if (p1 != null)
      circle(p1.x, p1.y, 30);
    if (p2 != null)
      circle(p2.x, p2.y, 30);
    if (p2 != null && p1 != null)
    {
      line(p2.x, p2.y, p1.x, p1.y);

      p3 = PVector.sub(p2, p1).normalize().rotate(HALF_PI);
      p3.mult(60).add(p1);
      p4 = PVector.sub(p2, p1).normalize().rotate(-HALF_PI);
      p4.mult(60).add(p1);

      line(p4.x, p4.y, p3.x, p3.y);

      p5 = PVector.sub(p2, p1).normalize().rotate(HALF_PI);
      p5.mult(60).add(p2);
      p6 = PVector.sub(p2, p1).normalize().rotate(-HALF_PI);
      p6.mult(60).add(p2);

      line(p6.x, p6.y, p5.x, p5.y);

      len = PVector.sub(p1, p2).mag();

      setGradient(0,0, 60+60, len, c1, c2);   //在新圖層上繪製漸變   注意這裡寬度設為120,預設基於原點開始畫
	  
      push();
      translate(p3.x, p3.y);
      rotate(PVector.sub(p2, p1).heading()-HALF_PI);  //作旋轉矩陣變換
      push();
      //translate(-p3.x, -p3.y);
      image(pg, 0, 0);   //渲染新圖層
      pop();
      pop();
    }

    pop();
  }
}

void setGradient(float x, float y, float w, float h, color c1, color c2) {
  pg.beginDraw();
  pg.background(0, 0);
  pg.noFill();
  for (float i = y; i <= y+h; i++) {
    float inter = map(i, y, y+h, 0, 1.0);
    color c = lerpColor(c1, c2, inter);
    pg.stroke(c);
    pg.line(x, i, x+w, i);
  }
  pg.endDraw();
}

void mousePressed() {
  p1 = null;
  p2 = null;

  p1 = new PVector(mouseX, mouseY);
}

void mouseDragged() {

  p2 = new PVector(mouseX, mouseY);
}

void mouseReleased() {
  //p2 = new PVector(mouseX, mouseY);
  println(len);
}

void keyPressed() {
  showUI = !showUI;
}

新建PGraphics pg,然後繪製函式改成:

void setGradient(float x, float y, float w, float h, color c1, color c2) {
  pg.beginDraw();
  pg.background(0, 0);
  pg.noFill();
  for (float i = y; i <= y+h; i++) {
    float inter = map(i, y, y+h, 0, 1.0);
    color c = lerpColor(c1, c2, inter);
    pg.stroke(c);
    pg.line(x, i, x+w, i);
  }
  pg.endDraw();
}

將漸變線繪製在新的圖層上,這樣呼叫rotate():

push();
  translate(p3.x, p3.y);
  rotate(PVector.sub(p2, p1).heading()-HALF_PI);
  image(pg, 0, 0);
pop();

效果如下圖:

image

image

很顯然,這種方法雖然討巧,不通用,但是效果很理想,沒有之前的細縫問題,而且效率很高,如果寬度調大,可以看成是全幅性的PS【拉漸變】了 ?~
(下圖為Processing全幅兩點漸變效果以及P5製作環境)

image

尾聲

最初的預想效果正是兩點線性漸變,那麼接下來要在此基礎上進行擴充,比如視覺化取點,像ps中的編輯器一樣,其次漸變風格可以切換,如圓型漸變、菱形漸變等,再次是非線性漸變演算法等,好吧,是有難度的,慢慢來吧 ~ 希望可以借這篇文章給讀者一些參考和借鑑,感謝閱讀!!!

相關文章