p5.js 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/category/tutorial/p5-js/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Wed, 05 Jul 2023 04:48:58 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png p5.js 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/category/tutorial/p5-js/ 32 32 【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 製作流程 https://creativecoding.in/2023/03/02/soul-fish-process/ Thu, 02 Mar 2023 08:59:45 +0000 https://creativecoding.in/?p=3510 此篇文章是前陣子發行的作品 - 靈魂魚 - 背後的製作流程,關於如何使用程式來創作一隻夢幻的數位生物!靈魂魚的作品是用理性的程式創造生命感的探索,也融合了互動生成音樂與視覺,是在互動型 NFT 邊界上的探索之作 。

這篇文章 【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 製作流程 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
本次內容將會以前陣子發布的作品「靈魂魚」作為主題,分為創作故事製作流程分別介紹,此篇主要是製作流程的分享。靈魂魚的作品是如何使用理性的程式創造生命感的探索,也融合了互動生成音樂與視覺,也是在互動型 NFT 邊界上的探索之作 。

靈魂魚的創作故事:p5.js 程式創作 | Soul Fish 數位永恆生命的靈魂魚 – 創作故事

老闆在直播中分享五月份至美國紐約參加研討會的感想,研討會上有像是 6529 等創作家在研討會上做分享交流。讓老闆想到,最一開始接觸 NFT 是因為創作,從創作上也有看到像是 Pak 這樣藝術家的作品,純粹、好玩卻也附有商業性價值,因此開始接觸區塊鏈,參加像是 Consesus 這樣的區塊鏈相關活動。雖然近期不論國內與國外,對於虛擬世界的交易開始有些反感的聲浪出現,但也因此某些事項反而逆勢而起,像是在 Art Blocks 上購買生成式藝術的趨勢逐漸成長。在創作上,老闆也建議,雖然觀眾在某一期間追求的風格會相似,導致大多創作人會往同風格進行創作,但也不要忘了,走出自己的路線才最有可能被記憶紀錄。

為什麼會選靈魂魚當主題與創作概念?

在準備個展期間,有關看到一部與金魚相關影集,觀察到金魚本身的姿態,因此以此為創作靈感來源。取名自老闆的姓名,靈魂魚誕生於虛空之中,由各式各樣的情緒凝結在深海中誕生,他靜靜的在那邊帶來平靜,自己游著,有些靈魂與全身長滿了亮麗的鱗片,有些則默默地緩緩地飄動,身軀幾乎消逝於深海的光線中。

p5.js 靈魂魚實作示範

創作的初始概念,先以 Shader 呈現(下方圖示參考),使用扭曲線條模擬金魚尾巴線條的流動。 Shader 本來是以點到點之間連成的線條組成,在其後,老闆應用 Sin 波的原理,將 Shader 線條隨著波長座移動變化。

再來進到金魚的輪廓姿態,下方第一版的金魚創作其實可以看出大致上金魚的形狀,以及圖樣上模糊的效果,呈現靈魂的感覺。

將金魚的輪廓以線條構出後,發現從身體上半步到尾巴下半部的線條,呈現 Sin 波的形狀,就以多重 Sin 波線條組成第一版金魚。老闆也實際操作一次供大家參考,首先,由左到右畫出連成一條線。

function setup() {
  createCanvas(windowWidth, windowHeight);

  background(100);
}

function draw() {
  background(255);

  beginShape();

  for (x = 0; x < width; x += 20) {
    stroke(0);

    strokeWeight(5);

    vertex(x, height / 2);
  }

  endShape();
}

當線條畫出來之後,如何形成金魚的輪廓形狀呢?由左到右以及上下的的曲線變化,這時就需要加上 Sin 波,呈現出魚的一半輪廓。

function draw() {
  background(255);

  beginShape();

  for (x = 0; x < width; x += 20) {
    let ang = map(x, 0, width, 0, PI * 1.5);

    let y = height / 2 + (sin(ang) * height) / 5;

    stroke(0);

    strokeWeight(5);

    vertex(x, y);

    circle(x, y, 20);
  }

  endShape();
}

得出單一線條後,以相反方向再繪製出另一條線,將完整金魚的輪廓勾勒出來。

function draw() {
  background(255);

  translate(width / 2, height / 2);

  scale(0.8);

  translate(-width / 2, -height / 2);

  for (direction = -1; direction <= 1; direction += 1) {
    beginShape();

    stroke(0);

    noFill();

    strokeWeight(5);

    for (x = 0; x < width; x += 20) {
      let ang = map(x, 0, width, 0, PI * 1.5);

      let y = height / 2 + ((sin(ang) * height) / 5) * direction;

      vertex(x, y);

      circle(x, y, 20);
    }

    endShape();
  }
}
第一版金魚圖示

上述大略是第一版金魚的組成方式,但老闆認為於本身可以再更活潑的方式做律動,於是進行第二版的製作。第二版的示範操作,著重在呈現魚尾巴,如雨水般的流動感外,也將除了輪廓外的線條作範例,呈現擁有底色與動態的尾巴。

function draw() {
  background(255);

  translate(width / 2, height / 2);

  scale(0.8);

  translate(-width / 2, -height / 2);

  for (direction = -1; direction <= 1; direction += 0.2) {
    beginShape();

    stroke(0);

    noFill();

    strokeWeight(5);

    for (x = 0; x < width; x += 20) {
      let ang = map(x, 0, width, 0, PI * 1.5);

      let y = height / 2 + ((sin(ang) * height) / 5) * direction;

      let xx = x;

      if (ang > PI) {
        xx += (ang - PI) * 50;

        xx += sin(xx / 40 + frameCount / 50) * 50;
      } else {
        if (x % 40) {
          arc(xx, y, 100, 100, -PI / 4, PI / 4);
        }
      }

      noStroke();

      vertex(xx, y);

      fill(255, 0, 0, 90);

      arc(xx, y, 5, 5, -PI / 2, PI / 2);

      circle(x, y, 1);
    }

    endShape();
  }
}
第二版金魚圖示

反覆編輯的過程中,從第一、第二版次的基本輪廓呈現,到了第三版大面積的色彩變化(下方圖示參考),以及最後,第四版層疊了鱗片出現發光發亮的質感(下方圖示參考),再繼續堆疊後,變會呈現帶有霧霧的靈魂感覺。這樣的靈魂質感,是如何執行的呢?

第三版金魚圖示
第四版金魚圖示

在第四版金魚上,主要強調使用 blendMode,並進行疊光 screen,就能發現微發光的質感。而後建議以 shader 再去修飾外型,下方程式與圖片為示範範例。

function draw() {
  push();

  beginShape();

  background(0);

  translate(width / 2, height / 2);

  scale(0.8);

  translate(-width / 2, -height / 2);

  blendMode(SCREEN);

  for (direction = -1; direction <= 1; direction += 0.2) {
    noFill();

    strokeWeight(5);

    for (x = 0; x < width; x += 20) {
      let ang = map(x, 0, width, 0, PI * 1.5);

      let y = height / 2 + ((sin(ang) * height) / 5) * direction;

      let xx = x;

      if (ang > PI) {
        xx += (ang - PI) * 50;

        xx += sin(xx / 40 + frameCount / 50) * 50;
      } else {
        if (x % 40) {
          arc(xx, y, 150, 150, -PI / 4, PI / 4);
        }
      }

      noStroke();

      vertex(xx, y);

      fill(255, x / 2, 0, 100);

      arc(xx, y, 5, 5, -PI / 2, PI / 2);

      circle(xx, y, 5);
    }
  }

  pop();
}
第四版金魚圖示

特殊技法介紹

在完成了基本霧面質感金魚後,老闆進到 shader 技法教學。在這邊使用到 shader 的原因是想呈現發亮的螢光感,以及 shader 特有的毛邊質感。在背景中的流體顏色,也是應用 shader去達到此效果,而這樣的概念從老闆早期作品中,就陸續有不同的變化。像是如同宇宙星河般的效果,堆疊的彩色雲層。這樣雲層的概念,又是取自於老闆本身的創作,純粹使用 p5.js 繪製出的雲層效果(下方圖示)

宇宙星河作品變化

從 p5.js 單點渲染,到 shader 即時動態渲染,後面更進化到會滑動的線條,甚至在線條與線條間也出現反光感的效果,呈現細緻,類似地層沉積剖面的質感,也應用這樣的技法呈現在靈魂魚的背景中。

背景範例

老闆也提到,他認為目前應用 shader 最極致的呈現是如同金屬液態的質感創作(下方圖示)

金屬液態圖示

要呈現這樣的效果,就是將背景進行扭曲。像此圖示最初是一張漸層平面圖(下方圖示),形成原理是以多個點點構成一張圖的基底平面,每個點點會有著大小不同的漩渦,再以每個點點在圖上偏移的角度要是多少,與漩渦的方向還有角度構出。

漸層平面圖

不過老闆也說,雖然 shader 很有趣,作品彈性高,但缺點是每次遇到不同型態的需求都會需要重新造輪子,所以目前實際手寫 shader 的人不多,通常都還是以 3D 軟體執行,或是使用 Unreal、Unity 和 shader note editor 等等。以 node based shader editor 為範例,一樣也是經由不同的部分進行疊加,但不一樣的是,已經進行了模組化,讓使用上更快速簡便。

(截圖自 node based shader editor

讓我們重新回到作品靈魂魚的技法上吧!

在創作金魚本體時,我們有使用到一個叫做 blendmode 的融合選項,在 blendmode 中其實有兩種不同的方式能呈現疊色。第一個就是上述有提及的 blendmode(SCREEN),這一項變化就是讓疊色時出現螢光的效果,再來第二個就是 blendmode(MULTIPLY),而這線就是將疊色效果呈現墨水的質感。以下使用範例是以多線條的方式,進行 blendmode(MULTIPLY),所呈現出的細緻金魚。

疊色技法金魚(截圖自影片

以魚鰭來舉例,下方圖示可以看到老闆簡單的說明

金魚魚鰭說明(截圖自影片

魚鰭是依據金魚身體上半部的輪廓點延伸,設定在特定距離上做不同點,再依這些點連成一條又一條組合成魚鰭的線。不過,也因為以線條作為主要組成金魚的來源,所以效能上並不是非常理想。在最終完成品上,可以觀察到,除了金魚本身與線條外,內部也有像是生命線般,會在魚擺動時跟著起舞,這個部分是老闆使用物理模擬操作。在 p5.js 中, Example 中有一個系列叫做 Simulation 物理模擬,在這系列裡,像是 chain,此功能就是將特定點與點之間連成線外,也能進行動態移動,除了作品上,老闆也將其運用在官網上面,將視覺衝擊更加優化。

官網應用(截圖自墨雨設計官網

作品在電腦上完成後,老闆也有將靈魂魚藉由台北藝術中心舉辦的活動,大範圍的投影在藝術中心外,最顯眼的位置做展示。

台北藝術中心展示(截圖自老闆推特

生成式音樂

在圖樣不斷變化的過程中,老闆也在本次創作中增加入生成式音樂做襯托,讓音樂配合游動的靈魂魚產生不同音調。不過生程式藝術結合音樂輔助呈現這個部分,目前是 fxhash 才提供可上架的服務,因為生成式音樂本身檔案較大,像是 artblocks 目前無法接收過大的夾帶檔案。靈魂魚的生成式音樂是將每一節的音樂 sample 檔案與空白格做不同的排列組合,使用 tone.sampler 做完整拼接,拼接完再套用 bpm 讓整段旋律有休止或是不同的音樂變化,最後,再以 reverb 營造具有回音的空間感,讓我們在觀看、收聽時,能夠聽到一段又一段的美妙旋律。

生成式音樂撰寫(截圖自影片

如何將作品上架 fxhash

接下來,我們進入統整上 fxhash 的流程,老闆習慣當作品在 openprocessing 修整到一定程度後,就回到 VsCode 上,使用 fxhash 的模板進行整合。 fxhash 的模板會提供一個固定的亂數,引導你如何使用這套亂數,讓導出的結果都是一樣的(參考連結),而 fxrand 就是提供的亂數名稱,只要給它特定的 hash,就會產生特定的 random。在 VsCode 編輯上,老闆就會針對例如說 features 的這個選項進行編輯。為何是 features 呢?在將生成式藝術上架到 fxhash 時,都會需要標記每一個作品產出的相關特徵,而老闆通常會使用 renderfeatureas() 的方式,將特徵全都渲染出來,但也有時候會因為抓取的值與原本創作呈現的值其實是不同的狀況,這時候就會需要再根據一定的規則算出,我們最終要顯示的是什麼。這樣,在告訴收藏家時,也能以較通俗的說明讓藏家了解作品本身,除了意象外的特徵內容。

fxhash 模板介面(截圖自影片
fxhash 模板 feature 介面(截圖自影片

為了應對較複雜的上架流程,老闆製作了一個名為「previewer」的專案,這個專案可以印出所有生成式藝術作品中的渲染結果。當今天一個作品渲染出了一千張圖片後,這個功能會將這一千張圖片綜合在一個小型網頁上,然後可以在此網頁上進行不同種類的過濾,以進行每一張渲染圖片的檢查。在上架方面,老闆分享了他在fxhash上架時遇到的問題,包括價格、數量和最初設定的販售名單和策略之間的差異,因此他建議同學們在這方面要多加注意。此外,他還提到了作品呈現方面的要求,希望之後製作的每一件作品都能具有足夠的精采度。在前置作業完成後,老闆帶領我們稍微講解了白名單處理流程。在NFT世界中,每當我們購買一件項目,都需要鑄造並購買NFT,但通常NFT在販售時會有數量限制,並非所有人都能鑄造,這就是「白名單」的概念。白名單主要是指只有在名單內的購買方才能鑄造項目,因為他們已經預先保留在名單中了。因此,賣方需要在fxhash上設定白名單流程,首先,要去「mint generation token」瀏覽要上傳的解壓縮檔案,然後按照每個步驟的要求進行上傳或選項勾選,最後,確定上架後就完成了。老闆還建議上架最好一次上完,並且數量控制在256到500之間,這樣對於購買單項作品的買家再繼續購入,帶來的後續效益會更大。老闆也順勢舉例近期執行的「FabDAO 百岳山脈計畫 」,此計畫除了老闆外也與多位藝術家合作,執行公益的 NFT 計畫。在計畫中,老闆的作品便是以 Shader 執行,也示範有無 Shader 的差異。作品內容都十分精彩,有興趣的同學都歡迎至官網做更進一步的了解。

新作品介紹

新作品呈現如同粒子搬形成的圖畫,經由 Shader 再去製造出邊緣不規則的狀態。

最初作品構想(參考連結

作品經反覆編輯製作後,以2D 生成3D 的方向勾勒出虛擬世界感。而此項目將會與老闆致英國作展出。展出的地方名為 Outernet London,主要就是在進行生成式藝術展示的展館,那到時至英國後,老闆也會與共同展出的藝術家們合作交流,後續有任何更新也都會開直播影片與大家分享心得。

預計展出的圖片參考(截圖自影片

總結

在最後也觀眾提出有關近期蔚為風潮,MidJourney – AI 生成藝術的相關問題,老闆也對於這個興起的議題提出了一些看法。其實,不論是生成式藝術與 AI 生成藝術都是經由更快速與便利,與傳統手繪不同的數位化方法進行藝術創作,個人特色或是創意能不能持續表現,才是更值得被記錄的,也希望大家都能勇於嘗試不同的方法做各式各樣的藝術創作探索。

如果想要更深入的了解,目前有與李婷婷講師合開針對 Web3的相關課程,也歡迎直接訂閱老闆的 Youtube 頻道,時不時會有直播 live coding或是近期有持續更新像是 Web3相關影片等等,都歡迎加入並進行踴躍發問喔!

以上就是本次影片和文章的介紹啦!別忘了加入互動藝術程式創作入門課程開始學習吧!還有不要忘了追蹤老闆 Twitter 和訂閱老闆,來點寇汀吧。Boss, CODING please. Youtube 頻道隨時補充新媒體藝術的養份,讓我們一起探索這多元的世界吧!下次見~

此篇直播筆記由幫手熊柑協助整理

這篇文章 【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 製作流程 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 創作故事 https://creativecoding.in/2023/01/30/soul-fish-story/ Mon, 30 Jan 2023 10:42:09 +0000 https://creativecoding.in/?p=3475 在這篇文章中,哲宇想跟大家介紹為什麼會製作靈魂魚,如何透過多重感官觀賞作品,他的完整故事與設計,以及在視覺跟聽覺和互動上的巧思,曾展出的空間形式和未來期待繼續發展的方向!

這篇文章 【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 創作故事 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
本次內容將會以前陣子發布的作品「靈魂魚」作為主題,分為創作故事製作流程分別介紹,此篇主要是製作流程的分享。靈魂魚的作品是如何使用理性的程式創造生命感的探索,也融合了互動生成音樂與視覺,也是在互動型 NFT 邊界上的探索之作 。

靈魂魚的製作過程:【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 製作過程

SoulFish #183

大家好,我是生成式藝術家吳哲宇,四五年前我開始製作動態的數位藝術,像是互動網站、平面設計等開始對踏入互動設計的領域,後來研究所到了紐約 NYU 互動媒體學系讀書,因此認識了 Creative Coding 這個領域而深深著迷,發現程式也有很多的可能性,不僅是為了設計或是達成目標而存在,而是寫程式的本身即是藝術

在這篇文章中,我想跟大家介紹為什麼會製作靈魂魚,如何透過多重感官觀賞作品,他的完整故事與設計,以及在視覺跟聽覺和互動上的巧思,曾展出的空間形式和未來期待繼續發展的方向!

Soul Fish Fxhash 作品專案連結 ( https://www.fxhash.xyz/generative/slug/soulfish )

我曾製作了很多互動型態的作品,像是 CryptoPochi 或是 Seahams,也曾在 Foundation 平台上嘗試傳統繪畫手法探索美術館質感的作品,我發現我最喜歡的是能夠製造有機的狀態,意味著作品的本身的隨機性除了表現在初始屬性之外,也隨著動態生長讓作品在執行的過程中有一定的隨機度,例如粒子的軌跡是由每個時刻的亂數累積決定的,最後繪製成一個完整的作品,得益於亂數原理與現在的區塊鏈技術,我們能夠確保在同一個數位作品中,完美而精準的重現隨機過程,這是我覺得最迷人的部分。

永恆的生命是什麼樣子?

剛開始想到製作靈魂魚的時候,我的靈感來自於深海生物,他們的形體透明的浮游在水中,通常會有一些自己的發光器官來照亮周圍的環境,我覺得這樣的有機感很迷人,也很喜歡一些像是光暈、觸鬚般物理上的物理模擬動態,因此想來以深海魚作為主題來創作一個作品,我想要創造出深海中精細結構帶有透明狀態的魚的感覺,這些魚像是靈魂似的漂浮在幽暗的水域中,因此取名為「靈魂魚」,這些魚能夠永恆的存在與游動,不跟著時間的進行而逝去。

靈魂魚其實也同時取名自我自己的名字 – 吳哲宇最後一個字,他們靈魂魚誕生於虛空之中,在各式各樣的情緒凝結,在深海中誕生,他靜靜的在那邊帶來平靜,自己游著,有些靈魂與全身長滿了亮麗的鱗片,有些則默默地緩緩地飄動,身軀幾乎消逝於深海的光線中。

如何去繪製魚柔軟有機的形體呢?

在製作初期,我仔細的觀察後發現,魚的形狀類似於一個波(sin/cos)的前半部所有狀態疊加的形狀,所以如果我把一條波的線條擷取出來,上下透過不同的比例複製重複,就能夠繪製出外觀。

Sin / Cos 波構成魚的線條

過程中除了計算一個一個點的精確位置之外,也確保不會死板的加入了不同頻率,也就是不同大小的波來建構形體,如果加上了時間函數讓他們慢慢的交錯移動,就能夠形成魚的動態質感。每一個點都有額外再經過噪聲跟波形處理,來創造靈魂般不穩定的形體跟質感,最後再加上從身體到尾部的波狀動態,產生出空間中尾巴來回擺動的效果。

使用波狀函數、自然噪聲以及物理模擬

我製作的第一個版本看起來像實心的卡通魚,接下來逐漸改的透明、加上shader背景,讓所有的線條跟筆觸在繪製時,能夠帶有透明度的重疊,也逐漸加入細節 像是魚骨頭、鬍鬚、魚鰭、尾巴等設計,為了創造出有機感,我混合了材質、形狀跟很多的結構設計,包括中心的魚骨頭一節一節的可以動、生命線的鬍鬚是由20幾個點透過鎖鏈物理模擬的形式來控制,與尾巴跟魚鰭的薄膜每一條線條,都透過精密的計算擺動隨機性的設計,每一隻魚都會有不同的線條數量、大小跟身體結構。

顏色設計上,我讓魚可以橫跨所有的光譜,並且在每一隻魚中,都會有類似泡泡表面的色偏質感,也讓他們的顏色更為有機,隨機性的產生了所有的顏色數值之後,我讓他們再對應到相對的顏色的名字。顏色上會有兩個主要的顏色過渡來建構一隻魚的形體,來確保能夠產生和諧漂亮的漸層,我想要作品呈現出類似極光般的質感以及擁有漂亮的光暈,因此用不同的顏色模式重疊了幾次混合,模擬用光線作為筆觸來去繪製靈魂般的小生物

光線色偏的效果 Soul Fish #123

另外也有一些魚是白色背景的,小時候的我很喜歡生物科學,不同於黑色背景螢光的效果,白色的背景更像是顏料暈染,也很像小時候在做標本時,會將植物的組織切片染色,在顯微鏡底下看到的狀態。

Soul Fish #302
SoulFish #3

背景中有什麼樣的巧思?

在靈魂魚的系列中,有三款主要的背景 – 虛空、波動跟深海,大部分的魚都是虛空背景,能夠讓你在投影或黑暗的空間感受時,能最直觀的感受到作品的神聖感,會將焦點全部都放在魚的身上。波動的背景來自於以前的另一個作品 – 星雲 (220413 Nebula Drift),是由 GPU計算所有的星星之後,動態的扭曲來創造流動的銀河。最後一種背景深海,是模擬由上照下的一束光線穿進深海的質感,在者三款背景中,都有額外加入一些灰塵跟懸浮物,會緩緩地在水中漂動,並會跟著音樂閃爍。

220413 Nebula Drift

如何搭配視覺產生有機的聲音系統設計?

在靈魂魚的作品中,聲音是很重要的部分,在前幾個作品如 Sliderverse 或是 Soul sea 中,我都有嘗試使用 Web audio api 的方式來合成聲響,而在 Soul Fish 這個作品中,我使用了一組鋼琴的 Sample,並在作品開始執行的時候,確定這個作品的和弦進行和旋律線,因為是動態生成的音樂,每一隻魚的旋律線都是獨一無二的,會搭配較高的弦律線與較低的伴奏聲音來構築曲子,每一隻魚都是旋律自動機,能夠合成屬於他的曲調。

SoulFish #59

就像是 生成式視覺一樣,生成式音樂的概念鮮為人知,是打散一首歌的長度跟制式編曲,不是根據既有的譜面來演奏,而是透過規則來去建構音符和和弦的進行,因此可以產生永不停止可以持續生成而有機的聲音編制,在 開啟基因 (B) 的模式中,你能夠看到每隻魚都有自己對應的和弦,是自己的旋律不斷的重複的自動機,另外泡泡破掉時,也可以聽到他觸發高音的鋼琴音符,這也對應到我當初對於作品的期望,是能夠創造視覺與聽覺的詩意同步,當這些所有的感官整合起來觀賞作品時能夠有最完整的體驗。

在互動設計中有哪些可以玩的元素?

在製作作品的最後,我加入了游動與泡泡的設計讓藏家能夠去控制魚的游動時快時慢,以及使用鍵盤往特定的方向移動,我搜尋了一些真實的魚的影片,發現他們其實魚鰭跟尾巴在高速游動時都比我預期的快跟靈活,因此加上了游動的機制之後變的很生動,更像自然中魚類運動的樣子。

除了魚的互動之外,泡泡會不斷的從下方冒上來,你能夠使用滑鼠戳破泡泡,或泡泡碰到魚的時候,會同時觸發高音的鋼琴聲,跟背景的音樂呼應。

發售的狀況與後續調整

在 2023/1/16 順利鑄造完售!

我在 2022 五月時發行了靈魂魚的作品,原本計畫是500版次,原本預期會有很多 Hamily 來 mint 所以調整成 1000 版,近期調整回 400 版確保作品的獨特性,雖然在過去的一年中沒有馬上 mint out,隨著時間過去,終於在 2023 Jan 16, 02:53:29 PM 鑄造完畢,達成了里程碑!

此外,我也曾受邀在台北表演藝術中心的大圓球建築物上投影靈魂魚,在大街上看到魚在圓球上游泳很像實體世界的巨型魚缸。我於台北101的個展 – 「混沌實驗室」中有展出,投影在沈浸式的空間中,沐浴在像是極光般的靈魂魚質感時很浪漫。

2022/5 月在台北 101 的個展 「Chaos Laboratory

除了單件作品的展示之外,我也很喜歡讓靈魂魚以群體的方式呈現,因此後續希望能夠在線上做一個虛擬的水族箱,一個發光的海底世界,並且結合場域投影讓魚有機會再實體空間持續存在與游動,如果能夠結合實體場域,製作一個全互動式的虛擬投影空間,讓人可以彷彿置於深海的體驗黑暗中的光輝,一定會非常感動。

如果你對更多的創作過程有興趣,可以看之前的靈魂魚製作分享直播!

這篇文章 【p5.js 程式創作】Soul Fish 數位永恆生命的靈魂魚 – 創作故事 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) https://creativecoding.in/2022/12/26/p5js-workshop-clab-2/ Mon, 26 Dec 2022 03:12:37 +0000 https://creativecoding.in/?p=3435 老闆在第一屆「Processing 臺灣國際社群日」活動中,受邀為設計工作坊擔任講師,針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享。分別為第一天的基礎練習與第二天的生程式藝術實作,本篇文章內容為第二天的實作練習,希望能讓同學能學習到以基礎的方式,將創意想像以自己的技術實現!

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
臺灣當代文化實驗場 C-LAB 在 2021 年 10 月 19 日到 10 月 24 日,邀請全球 Processing 使用者共同參與台灣第一屆「Processing 臺灣國際社群日」,活動中集結不同藝術家及設計師的觀點,帶領大眾以多方的視角,進入未來新媒體藝術的全新想像。
老闆在這一連串的活動中,除了擔任對談講者外(參考文章:創意程式設計:Processing/p5.js教學與趨勢觀察——王連晟、吳哲宇台美連線對談),也受邀為設計工作坊擔任講師,此次就是針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享,分別為第一天的基礎練習與第二天的生程式藝術實作,希望在兩天的時間內,讓同學能學習到生程式藝術創作的基礎,將自己的創意想像以技術實作呈現!在工作坊正式開始前,老闆提供下列素材讓學員進行課前準備,包含:p5.js 的簡短介紹與 Hahow 上的課程示範。
工作坊會從 p5.js 及工具介紹、p5.js 的開發入門、基本圖形繪製以及變數解說循序漸進。
本篇文章為第二天的生程式藝術實作,依據前一天的基礎練習再更進一階,影片總長兩個小時左右,那我們就事不宜遲開始進入工作坊啦!

上篇這邊走:

課程開始

首先,延續第一天的工作坊進度,老闆先大致瀏覽了昨日有進行創作練習的同學們的作品。有同學提出在創作時的疑問,例如,當設定圖形為筆刷時,雖然圖形是連續性的出現,但當滑鼠動作較快時,圖形就無法連續性的出現,這個問題能如何解決呢?

其實這個問題反向思考來說,就是要讓筆刷連在一起,所以老闆先設定 line(),將已經經過的滑鼠位置,與滑鼠正在進行的位置串起,形成如同在繪圖軟體中,筆刷繪製的效果。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  stroke(255)
  line(pmouseX, pmouseY, mouseX, pmouseY)
}

也可以再依據線條想要調整粗細、顏色變化,或是當滑鼠按下右鍵後再執行筆刷等等變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  stroke(255, frameCount%255, 100)
  strokeWeight(10)
  if(mouseIsPressed){
  line(pmouseX, pmouseY, mouseX, pmouseY)
}

第一階段 – 重複迴圈

短暫回顧完第一天的內容後,老闆進行第二天的主要內容的講解,包含以迴圈與互動、增加不同色彩或是複雜圖形的編輯做分享。先從迴圈與互動,建構創作規則上面開始。老闆以知名藝術家,草間彌生的作品說明建構創作規則的原因。草間彌生的知名創作,大多都建立在「觀察事物的形體」、「重複線條的位置」以及「大量的重複」等手法,去構成她的創作系統。或是莫內及梵谷,拆解其創作手法,就會發現使用連續性的線條和錯落的顏色,去重複堆疊出作品。而上述列出的方法,對程式創作來說,是低成本就能執行的技術,也因此我們能藉由這樣的方式去快速獲得一件具有「美術手法」的創作作品。以迴圈來說,可以設計出相同圖形但是不同排列,具有設計感或是抽象感的作品,像臺灣的傳統花磚,就是利用重複圖案,但是利用不同角度的鏡射,排列出具有韻律感的設計作品。

台灣傳統花磚(圖片來源
使用重複技巧的程式作品(作品連結

那我們也開始迴圈實作吧!

老闆先以最單純的圓圈重複做範例,在還沒學習到如何使用重複迴圈技法時,我們可能就是使用重複貼上同一組程式碼,再在位置上做些許變化執行。但這樣並不便利,也因此又能使用迴圈 for() 來解決此項問題,先來說明 for 的基本語法結構。

for (計數變數的起始狀態; 結束條件; 每次結束後變數如何變化) {
  概念相同,需要重複執行的事情
}

實際帶入數字撰寫示範說明:

function setup() {
  createCanvas(windowWidth, windowHeight);
   background(100);
}

function draw() {
  background(0)
  for(let i=0; i<10; i+=1){
    ellipse(width/2+i*50, height/2, 300, 300)
  }
}

在 i 這個變數下,根據前面設定的數字,起始到結束重複執行。

創作過程中,老闆也提供一些設計小技巧,像是顏色選擇。在創作中,顏色佔了很大的一部分,但當我們對於選色上想要有更快速的方式就是應用設計網站。老闆提供一個叫做 COOLORS 的網站給同學做參考,可以選擇自已想要的主要顏色,點按空白鍵就能產生多種配色選項。

COOLORS 網站首頁(網站連結

那要如何經由程式,去排列出不同顏色卻執行重複動作的流程呢?

老闆以陣列舉例,去執行顏色上的排列。在前一天的工作坊中,也有提到陣列以中括號執行即可。我們先列出需要的顏色,再將其存取起來。當形成一個完整陣列後,再套入重複圖形的顏色當中。

let colorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(0)
  noStroke()
  for(let i=0; i<50; i+=1){
    fill(colorList[i%5])
    circle(width/4+i*(mouseX/10), height/2, 300 -i*6)
  }
}

在程式創作的好處就是能夠快速地進行大量編輯,例如如果要替換配色,只要在陣列中變換色票即可,又或是,想要變化圖形的位置,我們可以從觀察程式中哪裡有重複進行編輯,以下示範:

先設置一個 function() ,將會重複使用到的程式包在裡面,再將設置的代表文字,套入 draw() 中,簡化重複的程式,增加多樣化編輯。

let colorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  drawCircles(random(width), random(height))
}

function drawCircles(posX, posY){
  for(let i=0; i<50; i+=1){
    fill(colorList[i%5])
    circle(posX+i*(mouseX/10), posY, 300 -i*6)
  }
}

不想制式的都從同一個顏色開始,可以在顏色上使用 random,讓顏色隨機出現,也需要 int 套入在設定前,讓出現的數字得以四捨五入為整數。

for(let i=0; i<count; i+=1){
  fill(colorList[int(i+random(10))%colorList.length])
  circle(posX+i*span, posY, 300 -i*(300/count))
}

增加了隨機的亂數,作品是否又多增加了不同的詩意呢?老闆在這邊提出了有趣的看法,他認為,程式雖然可大量重複的行為,但再加入無法預測的亂數後,出其不意的呈現,就會在視覺上趨向傳統所謂的藝術感。

迴圈教學到這邊,休息十分鐘讓同學提問以及練習。有同學提問,為何在此次練習中,老闆使用 let 而不是 var 做函數參數呢?老闆在這邊鼓勵大家,雖然在 javascipt 當中是以 var 做使用,但 let 會更加嚴謹。舉例說明,用在函數定義範圍時,let 就是在大括號的範圍內具有作用。但如果是 var 的話,是以 function 為範圍,所以定義較大,判斷上較不容易。在這十分鐘,老闆也持續的做小技巧教學。例如,要如何隨機對應不同顏色組合,做圖形上的顏色變化,我們必須先設定不同的 colorList 設定顏色組合,再依據需求變更。

延伸閱讀:使用 let / var / const 宣告變數的差異(鐵人賽:ES6 開始的新生活 let, const – 卡斯伯

let ColorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]
let ColorList2 =["#c9cba3","#ffe1a8","#e26d5c","#723d46","#472d30"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  drawCircles(random(width), random(height), mouseX/5, 50, ColorList2)
}

function drawCircles(posX, posY, span, count=50){
  for(let i=0;i<count;i+=1){
    // 從清單中選取顏色
    let colorIndex = int(i+random(10))%ColorList2.length
    fill(ColorList2[colorIndex])
    circle(posX+i*span, posY, 300 -i*(300/count))
  }
}

可以依照上方陣列設定,變化要使用 ColorList 或是 ColorList2 執行效果。另外,設定畫布的大小可以讓畫面更接近畫作感覺。

再來進入到使用變數紀錄狀態,如同一個簡化邏輯的過程,就像是假如今天我們設置了三種不同筆刷效果供使用,要如何快速切換這些效果呢?最快的方式又是設定變數。設定筆刷 paintMode,並且設定筆刷在不同模式時的效果,像是隨機大小變化,就能使用 random 進行。

let colorList = "32373b-4a5859-f4d6cc-f4b860-c83e4d".split("-").map(clr=>"#"+clr)
let colorList2 = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(clr=>"#"+clr)
let colorList3 = "6622cc-a755c2-b07c9e-b59194-d2a1b8-fff".split("-").map(clr=>"#"+clr)
let colorList4 = "2f2d2e-41292c-792359-d72483-fd3e81-fff".split("-").map(clr=>"#"+clr)

let paintMode = 0

function setup() {
  createCanvas(1000,1000);
  background(0);
  paintMode = int(random(2))
  // print(colorList[2])
}

function draw() {
  // background(0)
  noStroke()
  if (paintMode==0){
    drawCircles(
      random(width),random(height),
      mouseX/5, 50, random([colorList, colorList2])
    )
  } else if (paintMode==1) {
    drawRects(
      random(width), random(height),
      mouseX/5, 50, random([colorList3, colorList4])
    )
  }
}

function mousePressed(){
  paintMode++
  paintMode = paintMode %2
}

function drawCircles(posX, posY, span, count=50, useColorList){
  for(let i=0; i<count; i+=1){
    let colorIndex = int(i+random(10) )%useColorList.length
    fill(useColorList[colorIndex])
    circle(
      posX+i*span, posY,
      300 -i*(300/count)
    )
  }
}

function drawRects(posX, posY, span, count=50, useColorList){
  for (let i=0; i<count; i+=1) {
    let colorIndex = int(i+random(10) )%useColorList.length
    fill(useColorList[colorIndex])
    rectMode(CENTER)
    rect(
      posX+i*span, posY,
      300 -i*(300/count)
    )
  }
}

應用重複的紋理與符號,改變物件的大小、方向或是出現的頻率,讓畫面的效果更加豐富。

第二階段 – 色彩

在 p5.js 裡常用的色彩系統有 RGB 與 HSB 兩種,RGB 是指顏色紅、綠與藍三種交疊後產生,HSB 是由色相、飽和度與亮度三種維度組成。應用 p5.js 的 Color 物件,並填上以像是一個數值、RGB 數值、填入顏色名稱、填入色票號碼或是宣告色彩模式後再填入色碼等多樣化模式都是得以進行的。了解基本設定後,就來進行實際操作示範吧!

先由圖形的顏色變化開始,假設要執行顏色漸變的圓形,可以先設定色彩系統 HSB,再依據可能使用滑鼠讓顏色變化的技巧,進而使顏色做漸進變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  fill(mouseX%360, 100, 100)
  ellipse(mouseX, mouseY, 500, 500);
}

多方應用 random,使其在顏色或是圖形大小變化上,都能做一定範圍的控制。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  noStroke()
  fill(random(0,50), random(50,100), 100)
  circle(mouseX, mouseY, random(300));
}

或者是控制在特定範圍內,即使是 random 的效果也能控制想要的方向,像是指定色相的呈現,顏色偏移在指定的範圍內。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue =random(0,150)
  for(let i=0; i<20; i++) {
    fill(startHue+i*5, random(50,100), 100)
    circle(mouseX+i*20, mouseY, 200-i*10);
  }
}

另一種常見的顏色使用功能為  blendMode(模式名稱),與 p5.js 當中的疊色模式十分相近, 帶入不同的模式時,疊色的效果就會不同,以 SCREEN 作範例,就是亮系的疊色效果,老闆調整變化速度與圓形顆數示範疊色效果,過程中也可以應由透明度的調整變化出不同的感覺。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  frameRate(5)
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue = random(0, 150)
  blendMode(SCREEN)
  for(let i=0; i<1; i++){
    let currentStartHue = (startHue+mouseY)%360
    fill(currentStartHue+i*5, random(50,100), 100,1)
    circle(mouseX+i*100, mouseY, 200-i*10);
  }
}

或是設定變數,讓在不同位置圖形有指定的顏色,限制色相的範圍,使其在生成時顏色為固定效果,以及加上填塞與單純線條的選項,讓作品更多活潑變化性。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  // frameRate(5)
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue = random(0, 150)
  blendMode(SCREEN)
  // blendMode(MULTIPLY)
  for(let i=0;i<1;i++){
    let xx = random(width) + i*100
        yy= random(height)
    let currentStartHue = (startHue+yy/3)%360
    if(random()<0.5){
      fill(currentStartHue+i*5, random(50,100), 100, 1)
      noStroke()
    } else {
      stroke(currentStartHue+i*5, random(50,100), 100, 1)
      noFill()
    }
    circle(xx, yy, random(100));
  }
}

但,當在使用重複多種圖案顯示時,有些圖案並沒有顯示在畫布上,這時可以應用 pushpop,將圖案分別開來指定顯示,或是設定 frameRate() 圖形的出現速度變化,控制一秒中出現幾次。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  // frameRate(5)
}

function keyPressed(){
  if (key == "1") {
    frameRate(5)
  } else if (key == "2") {
    frameRate(30)
  } else if (key == "3") {
    frameRate(60)
  } else if (key == "4") {
    frameRate(120)
  }
}

function draw() {
  colorMode(RGB)
  background(0, 1)
  push()
    colorMode(HSB)
    noStroke()
    blendMode(SCREEN)
    let startHue =random(0,50)
    // blendMode(MULTIPLY)
    for(let i=0; i<1; i++){
      let xx = random(width)+i*100
          yy = random(height)
      let currentStartHue = (startHue+yy/3)%360
      if (random() < 0.5) {
        fill(currentStartHue+i*5, random(50,100), 100, 1)
        noStroke()
      } else {
        stroke(currentStartHue+i*5, random(50,100), 100, 1)
        noFill()
      }
      circle(xx,yy, random(50,500));
    }
  pop()
}

練習過程中,老闆也陸續回答同學的發問。

【問題一】請問 Openprocessing Editor 與 p5.js Editor 有所不同嗎?

回答:其實兩者的使用方式以及效能都是大同小異的,皆為提供給創作者即時顯示創作效果的地方。但老闆覺得在整體設計上,openprocessing 對於初學者來說較友善且容易上手,但每個人的感受不一,建議同學都能去嘗試看看自己比較適合哪種應用介面。

【問題二】請問 p5.js 可以做到如同 Team Lab 或是 梵谷光影展那樣的內容嗎?

回答:可以,但在 3D 的呈現上,因為函式庫的內容還沒有那麼完整,在建構呈現效果以及動畫上可能就需要再多加著墨。

【問題三】請問對於每行的分號加或不加有什麼心得或是建議嗎?

回答:老闆在撰寫時習慣不加,因為在 p5.js 撰寫時目前並不是必要的。但是如果同學們擔心的話,在網路上也都能找到自動幫你每行加分號的執行系統。

【問題四】請問 p5.js 有類似於 opencv 進行影像處理或是人臉辨識的進階 library 嗎?

回答:有的,但並不是官方的,名為 ml5.js。在臉部的輪廓或是 pixel 的呈現都是能夠抓到的。

臉部輪廓抓取 api 介面。(截圖自 ml5.js

老闆也有執行過類似的創作,抓取人體位置輪廓,以線條和原點呈現。(作品參考)也建議如果有更多函示庫應用的相關資訊想了解,可以觀看上一篇的影片介紹喔!(影片連結

第三階段 – 畫布操作

此階段以畫布操作,使同學在畫複雜圖形的時候,能以簡化的方式執行,快速且不需多樣計算,重點即是如何簡化繪製圖形的位置。

老闆先以要在畫布上畫多重圓形為舉例,比較先前執行與畫布操作後的差別。這邊應用到三角函數,因為需要使用三角函數去執行圖形的位置,而三角函數的取得需要使用到角度 degree() 功能去計算,今天以要得到一個在中心點圓形的45度角運行的圓形,以下為設定了角度、半徑與函數設定的程式。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  background(0)
  fill(255)
  let r = 200
  let ang =frameCount/100
  let x = r*cos(ang)
  let y = r*sin(ang)
  ellipse(width/2, height/2, 100)
  ellipse(width/2+x, height/2+y, 100)
}

此類的設定模式,會出現兩種問題。一,算式偏多,應該可以更加簡化。二,如果要畫多個圓形,就必須重複設定位置。對於這一系列的設定,老闆先解釋角度後說明如何應用,為何畫布的操作便是可以簡化此應用的方式。

我們在操作時,是為了要設定以一個圖形為中心,並依據此的某個特定角度,使用 translate() 進行其他圖形繪製。假設,今天要為中心的圖形,其實位在左上角,那我們移轉畫布就是讓左上角的圖形變成畫布的中央。(移轉畫布解說參考

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  background(0)
  fill(255)
  let r = 300
  let ang = frameCount/100
  let x = r*cos(ang)
  let y = r*sin(ang)
  translate(mouseX, mouseY)
  ellipse(0, 0, 100)
  ellipse(x, y, 400)
  ellipse(x, y, 300)
  ellipse(x, y, 200)
  ellipse(x, y, 100)
  fill(255, 0, 0)
  rect(200, 200, 50, 50)
}

此種移轉畫布的方式,讓圖形的總體位置相關參數,並不會跟程式邏輯混和在一起。偏移結束後,接下來進到畫布的旋轉。當設定為同種圖形在不同的位置重複出現,使用 translate()roatate() 繪製出多個圖形。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  fill(255)
  let r = 300
  let ang = frameCount/100
  translate(width/2, height/2)
  rotate(ang)
  translate(200, 0)
  rect(0, 0, 200, 200)
  fill(255, 0, 0)
  rect(200, 200, 50, 50)
}

嘗試多做點不同變化,像是旋轉的幅度以及圖形的大小隨著旋轉有所改變。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  push()
    fill(255)
    let r = 300-frameCount/2
    let ang = frameCount/100
    let currentScale = 1-frameCount/500
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

在顏色排列上,除了 random 的隨機排列外,也能使用 noise() 執行持續性的變化。什麼是 noise?其代表著連續性的、可預測性的亂數。依據 perlin 噪聲圖上的不同位置,去影響每個設定,給相同點的時候,出來的結果會是一樣的,所以就可以嘗試將 random 更換成 noise 去觀察結果的差異。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  push()
    colorMode(HSB)
    fill(noise(frameCount/50)*100,100,100)
    let r = 300-frameCount/2
    let ang = frameCount/20
    let currentScale = 1 - frameCount/500 + random(0.1, 0.5)
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

將 noise 與 random 共同使用,讓顏色變化不死板,有多樣性的呈現。使用 hue 色相作為變數,去引導顏色為漸進的變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  push()
    colorMode(HSB)
    let useHue = (random(20)+noise(frameCount/50)*100 +frameCount/3)%360
    fill(useHue,100,100)
    let r = 300-frameCount/2
    let ang = frameCount/20
    let currentScale = 1 -frameCount/500 +random(0.1,0.2) +noise(frameCount/50)/2
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

今日工作坊總結

在本次工作坊的下篇,我們總共學習到了三大項主題,包含迴圈的建構創作規則、變化色彩使用與留下痕跡,到最後的進階應用,畫布的變化。上述這些應用,老闆在做後一個示範中,將上述的功能都包含在裡面。由左到右畫一連串的長方形,這些動作是可以被累加的。先使用 translate 將圖形重複的執行,再設定變數以及出現的規則,將圖形依照滑鼠位置做變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(100);
}

function draw() {
  // ellipse(mouseX, mouseY, 20, 20);
  fill(random(255), 200, 200)
  translate(0, height/2)
  rectMode(CENTER)
  for(let i=0; i<50; i++){
    rotate(map(mouseY, 0, height, -0.5, 0.5))
    translate(50,0)
    scale(0.95)
    rect(0, 0, 500, 500)
  }
}

雖然快接近結束,同學們還是十分積極的詢問問題,像是,如果想針對資料進行統計的視覺化呈現,有什麼相關的 lib 或 sample 可以參考嗎?比如地區人口統計?針對此問題,老闆認為目前 p5.js 的函數庫在視覺化上的效果還不是最齊全的,可能會使用其他函數庫如 D3.js 來進行,但也還是可以參考先前的相關課程,針對 p5.js 在視覺化上的 api 應用教學,來嘗試進行(參考文章)。

最後,也不免俗的與同學分享老闆在互動藝術程式創作的其他課程,或是影片推廣給大家。包含 Creative Coding TW – 互動程式創作台灣站 的專業文章網站、社群軟體上的即時資訊分享 老闆 來點寇汀吧。 Boss,CODING please(臉書)、老闆來點寇汀吧 Boss, Coding Please (臉書)、老闆,來點寇汀吧。Boss, CODING please(Youtube 頻道),當然還有想要進一步正式開始創作,歡迎加入 互動藝術程式創作入門課程 開始學習!本次分享就到這邊結束啦,歡迎各位一起踏入程式與藝術交織的世界!

此篇直播筆記由幫手熊柑協助整理

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) https://creativecoding.in/2022/12/26/p5js-workshop-clab-1/ Mon, 26 Dec 2022 03:10:20 +0000 https://creativecoding.in/?p=3403 老闆在第一屆「Processing 臺灣國際社群日」活動中,受邀為設計工作坊擔任講師,針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享。分別為第一天的基礎練習與第二天的生程式藝術實作,本篇文章內容為第一天的基礎練習,希望能讓同學能學習到以基礎的方式,將創意想像以自己的技術實現!

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
臺灣當代文化實驗場 C-LAB 在 2021 年 10 月 19 日到 10 月 24 日,邀請全球 Processing 使用者共同參與台灣第一屆「Processing 臺灣國際社群日」,活動中集結不同藝術家及設計師的觀點,帶領大眾以多方的視角,進入未來新媒體藝術的全新想像。
老闆在這一連串的活動中,除了擔任對談講者外(參考文章:創意程式設計:Processing/p5.js教學與趨勢觀察——王連晟、吳哲宇台美連線對談),也受邀為設計工作坊擔任講師,此次就是針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享,分別為第一天的基礎練習與第二天的生程式藝術實作,希望在兩天的時間內,讓同學能學習到生程式藝術創作的基礎,將自己的創意想像以技術實作呈現!在工作坊正式開始前,老闆提供下列素材讓學員進行課前準備,包含:p5.js 的簡短介紹與 Hahow 上的課程示範。
工作坊會從 p5.js 及工具介紹、p5.js 的開發入門、基本圖形繪製以及變數解說循序漸進。本篇文章為第一天的基礎練習介紹,影片總長兩個小時左右,那我們就事不宜遲開始進入工作坊啦!

什麼是 Creative Coding ?

老闆以自我介紹作為開場,從大學的電機工程背景到至紐約就讀新媒體藝術相關研究所,而開始了一連串生成式藝術的創作。對於什麼是 Creative Coding,老闆說自己也是在不太理解的狀況下開始的。是用程式創作藝術嗎?那到底程式創作出來的藝術,最終呈現出來的效果又會是如何呢?這些想必是初接觸生成式藝術的人都會提出的問題,因此老闆提供不同的案例分享,作為答覆的參考。
以目前最常看到的例子來說,視覺上,像是原先平面的設計海報以動態的方式呈現增加趣味性,或是模擬傳統媒材(如水墨)等等,藉由制定規則,讓每一次不同互動產生出的作品都有不一樣的呈現,這使每件藝術作品都是獨一無二的,並且在參與度提升的情況下,成為對互動者更特別的藝術存在。還有在互動網頁上,像是老闆成立的墨雨設計工作室,就有許多應用相關技術的案例,例如與台灣啤酒合作的網頁。或是現在有很多網頁,當使用者將滑鼠向下移動要瀏覽整個畫面時,也會有不同的動畫呈現,整個瀏覽體驗就像是一個完整的時間軸。

老闆作品 Slient Sky 參考範例(連結
台灣啤酒合作案例(連結

在執行設計創作時,老闆也提出自己建立規則的範例供大家參考,以下方作品為例,從中心散開的圓心,其規則並不複雜。先從中心設定隨機數量生成的粒子,粒子會向外成長擴散,並且在成長的過程中變化成不同的顏色,以及施加不同的外力讓粒子扭曲變形。綜合以上這四項變化,就會得出每一次都是不同作品的互動創作。

視覺平面的傳達外,像是在美術館裡的互動式藝術,或是在街角呈現的大型裝置都是可以用 p5.js 執行的。

p5.js 是什麼?

Processing 為一種開源式語言,最初的目的是希望能以視覺化的方式幫助非程式設計師學習程式語言,而後演變成在電子藝術或是互動式設計上都能發現其應用。

Processing 網站首頁(截圖自 Processing網站首頁 )

p5.js 即是將 Processing 以 Javascript 語言做應用,使其可在網路瀏覽器上進行直接創作,免費及開放資料庫的特性,降低了學習門檻,也吸引更多人進入創意程式設計 Creative Coding 的領域。

p5.js 網站首頁(截圖自 p5.js網站首頁 )

此外還有像是 MaxMSP,以 Creative Coding 應用在音樂的視覺化效果呈現。其撰寫時與 p5.js 的不同在於,p5.js 一般會以程式碼顯示,但在 MaxMsp 是直接以視覺的圖案的方式做呈現,將語言命令可視化成一塊一塊的圖形,再依位置的安排去創造不一樣的效果,串接出不同的流程,進行音樂的創作。

MaxMSP 網站首頁(取自 MaxMSP 網站首頁)

或是 Sonic Pi,一個使用文字創作音樂的程式設計環境。

Sonic Pi 網站首頁(取自 Sonic Pi 網站首頁 )

Touch Designer 也屬於視覺為基礎的語言,採用圖形化的介面讓使用者創作,實現多媒體特效。

Touch Designer 網站首頁(取自 Touch Designer 網站首頁 )

不同的創作環境,都說明 Creative Coding 不論在視覺上,或是聲音上都有豐富的創作應用。而 p5.js 代表的就是更全面的環境,整合了從3D 到聲音都有涵蓋。有同學提到,既然 p5.js 與 processing 的語法幾乎相容,那還有什麼狀況下選擇 processing 比較好呢?老闆回覆是,其實 processing 在過去的優勢現在幾乎在 p5.js 上都能進行,建議剛入門的同學以好操作上手的 p5.js 開始創作入門,這樣到後面如果真的因為 pocessing 可能在效能上有相對好一些,要回 processing 上進行創作,入門時間也不會過長。歸類以下 p5.js 的優勢,簡化了語法、減少了無程式背景的創作者的入門門檻,以及多項資料庫可使用或是簡易自行建立。

課程開始

首先,登入進 Openprocessing ,直接點擊連結便能開始以創作,或是也可以追蹤老闆及不同的創作者,參考不同的創作概念。進入個人創作頁面後,便能看到 「Create a Sketch」,每建立一個 Sketch 都代表一個新的創作實驗場域。點進去創作介面會先看到基本程式裡分成兩個部分, setupdraw 兩種不同的規則。前者代表在畫布上的設定,一次性的準備動作,後者代表不斷在畫布上重複著畫上新東西的動作。在創作同時,我們也可以在右方欄位做 layout 設定,讓撰寫程式同時在同一版面看到作品呈現。

一、顏色置換示範

推薦使用 Chrome 瀏覽器的擴充功能「 ColorZilla 」,可以挑選想要的顏色並顯示出色票。當有了色票後,便可以依上色元素不同,在 background() 或是 fill() 做更換。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255,181,35);
}

function draw() {
  circle(mouseX, mouseY, 20);
}

二、繪製圖形、圖形大小與顏色變更示範(筆刷變換)

基本變化像是圖形中不填色 noFill()、去掉圖形外框 noStroke(),或是 strokwWeight() 控制線框粗細。另外,當你要暫停一行程式的功能,請按下 ctrl 鍵與 /,就能看到反灰並且暫停功能的程式碼。

基本圖形控制可以至 p5.js 資料庫中做多方搜尋,以本次範例橢圓形 ellipse 為主,有設定位置是特別指定或是以滑鼠 mouseX、mouseY 代替,與形狀的長寬大小設定。

進入到顏色階段,針對圖形總共有兩個部份可以做編輯,分別是圖形內的顏色以及圖形線條的顏色。fill() 提供圖形內的顏色,stroke()則負責圖形線條的顏色,在顏色選取上,能使用顏色色票、顏色名稱(請記得要加上雙引號,例如:”white”)以及 rgb 顏色(CSS Colors 網站)。如果想要使顏色變化根據互動而有更多元的呈現,可以將 rgb 色彩數值的其中一項更改成滑鼠位置,如此一來顏色就會根據滑鼠位置變化。

滑鼠效果除移動中會有變化外,在 if 與 else 間進行條件判斷,就能在不同情況中呈現不同的效果,下方是根據滑鼠點按之間的效果呈現。

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)
  
  if (mouseIsPressed){
    ellipse(mouseX, mouseY, 100,100);
  } else {
    stroke(255)
    noFill()
    rect(mouseX,mouseY,100,100)
  }
}

如果要增加顏色多樣性,可以使用 random() 設定需要的顏色數值,讓不同的顏色隨機出現。也另外補充個撰寫程式小技巧,可以在選擇多組相同數字時,使用 ctrl 鍵加上 D 進行多重選取減少時間。

上述是由變換位置去進行筆刷的變更,那藉由時間來變更的部分,可以使用 frameCount() 編輯。以示範案例中的橢圓形舉例來說,假使要讓橢圓形的 Y 軸隨著時間來進行筆刷大小的變化,在相對應的 Y 軸編輯位置放上 frameCount 就可以執行,下方範例:

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    ellipse(mouseX, mouseY, 200,frameCount);
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

講解到這邊後,老闆給線上參與的同學進行五分鐘的線上練習,題目為:請設計出一個隨機大小為 0 到 50 的圓形筆刷。與此同時,也開放同學在製作時有任何問題能當場詢問,以下問題歡迎參考。

【問題一】 在 p5.js 裡面是否有質感筆刷可以應用或是製作呢?像是油漆筆刷般的效果?

答:有許多方式可以處理,目前先以比較便捷的方式做示範。

先至網路上找尋相關筆刷的圖樣,比如此次是製作油漆類型,那就搜尋相近的筆刷圖案,先下載圖片。

下載其中自己比較喜歡的圖片後,再至 openprocessing 的編輯頁面上傳圖片檔案。便可以開 使進行質感操作啦!

首先,需要先使用 var 宣告與 preload function 預載入,設置質感 texture 的出現,再設置筆刷 image 進行應用,程式與呈現效果如下:

var paintTexture

function preload(){
  paintTexture =loadImage("20190806101953yop172.png")
}

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    image(paintTexture,mouseX,mouseY)
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

【問題二】 如何分享在 openprocessing 上創作的作品呢?

答:將作品儲存後,可以使用網址直接分享,或是 openprocessing 作品頁面右上方的分享功能都可以使用喔。

接下來進到線上練習的分享時間!

經過五分鐘的練習後,同學應用上述老闆教學說明到的不同應用進行創作。老闆也針對這些分享作品提供不同的建議,或是告訴其他同學這些不同創作是怎麼製作的。例如,有作品使用透明度進行不同編輯,在這邊透明度的應用分為兩種:第一種為圖案顏色的透明度,此類型是在圖案填色 fill() 中,除了 rgb 填寫外,再增加一個代表透明度的數字,變成 rgba 模式,例如 fill(255,255,255,10),代表顏色具有百分之十的透明度;另一種情況為圖形筆刷在背景留下軌跡與否,在 background() 編輯時,設定透明度,讓背景每秒覆蓋的顏色不會完全覆蓋,如此一來就能留下上一秒的軌跡。

三、變數解說教學

進入應用變數的環節,老闆以設計橢圓形為範例說明。如何快速的將兩個不同的,寬與高等比例放大呢?我們需要設置一個櫃子來存放將這些即將運用到的變數,在需要的時候就可以快速抓取使用。運用 let 設置存放變數,舉例來說 let r=50,後續在 draw 裡面,使用到 r 時程式就會帶入 50 這個數字。

延伸閱讀:使用 let / var / const 宣告變數的差異(鐵人賽:ES6 開始的新生活 let, const – 卡斯伯

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0,50);
}

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    let r =random(50,200)
    ellipse(mouseX, mouseY, r,r);
    image(paintTexture,mouseX,mouseY)
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

四、如何繪製形狀

經由上述的基礎設定、顏色選填、變數教學與基本圖形設定解說後,接下來進行繪製形狀的教學。形狀除了先前提到的圓形或是方形外,還有線條、3/4圓甚至是自己建立的幾何形狀,其實都能應用函數的變化去繪製出。老闆先以線條舉例,函數式為 line(x1,y1,x2,y2),分別代表連成線條的不同點,而線條粗細是以 strokeWeight() 調整。那該如何得知座標呢?可以先透過設定文字,顯示出滑鼠在座標上移動的位置。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(255)
  strokeWeight(10)
  line(0,0,500,500)
  textSize(50)
  text(mouseX + "," + mouseY,mouseX,mouseY)
}

老闆也提到如何將圖案融合文字,呈現對話框般的資訊。先設定隨機出現的長方形,並且將方角設定成圓角,使其更具有對話框的設定。並且運用變數設定,使文字與對話框在相同的設定下隨機出現。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  // background(255)
  strokeWeight(2)
  let x = random(width)
      y = random(height)
  rect(x,y,200,50,20)
  text("HELLO",x,y+40)
}

或者使用特定條件呈現更多層次的畫面,像是加上 if()else() 的二元判斷變化,以及使用變數讓不同文字隨機出現在不同的對話框中。執行的同時,老闆也會分享一些在寫程式的快捷鍵和小撇步,假使不想只選取單一文字,而是同行多個文字選取,可以根據游標位置,按下 alt 和 shift 再向右或左按下鍵盤左右鍵。或是因為撰寫項目太多,想要進行分類時,可以將無效文字標註在程式中,以防後續搞混或忘記。另外,建立好形狀繪製的基礎後,我們也可以控制出現的頻率。其中一種作法是使用  frameRate() 來編輯。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
  frameRate(10)
}

function draw() {
  strokeWeight(2)
  let x = random(width)
      y = random(height)

  if(random()<0.5){
    fill(0)
    rect(x,y,200,50,20)
    textSize(30)
    fill(255)
    text(random(["HELLO","TAIWAN","CREATIVE","CODING"]),x+20,y+40)
  } else {
    // 指定文字大小
    textSize(30)
    // 白底黑字
    let myText = random(["CLAB","TAIWAN","當代文化實驗場"])
    let w=textWidth(myText)
    // 對話框
    fill(255)
    rect(x,y,200,50,20)
    fill(0)
    // 文字
    text(myText,x+20,y+40)
  }
}

瞭解圖形與文字的隨機變化後,繼續進行隨機自行繪製形狀的產生,從三角形隨機顏色變化為例,以及自行繪製的圖形示範。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  strokeWeight(random(5))
  line(0,0,width/2,height/2)
  fill(random(0,255),random(0,255),200)
  triangle(0,0,width/2,height/2,mouseX,mouseY)
}

在隨機變化中,老闆習慣設定顏色要在哪種色調或色系間做變化進行設計,建議以這樣的方式進行才比較不易出現奇怪的顏色配置問題。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  strokeWeight(random(5))
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)
  triangle(0,0,width/2,height/2,mouseX,mouseY)
}

再來是自行繪製的圖形解說,老闆以王冠繪製示範。多邊形製作需要使用到 beginShape() 代表起始位置、 endShape() 代表終點位置,以及 vertex() 說明在起始與終點間會經過哪些點。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(255)
  // strokeWeight(random(5))
  strokeWeight(50)
  line(0,0,width/2,height/2)
  // line(width/2,height/2,mouseX,mouseY)
  // ellipse(mouseX, mouseY, 20,20);
  fill(random(50,100),random(100,150),200)
  // triangle(0,0,width/2,height/2,mouseX,mouseY)
  fill("#ffcc00")
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470,200)
    vertex(350,350)
  endShape(CLOSE)
  // 繪製座標文字
  fill("red")
  textSize(50)
  text(mouseX+","+mouseY,mouseX,mouseY)
  // circle(mouseX, mouseY, 20);
}

或者是互動式的顏色應用,將王冠與背景顏色或是新增圖形都設定成會依據滑鼠位置的改變進行更動。使用預設函數改變線條粗細、圖案顏色或是模式等等,去繪製圓形、方形,甚至是多邊形,提供作者與互動者不同的作品溝通模式。像是在皇冠上,老闆進行顏色、大小和特別圖形繪製,主要是根據 mouseXmouseY 的設定。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(mouseX/4,100,50)
  strokeWeight(20)
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)
  fill(255,mouseX/4,0)
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470+mouseX/10,200)
    vertex(350,350)
  endShape(CLOSE)
  fill(255)
  circle(325,480,100)
  circle(600,480,100+mouseY/10)
  fill(0)
  circle(325,480,30)
  circle(600,480,30)
  line(355,550,500,520+mouseX/30)
  // 繪製座標文字
  fill(255)
  textSize(50)
  text(mouseX + "," + mouseY, mouseX, mouseY)
}

說明完後,又進到同學們自行練習的時間啦!

那也一樣,老闆趁這時解答同學問題。其中有一位同學詢問到霓虹效果該如何呈現,老闆拿出先前做過類似效果的作品說明示範(作品連結)。主要使用 shadow() 的疊色模式編輯,總體上分為兩個步驟,一個是指定形狀中的顏色,另一個是周圍光暈的 dawingContext() 代表周圍光暈的顏色及顯現的程度。老闆以王冠作品進行實際操作, 將王冠周圍的光暈,從最原始的單一色調設定,進階到隨滑鼠移動改變顏色,到最後是點按滑鼠鍵進行光暈顏色變更,多樣化的創作方式供同學參考。

var lightColor

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(mouseX/4,100,50)
  strokeWeight(20)
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)

  // 畫王冠
  fill(255,mouseX/4,0)
  drawingContext.shadowColor = lightColor;
  drawingContext.shadowBlur =30;
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470+mouseX/10,200)
    vertex(350,350)
  endShape(CLOSE)

  // 畫眼睛
  fill(255)
  circle(325,480,100)
  circle(600,480,100+mouseY/10)
  fill(0)
  circle(325,480,30)
  circle(600,480,30)
  line(355,550,500,520+mouseX/30)

  // 繪製座標文字
  fill(255)
  textSize(50)
  text(mouseX + "," + mouseY, mouseX, mouseY)
  // circle(mouseX, mouseY, 20);
}

function mousePressed(){
  lightColor = color(
    random([
      "#F2C400",
      "#F9C784",
      "#8AE1FC"
    ])
  )
}

後續特別應用,例如想讓 frameCount() 的變化是限制在特定範圍內,可以使用 constrain() 限制 frameCount() 隨機大小的範圍,或是使用 sincos 設計。 sin 是指一到負一之間,譬如 sin(frameCount)*50+200,就可以得知是設定在 負五十加兩百到五十加兩百間。或是,如果要設定由左到右的數字大小,使用 map 設定初始範圍與限制範圍進行有意識的創作編輯。

結語

主要實作教學結束後,老闆也傳授了一些編輯使用上的常用事項。例如,openprocessing 封面設定是可以藉由上方編輯按鈕,進行特定封面圖案設定,如下圖圖示,在此頁面按下右上方 edit 編輯即可操作。

也有同學問到,mouse 的座標是否可以連結到外部感應器的點位?可以開啟編輯頁面左方需連結感應的部分,但如何應用就需要後面再多做分享。或是,想要將 p5.js 的效果呈現在網頁上,能如何操作呢?老闆說明,最快的方式是使用 iframe,先至分享區域做壓縮檔下載、直接使用 emded code 程式碼崁入,或是使用 GitHub 將程式碼轉換成 index 檔案後,到 setting 的 GitHub Pages 上傳,並且再選擇 main 之後存檔,就能藉由網址看到自己已 p5.js 創作的互動程式以網頁的方式呈現。上述精彩的課程教學以及同學們多樣化的提問,就是說明互動藝術程式創作還有好多應用面向可以進行探討,與繼續發掘它不同的樣貌。閱讀到這邊,相信你也對互動藝術程式抱持著更高的興趣了吧!就讓我們接續收看下半場的精彩解說吧!

但,如果你已經迫不急待要開始進行創作,加入 互動藝術程式創作入門課程 開始進行深度學習吧!還有不要忘了 追蹤老闆 Twitter 和訂閱 老闆,來點寇汀吧。Boss, CODING please. Youtube頻道隨時補充新媒體藝術的養份,讓我們一起探索這多元的世界!

此篇直播筆記由幫手熊柑協助整理

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
圓圓圈圈:利用迴圈呈現重覆的美 https://creativecoding.in/2022/07/28/cc-ch5-circle/ Thu, 28 Jul 2022 11:32:00 +0000 https://creativecoding.in/?p=2951 「重複」,在藝術表現或日常中很常見的概念,在Creative Coding的領域內更是大量被使用的表現手法,跟大家分享如何在程式創作內加入「重複」概念及實作。

這篇文章 圓圓圈圈:利用迴圈呈現重覆的美 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
在進入正題之前,先請大家觀察下方的圖片。花磚雖然不是台灣傳統建築的「專屬記憶」,但飄洋過海以後落地生根,逐漸在這裡發展出自己的特色。在單一磚片上設計出花紋,透過大量且重複地排列,形成獨特的空間氛圍,也連結成我們小時候的回憶。

「重複」──在藝術表現或日常中很常見的概念,或存在於大自然的晶體結構中、或存在於阿嬤家的廁所廚房內。以音樂而言,在同一首曲子中常可聽到反覆出現的旋律;以文學而言,詩歌中也常出現反覆的句子或單字等;以視覺而言,經由單一的圖案或形體,上下左右不斷的重複予人單純、規律的感受。

在Creative Coding的領域內重複更是大量被使用的表現手法,像是改變物件大小、方向、時間、頻率,或是重複粒子、文字、符號、紋理、以及動作等,善用重複可以讓畫面看起來有整體感。今天就要跟大家分享如何在程式創作內加入重複的概念,以及如何用利用「迴圈」快速製造相同的物件。

各式各樣的汽水罐整齊排列,形成一個奇妙的汽水秩序世界。 
Soda Fantasy @ Che-Yu wu
各式各樣的汽水罐整齊排列,形成一個奇妙的汽水秩序世界。
Soda Fantasy @ Che-Yu wu

目標介紹

  1. 瞭解程式中達成重複的「for 迴圈」概念與常見的應用方式
  2. 瞭解如何在重複中套用規則與創造變化
  3. 嘗試不同的重複概念應用與變化
  4. 結合變數、使用者操作創作重複的作品

迴圈可以重複執行同一組程式碼,幫我們處理不斷重複的事情。 for 迴圈 – 需要以程式碼執行次數作為迴圈的條件。

// for 的基本語法結構
for(計數變數的起始狀態; 結束條件; 每次結束後變數如何變化)
  {
    概念相同,需要重複執行的事件
  }

// 舉例
for(var i=0 ; i < 3 ; i++){
  console.log(i)
}

根據上面的概念,如果我們要在畫布上每 100px水平間隔畫一個圓且重複三次,可以有下面兩種寫法:

// 寫法一:手動複製貼上=人工迴圈
ellipse(0,100,30,30)
  ellipse(100,100,30,30) // 相較於上面,右移動了 100 px
  ellipse(200,100,30,30) // 相較於上面,又右移動了 100 px

// 寫法二:使用程式語言的 for 迴圈
for(var i=0; i<3 ; i++){
  ellipse(100*i, 100, 30, 30)  // 計數變數 i 只要小於 3 時都會做這件事,直到不滿足條件則停下並跳出該程式區塊
}

俗話說的好,能交給別人做的事就不要自己做,善用迴圈可以幫助我們快速處理類似的事件。除了單層的for迴圈以外還有巢狀個for迴圈結構:

for(var i=0; i<2; i++){
  for(var j=0; j<2; j++){    // 在每一個 i 中,都會這層迴圈完再跳往下一個 i
    ellipse(50*i, 50*j, 40)  // 分別於 (50*i, 0) 和 (50*i, 50) 畫圓,畫完之後再往下一個 i 繼續重複
  }
}

在上述的例子中,可以把巢狀迴圈想像成時鐘的分針跟秒針,秒針要跑完一圈,分針才會前進一格。同樣地,外層迴圈i要等內層迴圈j全部跑完才會再+1,所以當j是0和1的時候,分別在(50i, 0) 和 (50i, 50) 的地方畫圓,接著j又會從0開始印,直到外層迴圈結束。(延伸閱讀:[JS] 迴圈筆記

以上了解迴圈的概念後,我們試著用迴圈來創作屬於自己的作品吧。由於Ju編最近去了一個有趣的音樂表演活動Rangeselector,由台灣另類電子搖滾樂團眠腦主演,串連影像、空間動態感測技術所編製的新型態live session,除了隨著節奏跳動的幾何粒子等動態畫面外,觀眾也可以透過移動位置來和展場的視覺產生互動。聽完這場很不一樣的live現場後,覺得或許也可以將自己喜歡的音樂當成主題,來進行Creative Coding的創作。

這次的靈感來源為台灣樂團The Fur. – Friday Love,復古的曲風配上輕快活潑的歌詞,行版的節奏讓人忍不住想跳起舞來,就像一顆又一顆的彩球。藉著這首甜蜜的歌曲,來個類專輯封面的創作吧!

首先我們挑選一組具有復古感的色票,這邊選用的以粉色、紅色系為主,配上米白和橘咖等輔色。既然是專輯封面,那歌名是一大重點,為了凸顯歌名我們試試看在Illustrator製作標準字並輸出,所以在規劃作品草稿時可分為靜態的標準字圖片和動態生成的彩球兩個部分。

復古的配色常以暗濁的暖色調為主,明度和純度都比較低。

如何在OpenProcessing導入圖片呢?我們可以把圖片上傳到第三方平台Imgur,然後把圖片當作背景來使用。

接著來準備下方空白處的彩球,定義一個陣列colors把挑選好的色票放進去,再準備一個circles的空陣列。我們要做一組4*4共16顆的彩球,這邊定義5個變數x, y, d, num, col,分別代表彩球在x軸和y軸的產生位置、彩球的大小、個數和顏色,利用巢狀迴圈的結構產生共16顆:

function setup() {
  createCanvas(600,600);
    let seg = 4;
    let w = width / seg;
    for (let i = 0; i < seg; i++) {
      for (let j = 0; j < seg; j++) {
        let x = i * w + w / 2; //x軸產生位置
        let y = j * w + w / 2; //y軸產生位置
        let d = random(0.5, 1) * w; //彩球大小
        let num = int(random(1, 4)); //彩球個數
        let col = random(colors); //彩球顏色
        for (let k = 0; k < num; k++) {
          circles.push({
          x: x,
          y: y,
          d: d * 0.7,
          c: col
        });
        movers.push(new Mover(x, y, d * 0.5));
      }
    }
  }
}

把準備好的彩球畫出來,注意圖層的順序必須畫在background上方,為了避免跟畫面上的其他元素重疊,位置和大小也要稍微調整一下。

function draw() {
  background(img);
  translate(width / 2, height / 2); // 定位在畫布中間
  scale(0.5); //範圍大小
  translate(-width / 2, -height / 4); // 定位在畫布偏下

  for (let c of circles) {
    fill(c.c); // 用變數c的顏色來填充
    circle(c.x, c.y, c.d); // 畫在變數x,y的位置、變數d的大小
  }
  noStroke();
}

現在我們有一顆又一顆的基本彩球了,看起來是不是很像糖果呢?為了替畫面增加一些動態性,我們在彩球旁邊增加幾顆移動的小彩球,這邊需要分為三種function:定義小彩球參數的constructor、畫小彩球的show、移動小彩球的move。在OpenProcessing的語法庫中,有一些常見的數學常數可以做使用,比如下方用到的PI,可以很快速地引入使用。

let movers = [];

class Mover {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.cs = this.r * 0.4; //第一種大小 
    this.cs0 = this.r * 0.4; //第二種大小
    this.t = random(100);
    this.off = 0;
    this.tStep = random(0.01, 0.05);
    this.ang = random(PI); 
    this.aStep = random(-1, 1) * 0.01;
    this.col1 = random(colors); //第一種顏色
    this.col2 = random(colors); //第二種顏色
    while (this.col1 == this.col2) {
      this.col1 = random(colors);
    }
  }

  show() {
    push();
    translate(this.x, this.y);
    rotate(this.ang); //旋轉的角度
    stroke(255); //加上白色邊框以凸顯小球
    fill(this.col1); //
    if (this.cs0 * 0.15 < this.cs) {
      fill(this.col2); 
      circle(this.off, 0, this.cs);
    } //錯開大球和小球的顏色
    pop();
  }

  move() {
    this.off = map(sin(this.t), -1, 1, -1, 1) * this.r; //彩球正面的角度
    this.cs = map(cos(this.t), -1, 1, this.cs0, 0); //彩球背面的角度
    this.t += this.tStep; //彩球的速度 
    this.ang += this.aStep; //彩球的圓周速率
  }
}

記得在 setup 的地方把 movers 推入 Mover,這樣我們就有移動的小彩球囉!

movers.push(new Mover(x, y, d * 0.5));

這樣就完成啦,希望大家喜歡這次的分享唷,一起試試看如何利用迴圈創作吧!

成品請往這邊走 👉🏻 https://openprocessing.org/sketch/1560267

歡迎加入互動藝術程式創作入門(Creative Coding)線上課程,課程中你可以認識程式與互動藝術產業應用,開啟對工程跟設計的想像,學會使用 p5.js 開發互動介面,整合繪圖、音訊、視訊、文字、3D、互動與機器創作完整的作品,並將創作輸出應用在個人品牌或網站、主視覺或海報,甚至互動裝置、遊戲與教材製作等場景,讓你對進修的資源與路線更有方向。

有興趣的朋友歡迎加入我們的臉書社團,第一時間接收活動報名消息,希望不久的將來,就能看到你跟大家分享你的生成式藝術創作囉!

此篇文章由 Jeudi Kuo 撰寫

這篇文章 圓圓圈圈:利用迴圈呈現重覆的美 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】Sweet Trap 甜蜜陷阱 https://creativecoding.in/2022/04/25/p5-js-sweet-trap/ Mon, 25 Apr 2022 05:52:00 +0000 https://creativecoding.in/?p=2568 生成式藝術迷人的地方就在於它的程式邏輯、它的數學藝術呈現,有秩序卻又充滿了隨機。吳哲宇的<甜蜜陷阱>便是這樣有機的創作作品。此文帶大家一步步從 sin 波慢慢建構出目眩神迷的 p5.js 創作。

這篇文章 【p5.js創作教學】Sweet Trap 甜蜜陷阱 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
色彩繽紛的幾何形狀不斷輻合旋轉,細細的線條卻像是毒刺一樣。動態改變形狀一下密密麻麻一下稀疏鬆散,多變的風貌讓人甜蜜陶醉卻又像是陷阱般危險的感覺!

<甜蜜陷阱>成品圖
<甜蜜陷阱>成品圖

本文是【p5.js 程式創作直播】210731 Sweet Trap 甜蜜陷阱 的直播影片筆記,大家如果想要和老闆一起 chill 度過寫程式的時光,可以打開影片開啟這趟心流之旅,或者…繼續往下看!

這次直播是用 openprocessing 網頁平台來撰寫,打開網頁就可以開始 coding 創作了!成品在這裡。

直播時老闆聊到了設計的作品被剽竊的故事,但也因為這樣被 Art Blocks 平台看見。現在正是NFT藝術品百花齊放的時候,大家在這支影片中可以了解到生成式藝術迷人的地方,甚至開始創作自己的 NFT 。來吧,這次的作品運用到不少關於角度的概念,讓我們一起建立一個秩序又隨機的世界!

這次直播筆記會帶大家學會

  • 將三角函數的概念運用在極座標,透過計算角度來畫出花瓣狀的軌跡
  • 旋轉與移動座標系,簡單定位軌跡中的每個點
  • 利用存取滑鼠的座標,自由變化圖形的樣貌
  • 計算角度簡單繪製出三角形狀
  • 存下自己喜歡的色票並隨機呈現顏色,每一次播放都會產生不同顏色組合
  • 運用noise()製造出有規律的隨機

會使用到的 API

這次作品會使用以下的 API,大家可以先感受一下每個 API 的功能,還沒完全理解的話也沒關係,後續透過一步步實作會漸漸學會運用的。

  • createCanvas(width, height): 創建畫布,參數中分別傳入寬跟高。
  • background(colorCode): 加上背景色,可依照文件傳入色碼參數。
  • noStroke(): 取消繪製圖形的邊框。
  • colorMode(): 定義顏色的方式,預設為 RGB 顏色,HSB 模式依序要填入的值則為(色相, 飽和度, 明度)。
  • fill(): 選擇填入的顏色,依照 colorMode 選擇的填色模式填入對應的參數。
  • ellipse(posX, posY, width, height): 在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形。
  • rect(x, y, width, height): 以 (x, y) 的位置為左上角的點,畫一個寬度 width 高度 height 的方形(如果要畫正方形的話,即寬度=高度)。
  • triangle(x1, y1, x2, y2, x3, y3):以三個頂點座標繪製出三角形。
  • text(str, x, y):在(x,y)座標呈現出文字str
  • rotate(angle): 將座標系依照該角度旋轉
  • translate(x, y): 將座標系移到(x,y) 上
  • push(): 儲存目前畫筆設定的狀態
  • pop(): 恢復畫筆在push()時儲存的狀態,與push()合併使用
  • random(): 沒有傳參數時,會返回一個0~1之間的隨機浮點數。
  • noise(x,[y],[z]): 產生0~1之間的浮點數。傳入的x,y,z代表座標,會在一、二、三維的Perlin noise噪聲空間取出對應該座標在0~1之間的值。這個方法會使得相近的座標取到的值也相近,比較有連續性,不會像random()每次取值都是完全隨機的。有興趣的同學也可以延伸閱讀相關資訊:2D Noise – Perlin Noise and p5.js Tutorial
  • map(value, start1, stop1, start2, stop2, [withinBounds]):會回傳某個位於start1~stop1範圍的值如果對應到start2~stop2範圍中是多少。最後一個參數的意義可以參考文件描述。
  • pow(n,e):計算n的e次方
  • blendMode(mode):讓圖形相互以不同的方式疊加色彩,有各種模式可以選擇,例如:DARKEST、LIGHTEST、DIFFERENCE等。
  • image(img, x, y, [width], [height]):以img材質在x,y座標畫出圖片。
  • pixelDensity(val):增加像素的密度,預設像素的密度是與螢幕相同。

跟著老闆開始動手做

1. 簡單的起手式

在 openprocessing 網頁右上角可以Create a Sketch,會來到一個已有預設程式碼的新頁面。從這裡開始我們來認識setup()、draw()與mouseX()、mouseY()。

  • setup(): 可以視為程式環境的初始化,在每次按下撥放鍵開始執行時,會呼叫 setup() 裡的程式碼一次。
    • createCanvas(width, height):創建畫布,參數中分別設定寬跟高(單位是px)。也可以直接寫(windowWidth, windowHeight),會自動判斷螢幕的寬高變為滿版畫布。
    • background(colorCode):設定背景顏色,依照 p5.js 文件說明傳入不同的色碼參數表示方式,這邊寫的 100 是代表 0(黑)~255(白) 之間的灰色值 100。
  • draw(): 在不按停止播放的狀況下,會不斷重複執行在 draw() 裡面的程式碼,要繪製的內容主要會寫在這裡。
    • ellipse(posX, posY, width, height):在 (posX, posY) 上繪製寬高 (width, height) 的橢圓形,如果 (posX, posY) 帶入 (mouseX,mouseY) ,表示取滑鼠的座標當作繪製圓圈的位置。
function setup() {
  createCanvas(windowWidth, windowHeight);
  background(100);
}
function draw() {
  ellipse(mouseX, mouseY, 20, 20);
}

2. 繪製sin波形

這次老闆從自己日常紀錄的創作靈感筆記中,選擇創作類似花的圖案,可以用 sin 波來實踐-想想 sin 波的形狀是不是很像一片片的花瓣?正式的說法是,我們將在極座標(0~360度)上畫出 sin波 ,下圖的 θ 是從 0~360 度,可以看到不同的算式真的會讓 sin 波變成花瓣呢!

變成花瓣的 sin 波
變成花瓣的 sin 波

我們就先從畫出一個正常的 sin 波開始吧!老闆喜歡在畫布上再畫一個黑色矩形當作背景,這個可以寫在setup()中畫一次就好。接著在 draw() 裡透過 for 迴圈,讓 x 由左到右,每隔 20 就畫一個白色的圓點來描繪 sin 波波形。以下是 API 的相關參數意義。

  • fill(colorCode):設定接下來要填入形狀的顏色,色碼0為黑色,色碼255為白色。
  • rect(0,0,width,height):width,height是兩個可以方便取用的變數,儲存曾在createCanvas(w,h)中設定的寬高值。矩形的繪製會以(0,0)為左上角頂點,往右為寬、往下為高畫出與畫布一樣大的矩形。
  • noStroke():設定接下來畫出的圖形沒有邊框。
  • translate(0,height/2):座標系是從整個畫布的左上角為原點(0,0),往右方x越大,往下方y越大,我們運用translate()把座標系的原點改到(0,height/2),接下來座標的計算都可以重新依這個新原點為準。
  • frameCount:從程式開始執行畫面不斷更新的次數,其實也就是draw()反覆執行的次數,所以frameCount是以固定的速度增加其數值。
  • ellipse(x,y, 50):在(x,y)畫出寬高皆為50的圓形。

y = sin(x) 可以繪製出 sin 波形,如果我們想要調整波形的樣貌,可以進一步運用不同的參數來調整!在這裡如果把它表達成 y=sin(x/a+b)*c 來思考,會發現 a 越大波長越長;而 b 如果是個變動的數字,程式反覆執行時,就會讓波上的點開始垂直動起來(不然會是靜止的)。老闆即運用 frameCount/100 來當作垂直運動的速度,大家可以試試看如果將 frameCount 除以10、50 會有什麼不同?另外由於 sin() 只會給出 -1~1 之間的值,因此可以乘以一個倍數 c 來控制 y 的高度,在這裡用的是 height/5。大家可以在 y=sin(x/a+b)*c 中試驗不同 的a、b、c 參數來創作你喜歡的 sin 波模樣喔!

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
}
 
function draw() {   
  fill(255)
  noStroke()
  translate(0,height/2)
  for(var x=0;x<width;x+=20){
    let y=sin(x/10+frameCount/100)*height/5
    ellipse(x,y, 50)
  }
}
動態的 sin 波
動態的 sin 波

如果我們想要綜觀不同的參數設定會讓 sin 波長得如何不同,這時候可以好好運用滑鼠座標 mouseX、 mouseY 啦!老闆這邊想要觀察的是圓點取樣的多寡還有波的長短,因此利用 mouseX 由小到大的值對應為圓點取樣的間隔, mouseY 的大小則對應著波長的長短,並分別由變數 span、freq 把對應的值儲存下來。大家可以試驗滑鼠在不同的位置是如何影響波的樣貌?你也會發現很有趣的是當取樣的點(由 mouseX 決定)由多至少時,本身波長很短也會變得像波長很長的波,甚至看似多條 sin 波複合。

  • map(mouseX,0,width,0,100,true):將原先 mouseX 的值本來從 0~width 大小,對應到 1~100 之間,最後的 true 是當 mouseX 的值超出 0~width,也嚴格限制值落在 1~100 之間。
function setup() {  
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
}

function draw() {
  fill(0)
  rect(0,0,width,height)
   
  fill(255)
  noStroke()
  translate(0,height/2)
  let span = map(mouseX,0,width,1,100,true)
  let freq = map(mouseY,0,height,5,100,true)
  for(var x=0;x<width;x+=span){
    let y=sin(x/freq+frameCount/100)*height/5
    ellipse(x,y, 5)
  }
}
偵測滑鼠位置控制 sin 波疏密
偵測滑鼠位置控制 sin 波疏密

3. 把 sin 波轉到極座標上

為了要讓 sin 波變成花狀,我們要運用極座標,把 x 當成 0~360 度,sin(x) 的值當成長度。首先,先把座標系的原點改移到畫布中央 (width/2,height/2) 。接著很有趣的是,老闆不直接算出圓點的位置,而是再度移動座標系:讓座標系旋轉 x 度數再移動整個座標系讓原點移至 (sin(x),0) ,因此每一個圓形只要繪製在原點 (0,0) 上就好了!

座標系移動過後都要讓它回到原位再做下一次的移動,所以移動前都先用 push() 儲存目前的設定。每次旋轉+移動完座標系後,再透過 pop() 恢復原廠設定,下一次就又會從原先設定的狀態也就是座標系原點畫布中央開始!

  • push()、pop():前者存下當前的畫筆設定、後者恢復 push() 時儲存的設定。通常我們會把想要大動特動的畫筆設定寫在 push() 與 pop() 之間,執行完想繪製的東西後就能夠恢復成原本冷靜的狀態。
  • rotate(x/width*2*PI):座標系的旋轉。根據設定的 angleMode,可以填入弧度或是角度,為了避免搞混,我們使用在裡面填入弧度 PI。當 x 在 0~width 之間,x/width*2*PI 就是從 0~360 度的範圍。
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  ellipse(0,10, 15)
}

function draw() {
  //畫背景
  fill(0)
  rect(0,0,width,height)
  
  //畫圓圈 
  fill(255)
  noStroke()
  translate(width/2,height/2) //將原點設定到畫面中央
  rect(0,0,50,50) //畫個矩形確認座標系原點是否移到畫布中央

  let span = map(mouseX,0,width,1,100,true)
  let freq = map(mouseY,0,height,5,100,true)
  for(var x=0;x<width;x+=span){
    push()
      rotate(x/width*2*PI) //把座標系旋轉到0-360度之間
      let y=sin(x/freq+frameCount/100)*height/2 //藉由height/2讓波幅是畫面的一半高
      translate(y,0) //把旋轉過座標系在X軸上移動y個距離
      ellipse(0,0,10)
    pop()
  }
}

4. 來幫圖形上色吧

上色時老闆喜歡運用 coolors 這個網站挑選喜歡的配色,可以用空白鍵隨選5個顏色的搭配,也可以鎖住喜歡的顏色、繼續點空白鍵直到找到五個最喜歡的顏色搭配為止。每個顏色條裡也有一些提供調整的選擇。小撇步是當你決定好時,可以複製上方的代表顏色的字碼回到程式世界喔!

老闆想嘗試看看不同的視覺效果,將原先的圓形改為方形。接著指定一個變數 colors 來儲存這串字碼,並用程式將一個個色碼分開後,將每個色碼前面加上「#」成為完整的表示,例如:#1be7ff,#6eeb83。

為了讓每個方形輪流上不同的顏色,採用取餘數的方式:colors[int(x%colors.length)],可以讓餘數落在 0~colors.length-1,對應到 colors 陣列裡的各個色碼,在這裡外面包了一層 int() 是因為有時候j avascript 餘數運算出來是浮點數,因此要讓它強制取整。

//指定一個色票陣列
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
}

function draw() {
  //畫背景
  fill(0)
  rect(0,0,width,height)
  
  //畫圈圈 
  fill(255)
  noStroke()
  translate(width/2,height/2) 
  rect(0,0,50,50) 
  
  let span = map(mouseX,0,width,1,100,true)
  let freq = map(mouseY,0,height,5,100,true)
  for(var x=0;x<width;x+=span){
    push()
      fill(colors[int(x%colors.length)]) //選取色票陣列裡的特定顏色
      rotate(x/width*2*PI) 
      let y=sin(x/freq+frameCount/100)*height/2
      translate(y,0)
      rect(0,0,50)
    pop()
  }
}
<甜蜜陷阱>步驟四:加入顏色
<甜蜜陷阱>步驟四:加入顏色

記得中途若是做到喜歡的圖樣,可以自訂範圍截圖存取(mac:command+shift+4、window:win+shift+s),如果要將程式碼階段性保存起來,在 openprocessing 右上角有樹枝狀的按鈕 fork,就可以再複製一個出來繼續往下做喔!

5. 用圓形、方形、三角形來豐富

x 是我們畫每個點的依據,現在如果要讓每個位置可以分別呈現圓形、方形、三角形可以怎麼做呢?老闆是運用 x 除以3(代表 3 種形狀的餘數)與 if 條件式來實現,藉由餘數 0、1、2 分別對應到繪製不同的形狀。但在這裡還有一個關於繪製正三角形的挑戰:如果直接去計算 triangle(x1, y1, x2, y2, x3, y3) 的每一點座標是有些困難的,於是我們用三角函數的方式來計算。看著下圖我們可以看到透過角度 0、120、240 度,可以取得頂點的 x、y 座標 (r*cos(θ),r*sin(θ))。

var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
}
 
//將製作三角形定義成一支function可以隨時在draw()裡呼叫取用
function myTriangle(x,y,r){      //function可以設定想定義的參數
  push()
    translate(x,y)
    let points=[] //存放三角形的頂點座標
    for(var i=0;i<3;i++){   
      let rr =r
      let angle=i*120
      let xx = rr*cos(angle/360*2*PI) //將角度數值轉換為角度
      let yy = rr*sin(angle/360*2*PI)
      points.push(xx,yy)	//將各頂點座標依序放入points陣列
    }
    triangle(...points) //用ES6語法...展開points從一陣列變成個別的6個值
  pop()
}

function draw() {
  //畫背景
  fill(0)
  rect(0,0,width,height)
  
  //畫圈圈 
  fill(255)
  noStroke()
  translate(width/2,height/2)
  rect(0,0,50,50) 
 
  let span = map(mouseX,0,width,1,100,true)
  let freq = map(mouseY,0,height,5,100,true)
  for(var x=0;x<width;x+=span){
    push()
      fill(colors[int(x%colors.length)])
      rotate(x/width*2*PI) //把座標系旋轉到0-360度之間
      let y=sin(x/freq+frameCount/100)*height/3
      translate(y,0) //把旋轉過座標系在X軸上移動y個距離
      
      let shapeId = int(x)%3
      if(shapeId == 0){
        rect(0,0,50)
      }
      if(shapeId == 1){
        ellipse(0,0,50)
      }
      if(shapeId == 2){
        myTriangle(0,0,50)
      }
    pop()
  }
}
<甜蜜陷阱>步驟五:用不同的幾何圖形豐富圖面
<甜蜜陷阱>步驟五:用不同的幾何圖形豐富圖面

6. 妝點-陰影、材質

 再來老闆使出自己愛用的方法,給予圖樣更豐富的變化。一開始嘗試陰影效果,有兩種陰影製作的方式可以選擇,除了陰影的顏色要設定外,分別結合陰影模糊程度的設定、陰影偏離物體多少。

  • drawingContext:HTML5 Canvas的功能可以用這個API取得。
    • drawingContext.shadowBlur:設定陰影模糊的程度
    • drawingContext.shadowColor:設定陰影的顏色
    • drawingContext.shadowOffsetX:設定陰影偏離物體多少x距離
    • drawingContext.shadowOffsetY:設定陰影偏離物體多少y距離
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  ellipse(0,10, 15)

  //選擇一
  drawingContext.shadowBlur=5 
  drawingContext.shadowColor = color(0,100)//透明度0-255

  //選擇二,drawingContext.shadowBlur很當時可以使用
  drawingContext.shadowColor = color(0,100)//透明度0-255
  drawingContext.shadowOffsetX = 10
  drawingContext.shadowOffsetY = 10
}
<甜蜜陷阱>步驟六:加上陰影
<甜蜜陷阱>步驟六:加上陰影

如果想要加入材質感,可以學習製作一塊材質圖樣,再把材質圖樣疊加到畫布中。在這裡老闆設計的是噪點感的材質,噪點由許多深淺不一的灰階值組成。在 p5.js 裡可以透過指定一個變數製作出空白圖樣範圍,把圖樣像素化後可以用 for 迴圈指定每一個像素要畫什麼顏色。在這裡顏色的設定利用了 noise() 產生較有規律的 0~1 數值、random([a,b,c]) 決定 noise() 值放大的倍率來設定顏色的透明度。大家也可以試試在 noise() 傳入不同的參數、random() 陣列裡設定不同的倍率來製作不同的噪點感。製作好材質後可以選擇特定的疊加方法繪製出圖片。

  • createGraphics(width,height):設定一塊圖樣,傳入想要的寬高大小。
  • loadPixels():將圖樣的像素傳到 pixels[] 陣列,後續才可以讀取或者寫入想要的圖樣。
  • updatePixels():在設定完每一個像素的顏色後,可以用這個 api 更新成為新圖樣。
  • color(gray, [alpha]):第一個參數代表 0~255 的灰階值,第二個參數代表透明度。
  • noise(x,[y],[z]): 根據傳入的座標產生 0~1 之間浮點數,傳入的座標值越相近,產生出的浮點數會較有規律,不會變動很大。
  • random([array]):如果沒有特別傳入參數,random() 會返回0~1之間的浮點數,如果有寫明一個陣列,則每次會隨機在陣列裡挑選一個元素返回。
  • image(img, x, y, [width], [height]):在特定座標繪製出圖片。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)
let overallTexture
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  
  //選擇二,drawingContext.shadowBlur很當時可以使用
  drawingContext.shadowColor = color(0,100)//透明度0-255
  drawingContext.shadowOffsetX = 10
  drawingContext.shadowOffsetY = 10
  
  //製作噪點材質
  overAllTexture=createGraphics(width,height)
  overAllTexture.loadPixels()
 
  // noStroke()
  for(var i=0;i<width+50;i++){
    for(var o=0;o<height+50;o++){
      overAllTexture.set(i,o,color(150,noise(i/10,i*o/300)*random([50,100,200])))  
      //每一個像素指定特定的顏色
      //如果將random的值改小材質就不會太黑太明顯
    }
  }  
  overAllTexture.updatePixels()
}
 
function draw() {
  //畫背景
  fill(0)
  rect(0,0,width,height)
  
  push()
    //畫圈圈 
    fill(255)
    noStroke()
    translate(width/2,height/2)
    rect(0,0,50,50) 
 
    //製作三角形的函式
    function myTriangle(x,y,r){
      let points=[] 
      for(var i=0;i<3;i++){   
        let rr =r
        let angle=i*120
        let xx = rr*cos(angle/360*2*PI) 
        let yy = rr*sin(angle/360*2*PI)
        points.push(xx,yy)
      }
      triangle(...points) 
    }

    let span = map(mouseX,0,width,1,100,true)
    let freq = map(mouseY,0,height,5,100,true)
    for(var x=0;x<width;x+=span){
      push()
        fill(colors[int(x%colors.length)])
        rotate(x/width*2*PI) 
        let y=sin(x/freq+frameCount/100)*height/2
        anslate(y,0)

        let shapeId = int(x)%3
        if(shapeId == 0){
          rect(0,0,50)
        }
        if(shapeId == 1){
          ellipse(0,0,50)
        }
        if(shapeId == 2){
          myTriangle(0,0,50)
        }
      pop()
    }
  pop()

  //將噪點材質疊加到畫布上
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0) //
  pop()
}
<甜蜜陷阱>步驟六:加上材質

7. 讓圖形大小變化與自轉、長出刺與小圓點

為了讓整個互動的畫面更豐富有變化,老闆運用sin() 來設定形狀的大小。此外,形狀們除了不斷輻合到畫面中央外,也用rotate() 讓它開始自轉,並且形狀上、周圍加上一些裝飾:看起來像是刺的長短不一的線條、修改利用前面製作三角形的函式讓每個形狀的周圍環繞三個小圓形。

for(var x=0;x<width;x+=span){
  push()
    fill(colors[int(x%colors.length)])
    rotate(x/width*2*PI) 
    let y=sin(x/freq+frameCount/100)*height/3
    translate(y,0) 
    let shapeId = int(x)%3
	
    let rr= sin(x)*80 //讓每個圖形大小變化
    rotate(frameCount/50) //讓每個圖形自轉 


    if(shapeId == 0){
      rect(0,0,rr)
    }
    if(shapeId == 1){
      ellipse(0,0,rr)
    }
    if(shapeId == 2){
      myTriangle(0,0,rr)
    }

    //畫上刺
    strokeWeight(3)
    stroke(255)
    line(0,0,-rr,-rr) //線條與圖形用的是同一個座標系設定

    //畫環繞的圓形
    for(var i=0;i<3;i++){   
      noStroke()
      let rr =50
      let angle=i*120
      let xx = rr*cos(angle/360*2*PI) 
      let yy = rr*sin(angle/360*2*PI)
      ellipse(xx,yy,5)
    }
  pop()
}
<甜蜜陷阱>步驟七:讓圖形大小變化與自轉、長出刺與小圓點
<甜蜜陷阱>步驟七:讓圖形大小變化與自轉、長出刺與小圓點

8. 製作網格背景

再來我們要來製作現代感的網格背景,因此在座標系設定到畫面中央後,我們新增一段程式碼,設定線條的顏色並分別畫上水平線條與垂直線條。這裡老闆運用了取餘數,讓線條每 5 條就增強它的粗度與變得更明顯(調整透明度),這邊也用到了一些 javascript 的數學與邏輯表示方式。

  • abs():取絕對值
  • boolean?a:b:如果前面的變數 boolean 值是 true,就返回 a 值;是 false,就返回 b 值
translate(width/2,height/2) //將原點設定到畫面中央
			
 //畫網格線
stroke(255,100)
for(let xx=-width/2;xx<width/2;xx+=40){
let isSpan = (abs(xx/20)%5==0)
  strokeWeight(isSpan?3:1)
  stroke(255,20+isSpan?200:0)
  line(xx,-height/2,xx,height/2)
  }
		
for(let yy=-height/2;yy<height/2;yy+=40){
  let isSpan = (abs(yy/20)%5==0)
  strokeWeight(isSpan?3:1)
  stroke(255,20+isSpan?200:0)
  line(-width/2,yy,width/2,yy)
}
noStroke()
<甜蜜陷阱>步驟八:製作網格背景
<甜蜜陷阱>步驟八:製作網格背景

9. 隨機選取顏色子集合、印出文字

為了讓圖樣在程式每次開始執行時都可以選取不同的顏色來繪製,老闆運用隨機的概念,讓每個在顏色陣列裡的色碼,會透過機率的方式決定會不會被選到。為了避免所有顏色都未能被選入,也預先儲存一些絕對會畫上去的顏色。

var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)
var useColors = ['#000','#fff'] //真正用於著色的陣列,可以預先填入一些顏色
let overallTexture
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  ellipse(0,10, 15)           
         
  //繪製陰影
  drawingContext.shadowColor = color(0,100)//透明度0-255
  drawingContext.shadowOffsetX = 10
  drawingContext.shadowOffsetY = 10

  //製作噪點材質
  overAllTexture=createGraphics(width,height)
  overAllTexture.loadPixels()
 
  // noStroke()
  for(var i=0;i<width+50;i++){
    for(var o=0;o<height+50;o++){
      overAllTexture.set(i,o,color(150,noise(i/10,i*o/300)*random([50,100,200])))  
      //每一個像素指定特定的顏色
      //如果將random的值改小材質就不會太黑太明顯
    }
  }
  overAllTexture.updatePixels()

  //顏色子集合
  colors = colors.concat(colors) //隨機條件若很嚴格可以藉由讓顏色陣列複製自己,增加顏色被選到的機率
  randomSeed(Date.now()) //讓隨機依據變動的數字(如用當下的時間)會更隨機
  colors.forEach(clr=>{ //對於顏色陣列裡的每個顏色(設定變數clr)會逐一的執行{}裡的指令
    if(random()<0.25){ //當random()的值小於某數時才會執行
      useColors.push(clr)  //執行將某色存入useColors陣列
    }
  })
}

別忘了要將填色的部分改選用useColors陣列喔!

function draw(){
  ...
  for(var x=0;x<width;x+=span){
    push()
      fill(useColors[int(x%useColors.length)]) 
      rotate(x/width*2*PI) 
      let y=sin(x/freq+frameCount/100)*height/3 				
      translate(y,0) 
      let shapeId = int(x)%3
    pop()
  }
}

再來可以在畫面上以文字呈現一些參數是如何變化,讓作品看起來很有科幻系統的感覺。為了讓形狀都會有陰影但文字不會,將原本在 setup() 關於陰影的設定搬到 draw(),但在要繪製文字之前將陰影設定取消。這邊很有趣的是,老闆還繪製出了填色矩形記錄每次圖樣是由哪幾個顏色構成。

  • text(str, x, y):在 (x,y) 座標呈現出文字
function draw() {	
  drawingContext.shadowColor = color(0,200)//透明度0-255
  drawingContext.shadowOffsetX = 10
  drawingContext.shadowOffsetY = 10

  //畫背景
  fill(0)
  rect(0,0,width,height)
    
  push()         
    // blendMode(SCREEN)
    //畫圈圈 
    fill(255)
    noStroke()
    translate(width/2,height/2) //將原點設定到畫面中央
			
    //畫網格線
    stroke(255,100)
    for(let xx=-width/2;xx<width/2;xx+=40){ //
      let isSpan = (abs(xx/20)%5==0)
      strokeWeight(isSpan?3:1)
      stroke(255,20+isSpan?200:0)
      line(xx,-height/2,xx,height/2)
    }
		
    for(let yy=-height/2;yy<height/2;yy+=40){ //
      let isSpan = (abs(yy/20)%5==0)
      strokeWeight(isSpan?3:1)
      stroke(255,20+isSpan?200:0)
      line(-width/2,yy,width/2,yy)
    }
    noStroke()
		
			
    //畫形狀
    let span = map(mouseX,0,width,1,100,true)
    let freq = map(mouseY,0,height,1,100,true)
    let curveFactor = noise(frameCount/1000)*3+5 
    for(var x=0;x<width;x+=span){
      push()
        fill(useColors[int(x%useColors.length)])
        rotate(x/width*2*PI) 
        let y=sin(x/freq+frameCount/100)*height/2 
        translate(y,0) //把旋轉過的X軸上移y個距離
        let shapeId = int(x)%3
				
        let rr=(pow(noise(x),2)+pow(sin(x),1.2))*100 //製作大小不一的形狀
        rotate(frameCount/50)//自轉 
				
        if(shapeId == 0){
          rect(0,0,rr)
        }
        if(shapeId == 1){
          ellipse(0,0,rr)
        }
        if(shapeId == 2){
          myTriangle(0,0,rr)
        }
        strokeWeight(3)
        stroke(255)
        line(0,0,-rr,-rr)
				
        //環繞的小圓形
        for(var i=0;i<3;i++){  
          noStroke()
          let rr =50
          // let cirR =10 *sin(x)
          let cirR =10
          let angle=i*120+frameCount/100+x*curveFactor//?
          let xx = rr*cos(angle/360*2*PI) //將角度數值轉換為角度
          let yy = rr*sin(angle/360*2*PI)
          ellipse(xx,yy,cirR)
        }			
      pop()
    }
  pop()     
		
    //為了寫文字取消陰影
    drawingContext.shadowColor = color(0,200)//透明度0-255
    drawingContext.shadowOffsetX = 0
    drawingContext.shadowOffsetY = 0
		
  push()
    
    for(var colorId = 0;colorId<useColors.length;colorId++){
      fill(useColors[colorId])
      strokeWeight(2)
      rect(colorId*40+40,height-210,30,30) //注意這裡的座標系原點是以左上
                                        //角(0,0)計算,每個方形間隔40,寬高30
    }
    fill(255) //字體設定白色
    textSize(24)
    textStyle(BOLD)
    text("TIME: "+frameCount+"fp",50,height-130)
    text("SPAN: "+span.toFixed(2)+"\"",50,height-90) //這裡值得注意為了要顯示”
                                                     //需要在前面加一條\方便程式辨識喔
    text("FREQ: "+span.toFixed(2)+"Hz",50,height-50)
  pop()
		
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
<甜蜜陷阱>步驟九:隨機選取顏色組合,並在圖的左下角新增文字
<甜蜜陷阱>步驟九:隨機選取顏色組合,並在圖的左下角新增文字

10. 最後一點小調整!

為了讓很多東西不要只隨著 sin 變化,減少單調以及增加更多的韻律,例如小圓形原本只會跟著大圓形、方形、三角形一起同週期旋轉,為了讓它有自己的旋轉,加上了 frameCount/100,再透過加上 x*a(a 代表一個設定的倍數),讓每個位置上的三個小圓形都有不同的偏轉角度,看起來就像是扭轉纏繞的模樣。另外,利用 noise() 讓本來只會隨著 sin(x) 值規律變大變小的形狀可以增加一點隨機的變化,合併使用 pow() 次方的相乘讓值更極端。

我們可以在 setup() 設定一開始滑鼠的位置來規範一開始執行程式時就出現想要的圖樣,最後方便儲存圖片可使用 mousePressed() 偵測滑鼠點按事件的發生並以 save() 存下圖片。

  • pow(n,e):n的e次方。
  • save():存取當前畫面。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a)
var useColors =["#000","#fff"]
let overAllTexture

function mousePressed(){ //偵測滑鼠點按
  save()   //儲存畫面
}

function setup() {
  colors = colors.concat(colors)
	
  createCanvas(1000,1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  pixelDensity(2)     //增加像素密度
  // drawingContext.shadowBlur=5
	
  randomSeed(Date.now())
  mouseX = random(1,width/10) //設定一開始的滑鼠座標
  mouseY = random(1,height/2) //設定一開始的滑鼠座標
	
  colors.forEach(clr=>{
    if (random()<0.25){
      useColors.push(clr)
    }
  })
	
  overAllTexture=createGraphics(width,height)
  overAllTexture.loadPixels()
  // noprotect
  // noStroke()
  for(var i=0;i<width+50;i++){
    for(var o=0;o<height+50;o++){
      overAllTexture.set(i,o,color(150,noise(i/10,i*o/300)*random([0,0,0,80,200]))) 
      //可以透過在陣列裡複製多一點某個值讓它被隨選到的機率增加
    }
  }
  overAllTexture.updatePixels()
}

function myTriangle(x,y,r){
  push()
    translate(x,y)
    let points = []
    for(var i=0;i<3;i++){
      let rr = r
      let angle =i*120
      let xx = rr* cos(angle/360*2*PI)
      let yy = rr* sin(angle/360*2*PI)
      points.push(xx,yy)
    }
    triangle(...points)
  pop()
}

function draw() {
  drawingContext.shadowColor=color(0,200)
  drawingContext.shadowOffsetX=10
  drawingContext.shadowOffsetY=10

  // print(mouseX,mouseY)
  //畫背景
  fill("#000")
  rect(0,0,width,height)
  // push()
  //  fill(0,0.1)
  //  rect(0,0,width,height)
  // pop()

  push()
    // blendMode(SCREEN)
    //畫圈圈
    fill(255)
    noStroke()

    //translate to center
    translate(width/2,height/2)

    stroke(255,100)
    for(let xx=-width/2;xx<width/2;xx+=40){
      let isSpan = (abs(xx/20)%5==0?150:0) 
      strokeWeight(isSpan?2:1)
      stroke(255,20+ isSpan?100:0)
      line(xx,-height/2,xx,height/2)
    }

    for(let yy=-height/2;yy<height/2;yy+=40){
      let isSpan = (abs(yy/20)%5==0?150:0) 
      strokeWeight(isSpan?2:1)
      stroke(255,20+ isSpan?100:0)
      line(-width/2,yy,width/2,yy)
    }
    noStroke()

    // rect(0,0,50,50)
    let span = map(mouseX,0,width,1,10,true)
    // print(span)
    let freq = map(mouseY,0,height,1,100,true)
    let curveFactor = noise(frameCount/1000)*3+5 //小圓形扭轉的程度
    for(var x=0;x<width;x+=span){
      push()
        fill(useColors[int(x%useColors.length)])
        rotate(x/width*2*PI)
        let y = sin(x/freq+frameCount/100)*height/2
        translate(y,0)
        let shapeId = int(x)%3
        let rr = ( pow(noise(x),2)+ pow(sin(x),1.2))*80 //讓形狀大小變化度更大
        rotate(frameCount/50)
        if (shapeId==0){
          rect(0,0,rr)
        }
        if (shapeId==1){
          ellipse(0,0,rr)
        }
        if (shapeId==2){
          myTriangle(0,0,rr)
        }
        strokeWeight(3)
        stroke(255)
        line(0,0,-rr,-rr)

        for(var i=0;i<3;i++){
          noStroke()
          let rr = 50
          let cirR = 10
          let angle =i*120+frameCount/100 + x*curveFactor //製造三個小圓形第二層旋轉、不同位置的三個小圓形偏轉不同角度
          let xx = rr* cos(angle/360*2*PI)
          let yy = rr* sin(angle/360*2*PI)
          ellipse(xx,yy,cirR)
        }
        // ellipse(0,0,50)
      pop()
    }
  pop()

  //把陰影取消掉
  drawingContext.shadowColor=color(0,200)
  drawingContext.shadowOffsetX=0
  drawingContext.shadowOffsetY=0

  push()
    textSize(24)
    textStyle(BOLD);
    for(var colorId =0;colorId<useColors.length;colorId++){
      fill(useColors[colorId])
      strokeWeight(2)
      rect(colorId*40+40,height-210,30,30)
    }
    fill(255)
    text("TIME: "+frameCount+ "fp", 50,height-130)
    text("SPAN: "+span.toFixed(2) + "\"", 50,height-90)
    text("FREQ: "+freq.toFixed(2) + "Hz", 50,height-50)
  pop()
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
<甜蜜陷阱>成品圖
<甜蜜陷阱>成品圖

老闆來結語

再次附上這次範例的成品<甜蜜陷阱>讓大家在開發時參考。這次的創作是從一個點子開始慢慢精修,一邊做一邊調整,讓我們快速回顧一下甜蜜陷阱的創作過程:

  • 了解 openprocessing 創作的起手式 – setup() 與 draw()
  • 運用滑鼠座標來動態改變sin波的樣貌
  • 運用旋轉與移動座標系來繪製花狀波形
  • 上色與變化形狀
  • 加入陰影、噪點材質
  • 調整形狀大小
  • 自轉、毒刺與環繞的小圓形
  • 加上網格背景與文字
  • 最後的調整修飾

這部影片結合了許多好用的數學概念與 API,讓我們可以邏輯化的選取或製作特定效果。在寫程式時也可以善用註解,比較好區塊化地理解與管理每一段程式影響了畫面哪些部分。大家會發現老闆在過程中會不斷微調參數值或是回頭修改使用的 API 試驗不同的效果,這也是創作磨人卻有趣的地方,大家一起探索與試驗吧!

如果你喜歡老闆的教學,《互動藝術程式創作入門》課程中也有手把手的實作引導。寫程式製作生成藝術世紀是一趟需要精準又沿路充滿驚喜的旅程,需要腦瓜裡有彈性的空間-細心規劃,但也放膽試驗、歡迎意外!

此篇直播筆記由幫手 C 協助整理

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 【p5.js創作教學】Sweet Trap 甜蜜陷阱 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】Coral Carnival 珊瑚嘉年華 https://creativecoding.in/2022/04/07/p5-js-coral-carnival/ Thu, 07 Apr 2022 06:27:00 +0000 https://creativecoding.in/?p=2092 藝術創作的有趣之處在於它有無數可能與千萬種解讀,閱讀吳哲宇在創作<珊瑚嘉年華>這件作品時的逐步調整,以及一些大膽實驗的意外收穫,慢慢塑造成一件又一件的 p5.js 生成式藝術作品。

這篇文章 【p5.js創作教學】Coral Carnival 珊瑚嘉年華 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
前言

乘著生成式藝術的浪潮,讓我們一同體驗<珊瑚嘉年華>這件藝術作品的創作過程。使用 p5.js 的技術創作出絢爛夢幻、隨波蕩漾的珊瑚,看著它在海底深處搖曳擺盪,在暗夜中充滿生命力地熠熠生輝。

開場

這一次分享的內容比較特別,除了紀錄如何使用 P5.js 創作藝術之外,同時也記錄生成式藝術的創作過程。從初步的概念發想、創作中的顏色/動態/細節調整嘗試,一路到完成作品後的圖片保存處理、文案構思與撰寫,帶領大家一同體會藝術創作的趣味。

<珊瑚嘉年華>作品完成截圖
<珊瑚嘉年華>作品完成截圖

這次直播筆記會帶大家學會以下內容:

  • 瞭解一幅生成式藝術作品的完整創作過程
  • 使用 coolors 快速選擇色票並進行色彩搭配
  • 使用 frameCount() 在作品中加入逐幀動態效果,賦予作品生命力

事前準備

  1. 開發環境: 本次開發會使用 openprocessing 線上撰寫程式碼,如果想知道較詳細的設定,可以到老闆作品的 成品 查看相關的設定。
  2. 本次範例使用到的 API:

步驟講解

一、初步靈感發想

最初的靈感發想可以從自己喜歡的事物著手,像是老闆喜歡鋼琴、琴譜、植物,因此這次腦海中的雛型是「V字型、輻射狀、往外擴張」的圖案。

<珊瑚嘉年華>作品步驟一:靈感發想
<珊瑚嘉年華>作品步驟一:靈感發想

有了初步的想法後,接著老闆便參考自己在 Pinterest 上蒐集的各種海報、平面設計、生成式藝術等等,看它們的配色、使用的材質、文字樣式、3D等等,給自己更多作品靈感。此外,老闆常用的手法還有使用合適的材質跟顏色疊色,能快速地提升作品質感。

二、Sketch 起手式

使用 openprocessing 開啟一個新的 Sketch 之後,可以看到程式碼頁面已經有一段預設的程式碼:隨著滑鼠的移動,會沿路產生小球。

  • setup(): 環境建立/初始化,只在開始執行的當下會呼叫一次。以下的程式碼使用了兩個 API
    • createCanvas(width, height):創建畫布,參數中分別傳入寬跟高,如果直接寫螢幕的寬高( windowWidth, windowHeight),就會成為滿版的互動區塊。但老闆在創作<每日生成式藝術>系列時,習慣把寬高設為 (1000,1000) ,有一個比較固定的格式。
    • background(colorCode):加上背景色,可依照文件傳入色碼參數。
  • draw(): 會以每秒 60 次的循環重新呼叫並進行裡面的程式碼,想製作互動效果都可以在這個 function 中呼叫。
    • ellipse(posX, posY, width, height):在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形,這邊用 (mouseX, mouseY) 的話便是隨著滑鼠移動的軌跡產生橢圓形
function setup() {
  createCanvas(1000,1000);
  background(100);
}

function draw() {
  ellipse(mouseX, mouseY, 200, 200);
}

三、繪製 V 圖案,複製並旋轉為輻射狀圖形

接下來,根據原本的靈感先畫出一串 V 型圖案,然後旋轉複製這些圖案。在開始前,先用 background()、fill()、rect() 這三個 API 將背景設為畫框的樣式。接著到中心點開始構圖,使用兩個長方形去組合成V型。要注意的是,只要有用到旋轉相關的API,都要記得 push() 跟 pop(),不然會影響到後續的程式碼。

  • background():設定背景顏色。
  • fill():選擇填入的顏色。
  • rect(x, y, width, height):在 (x, y) 的位置畫一個寬度 width、高度為 height 的方形。
  • translate(x,y):將畫筆移動到 x, y 的位置。
  • push():紀錄目前畫筆狀態。
  • pop():恢復畫筆狀態。
  • rotate(angle:依照傳入的參數進行旋轉。
  • PI():180度角。
  • rectMode(mode):設定四方型從什麼地方開始繪製。模式分別有 CENTER、CORNER、RADIUS 三種,注意模式的字必須是大寫。
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  fill(255);
  noStroke();
	
  push();
    rotate(-PI/4*3);
    rect(75,0,200,50);
    rotate(PI/4*2);
    rect(75,0,200,50);
  pop();
}
<珊瑚嘉年華>作品步驟三:從簡單的圖樣開始畫起
<珊瑚嘉年華>作品步驟三:從簡單的圖樣開始畫起

組合出 V 型後,接著就用迴圈來複製它,然後加上 translate() 進行角度偏移,讓它每繪完一次圖案就往上移動一點再繼續繪製,這樣就能得到一串V的圖形。在迴圈外面要記得包一層 push()、pop(),讓它不影響到後續程式。

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  fill(255);
  noStroke();
	
  push();
    for(let i=0;i<10;i++){
      translate(0,-45);
      push();
        scale(0.5)
        rotate(-PI/4*3);
        rect(75,0,200,50);
        rotate(PI/4*2);
        rect(75,0,200,50);
      pop();
    }
  pop();
}
<珊瑚嘉年華>作品步驟三:利用迴圈快速大量複製簡單的圖樣
<珊瑚嘉年華>作品步驟三:利用迴圈快速大量複製簡單的圖樣

四、進行配色、旋轉與各種細節嘗試

基本的圖形完成之後,接下來就可以進行各種不同的嘗試與色彩搭配。老闆在這邊嘗試了:

  • 迴圈數增多:增加迴圈的數量、搭配 scale() 把圖形縮小、降低 translate() 數值讓圖形排列緊密。
  • 圖形旋轉:使用 rotate() 讓圖形旋轉。rotate()的參數除了代入數字,也可以帶入 sin(i) 之類的,嘗試不同的數值帶來的變化;或甚至可以代入rotate(sin(i/(mouseY/100))),讓圖形隨著滑鼠移動的角度旋轉,然後記得加個 background(0) 把背景軌跡清掉 (有些作品也會留下軌跡,但這邊先不用)
  • 色彩搭配:可以從蒐集的海報作品中找搭配靈感,或是 coolors 網站提供的色票搭配來快速搭配出喜歡的顏色。

找到喜歡的色票之後,可以直接從 coolors 的網址上複製色票的數值。

在 Coolors 找到喜愛的色票組合後複製
在 Coolors 找到喜愛的色票組合後複製

但複製完後,還需要先把色票轉成陣列的色彩,才能在P5.js 中使用。因此這邊會需要用到 JS 的語法 split() 與 map() 把色票轉成色彩字串的陣列。

const colors = 'cacf85-8cba80-658e9c-4d5382-514663'.split('-').map(a=>`#${a}`);

完成後就能把我們的圖形加入色彩啦!

const colors = 'cacf85-8cba80-658e9c-4d5382-514663'.split('-').map(a=>`#${a}`);

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  fill(255);
  noStroke();
  background(0); // 清除軌跡
	
  push();
    for(let i=0;i<20;i++){
      translate(0,-45);
      rotate(sin(i/(mouseX/100))); // 旋轉角度跟隨滑鼠移動
      fill(colors[i%colors.length]);
      push();
        scale(0.5)
        rotate(-PI/4*3);
        rect(75,0,200,50);
        rotate(PI/4*2);
        rect(75,0,200,50);
      pop();
    }
  pop();
}
<珊瑚嘉年華>作品步驟四:加上色彩、旋轉等變化
<珊瑚嘉年華>作品步驟四:加上色彩、旋轉等變化

五、構成萬花筒

只有一條太單調了,接下來一樣使用迴圈跟旋轉的方法,把圖形變成類似萬花筒的形狀吧!這樣一來就達成原本的構想 — 同心圓放射狀的V圖形。

const colors = 'cacf85-8cba80-658e9c-4d5382-514663'.split('-').map(a=>`#${a}`);

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  fill(255);
  noStroke();
  fill(0);
  rect(0,0 ,width,height);
	
  for(let o=0; o<8; o++){
    rotate(PI/4);
    push();
      for(let i=0;i<20;i++){
        translate(0,-45);
        rotate(sin(i/(mouseX/100)));
        fill(colors[i%colors.length]);
        push();
          scale(0.5)
          rotate(-PI/4*3);
          rect(75,0,200,50);
          rotate(PI/4*2);
          rect(75,0,200,50);
        pop();
      }
    pop();
  }
}
<珊瑚嘉年華>作品步驟五:慢慢建構成了萬花筒
<珊瑚嘉年華>作品步驟五:慢慢建構成了萬花筒

六、提升質感

剛開始創作藝術作品時通常會稍微缺乏質感,主要是「材質、顏色、細節、變化」這四種狀態技巧掌握度不足。因此,想提升質感就要進行一些優化,例如:

1. 添加顏色

const colors = '18206f-17255a-f5e2c8-d88373-bd1e1e-cacf85-8cba80-658e9c-4d5382-514663'.split('-').map(a=>`#${a}`);

2. 增添大小變化:

scale(random(1))scale(noise(1)) 將每一個 V 的大小變不一致。要注意的是,使用 random() 會在每次繪製時重新產生亂數,所以畫面會感覺比較不受控;使用 noise() 則是類似在一個亂數表上取值,跟 random() 概念不同,呈現的畫面也比較沒那麼亂。

scale(random(0.6, 1.1)); 
or 
scale(noise(i,o)/5+0.9);

3. 添加動態變化:

除了使用 mouseX 去控制變化之外,也可以加上 frameCount() 讓圖形隨著時間變動。另外,如果想同時保留 mouseX 跟 frameCount,記得幫 mouseX 加上0.1,否則滑鼠一旦不動,畫面也就不會更動 。

rotate(sin(i/(mouseX/100+0.1)+frameCount/50));<

4. 使用 BlendMode() 進行一些色彩混搭組合:

使用BlendMode() 這個 API 可以進行依些色彩混搭,多些顏色的變化。如果是黑色的背景,參數建議選 SOFT_LIGHT、SCREEN 來提亮。

blendMode(SCREEN);

5. 改變材質

可以上網找 Canvas texture,就可以找到許多種材質的圖片並下載使用。如果想下載本次範例使用素材,可以到 成品 這邊,點開查看原始碼、以及右邊的 File 檔案區,找到 <canvas.jpeg>並下載到自己電腦裡

下載<珊瑚嘉年華>作品使用的材質素材說明
下載<珊瑚嘉年華>作品使用的材質素材說明

再來一樣到自己的 Sketch 頁面右側欄一樣的 File 內,點選上傳就可以上傳這張圖片了。接著會使用 preload()loadImage() 這兩個API 在程式碼內載入這張背景,然後使用 blendMode() 進行材質疊加。

push()
  blendMode(MULTIPLY);	
  image(canvasTexture, -width/2,-height/2)
pop()

完整程式碼如下

const colors = '18206f-17255a-f5e2c8-d88373-bd1e1e-cacf85-8cba80-658e9c-4d5382-514663'.split('-').map(a=>`#${a}`);

var canvasTexture;
function preload(){
  canvasTexture = loadImage("canvas.jpeg")
}

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  fill(255);
  noStroke();
  fill(0);
  rect(0,0 ,width,height);
	
  push()
    blendMode(SCREEN);
    for(let o=0; o<16; o++){
      rotate(PI/8);
			
      push();
        for(let i=0;i<20;i++){
          translate(0,-20);
          rotate(sin(i/(mouseX/500+0.1)+frameCount/100));
          scale(noise(i,o,frameCount/50)/5+0.9);
          fill(colors[i%colors.length]);
					
          push();
            scale(0.1)
            rotate(-PI/4*3);
            rect(75,0,200,50);
            rotate(PI/4*2);
            rect(75,0,200,50);
          pop();
        }
      pop();
    }
  pop()
	
  push()
    blendMode(MULTIPLY);	
    image(canvasTexture, -width/2,-height/2)
  pop()
}

範例

<珊瑚嘉年華>作品步驟六:透過改變顏色、大小、材質、動態等,增加作品質感
<珊瑚嘉年華>作品步驟六:透過改變顏色、大小、材質、動態等,增加作品質感

七、細節調整

接下來就進入好玩的部分啦~這邊開始可以做一些大膽的嘗試或調整,像是:

  • 把四方型加點圓角
  • 減輕旋轉幅度
  • 改變背景顏色
  • 多加一組色彩進行搭配
  • 改變四方型的高度、寬度,營造錯落感
  • 使用 ellipse() 增加小點點

在這個階段,可以根據自己的喜好自由調整與搭配,好好感受藝術的趣味~

const colors = '26f0f1-bd93bd-f2edeb-ffdd4a-fabc2a'.split('-').map(a=>`#${a}`);
const colors2 = '0b3954-bfd7ea-ff5a5f-c81d25-44ffd1-6153cc-a60067-961d4e'.split('-').map(a=>`#${a}`);
var canvasTexture;
function preload(){
  canvasTexture = loadImage("canvas.jpeg")
}

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

function draw() {
  translate(width/2, height/2);
  noStroke();
  fill('#3a3b47');
  rect(0,0 ,width,height);
	
  push()
    // blendMode(SCREEN);
    for(var o=0; o<8; o++){
      rotate(PI/4);
			
      push();
        // 讓每一條觸手大小各異
        scale(sin(i/5+o/5)/20+0.8);
				
        let useColors =([colors, colors2][int(o%2)])
        for(var i=0;i<20;i++){
          translate(0,-30);
          rotate(sin(i/((sin(frameCount/10))/2000+0.1)+frameCount/100)+o/80);
          // 使用pow() 讓大的更大,小的更小
          scale(pow(noise(i,o,frameCount/100), 1.2)*0.5+1);
          fill(useColors[i%useColors.length]);
					
          push();
            let useWidth = 50*noise(i,o+5000)+20;
            scale(sin(i/5+o/50)/5+0.2);
            rotate(-PI/4*3);
            rect(75,0,200+noise(i/40,o/40)*500,useWidth,useWidth);
            rotate(PI/4*2);
            rect(75,0,100+noise(i,o)*100,useWidth,useWidth);
          pop();
          // 增加小點點
          for(let k=0; k<8;k++){
            let useSize = 10*noise(k,50000)
            fill(useColors[k%5]);
            ellipse(noise(k+frameCount/100)*50,noise(k+frameCount/200,5000)*50,useSize,useSize);
          }
        }
      pop();
    }
  pop()
	
  push()
    blendMode(MULTIPLY);	
    image(canvasTexture, -width/2,-height/2)
  pop()
}
<珊瑚嘉年華>作品步驟七:細修作品
<珊瑚嘉年華>作品步驟七:細修作品

八、作品輸出

作品完成後,接下來就是輸出啦!輸出時很重要的一件事情是 — 要先把輸出畫作的品質提升,因此要用的API是:

接著我們要決定存下這幅藝術畫作的哪個畫面。程式藝術雖然是由創作者產出的作品,但它總會有某些時刻比較好看,因此當那些時刻出現時我們要把畫面存下來,這邊使用到的 API 是:

  • mousePressed(): 滑鼠點擊時
  • save():儲存畫面
function mousePressed(){
  save();
}

九、額外添加細節

儲存圖片之後,如果有什麼靈感也都可以再調整作品,像是點點上加入一些線條等等 (不過有點耗電腦效能,請自行斟酌使用)

const colors = '363636-242f40-cca43b-e5e5e5-ffffff'.split('-').map(a=>`#${a}`);
const colors2 = 'ffcb3d-2b2d42-8d99ae-edf2f4-ef233c-d80032'.split('-').map(a=>`#${a}`);

// 26f0f1-bd93bd-f2edeb-ffdd4a-fabc2a
// 0b3954-bfd7ea-ff5a5f-c81d25-44ffd1-6153cc-a60067-961d4e

var canvasTexture;
function preload(){
  canvasTexture = loadImage("canvas.jpeg")
}

function setup() {
  createCanvas(1000, 1000);
  background(400);
  pixelDensity(2);
  fill(0);
  rect(0,0 ,width,height);
  rectMode(CENTER);
}

// 按下儲存
function mousePressed(){
  save();
}

function draw() {
  translate(width/2, height/2);
  noStroke();
  fill('#3a3b47');
  rect(0,0 ,width,height);
	
  push()
    // blendMode(SCREEN);
    for(var o=0; o<8; o++){
    rotate(PI/4);
			
    push();
      // 讓每一條觸手大小各異
      scale(sin(i/5+o/5)/20+0.8);
				
      let useColors =([colors, colors2][int(o%2)])
      for(var i=0;i<20;i++){
        translate(0,-30);
        rotate(sin(i/((sin(frameCount/10))/2000+0.1)+frameCount/100)+o/80);
        // 使用pow() 讓大的更大,小的更小
        scale(pow(noise(i,o,frameCount/100), 1.2)*0.4+0.9)
        fill(useColors[i%useColors.length]);
					
        push();
          let useWidth = 50*noise(i,o+5000)+20;
          scale(sin(i/5+o/50)/5+0.2);
          rotate(-PI/4*3);
          rect(75,0,200+noise(i/40,o/40)*500,useWidth,useWidth);
          rotate(PI/4*2);
          rect(75,0,100+noise(i,o)*100,useWidth,useWidth);
        pop();
        // 增加小點點
        for(let k=0; k<8;k++){
          let useSize = 10*noise(k,50000)
          fill(useColors[k%5]);
							
          // 加上點的線條
          stroke(useColors[k%5]);
          line(0, 0, noise(k+frameCount/100)*50,noise(k+frameCount/200,5000)*50);
          noStroke();
          ellipse(noise(k+frameCount/100)*50,noise(k+frameCount/200,5000)*50,useSize,useSize);
        }
      }
      pop();
    }
  pop()
	
  push()
    blendMode(MULTIPLY);	
    image(canvasTexture, -width/2,-height/2)
  pop()
}
<珊瑚嘉年華>加上線條後的完成品

十、作品命名、發布、與構思搭配文案

圖片儲存下來後,接著便是構想作品的名稱,並為作品搭配能相呼應的一段文字。由於作品充滿絢爛的色彩,又有點像是海底生物一樣隨波擺盪,所以老闆決定將它命名為<珊瑚嘉年華>,並搭配它的顏色與整體感受加了一段文字:

又變得更熱了,連回憶都開始白化,
小丑魚永遠的離開了,沒有了他的珊瑚,
其實也是只剩多彩的空殼了吧,
畢竟還是群居的動物離不開彼此,
卻走散僅留下了時間的遺跡。

結語

這次帶大家從零到一體驗了生成式藝術的創作過程。從一開始只是腦中有 V 字型的靈感,經過一系列的調整與操作,最終才產出<珊瑚嘉年華>這個作品,讓我們快速回顧一下<珊瑚嘉年華>的創作過程:

  1. 從平日蒐集的藝術素材中發想靈感,決定做一個「V字型、輻射狀、往外擴張」的圖案。
  2. 使用 P5.js 先繪製出單一個 V 圖案。
  3. 使用迴圈與位移的API,將 V 圖案複製並形成放射狀的圓圈。
  4. 參考藝術素材或配色網站,將圖形搭配上色彩。
  5. 增加材質、變化、顏色搭配、動態等細節修飾與調整,來提升作品質感。
  6. 大膽的嘗試各種細節調整,一邊試一邊看自己喜歡的風格。
  7. 作品命名、輸出、發佈與文案撰寫。

藝術創作的有趣之處在於它有無數可能與千萬種解讀,在這個過程中,每一次的細節調整都賦予這幅創作不一樣的感受,也能看到這個作品慢慢的演化成果,最後再決定這個作品想成為什麼樣子。這次的直播創作透過逐步的調整以及一些大膽嘗試,創造出許多意想不到的感受。

藝術很多時候不僅反映當下的心情狀態,也能透過創作的過程突破自己的舒適圈,或是療癒自己的內心。所以不必糾結於該如何寫出完美的作品,直接照著老闆的教學上手試一試吧,也許在嘗試的過程中,會迸發出意料之外的藝術品!這邊附上本次範例的成品<珊瑚嘉年華>,提供大家在開發時參考。

看到這裡,你對 Creative Coding 更有興趣了嗎?
歡迎加入互動藝術程式創作入門(Creative Coding)線上課程,課程中你可以認識程式與互動藝術產業應用,開啟對工程跟設計的想像,學會使用 p5.js 開發互動介面,整合繪圖、音訊、視訊、文字、3D、互動與機器創作完整的作品,並將創作輸出應用在個人品牌或網站、主視覺或海報,甚至互動裝置、遊戲與教材製作等場景,讓你對進修的資源與路線更有方向。

此篇直播筆記由幫手 金金 協助整理

墨雨設計banner

這篇文章 【p5.js創作教學】Coral Carnival 珊瑚嘉年華 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】鳥與電線桿 https://creativecoding.in/2022/03/24/p5-js-birds-on-poles/ Thu, 24 Mar 2022 02:41:00 +0000 https://creativecoding.in/?p=2056 利用p5.js創作一點都不難!跟著老闆直播,利用簡單的random、class和參數概念,完成童趣的都市景觀《鳥與電線桿》。

這篇文章 【p5.js創作教學】鳥與電線桿 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
《鳥與電線桿》作品
《鳥與電線桿》作品

今天我們要來做的作品《鳥與電線桿》,可以分為三個項目,分別是鳥、電線桿以及顏色的設計。上圖的完成品,不管是鳥或是電線桿,它們看上去都有規律性,但是彼此之間又同時帶有一些差異性,像是每一隻鳥的大小以及所站的位置等,而電線桿則因為前後粗度不一樣以及每條左右的高度不同而形成交錯感。

透過此次互動藝術創作教學,你會學到

  • 如何透過建立 class 的方式,讓我們可以一次新增多個相同的物件
  • 透過參數設定,讓每個所產生的物件有不一樣的形體變化
  • 透過將背景顏色參數化的設定,製作出不同作品的效果

在開始製作電線桿之前你該知道的p5.js與 原生 js 小技巧

  • class 與 constructor : 要建立物件就必須使用到 class,一般來說在 class 中會搭配另一個關鍵字 constructor,這代表建構子,有了它就可以初始化一個新的物件。
  • lerp : 用於計算出兩個數值之間的相對比例的值,在本範例中用於繪製電線桿的線以及鳥的位置上,還有在後續製作漸層背景顏色時也會使用到相同的概念。
  • push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解)
  • Object.assign : 原生 js,用於物件的值指定給另一個物件。
  • push: 原生 js,將生成的電線桿物件放進陣列當中。

❗由於這次的範例中老闆有使用到物件的概念,所以在檔案上分為兩個檔案 mysketchtab2,在每段程式碼的最上方會標註這是屬於哪一個檔案的程式,提供同學們參考。

一、建立第一組電線桿

第一步驟是要畫一組電線桿,而電線桿可以細分為兩個物件,分別是電線桿的柱子以及連接兩根柱子之間的線,下面先從連接的曲線開始做起。

  1. 繪製弧線

在畫曲線前,首先定義兩邊電線桿的位置,分別是 leftPointrightPoint,接著在這兩位置之間切成十等分,並且需要知道每一等分的位置點。舉例來說,如果電線桿的兩個位置分別是 x1 = 0X2 = 100,切成十等分就會得到一組座標為 [0, 10, 20, 30, ...,90, 100],這裡就要使用到 lerp ,它一共需要三個參數 (數字A,數字B,插值量),這可以依照插值量的大小來得到介於數字A與數字B比例的值,像是 [0, 100, 0.3] 就會是 30,而 [0, 100, 0.6] 則是 60,要注意的是插值量的值必須介於0.0 與1.0 之間。

有了各別點的位置後,接著就要讓每個點的位置移動一點位置,並將其連接起來形成弧線的效果,要做出向下弧線的效果可以加上 sin 值,透過觀察 sin 波可以發現從 0 到 π 的弧形是向下的,而 π 到 2π 的弧形是向上的,所以在 sin值中要取 0 到 π。

2. 加上電線桿

畫好曲線後,桿子相對比較簡單,就在兩邊的位置以 rect() 畫上寬 10,高度為 500 的長方形。在這裡可以將剛剛在曲線上用於辨識點位置的圓形註解起來,留下一條乾淨的弧線。

3. 加上滑鼠互動

建置好電線桿後,這裡可以來嘗試加上一點小小的互動,讓電線桿的左側位置是根據滑鼠的位置來決定,這樣一來在移動滑鼠時,電線桿的寬度也會跟著改變囉 ! 不過這裡要記得在前面加上 background(255),讓背景不斷更新,不然就會變成許多電線桿疊在一起的畫面。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(255);
}

function draw() {
  // 3. 重置背景
  background(255)
  let leftPoint = createVector(50, 350)
  let rightPoint = createVector(800, 350)
  // 3. 加上滑鼠互動
  rightPoint.x = mouseX

  stroke(0)
  strokeWeight(5)
  noFill(0)
  // 1. 加上弧線
  beginShape()
  for (var i = 0; i <= 10; i++) {
    let midPoint = p5.Vector.lerp(leftPoint, rightPoint, i / 10)
    midPoint.y += sin(i / 10 * PI) * 60
    //鳥大概的位置
    //ellipse(midPoint.x, midPoint.y, 30)
    vertex(midPoint.x, midPoint.y)
  }
  endShape()

  // 2. 加上桿子
  fill(0)
  rect(leftPoint.x, leftPoint.y, 10, 500)
  rect(rightPoint.x, rightPoint.y, 10, 500)
}

二、建立物件形式

接著將電線桿包裝成 class,class就像是一種模板一樣,當把模板設定好後,我們就可以快速生成同樣的物件。

首先在 openprocessing 上面開一個新的 tab,在裡面開始撰寫 class。在openprocessing 中,不須要使用 import 的方式來引入其他的檔案,它就會自動幫我們引入,非常地方便。

1. 建立 constructor

在初始化時會使用到關鍵字 constructor ,並且帶入參數 args ,這可以讓我們設定每一個物件所要指定的特定值。而避免在建立物件時,有些屬性是沒有被傳入的,所以需要建立一個預設的數值 def。在 Object.assign(def, args) 中,如果使用者有特別設定,就會把使用者引入的參數 args 蓋到預設值 def 上,而 Object.assign(this, def) 則是在將客製化後的設定值蓋到這個物件本體 this 上。

  1. 建立 draw() 與 update()

每一個物件中會需要 draw() 與 update() 來繪製圖形,這裡將剛剛繪製第一個電線桿的相關內容複製到 draw 中,並在前後分別加入 push, pop ,避免每次畫完一個電線桿,畫布狀態會被影響。

// tab 2
class BirdBar {
  // 1. 建立 constructor
  constructor(args) {
    let def = {
      leftPoint: createVector(50, 350),
      rightPoint: createVector(50, 350),
    }
    Object.assign(def, args)
    Object.assign(this, def)
  }
  // 2. 建立 draw() 與 update()
  draw() {
    // 要用 push() pop() 包起來
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        // ellipse(midPoint.x, midPoint.y, 30)
        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
  update() {
  }
}

三、在主程式新增物件

在設定好模板後,就可以來到主程式來建立物件。由於是要一次建立好幾個電線桿,所以首先建立存放電線桿的陣列 birdBars,接著使用迴圈的方式建立五個電線桿,在這裡設定每個電線桿的左右間格為 400,而每個電線桿左側的起點位置為則是 -300 + 400 * i,可以注意到與 i 相乘的常數同為 400,與電線桿兩側的距離一樣,這樣才會形成所有的電線桿相連在一起的效果。

設定好參數後,再來在主程式的 draw 裡面透過 forEach 的方式呼叫每一個物件中的 draw 與 update,將圖形繪製出來。

// mysketch
let birdBars = []
function setup() {
  createCanvas(1000, 1000);
  background(0);
  for (var i = 0; i < 5; i++) {
    let startX = -300 + 400 * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, 350),
      rightPoint: createVector(startX + 400, 350),
    }))
  }
}

function draw() {
  fill(200)
  rect(0, 0, width, height)

  let leftPoint = createVector(50, 350)
  let rightPoint = createVector(800, 350)
  rightPoint.x = mouseX

  birdBars.forEach(bar => {
    bar.update()
    bar.draw()
  })
}
《鳥與電線桿》第三步驟:完成一組電線桿與電線
《鳥與電線桿》第三步驟:完成一組電線桿與電線

四、鳥兒站在電線桿上

透過物件的方式完成了電線桿後,再來要加上鳥的形狀。這個步驟不困難,還記得在第一步驟的時候就已經畫出每一個點的圓形樣式了,在這裡只需要稍微做修改即可。

為了要讓鳥看起來是站在電線桿上的樣子,所以 y 的位置要向上移動高度 30 的一半,也就是 15,另外鳥也不太會站在電線桿上,所以要設定條件,如果是電線桿頭尾的話,不會繪製鳥兒。

// tab 2    
  draw() {
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        // 其實就是剛剛所畫的橢圓,但是鳥不會站在桿子上,所以要扣除掉頭尾
        if (i != 0 && i != 10) {
          ellipse(midPoint.x, midPoint.y - 15, 20, 30)
        }
        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
《鳥與電線桿》第四步驟:確認鳥的位置
《鳥與電線桿》第四步驟:確認鳥的位置

五、為鳥添上顏色

老闆這裡一樣透過配色網站 coolors 來找尋配色的靈感,將網址中色票的部分複製進來,透過字串的處理後,變成可使用的顏色形式。在填色上老闆先以取於餘數的方式,讓五個顏色以規律方式依序呈現在鳥的身上,不過這時候會發現填色後的效果有點怪怪的,原因在於後面我們有把線條連起來,為了讓填色的效果是正常的,需要在這裡要加上 push()和pop() 將繪製鳥的地方包起來。

//tab 2
// 新增顏色
let colors = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(a => "#" + a)

class BirdBar {
  constructor(args) {
    ...
  }

  draw() {
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        if (i != 0 && i != 10) {
          // 指定隨機顏色,並且使用 push() pop() 包起來
          push()
            noStroke()
            fill(colors[i % 5])
            ellipse(midPoint.x, midPoint.y - 15, 20, 30)
          pop()
        }

        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
  update() {
  }
}
// mysketch
function draw() {
  // 更改背景顏色
  fill("#679dbf")
  rect(0, 0, width, height)
}
《鳥與電線桿》第五步驟:彩色的鳥隻
《鳥與電線桿》第五步驟:彩色的鳥隻

目前畫面上所看到的是依序排列的顏色,為了讓它有點變化,就讓顏色隨機呈現。說到隨機,你可能會想到使用 random,不過這裡無法使用的原因在於 draw 會一直不斷地執行,所以每一幀都會重新產生一組隨機的顏色,以至於畫面中的顏色是會不斷變化的。

在這裡我們要呼叫另一個與隨機也有相關的函式 noise,在 noise中,只要輸入一個固定的數值,那它出來的也會是一個固定的數值,不會變動,這樣一來就不會隨機跳動了。不過也因為這樣的特性,它看上去又再次呈現規律的樣子,原因是其中的 i 值都是固定的從 1~9,所以每一組電線桿的排列的顏色組合會是一樣的,在這裡可以透過在屬性上新增 randomId: random(100000),並加到 noise 當中,讓每一次所輸入的數值都是隨機不同的,這樣一來所輸出的數值也會形成隨機。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  // 新增 randomId 增加顏色的隨機性
  randomId: random(100000)
}
//tab 2
if (i != 0 && i != 10) {
  push()
    noStroke()
    // fill(colors[i%5])
    // 以 noise 的方式來隨機取顏色,記得要加上 this.randomId
    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    ellipse(midPoint.x, midPoint.y - 15, 20, 30)
  pop()
}
《鳥與電線桿》第五步驟:鳥的隨機顏色
《鳥與電線桿》第五步驟:鳥的隨機顏色

六、修正電線桿位置

現在的接點有點怪怪的,因為 rectangle 是針對左上角來做繪製的,所以這裡要調整一下電線桿的位置,要將它往左移動自己一半的寬度。

另外也將電線桿的高度與寬度改以屬性的方式呈現,後續在新增其他組電線桿的時候會比較方便一些。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  // 將電線桿也同樣變成屬性
  barSize: createVector(10, 500)
}
//tab 2
// 減去電線桿寬度一半的長度 this.barSize.x / 2
rect(this.leftPoint.x - this.barSize.x / 2, this.leftPoint.y, this.barSize.x, this.barSize.y)
rect(this.rightPoint.x - this.barSize.x / 2, this.rightPoint.y, this.barSize.x, this.barSize.y)
《鳥與電線桿》第六步驟:修正電線桿位置
《鳥與電線桿》第六步驟:修正電線桿位置

七、做出不同組的電線桿

目前一組的電線桿已完成,接下來新增其他組的電線桿,老闆希望每一組的電線桿的寬度以及彼此之間的前後距離是不同,下面我們分成兩個步驟來實作。

  1. 電線桿不同的寬度

在第三步驟時,我們將電線桿彼此的寬度設定為 400,而現在則設定為一個隨機的寬度,使用 random 隨機產生 300 至 500 的數值並存至變數 barDist。

在先前的 startX 其值為 -300 + 400 * i,-300 所代表的為整排電線桿的起始位置,而 400 則代表每根電線桿的距離,i 表示是第幾根電線桿,所以這裡要將代表距離的 400 取代為 barDist。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(0);
  // 新增變數  barDist
  let barDist = random(300, 500)
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, 350),
      rightPoint: createVector(startX + barDist, 350),
    }))
  }
}

2. 前後層距離不同

在前一個步驟設定寬度時是改變 X 的值,而要設定前後位置則是要改變 Y 的值,在外層再新增一層迴圈,並設定 startY = o * 100 + 200,這表示第一層的 Y 位置為 200,而每一層所間隔的距離為 100。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(0);
  for (var o = 0; o < 4; o++) {
    let barDist = random(300, 500)
    // 垂直距離不一樣,所以新增 Y 的距離
    let startY = o * 100 + 200
    for (var i = 0; i < 5; i++) {
      let startX = -300 + barDist * i
      birdBars.push(new BirdBar({
        leftPoint: createVector(startX, startY),
        rightPoint: createVector(startX + barDist, startY),
      }))
    }
  }
}
《鳥與電線桿》第七步驟:複製多組鳥與電線桿,豐富畫面
《鳥與電線桿》第七步驟:複製多組鳥與電線桿,豐富畫面

八、製作漸層背景

在 p5.js 中沒有可以直接繪製漸層的方式,不過我們可以透過好幾個方形來達成。先設定漸層兩邊的顏色,並命名為 startColorendColor,這裡我們製作的是垂直方向的漸層,所以在外層 for 迴圈的數值範圍是 0~height,並以每 30 為單位繪製一個方形。

在顏色設定上使用了 lerpColor,它與一開始在畫電線桿所使用到的 lerp 概念上是一樣的,都是前兩個參數是給值 A 與 B ,而第三個參數則是給予一個插值,即可找到介於 A 與 B 之間插值比例的顏色。

// mysketch
// 新增漸層顏色
let startColor = color("#aaa")
let endColor = color("#ddd")

noStroke()
for (let y = 0; y < height; y += 30) {
  let currentColor = lerpColor(startColor, endColor, y / height)
  fill(currentColor)
  rect(0, y, width, 30)
}
《鳥與電線桿》第八步驟:製作漸層背景
《鳥與電線桿》第八步驟:製作漸層背景

九、鳥

完成了背景之後,要精細製作主角「鳥」,這個步驟一共分為五個小節,先後針對了鳥的外觀、數量以及動態的呈現去做調整。

  1. 將身體上下拆分

老闆首先將鳥的身體從原本的橢圓形拆分成兩個半圓形,不過在重新繪製之前,為了方便圖形的定位,所以在畫每個點的位置之前,先將原點移動至先前繪製鳥身體中心的位置。

我們可以用 arc 來繪製半圓形,一共給定六個參數,最後兩個參數是指定角度的範圍,上半圓的範圍是 0~PI,而下半圓的範圍則是 PI~2 * PI,而因為剛剛已經 translate 座標位置了,在中心點的設定上皆為 (0, 0)。

//tab 2
if (i != 0 && i != 10) {
  push()
    // 移動中心點
    translate(midPoint.x, midPoint.y - 15)
    noStroke()
    // fill(colors[i%5])

    // 原本用橢圓畫,改為使用兩個半圓形畫
    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    arc(0, 0, 20, 30, 0, PI)
    // 下半身建立另外一個隨機的顏色
    let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex2])
    arc(0, 0, 20, 30, PI, 2 * PI)
  pop()
}
《鳥與電線桿》第九步驟:調整鳥的身軀
《鳥與電線桿》第九步驟:調整鳥的身軀

2. 設定鳥的隨機大小

在鳥的屬性值中新增 birdSize,代表鳥的大小,接著將與鳥身體大小有關的都改使用參數的方式呈現,第一個是中心點的位移,每隻鳥所要向上位移的量為自己身高的一半,所以要減去 this.birdSize.y / 2,而在繪製半圓形的身體中則改以 this.birdSize.xthis.birdSize.y 表示。

當上述都設定好後,在產生出鳥的物件時,就可以以 random 的方式產生出不同高度與寬度的鳥囉。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(5, 500),
  // 新增 birdSize 屬性
  birdSize: createVector(20, 30)
}
//tab 2
if (i != 0 && i != 10) {
  push()
    // 中心點 Y 軸移動改為參數
    translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
    noStroke()
    // fill(colors[i%5])

    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    // 身體大小改為參數
    arc(0, 0, this.birdSize.x, this.birdSize.y, 0, PI)
    let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex2])
    // 身體大小改為參數
    arc(0, 0, this.birdSize.x, this.birdSize.y, PI, 2 * PI)
  pop()
}
// mysketch
birdBars.push(new BirdBar({
  leftPoint: createVector(startX, startY),
  rightPoint: createVector(startX + barDist, startY),
  // 在設定好鳥大小的屬性後,便可以隨機指定鳥的大小
  birdSize: createVector(random(20, 20), random(20, 40))
}))
《鳥與電線桿》第九步驟:設定鳥的隨機大小
《鳥與電線桿》第九步驟:設定鳥的隨機大小

3. 畫鳥嘴

在畫鳥嘴前,一樣再新增一組隨機的顏色。老闆以三角形的方式來呈現鳥嘴的樣式,它一共需要六個參數,分別是三個點的 x 與 y 位置。

// tab 2
let colorIndex3 = int(2 + noise(i, this.randomId) * 20) % colors.length
fill(colors[colorIndex3])
translate(5, 0)
triangle(0, 0, 0, -10, 10, -5)

4. 改變鳥的數量與密集程度

先前在每條電線上是以規律的方式站了十隻鳥,在這裡要將鳥的數量改為變數的方式呈現,好讓鳥在數量的設定上會比較方便。建立一個計算鳥數量的變數 nodeCount,並且將與鳥數量的數值替換成變數。 另外在繪製鳥的條件下,除了頭與尾不畫鳥之外,老闆也設定了只有當 i 是 3 或 5 的倍數時才畫鳥,這樣一來會讓畫面更有錯落的感覺。

// tab 2
// 新增  nodeCount,並更換將常數都更換為變數 (總共4個地方要替換)
let nodeCount = 30
for (var i = 0; i <= nodeCount; i++) {
  let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / nodeCount)
  midPoint.y += sin(i / nodeCount * PI) * 70
  // 取 5 跟 3 的餘數,這樣會比較有錯落的感覺
  if (i != 0 && i != nodeCount && (i % 5 == 0 || i % 3 == 0)) {
    push()
      translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
      noStroke()
      // fill(colors[i%5])
      let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex])
      arc(0, 0, this.birdSize.x, this.birdSize.y, 0, PI)

      let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex2])
      arc(0, 0, this.birdSize.x, this.birdSize.y, PI, 2 * PI)

      let colorIndex3 = int(2 + noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex3])
      translate(5, 0)
      triangle(0, 0, 0, -10, 10, -5)
    pop()
  }

  vertex(midPoint.x, midPoint.y)
}
《鳥與電線桿》第九步驟:加上鳥嘴
《鳥與電線桿》第九步驟:加上鳥嘴

5. 鳥動態跳起

到這裡鳥的設定上差不多了,老闆這裡加上translate,並以 noise 的方式讓鳥以隨機的方式向上跳動。

// tab 2
translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
translate(0, noise(i + frameCount / 50) * 50 - 30)
《鳥與電線桿》第九步驟:隨機跳動的鳥嘴
《鳥與電線桿》第九步驟:隨機跳動的鳥嘴

十、調整電桿感外觀

  1. 提升電線桿質感

在開始調整之前,老闆先將剛剛所製作鳥跳動的效果註解起來,視覺上比較不會影響到我們的調整過程。首先將電線桿的寬度與線條的粗度 ( strokeWeight(1) )縮小之外,也將電線桿的間距都拉大,讓看上去的質感更加提升。

// tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  // 將電線桿寬度調小
  barSize: createVector(3, 500),
  birdSize: createVector(20, 30)
}
// mysketch
let startY = o * 200 + 50
《鳥與電線桿》第十步驟:調整電線桿
《鳥與電線桿》第十步驟:調整電線桿

2. 新增多條電線

老闆在找實體電線桿參考圖的時候,注意到每根電線桿實際上彼此之間是有多條電線的,所以老闆決定再多增加兩條電線,可以直接複製第一組的程式後貼上,再微調參數。在決定上下高度的參數 y 上,第二組的電線桿左側加上 50,右側加上 100,而第三組則是左側增加 120 ,右側加上 50,這樣子形成高低與交錯的效果。

// mysketch
for (var i = 0; i < 5; i++) {
  let startX = -300 + barDist * i
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY),
    rightPoint: createVector(startX + barDist, startY),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  // 新增其他組別的電線桿,左右兩測的高度不一樣,由低到高
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 50),
    rightPoint: createVector(startX + barDist, startY + 50 + 50),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  // 新增其他組別的電線桿,左右兩測的高度不一樣,由高到低
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 120),
    rightPoint: createVector(startX + barDist, startY + 50),
    birdSize: createVector(random(10, 20), random(30, 40))
  }))
}
《鳥與電線桿》第十步驟:增加電線
《鳥與電線桿》第十步驟:增加電線

十一、新增與調整顏色

  1. 多彩的鳥

接著要來替剛剛所增加的鳥填上不同的顏色。老闆一樣至配色網站 coolors 找合適的配色組合。接著在屬性上新增屬性 colors ,預設值設定為黑色。設定好後就可以在生成鳥的物件時候,以 random 隨機選取三組顏色方式來讓鳥有不同的色彩組合。

// tab 2
let colors1 = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(a => "#" + a)
let colors2 = "447604-6cc551-9ffcdf-52ad9c-47624f".split("-").map(a => "#" + a)
let colors3 = "173753-6daedb-2892d7-1b4353-1d70a2-fff-222".split("-").map(a => "#" + a)

let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(4, 500),
  birdSize: createVector(20, 30),
  // 新增預設顏色
  colors: ["#000"]
}
// mysketch
for (var i = 0; i < 5; i++) {
  let startX = -300 + barDist * i
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY),
    rightPoint: createVector(startX + barDist, startY),
    // random 三組顏色,下面兩個也是一樣
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 50),
    rightPoint: createVector(startX + barDist, startY + 50 + 50),
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 120),
    rightPoint: createVector(startX + barDist, startY + 50),
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 40))
  }))
}
《鳥與電線桿》第十一步驟:改變鳥的顏色
《鳥與電線桿》第十一步驟:改變鳥的顏色

2. 黑暗模式

這裡老闆來嘗試看看如果是背景黑色,而電線桿是白色的話效果如何,結果效果意外地不錯。電線桿的部分新增電線顏色的屬性,並且分別加在電線桿與電線上。

// mysketch
// 將背景改為黑色
let startColor = color("#000")
let endColor = color("#333")
// tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(4, 500),
  birdSize: createVector(20, 30),
  colors: ["#000"],
  // 新增電線桿顏色
  barColor: color(255)
}

// 更改電線顏色
stroke(this.barColor)
strokeWeight(1)
noFill(0)
beginShape()
let nodeCount = 30
for (var i = 0; i <= nodeCount; i++) {...}


// 更改電線桿顏色
fill(this.barColor)
rect(this.leftPoint.x - this.barSize.x / 2, this.leftPoint.y, this.barSize.x, this.barSize.y)
rect(this.rightPoint.x - this.barSize.x / 2, this.rightPoint.y, this.barSize.x, this.barSize.y)
《鳥與電線桿》第十一步驟:調整背景顏色
《鳥與電線桿》第十一步驟:調整背景顏色

十二、調整鳥的細節

  1. 跳動方式

將之前鳥跳動的設定打開,由於現在在 noise 的參數中只給第幾個 i 以及時間參數 frameCount,所以會發現當它跨不同組的時侯,上下擺動的頻率會是一樣的,這裡可以使用到之前為了製造隨機顏色所創建 randomId,將它加到 noise 之中。

// tab 2
translate(0, noise(i + frameCount / 50, this.randomId) * 50 - 30)

2. 改變鳥嘴方向

老闆在這裡有曾嘗試過替鳥加上眼睛,不過效果不是很好,所以後來就拿掉了。除了眼睛外,老闆這裡調整了嘴巴的位置,讓它以置中的方式呈現。

// tab 2
translate(0, -5)
triangle(-5, 0, 0, 8, 5, 0)

3. 調整鳥的密集程度

現在看上去鳥的數量上有點多,所以將取餘數的數值加大,降低鳥的密集程度。

// tab 2
if (i != 0 && i != nodeCount && (i % 6 == 0 || i % 4 == 0)) {...}
《鳥與電線桿》第十二步驟:調整鳥隻的細節

十三、調整電線桿顏色與背景色

為了不讓畫面上看起來那麼的單調,老闆從兩個地方著手,第一是改變每一組電線桿的顏色,以隨機的方式指定顏色,另一個則是指定背景顏色,讓作品的主體可以呈現出來。

// mysketch
//  2. 調整背景顏色
background(255);
for (var o = 0; o < 4; o++) {
  let barDist = random(300, 500)
  let startY = o * 200 + 50
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY),
      rightPoint: createVector(startX + barDist, startY),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      // 1. 隨機取電線桿的顏色
      barColor: random(colors1)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 50),
      rightPoint: createVector(startX + barDist, startY + 50 + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      // 隨機取電線桿的顏色
      barColor: random(colors2)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 120),
      rightPoint: createVector(startX + barDist, startY + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 40))
    }))
  }
}
《鳥與電線桿》第十三步驟:調整電線桿及背景顏色
《鳥與電線桿》第十三步驟:調整電線桿及背景顏色

十四、多種情境

接下來要做的是如何用同一組演算法,來做出不同作品的效果。首先在 setup 中定義出幾種不同的情境,像是早上、中午、晚上,接著在 draw 裡面設定每種情境之下的背景漸層顏色,這樣一來當我們每次按下重新整理時,就可以隨機看到不同色系的作品了。

// mysketch
function setup() {
  // 定義 currentTime,需要再 setup 裡面,是因為要在這裡才能夠使用 random
  currentTime = random(["day", "night", "evening"])
}

function draw() {
  // 根據不同的狀態設定不同的顏色
  let startColor = color("#000")
  let endColor = color("#333")

  if (currentTime == "day") {
    startColor = color("#92a0a5")
    endColor = color("#eee")
  }

  if (currentTime == "evening") {
    startColor = color("#ffca59")
    endColor = color("#f43030")
  }
}
《鳥與電線桿》第十四步驟:早中晚的不同背景顏色

十五、滑鼠互動

除了讓鳥隨機的跳動之外,這裡增加當滑鼠移動到鳥身上後會跳起的效果,使用計算距離的 dist,計算出鳥的位置與滑鼠之間的距離小於一定距離時,便會向上移動。

translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
translate(0, noise(i + frameCount / 50, this.randomId) * 50 - 30)
// 當滑鼠的距離跟鳥很接近時,會向上移動
if (midPoint.dist(createVector(mouseX, mouseY)) < 100) {
  translate(0, sin(i + frameCount / 30) * 30 - 15)
}
《鳥與電線桿》第十五步驟:增加滑鼠互動效果
《鳥與電線桿》第十五步驟:增加滑鼠互動效果

十六、電線桿粗細

最後在電線桿的粗細上做一點調整,讓前面的電線桿比較粗、後面的電線桿比較細。決定電線桿前後的參數是 o,一共有五層,所以在桿子的寬度上可以用常數乘上 o 的方式,讓越下面的電線桿越粗,製造出層次感。

for (var o = 0; o < 4; o++) {
  let barDist = random(300, 500)
  let startY = o * 200 + 50
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY),
      rightPoint: createVector(startX + barDist, startY),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      barColor: random(colors1),
      // 讓前面的電線桿比較租
      barSize: createVector(4 + o * 5, 500)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 50),
      rightPoint: createVector(startX + barDist, startY + 50 + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      barColor: random(colors2),
      barSize: createVector(4 + o * 5, 500)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 120),
      rightPoint: createVector(startX + barDist, startY + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 40)),
      barSize: createVector(4 + o * 5, 500)
    }))
  }
}
《鳥與電線桿》完成圖
《鳥與電線桿》完成圖

總結

回顧一下這次的鳥與電線桿的製作過程

  1. 建立建立第一組電線桿,使用 lerp 方式製作弧形的電線
  2. 新增一個 tab 檔案,用於建立 class,並在主程式 sketch 中透過 class 建立物件
  3. 在電線桿上每一個點的位子 midPoint 上劃出鳥的外型並且上色
  4. 以 for 迴圈的方式建立四組電線桿
  5. 使用一格格色塊的方式製作出背景漸層的效果
  6. 在鳥的外觀、數量以及動態的呈現上做調整
  7. 修正電線桿的間距以及數量
  8. 對鳥的部分細節做調整
  9. 改變電線桿與背景顏色
  10. 針對不同情境設定不同的背景顏色
  11. 調整電線桿粗度,產生前後層次感

以上就是我們用p5.js寫出來的作品啦!相同的繪製原理還能應用在甚麼作品上呢?

延伸閱讀:
用p5.js玩創作,讓小機器人動起來!哲宇的互動藝術體驗(直播筆記)
【p5.js創作教學】Aqua Planet 水色星球 – 來製作發光碰撞的行星吧!(直播筆記)
【p5.js創作教學】Quantum Unstable 量子不穩定 – 發光糾纏的量子系統

若對互動藝術程式創作有興趣,歡迎加入老闆開的Hahow課程互動藝術程式創作入門,與另外將近兩千位同學一起創作吧!

此篇直播筆記由幫手 阮柏燁 協助整理

墨雨設計banner

這篇文章 【p5.js創作教學】鳥與電線桿 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】Dreamy Bird 夢幻鳥 https://creativecoding.in/2021/10/12/p5-js-dreamy-bird/ Tue, 12 Oct 2021 09:12:00 +0000 https://creativecoding.in/?p=1568 本篇記錄創作<夢幻鳥>這件作品的過程,利用p5.js在短短的時間內創作出繽紛多彩、眼睛跟著滑鼠游標轉的夢幻鳥,看著他不斷地動,好像趴著在看水族館裡的魚一樣,心情也會跟著雀躍起來唷!

這篇文章 【p5.js創作教學】Dreamy Bird 夢幻鳥 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>

本文翻自【Coding Vlog | p5.js】200514 Dreamy Bird 夢幻鳥 – 來做彩色又毛毛不知道是魚還是鳥的生物吧!若是想要老闆手把手帶你飛,可以跟著影片進行,這邊也附上成品歡迎大家一起動手做。

這一次分享的內容比較特別,是紀錄老闆創作的過程,起初只是想做金屬色的練習,調整不同屬性以及數值後,慢慢產生了生物的形體,而有了夢幻鳥的誕生。這個作品會利用線上的工具 openprocessing 來進行 p5.js 的創作。完成作品後會發現,其實使用到的 api 就只有那幾個,卻能創作出獨特又有趣的作品,大家了解 api 後,也能勇敢去嘗試調整,說不定會有更意想不到的作品產生。如果想要了解更詳細的製作流程和其他創作內容,可以去支持老闆的互動藝術程式創作課程哦!

這次直播筆記會帶大家學會以下內容:

  • 利用 p5.js 進行創作互動作品
  • 使用 noise 產生自然有序的隨機數
  • 在作品中加入滑鼠互動,讓作品與觀賞者產生連結

事前準備

開發環境

開發會使用 openprocessing 線上撰寫程式碼,如果想知道較詳細的設定,可以到成品看老闆的開發環境設定。

  • openprocessing:提供大家在網頁中直接使用 p5.js 進行開發,只要利用所提供的 api ,就能製作出有趣的效果。想要了解更多相關效果的開發,除了參考網站中其他的p5.js創作教學之外,也歡迎看看老闆的線上課程,跟老闆跟一起進入 processing 的世界。

會使用到的 API:

這次專案會使用以下的 api,先重點整理給大家,不清楚的地方,可以透過後面跟著老闆操作,了解每個 api 使用時機。

  • createCanvas(width, height): 創建畫布,參數中分別傳入寬跟高。
  • background(colorCode): 加上背景色,可依照文件傳入色碼參數。
  • noStroke(): 取消繪製圖形的邊框。
  • colorMode(): 定義顏色的方式,預設為 RGB 顏色,HSB 模式依序要填入的值則為(色相, 飽和度, 明度)。
  • fill(): 選擇填入的顏色,依照 colorMode 選擇的填色模式填入對應的參數。
  • ellipse(posX, posY, width, height): 在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形。
  • rect(x, y, width, height): 以 (x, y) 的位置為左上角的點,畫一個寬度 width 高度 height 的方形(如果要畫正方形的話,即寬度=高度)。
  • random(): 沒有傳參數時,會返回一個隨機浮點數。
  • noise(): 產生自然有序的隨機值,與 random 的概念不一樣。在範例中會使用到 noise,所以建議先稍微理解 noise 的概念,有興趣的同學也可以延伸閱讀相關資訊:2D Noise – Perlin Noise and p5.js Tutorial
  • rotate(angle): 依照傳入的參數進行旋轉。
  • translate(x, y): 將畫筆移到(x,y) 上。
  • push(): 紀錄目前畫筆狀態。
  • pop(): 恢復畫筆狀態。
  • sin(): 正弦,將傳入的數值做為角度值,換算成 1~-1 的值。
  • cos(): 餘弦,將傳入的數值做為角度值,換算成 1~-1 的值。
  • atan2(y, x): 計算從指定點(y,x)到座標原點的角度。

跟著老闆開始動手做

1. 起手式

開啟新的 openprocessing > Create a Sketch,可以看到程式碼頁面已經有一段預設的程式碼,隨著滑鼠的移動,會沿路產生小球,理解這段程式碼後,接著只留下我們需要的部份。

  • setup(): 可以視為環境初始化,只在開始執行的當下會呼叫一次,以下的程式碼使用了兩個 api
    • createCanvas(width, height):創建畫布,參數中分別傳入寬跟高,如果直接寫螢幕的寬高(windowWidth, windowHeight),就會成為滿版的互動區塊。
    • background(colorCode):加上背景色,可依照文件傳入色碼參數。
  • draw(): 會依照時間不停地重跑裡面的程式碼,要製作互動的內容可以在這個 function 中呼叫。
    • ellipse(posX, posY, width, height):在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形。
function setup() {
  createCanvas(windowWidth, windowHeight);
  background(100);
}

function draw() {
  ellipse(mouseX, mouseY, 20, 20);
}

2. 繪製基礎噪聲

在 draw 中,我們先調整顏色模式改成 HSB,後續與填色有關的 api 就會改成依序填入(色相, 飽和度, 明度),

  • noStroke() 將每次繪製圖形的邊框取消掉,每個方塊間就不會有 stroke。
  • colorMode():定義顏色的方式,預設為 RGB 顏色,HSB 模式依序要填入的值為(色相, 飽和度, 明度)。

接下來我們可以看到有兩個 for 迴圈,第一個 for 迴圈會每隔高度 20 ,再進行一次第二個 for 迴圈的內容,重新從左至右繪製一長串的方形。

  • rotate(angle): 依照傳入的參數進行旋轉。
  • fill():選擇填入的顏色,由於前面選擇了 HSB ,所以這邊要改使用 HSB 的方式填色。
  • sin():將傳入的數值做為角度值,換算成 1~-1 的值。
  • noise():躁聲,隨機序列生成器。跟 random 相比,可以利用多維的座標產生自然有序的序列,產出的值介於 0~1之間。
  • rect(x, y, width):在 (x, y) 的位置畫一個寬度 width 的方形。

經過調整後,讓呈現的顏色有時偏白,有時飽和度不會那麼高。在 sin 或 noise 中代入的值,老闆會多除上一些數字,目的是為了讓呈現的顏色變化不要太快,但這沒有正確答案,同學可以在了解每個 api 的操作方式後,依照自己的經歷或感受,去嘗試自己喜歡的氛圍。

最後一步驟,老闆希望能讓每一個橫條看起來都不同進度,所以在每一條橫條繪製前,都旋轉一下,就完成繪製基礎噪聲階段了,產生類似彩虹的畫面。

function setup() {
  createCanvas(800, 800);
  background(100);
}

function draw() {
  colorMode(HSB)
  noStroke()
  for(var o = 0; o<height; o+=20) {
    rotate(PI/1000)
    for(var i = 0; i<width; i++) {
      fill(sin(i/100)*300, noise(i/50, o/1000)*100, sin(i/40,o/1000)*30+80)
      rect(i, o, 30)
    }
  }
}
【p5.js創作教學】 夢幻鳥-步驟二:繪製基礎噪聲
【p5.js創作教學】 夢幻鳥-步驟二:繪製基礎噪聲

3. 依據噪聲橫列的影響色彩分佈跟變化

這個階段,老闆對每一行的波進行尺寸與填色的微調,也進行了波型的嘗試:

  • 讓每一橫列產生偏移:填色位置加入 o ,隨著每一行 o 的值逐漸增加,使得每一橫列的波產生偏移。
  • 讓波跟著時間動起來:填色位置加入時間因子 frameCount,讓整幅跟著時間的前進而動起來。
  • 更豐富的顏色:希望每一行波使用了 o 之後,不是只有偏移效果,所以在 noise 中又加入了 noise。
  • 降低波顏色變化速度:希望顏色變化的速度能更慢一點,針對 fill 內色相位置的值,除上更小的數字。
  • 加上插畫材質:為了讓作品更有質感,所以我們為作品加上插畫材質,材質製作方式這邊不詳細介紹,同學可以將程式碼貼到對應的地方直接使用。
let overAllTexture
function setup() {
  createCanvas(800, 800);
  background(0);
	
  // 插畫材質
  overAllTexture=createGraphics(width,height)
  overAllTexture.loadPixels()
  for(var i=0;i<width+50;i++){
    for(var o=0;o<height+50;o++){
      overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,40,80])))
    }
  }
  overAllTexture.updatePixels()
}
function draw() {
  colorMode(HSB)
  noStroke()
  for(var o = 0; o<height; o+=100) {
    for(var i = 0; i<width; i++) {
      fill(
        noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360,
        noise(i/50, o/1000, frameCount/100)*100,
        noise(i/40, o/1000, frameCount/100)*30+80
      )
      rect(i, o, 80)
    }
  }

  // 加上插畫材質
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟三:依據噪聲橫列的影響色彩分佈跟變化
【p5.js創作教學】 夢幻鳥-步驟三:依據噪聲橫列的影響色彩分佈跟變化,隨著時間前進具有插畫材質的波

4. 修飾幾何形狀與加入隨機大小

接下來,我們只會動到 draw 裡面的內容,老闆在這個階段做了以下嘗試:

  • 貓毛效果:畫方塊的時候,利用變數 glitchAmount ,在 x, y 座標加上隨機偏移值。
  • 隨著滑鼠位置改變的貓毛:將隨機偏移的值加上滑鼠的值,使作品與滑鼠產生了互動。
  • 微調樣式,將外層的 for 迴圈 o 從 10 開始,讓作品與上下邊界的距離一樣

老闆也有嘗試加入隨機的黑線在波形中,產生類似現代藝術的感覺,但實際做出來的效果不好(下圖左),大家想要嘗試,可以將以下程式碼斜線的部分恢復。

let glitchAmount = 20
function draw() {
  colorMode(HSB)
  noStroke()
  glitchAmount = mouseX/10 // 隨著滑鼠變更數字的貓毛
  for(var o = 10; o<height; o+=100) {
    for(var i = 0; i<width; i++) {
      fill(
        noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360,
        noise(i/90, o/1000, frameCount/100)*100,
        noise(i/80, o/1000, frameCount/100)*30+80
      )
      rect(// 繪製方塊時,繪製的座標結合隨機的值
        i + random(-glitchAmount, glitchAmount), 
        o + random(-glitchAmount, glitchAmount),
        80
      )
      // if(noise(i, o, frameCount/100) < 0.1){
      //   push()
      //     stroke(0)
      //     strokeWeight(20)
      //     rect(i, o, 80)
      //   pop()
      // }
    }
  }
	
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟四:加入隨機的黑線
【p5.js創作教學】 夢幻鳥-步驟四:加入隨機的黑線
【p5.js創作教學】 夢幻鳥-步驟四:隨著滑鼠產生不同波形尺寸
【p5.js創作教學】 夢幻鳥-步驟四:隨著滑鼠產生不同波形尺寸

5. 使用三角函數繪製波型

接下來老闆想做出類似極光的效果,一系列的調整與操作後,慢慢地變成一塊一塊的物體往前移動中,過程中做了以下嘗試,大家也能跟著老闆一起嘗試:

  • 由上到下、粗到細:極光從上到下粗到細,改變 rect 的第三個參數來實現改變波的大小
  • 結合 sin 產生波形:使用 sin 來繪製方形所產生波形,比較像極光或海浪,
  • 將波形結合時間因子 frameCount,讓波動起來
  • 改變 rectMode 為 CENTER,讓波上下同時變大
let glitchAmount = 5
function draw() {
  colorMode(HSB)
  noStroke()
  glitchAmount = mouseX/10
  rectMode(CENTER) // 調整繪製方形的模式
  for(var o = 50; o<height; o+=100) {
    for(var i = 0; i<width; i++) {
      fill(
        noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360,
        noise(i/90, o/1000, frameCount/100)*100,
        noise(i/80, o/1000, frameCount/100)*30+80
      )
      rect(
        i + random(0, glitchAmount),
        o + random(-glitchAmount, glitchAmount),
        (sin(i/40 + frameCount/20+o*50)+1)*30+20 // 結合 sin 繪製波形,加上 frameCount 讓波能跟著時間動起來
      )
    }
  }
	
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟五:結合三角函數,產生向前的波形物體

6. 加上眼睛與調整生物外觀

老闆認為一塊一塊向前的波形很像生物的身體,雖然還沒確定是魚還是鳥,但是老闆決定賦予每一個區塊眼睛。這階段老闆將一些值整理成變數,大家可以來回參照上一階段與這個階段的程式碼比較,在加上眼睛與生物外觀的調整過程做了以下嘗試:

  • push 與 pop:因為老闆將眼睛位置統一記錄在 eyes 中,所以程式碼會先將所有生物的身體繪製完畢後,再繪製所有生物的眼睛,這邊就會需要把畫筆位置移到對的地方,所以使用了 translate。要注意的是,做畫筆的移動或是旋轉畫布時,我們會使用 push 將原本的狀態記錄著,當完成位置時再搭配 pop 去恢復原本畫筆的狀態。
    • translate(x,y):將畫筆移動到 x, y 的位置
  • 以波形的進度 (progAng) 作為眼球的位置:將每個完整波形的長度百分之 3 的位置存進陣列中,同學要記得使用餘數,因為隨著時間增加,frameCount 是一直增加的,利用 PI * 2 去處理餘數,就能取得每個波形進度。
  • 生物的位置與滑鼠關聯:除了讓方塊的位置隨著時間去改變,這邊也做了滑鼠的互動,讓波形進度的值結合滑鼠位置。
  • 繪製生物的方塊:生物的身體,是依不同時間點來決定出不同大小的方塊所組成,老闆將原本的方大小作為 progAng 變數的值,再由 hh 變數來組合使用 progAng。
  • 區塊的大小更加生動:原本的區塊大小只是隨著滑鼠位置去變化,在 hh 的值中,除了利用 sin 之外,也加入 cos ,讓這個生物的外觀更有趣,產生毛邊金魚的感覺。
  • 繪製眼睛:眼睛陣列(eyes)裡的物件,是所有符合條件的眼睛 x 座標,結合 ellipse,繪製眼白與眼珠。
let glitchAmount = 5
function draw() {
  colorMode(HSB)
  noStroke()
  glitchAmount = mouseX/100
  rectMode(CENTER)
  for(var o = 50; o<height; o+=100) {
    let eyes = []
    push() // 記錄當下初始畫筆的狀態
      translate(0, o) // 移動畫筆到 (0, o) 的位置
      for(var i = 0; i<width; i++) {
        push() // 再次紀錄當下畫筆狀態
          translate(i,0) // 移動畫筆到(i, 0) 的位置
          fill(
            noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360,
            noise(i/90, o/1000, frameCount/100)*100,
            noise(i/80, o/1000, frameCount/100)*30+80
          )
          let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2) // 結合餘數計算,讓值介於 0~100 之間
          let hh = (sin(progAng) + 1 + cos(progAng/2))*30 +20  // 結合波的進度作為每次繪製方塊的大小
          rotate(sin(i/10))
          rect(
            random(0, glitchAmount),
            random(-glitchAmount, glitchAmount),
            hh
          )
          if( int(progAng/PI/2*100 ) == 2) { // 符合進度條件則儲存 x 座標
            eyes.push(i)
          }
        pop() // 釋放畫筆位置
      }
      eyes.forEach( eyeX => { // 將陣列內的物件全部拿出來繪製眼睛
        fill('white')
        ellipse(eyeX, 0, 25)
        fill('#333')
        ellipse(eyeX, 0, 10)
      })
    pop() // 釋放畫筆位置
  }
	
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟六:加上眼睛呆呆前進
【p5.js創作教學】 夢幻鳥-步驟六:加上眼睛呆呆前進

7. 眼睛看向滑鼠、細調樣式

大致的生物形體告一段落後,除了細調樣式外,老闆也開始在作品中嘗試加入更多的互動性,例如讓眼睛看向滑鼠的位置,做了以下的操作:

  • 讓眼球看向滑鼠位置:這邊需要先取得滑鼠與眼球的角度,再利用 cos, sin 讓眼球能擺放到對的位置。使用到了新的 api – atan2
    • atan2(y2-y1, x2-x1):以弧度為單位,計算從指定的點 (y2,x2) 到 (y1,x1) 的角度,要注意這邊的 api 參數,第一個是 y 座標的計算,第二個才是 x 座標的計算。(https://p5js.org/reference/#/p5/atan2)
  • 清掉雜訊:因為 p5 是不停的地繪製新的畫面,畫面出現了許多雜點,是因為沒有在每次繪製前,先將畫面清空,這邊只要在繪製前,在畫布上蓋上一個滿版的方形就能達成。需要注意的是,利用覆蓋滿版方塊來清除雜點時,由於我們前面使用的 rectMode(CENTER),除了調整繪製的座標外,也可以先改回使用 rectMode(CORNER),等清除畫面完成後,再繼續原本的程式碼。
  • 微調生物的身體大小:微調的數值可以參考以下的程式碼,大家也可以嘗試看看不同的數值,看看會有什麼有趣的效果。
  • 取消毛邊與滑鼠的互動:固定生物毛邊的程度。
  • 調整背景色:老闆試著改變背景色,希望不要每個作品背景都是黑色。因為生物的顏色比較鮮豔,所以最後老闆挑了較深的顏色,來對比出作品的主角。
let glitchAmount = 5
function draw() {
  noStroke()
  // glitchAmount = mouseX/100 // 取消毛邊與滑鼠的互動
  rectMode(CORNER) // 改變繪製方塊的模式
  colorMode(RGB) // 使用 RGB 作為填色模式
  fill(156, 104, 104, 200) // 每次重新繪製時加上底色
  rect(0, 0, width, height)

  rectMode(CENTER)
  colorMode(HSB)
	
  for(var o = 50; o<height; o+=100) {
    let eyes = []
    push()
      translate(0, o)
      for(var i = 0; i<width; i++) {
        push()
          translate(i,0)
          fill(
            noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360,
            noise(i/90, o/1000, frameCount/100)*100,
            noise(i/80, o/1000, frameCount/100)*30+80
          )
          let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2)
          let hh = (sin(progAng) + cos(progAng/2) + cos(progAng/5)/3 +1)*30 // 微調毛邊樣式
          rotate(sin(i/10))
          rect(
            random(0, glitchAmount),
            random(-glitchAmount, glitchAmount),
            +hh
          )
          if( int(progAng/PI/2*100 ) == 2) {
            eyes.push(i)
          }
        pop()
      }
      eyes.forEach( eyeX => {
        let mAng = atan2(mouseY - o, mouseX - eyeX) // 取得滑鼠與眼珠的相對位置
        fill('white')
        ellipse(eyeX, 0, 25)
        fill('#333')
        ellipse(eyeX + cos(mAng)*5, sin(mAng)*5, 10) // 利用 cos, sin 將眼珠放置在對的位置
      })
    pop()
  }
	
  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟七:用滑鼠和眼睛互動,並調整整體畫面及顏色
【p5.js創作教學】 夢幻鳥-步驟七:用滑鼠和眼睛互動,並調整整體畫面及顏色

8. 加入魚鰭與最後修飾

創作到尾聲,其實老闆還沒決定他是什麼樣的生物,看起來類似尖嘴巴的魚,為了讓作品更完整,在這裡我們賦予生物們魚鰭,並做最後的微調:

  • 加上魚鰭:前面我們有記錄所有眼睛的位置,利用這個 for 迴圈,去繪製旋轉的三角形,讓它成為生物的鰭。這邊要記得使用 push 及 pop,不然會導致你下一次在繪製眼睛時出錯。
  • 調整背景色:最後老闆選擇了深藍色的背景作為定調。
  • 扭動的身體:繪製身體前的畫筆移動,在 y 參數的位置加上 sin ,可以繪製出魚移動時身體扭動的感覺。
let glitchAmount = 5
function draw() {
  noStroke()
  rectMode(CORNER)
  colorMode(RGB)
  fill(0, 0, 80, 180)
  rect(0, 0, width, height)
  rectMode(CENTER)
  colorMode(HSB)

  for(var o = 50; o<height; o+=100) {
    let eyes = []
    push()
      translate(0, o)
      for(var i = 0; i<width; i++) {
        push()
          translate(i, sin(i/30)*20) // 利用畫筆的位移,讓鳥在往前時,身體也有了變化
          fill(
            noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360,
            noise(i/90, o/1000, frameCount/100)*100,
            noise(i/80, o/1000, frameCount/100)*30+80
          )
          let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2)
          let hh = (sin(progAng) + cos(progAng/2) + cos(progAng/5)/3 +1)*30
          rotate(sin(i/10))
          rect(
            random(0, glitchAmount),
            random(-glitchAmount, glitchAmount),
            +hh
          )
          if( int(progAng/PI/2*100 ) == 2) {
            eyes.push(i)
          }
        pop()
      }
      eyes.forEach( eyeX => {
        let mAng = atan2(mouseY - o, mouseX - eyeX)
        fill('white')
        ellipse(eyeX, 0, 25)
        fill('#333')
        ellipse(eyeX + cos(mAng)*5, sin(mAng)*5, 10)

        push() // 繪製魚鰭時,記得使用 push, pop 來記錄與釋放畫筆狀態
          stroke(0)
          noFill()
          translate(eyeX+50, 0)
          rotate(sin(eyeX/2+o/10)/2) // 畫筆進行旋轉,畫面會呈現魚鰭擺動的效果
          triangle(
            0, 0,
            50, -20,
            50, 20
          )
            pop()
        })
    pop()
  }

  push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
  pop()
}
【p5.js創作教學】 夢幻鳥-步驟八:加上魚鰭之後完成成品

老闆來結語

這次的創作一開始是老闆想要練習金屬色,一系列的調整與操作,最後才產出夢幻鳥這個作品,讓我們快速回顧一下夢幻鳥的創作過程:

  1. 了解 openprocessing 創作的起手式 – setup 與 draw
  2. 利用噪聲 noise 決定方塊的顏色
  3. 利用 for 迴圈的變數、噪聲與時間變數 frameCount,影響每一橫列的色彩分佈與變化
  4. 調整繪製方塊的形狀與大小
  5. 結合三角函數繪製出波形
  6. 為生物加上眼睛,並微調生物外觀
  7. 讓眼睛與滑鼠產生互動
  8. 加上魚鰭與最後修飾

萬事起頭難,一個作品不可能一步到位,將最終目標拆分成不同階段任務,從一開始的雛型慢慢開發出每個區塊,最後組裝在一起,也可以加上個人的創意去實現其他功能,讓作品更豐富。

由於這部影片比較特別,是紀錄老闆在練習與發想後,老闆回頭解說製作過程,所以中間會不停地去微調數值。創作的過程一定會有這種狀況發生,在創作時沒有所謂的正確答案,大家在了解工具之後,就勇敢地去嘗試吧!再附上這次範例的成品<夢幻鳥>,讓大家在開發時參考。

如果你喜歡老闆的教學,歡迎支持老闆,讓老闆在《互動藝術程式創作入門》課程中帶你一起學習。課程裡會帶你看看不一樣的作品,並引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓寫 code 不再是這麼困難的一件事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠在網頁上做出與眾不同的創作。

此篇直播筆記由幫手 H 協助整理

墨雨設計banner

這篇文章 【p5.js創作教學】Dreamy Bird 夢幻鳥 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】CreativeCoding 花火大會(直播筆記) https://creativecoding.in/2021/09/16/p5-js-creativecoding%e8%8a%b1%e7%81%ab%e5%a4%a7%e6%9c%83/ Thu, 16 Sep 2021 03:08:00 +0000 https://creativecoding.in/?p=1430 為響應日本的一群Creative Coder在Processing Community Day的社群串聯,扮起虛擬的花火大會,我們也來利用p5.js,結合粒子系統、漸變顏色甚至是聲控模組,一起在夜空中創作出絢麗的煙火吧!

這篇文章 【p5.js創作教學】CreativeCoding 花火大會(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
說到夏天,就想到海邊;說到海邊,就想到日劇裡的西瓜跟煙火。一束束的煙火短暫但繽紛,燃燒自己的生命點燃絢麗的光譜,珍惜每次綻放都是不同的樣貌。最近(2021年8月)有一群日本Creative Coder在Processing Community Day時串連起社群,在Twitter上辦起虛擬的花火大會,透過各自的作品在版面上綻放了大大小小的煙火,替最近被疫情拉開實體距離的生活中,增添一些夏天的顏色。

讓我們抓住夏天的尾巴,一起用粒子系統與漸變顏色創作煙火吧 🎆

今天要使用的是OpenProcessing搭配p5.js函式庫的大禮包組合,如果對這兩個工具還不太熟悉在這篇文章可以看到更多介紹 👉🏻 p5.js 快速上手

讓我們用草稿規劃一下煙火的概念,如果要做以粒子為基礎、從中心炸開的煙火,應該是一顆粒子從畫面水平線的底部往上移動特定距離,在上方炸開很多不同的粒子、且粒子各自擁有不同的運動方向。

根據以上的概念,我們今天會切分為以下步驟來進行:

  1. 粒子系統
  2. 動態延伸(移動、爆炸分裂)
  3. 顏色變換
花火大會作品草稿示意圖
花火大會作品草稿示意圖

製作粒子系統

首先第一個步驟我們先完成煙火的核心——粒子系統,以單顆粒子的物理模型來說會有位置(P)、速度(V)、加速度(a)和顏色(Color)、大小(r)等變數。在OpenProcessing先把畫布設成1000×1000、黑色的夜空之後,另外新增一個Tab,用來放置我的們的Class particle,在初始化時我們希望引入一些變數args裡面帶入一些固定的參數做使用,如果使用者有特別設定,把使用者引入的參數args蓋到預設值def上,再把客製化後的設定值蓋到這個物件本體this上。

//Tab2
class Particle {
  constructor(args){
    let def = {
      p: createVector(0,0), //位置
      v: createVector(0,0), //速度
      a: createVector(0,0), //加速度
      color: color('red'), //顏色
      r: 10, //大小、半徑
    }
    Object.assign(def,args)
    Object.assign(this,def)
  }
}

接下來介紹兩個關鍵的method分別是draw()update(),分別負責顯示和更新,切分成兩個部分是為了在更新的時後不動到最初始的顯示,把邏輯層區分出來,這樣對模組化的製作與管理也比較容易。

在同一個 tab2,先來處理draw()push()會保留目前的drawing style、而pop()則會回復這些設定,兩個必須搭配使用。假設粒子移動到this.p位置、顏色this.color、尺寸是this.r

class Particle{
  ...
  draw(){
    //顯示
    push()
      noStroke()
      translate(this.p) //processing可以只給向量,不一定要x,y
      fill(this.color)
      circle(0,0,this.r)
    pop()
  }
  update(){
    //資料更新
  }
}

在主要的程式定義一個陣列particles把粒子都裝進去,我們來初始化一顆粒子試試看,讓objParticle根據剛剛的規範來製作,放在外面並用let比較不會有全域打架的問題objParticle = new Particle() ,成像的位置在畫布寬高一半處,這時在畫布中間就可以看到我們千辛萬苦的第一顆隨機色粒子啦。

let particles = []
let objParticle 
function setup() {
  createCanvas(1000, 1000);
  background(0);
  objParticle = new Particle({
    p: createVector(width/2,height/2),
    r: 100,
    color: color(random(255),random(255),random(255))
  })
}

function draw() {
  objParticle.update()
  objParticle.draw()
}
萬事起頭難,頭過身就過,黑夜中的一顆小紅點。
萬事起頭難,頭過身就過,黑夜中的一顆小紅點。

單一粒子動態軌跡

update()處理位置(P)、速度(V)、加速度(a)和大小(r)的變化,每一顆的位置都會加上速度、而速度都會加上加速度。

update(){
  //資料更新
  this.p.add(this.v)
  this.v.add(this.a)
  this.r*=0.993 //由大變小
}

有物理模型後,我們來處理速度(v)和加速度(a),這邊介紹一個函式random2D(),可以在vector上隨機產生一個2D的向量。套用到速度上隨機產生方向,在爆炸初始時粒子會往原先的方向衝再往下掉,乘5倍讓初始速度>加速度,設定加速度為0.1,這樣我們就得到單一粒子的運動軌跡,也就是煙火炸開時的單一根花瓣。

單獨一顆的粒子運動軌跡。
單獨一顆的粒子運動軌跡。

製作束狀粒子群

在陣列內紀錄產生的粒子,先draw完後再update產生動態。
在陣列內紀錄產生的粒子,先draw完後再update產生動態。

有了一個粒子後,我們可以來做一束的煙火,用這些粒子加起來做成陣列。我們先把objParticle拿進來,用for迴圈做出50個粒子objParticle,再push到陣列particles內。在draw的地方把清單一個一個抓出來,我們就得到初步的美麗煙火了。

let particles = []
 
function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0) 
  rect(0,0,width,height)
	
  for(let i=0;i<50;i++){
		
    let objParticle = new Particle({
      p: createVector(width/2,height/2),
      v: p5.Vector.random2D().mult(5),
      a: createVector(0,0.1),
      r: 20,
      color: color(random(255),random(255),random(255))
    })
    particles.push(objParticle)
  }
}

function draw() {
  fill(0,5) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
}
在夜空中綻放的一束煙火,看起來有點像下垂的海葵(?)
在夜空中綻放的一束煙火,看起來有點像下垂的海葵(?)

模組化並自動發射

完成了一束煙火後,我們要接著做此起彼落發射的夏日花火祭,把發射的動作包成一個function firework就可以重複呼叫它。包起來後先在setUp呼叫一次,也可以引入位置參數(p),如果該參數有值就顯示、沒有則出現在畫面中央。接著設定他產生的頻率,每隔100個frame放一次煙火。

大家可以發現我們調高了煙火的數量,從50到100個,在這個情況中為了預防畫面因為生成的東西越來越慢,我們來消除超出畫面的煙火,用filter()留下小於畫面的物件。

仔細觀察煙火的粒子除了大小不同外,每顆的初始速度也不同,如果初始速度相同就會較規則,看起來像下垂的海葵(?),這邊用random()給予任意值處理,煙火的顏色也調整成HSB模式,相較於RGB模式有更彈性的明度暗度可以使用,色調的變數請參考下圖。

我們把HSB色調分為兩個部分:baseHue和Hue。BaseHue為固定的偏移量,hue為根據每個粒子隨機產生出的値。
我們把HSB色調分為兩個部分:baseHue和Hue。BaseHue為固定的偏移量,hue為根據每個粒子隨機產生出的値。
let particles = []

function firework(p){
  push()
	
    let baseHue = random(300)
	
    colorMode(HSB)
    for(let i=0;i<100;i++){
      let hue = random(0,120)
      let objParticle = new Particle({
        p: p || createVector(width/2,height/2), //有位置p時取用p,沒有時就從畫面中央
        v: p5.Vector.random2D().mult(random(1,10)),
        a: createVector(0,0.1),
        r: random(40),
        color: color((baseHue+hue)%360,360,360) //避免>360的數字都是紅色
      })
      particles.push(objParticle)
    }
  pop()
}

function setup() {
  createCanvas(1000, 1000);
  background(100);
  fill(0) 
  rect(0,0,width,height)
  firework()
}

function draw() {
  fill(0,5) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
  if (frameCount%100==0){
    firework()
  }
  particles = particles.filter(obj=>obj.p.y<height) //留下小於畫面的物件
	
  fill(0)
  rect(0,0,100,50)
  //計算畫面中粒子數
  fill(255)
  textSize(20)
  text(particles.length,50,50)
}
調整過後的煙火,終於看起來比較有層次感,不像下垂的海葵了。
調整過後的煙火,終於看起來比較有層次感,不像下垂的海葵了。

基礎版:滑鼠觸發煙火

接下來加入mouse的互動。首先註解掉自動產生的frameCount,每當滑鼠按壓時就在該位置呼叫firework,為了避免重複參數造成順序混亂,在呼叫時把p包成一個物件{p},記得setUp時也要回傳一個空的物件firework({})

這時候會產生一個問題,因為p引入firework被所有的粒子共用,所以有幾顆粒子他就會被update幾次,我們用copy()複製p出來給當下的粒子,避免所有的粒子共用位置。再加入fireRpraticleR等參數做出隨機粒子大小和隨機煙火大小。

function mousePressed(){
  firework({
    p: createVector(mouseX,mouseY),
    fireR: random(1,100), //煙火的大小
    particleR: random(1,10) //粒子的大小
  })
}
function firework({p, fireR, particleR}){
  push()
    let baseHue = random(300)
	
    colorMode(HSB)
    for(let i=0;i<100;i++){
      let hue = random(0,120)
      let objParticle = new Particle({
        p: (p && p.copy()) || createVector(width/2,height/2), //複製新的位置給當下的粒子,讓它重複100遍
        v: p5.Vector.random2D().mult(random(1,fireR || 5)), 
        a: createVector(0,0.1),
        r: particleR || random(40),
        color: color((baseHue+hue)%360,360,360)
      })
      particles.push(objParticle)
    }
  pop()
}
在夜空中用滑鼠點點點,我們就有初階的煙火大會囉
在夜空中用滑鼠點點點,我們就有初階的煙火大會囉~

進階篇:用聲音觸發煙火

做完基礎煙火互動後,如果可以用聲音來觸發煙火那一定很酷,p5.js裡有一些關於「聲音」相關的函式,今天介紹的是Mic Input可以截取電腦麥克風的聲音,我們先開啟p5.sound的library。

開啟p5.sound
開啟p5.sound

套用官方語法在setUp加上input = new p5.AudioIn()開始取用聲音。

let input

function setup() {
  createCanvas(1000,1000);
  background(100);
  fill(0)
  rect(0,0,width,height)
  firework({})
	
  input = new p5.AudioIn()
  input.start()
}

draw()加上觸發條件,如果有聲音,即在畫布上放煙火。這樣我們聲控的煙火大會就大功告成啦!

function draw() {
	
  let volume = input.getLevel()
  let speaking = voulume>0.15
	
  fill(0,8) //留下煙火軌跡
  rect(0,0,width,height)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
  particles = particles.filter(obj=>obj.p.y<height) //留下小於畫面的物件
	
  fill(0)
  rect(0,0,400,200)
  fill(255)
  textSize(20)
  text(speaking,50,50)
	
  if (speaking){
    firework({
      p: createVector(mouseX,mouseY),
      fireR: random(1,5),
      particleR: random(1,10)
    })
  }
}

小試身手

做完上面的煙火後,這邊提供幾個大家可以繼續嘗試看看的方向,希望大家可以長出各式各樣的煙火,讓夏天的夜晚更為熱鬧!

  • 粒子的顏色漸層
    可以在def的地方新增endColor: color('yellow'),利用lerpColor()這個漸變函式在Update()指定顏色的變化跟階數。
this.color = lerpColor(this.color, this.endColor, 0.05) //每次變換0.05
  • 扭曲的粒子運動軌跡
    在粒子translate的時候如果根據sin/cos偏移,可以做出扭曲的煙火效果會更漂亮。
curve: random(5),
curveFreq: random(2,40),

translate(this.p.x+sin(this.p.y/this.curveFreq)*this.curve,this.p.y+cos(this.p.x/this.curveFreq)*this.curve)⁠
  • 製造煙火的霧氣
    透過在Particle中的draw()增加一些半透明且半徑較大的粒子,來增加模糊的光影。Color先複製一份避免動到原先的設定,用函示setAlpha()製作透明度。
let copyColor = color(this.color.toString())
  copyColor.setAlpha(10)
  for(var i=0;i<100;i+=10){ //重複畫圓形
  fill(copyColor)
  circle(0,0,this.r*i/20) ⁠
}
  • 混合模式
    加入混合效果,blendMode()調整顏色呈現。
push()
  blendMode(SCREEN)
  for(let objParticle of particles){
    objParticle.update()
    objParticle.draw()
  }
pop()
  • 效能處理
    設定當r小於某個半徑時就不顯現,減少效能負擔。
particles = particles.filter(obj=>
            obj.p.y<height &&
            obj.r>0.01
  )

以上就是這次的教學,成品請參考這裡,希望大家還玩得開心,那我們就下次再見啦!

也許你對互動生成式藝術比較有興趣?來看看老闆的《互動藝術程式創作入門》課程,跟著將近兩千位同學一起把程式碼當作畫筆創作,或是先看看這篇文章,欣賞同學們完成的作品吧!

此篇直播筆記由幫手 Jeudi Kuo 協助整理

墨雨設計banner

這篇文章 【p5.js創作教學】CreativeCoding 花火大會(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>