Try   HackMD

CHANGE LOG(有任何修改請在此通知)

andrew 2023/12/10 02:19pm 目前已經上傳至 gitlab repo
Chia0Sun, Oct 13, 2024 10:55 AM Line 489 修正了一個錯字 @andrewintw
andrew 2024/10/14 14:27pm 已修正,事實上有兩處,一併修訂了。已同步到 GitLab repo 以及網頁。

setup()


圖 3.1:早期鍊金術插圖裡的銜尾蛇,中間寫有 10 世紀鍊金術士克麗奧佩脫拉(Cleopatra the Alchemist)著作中的文字「萬物一體」(ἓν τὸ πᾶν)。圖片來自維基百科

迴圈(loop)提供了另一種想像,就像是古老的銜尾蛇圖騰一般。在希臘傳說中,銜尾蛇(Ouroborous)代表了出生與死亡的無盡循環,展現一種能夠無限自我更新的過程。迴圈除了讓人聯想到自我吞噬和鍊金術之外,也關乎控制與自動化步驟,以及日常生活中的重複程序,就如在音樂中聽到的重複聲音素材片段。[1]在程式設計裡,迴圈能讓原始碼的片段重複執行,一直持續到滿足特定條件為止,例如真(true)或假(false)。實際上,如果條件永遠不會變為假(false),那就會產生出無限(或沒有終點)的迴圈。

數學家兼作家奧古斯塔・愛達・拜倫・勒芙蕾絲(Augusta Ada Byron Lovelace)在 19 世紀初首次介紹並描述程式的迴圈概念。她發現有史以來第一台自動通用計算機——查爾斯・巴貝奇分析機(Charles Babbage’s Analytical Engine)具備可重複操作的設計概念。在她針對這台分析機所寫的「G筆記」中[2],她以「週期」(cycle)來稱呼迴圈,並描述所謂的伯努利數字程式(Bernoulli numbers program),如下圖所示。這個程式利用兩個迴圈來示意一系列附條件之指令的重複[3],從而大幅降低編寫多個一模一樣的可重複性操作所需耗費的精力。因此,迴圈也為重複操作節省了時間。

圖 3.2:伯努利數字引擎運算圖表,摘自愛達・拜倫・勒芙蕾絲的「G筆記」(1843)。圖片來自維基共享資源

當代程式設計中的迴圈深受這些以圖表形式表示、用來處理機器重複性操作的洞見的影響。如 p5.js 等高階程式語言便包含此種迴圈概念,因此得以重複執行原始碼片段,例如 draw() 函式將持續運行直到程式被停止,或以語法 noLoop() 來終止迴圈。迴圈是寫程式時最基本、最強大實用的概念之一。

本章的主要範例是有「跳動者」(throbber)[4] 之稱的圓形旋轉載入圖示。此圖示代表一個程式正在執行如下載或大量運算等動作。我們認為這個符號很值得玩味,因為它點出針對機器隱藏的勞動,我們自以為所知和我們所不知之間的差距,以及在任何給定的運算作業期間,都存有多種錯綜複雜的時間性。[5]這個標誌恰如其分地說明了迴圈的運作方式,讓我們能夠思考可感知串流與運算邏輯之間糾纏不清的關係,以及如何經由數位媒體來體驗逝去的「當下」。[6]隨著我們從靜態物件轉往動態,「跳動者」的動畫將引導與思考轉換(如旋轉和平移)相關的程式設計任務,也將作為一個適合的分析物件,供我們以更概念化的方式,徹底思考迴圈這個想法、相關的時間元素,以及與時間相關的語法。

start()

我們常常會在下載或串流時,看到無所不在的旋轉圖示,它代表某項操作正在進行,但究竟發生了什麼事、以及需要多長時間,我們卻不完全清楚。這個圖示不像進度列,並沒有進度或狀態的指示。我們看到圖示在旋轉,但它幾乎無法解釋背景中進行的事項或時間長度。我們會先學習寫出「跳動者」的程式,然後再來查看約翰・P・貝爾(John P. Bell)的〈星號繪製〉(Asterisk Painting),該作品藉由重複在畫布上留下自程式開啟以來經歷的毫秒數,來創造出一系列星號,這些將幫助您深入了解程式設計如何使用轉換式動作和迴圈結構,以及運算過程的時間操作。


課堂練習(解碼)

如前所述,本章所寫的程式已從純粹靜態的物件,轉變為混合靜態與動態的物件。這邊的例子是旋轉的圓形,就如同物件正在吞食自己的尾巴。

圖 3.3:範例程式碼的 RunMe 執行檔 - sketch 3_1

RunMe, https://aesthetic-programming.gitlab.io/book/p5_SampleCode/ch3_InfiniteLoops/sketch3_1/

您可否用自己的話來描述執行檔裡的各種元素,以及它們是如何在電腦上運行的?


1. 推測

根據您在螢幕上的所見所感,請試著描述:

  • 有哪些元素?請列出一份功能與特色清單。
  • 有哪些東西在動,哪些不動?
  • 中間有多少個圓?
  • 請試著調整視窗大小,看看會發生什麼事。

更進一步的問題:

  • 如何設定背景顏色?
  • 圓形怎麼旋轉?
  • 如何讓圓形淡出並旋轉到下一個位置?
  • 如何在同一個草稿碼中同時定位靜態的黃線以及動態的圓形?

2. 實驗

  • 請隨意更改一下原始碼
  • 請嘗試改變部分的參數,例如background()framerate(),以及 drawElements() 裡面的變數
  • p5.js 網站參考資料中,您可以查看一些新的函式說明,例如 push()pop()translate()rotate()

3. 對應

  • 將您所做的推測中的發現/功能特色對應到原始碼。哪個區塊的程式碼與您的發現有關?
  • 您能否辨識出哪些部分/區塊的程式碼反應了您所推測的元素?

4. 技術問題/議題

  • let cir = 360/num*(frameCount%num);(請參見第 21 行)
    使用了計算除法後取得餘數的「模數運算子」(%)[7],試著解釋這一行程式碼的用意及作用。

5. 其他概念性問題

  • 您通常會在哪裡看到這個圖示?您的經驗是什麼?
  • 如果此圖示與等待(或浪費)的時間有關,那您對與機器相關的時間所知多少呢?
  • 同步是機器時間中一個很重要的層面,您可以描述一下同步過程方面的經驗嗎?

原始碼

//跳動者 function setup() { //建立繪圖畫布 createCanvas(windowWidth, windowHeight); frameRate (8); //請試著改變這項參數 } function draw() { background(70, 80); //請透過 alpha 值檢查此語法 drawElements(); } function drawElements() { let num = 9; push(); //將物件移至中央 translate(width/2, height/2); /* 360/num(數字)>>各個圓形轉動的角度;frameCount%num >> 得到餘數,以得知是在八個位置中的哪一個*/ let cir = 360/num*(frameCount%num); rotate(radians(cir)); noStroke(); fill(255, 255, 0); //參數 x 即是圓形與中央的距離 ellipse(35, 0, 22, 22); pop(); stroke(255, 255, 0, 18); //靜態的線條 line(60, 0, 60, height); line(width-60, 0, width-60, height); line(0, 60, width, 60); line(0, height-60, width, height-60); } function windowResized() { resizeCanvas(windowWidth, windowHeight); }

函式

p5.js 函式以 function() {} 語法開頭,包含可「獨立執行特定任務的一段程式碼」。[8]p5.js 最基本的內建函式是 setup()draw(),可用來使程式運作特定目的之程式碼,例如:環境設定,也可隨著時間推移來執行指令。範例中提供了其它內建函式,例如 windowResized(),此函式可在發生任何視窗調整事件時讀取畫布大小。畫布大小並未設定為固定尺寸,而是按照程式碼的指示隨視窗大小調整,這與前面章節中討論的函式 createCanvas(windowWidth, windowHeight); 相同。函式 windowResized() 可說是一個於程式碼層級啟動的「事件監聽器」(在電腦程式中偵測一個事件發生的程序或函式),它不只運行了一次,而是「持續」運行。此函式專門「監聽」視窗尺寸調整的事件,類似上一章介紹,用來監聽滑鼠點按事件的 mouseIsPressed()windowResized() 被視為非同步的(asynchronous)函式,這表示其他事件會與繪製形狀等主要程式流程同時發生。

除了內建函式,範例程式碼中還有一個自訂函式 function drawElements(); 這個函式出現於第 13 行,並包含在 draw() 函式中。在 JavaScript 中定義函式相對簡單,只要鍵入關鍵字「function」,然後在後面加上您的函式命名即可。從函式名稱「drawElements」便能推知該函式的作用,即繪製圓形、特定大小、位置和顏色的線條,以及順時針旋轉或靜態保持原位的圓形和線條。繪製出相同結果的方法有很多,但目前仍處程式學習的初期階段,因此我們將嘗試一個可以做出同樣的事情,但更符合學習進度的範例。考慮到這點,我們特意用較無效率的方式編寫某些程式碼,藉此闡明一些關鍵概念。

程式設計師喜歡將大的任務拆分成更小的動作及步驟,因為這樣會容易於建構、管理、除錯、閱讀,並且有助於讓多個程式設計師共同合作。在範例程式碼中,function drawElements(); 函式與 draw() 函式分開,清楚表明這一部分的程式碼與繪製畫面上的各種元素有關。當然,您也可以將圓形和直線的繪製分開,畢竟不同任務的劃分妥善與否不僅主觀,更依據情境而定。

還有另一種類型的函式,可讓您用引數來指定特定的任務,將引數提供給函式後,再接收函式回傳的值。請參閱以下範例:

let x = sum(4, 3, 2);   
print(x);
//將值 4 指派給 a、3 指派給 b、2 指派給 c,傳遞至函式「sum」中
function sum(a, b, c) {
  return a + b + c; //回傳陳述式
}

Output:

"9"

課堂練習

您也可以嘗試將上述程式碼輸入/複製到自己的草稿碼中,它將在網頁主控台區域輸出數字 9,因為這是值 4、3 和 2 的總和。這些值是傳遞給函式(即 sum())的「引數」(arguments)。在範例中,做為變數的 a、b 和 c 等同於做為引數的實際值 4、3 和 2,但變數的值可以更改。您可以賦予「sum」函式其他引數/值,從而重複使用這個函式,舉例來說,另一行程式碼 let y = sum(5,6,7); y 傳回來的值將會是 18。您可以試著創造自己的函式和引數。

轉換

通常,轉換相關函式[9]會對一個元素或物件進行二維或三維的轉換。「跳動者」的範例程式碼便使用了兩個特定的轉換函式來移動畫布,並創造出物件轉換的錯覺(重點是要知道轉換是在畫布的背景層級完成的,而非單一形狀/物件層級。


圖 3.4:在畫布層級移動座標系。圖片取自 processing.org

  1. translate():此函式會在視窗內顯示/移動物件。例如說,若將畫布移動到中心,整個草圖有會被定位在中心(translate(width/2, height/2);)。圓形以 ellipse(35, 0, 22, 22) 繪製,x 和 y 座標為 (35, 0),大小為「22」。如果前面沒有放上 translate() 函式,圓形將被置於左上角而非中央(因為 x 座標值「35」是代表旋轉圓形與中心位置的距離)。藉由使用 translate() 函式將座標原點 (0,0) 移動到螢幕中心,就能讓圓形出現在畫布中間。在上一章關於座標系的空間維度基礎上,「平移」(translate)為移動畫布和定位物件增添了另一個層次的思考。
  2. rotate():「跳動者」的程式碼使用函式 rotate() 讓圓形旋轉了特定度數。旋轉函式的預設單位是弧度,因此,程式碼會編寫為rotate(radians(cir)); 的形式。函式rotate() 在預設狀態下採用弧度為單位,若想更改為度數,只需加上程式碼 angleMode(DEGREES) 即可。

為了繼續擴展空間關係,此範例藉由使用與 draw() 函式裡的其他時間相關語法一起運行的 rotate() 函式,讓時空的交纏愈趨明顯。這裡總共有 9 個圓形(以 let num=9; 表示),每個圓皆與下一個圓相距 40 度(即 0.968 弧度),這個數字源自 360 除以 9。一個圓形有 360 度,為了讓圓形隨時間經過而旋轉,需要有時間元素來計算何時和如何移動,以及移動到何處。這就是函式 frameCount() 的作業原理,它計算了程式自啟動以來畫面影格的幀數。[10]第 21 行 let cir = 360/num*(frameCount%num); 說明如何使用「模除」運算來查詢餘數(或某數除以另一個值後除不盡的數字)。因此,變數 cir 的值會被限定為 40 的倍數:「0、40、80、120、160、240、280 和 320」。在 cir 值的基礎上,程式會按照這樣的順序,隨著時間經過,根據原始位置一個接一個地旋轉圓形,然後不斷重複以上過程。

push() 和 pop()

通常,push()pop() 函式分別是用於保存當前樣式(如顏色)和恢復原本的設定(如旋轉和平移)。範例程式碼中,僅在四邊的線條維持固定時,旋轉的設定才會應用在居中的圓形。以下程式碼可以幫助解釋:


function drawElements() { let num = 9; push(); //將物件移至中央pop(); stroke(255, 255, 0,18); //靜態線條 line(60, 0, 60, height); line(width-60, 0, width-60, height); line(0, 60, width, 60); line(0, height-60, width, height-60); }

最後四行程式碼繪製了四條靜態黃線。從邏輯上來說,這些線條應該也會套用 translate 和rotate 函式,但因為在繪製完所有的圓之後,便立刻放上 pop() 函式,故此二函式不會影響到這些線條(見圖 3.5)。但若將 pop() 移到程式尾端,線條便會像圓形一樣旋轉平移(見圖 3.6)。這說明了如何使用 push()pop() 來保存和恢復樣式,以及其位置的重要性。[11]

圖 3.5:不同位置的 pop() 函式 - 四條靜態黃線

圖 3.6:不同位置的 pop() 函式 - 四條旋轉的黃線

課堂練習

  1. 請更改引數/值,以及範例程式碼的位置/順序,以了解函式和語法,例如 num 變數、平移 translate() 與旋轉 rotate() 等轉換函式,以及保存 push() 和恢復 pop() 等樣式相關函式。

  2. 我們已經解釋了如何使用旋轉 rotate() 來顯示出不同旋轉度數的圓,但是在草稿碼中淡入淡出每個圓呢?(提示:這裡的淡入淡出是不斷反覆的,因此 draw() 函式下的 background() 語法會是產生這種效果的關鍵)

  3. 此練習關乎程式碼的結構。您會如何重構範例程式碼,方便其他人更容易理解,同時仍保有相同的視覺效果?這題的答案沒有對錯之分,但以下的一些提示可能有助於推動討論:

  • 您可以重命名函式和/或加入新函式
  • 除了 drawElements(),是否也可能使用 drawThrobber()drawLines() 之類的函式?

星號繪製

下一節的關注焦點將從重複和規律,轉向重複和差異。《星號繪製》是藝術家兼軟體開發人員約翰・P・貝爾的作品[12],由許多類似跳動者的旋轉圖示所組成,然而,每個跳動者(或他所謂的「星號」)的旋轉方式都不一樣,具備不同的顏色和質地。貝爾使用的許多語法都與時間相關,例如計時器的設置、以毫秒為單位的計算方式、旋轉速度、開始新迴圈之前的等待時間等,都是透過編寫程式來操縱與時間有關的功能,從而重新處理時間,並進一步「開發另類的時間實踐和體驗」。[13]此外,若仔細觀察,可發現星號並非幾何形狀,而是由一系列數字構成,而這些數字實為排成一直線的毫秒計數器。

圖 3.7:約翰・P・貝爾的〈星號繪製〉(2014 年)。由藝術家本人提供

根據貝爾所言,

「《星號繪製》的設計,是讓程式在螢幕上反覆輸出自繪圖開始以來經過的毫秒數,從而創造一系列星號。如果讓它自己運行,它就會這樣做; 然而,當在真實系統上啟動時,外部延遲可能會使我作品裡的星號看起來更像一個一個的點……」

原始碼

《星號繪製》的原作是用 Processing 編寫的,經修改後移植到 p5.js 上。這個程式比第一個程式複雜得多,但我們仍希望以此作為本章的補充,因為該程式可以展現出進一步開發迴圈草圖的潛力,並更深入地思考無限迴圈,以及時間相關語法的使用。

/* Asterisk Painting by John P.Bell (http://www.johnpbell.com/asterisk-painting/) Original code in Processing: https://tinyurl.com/AsteriskPainting Port to p5js and modified by Winnie Soon with comment notes, last update: 25 Aug 2020 changes log: 1. The color mode has been changed to a variable as the push/pop function will restore the previous fill color state. 2. Remove font 3. change the bg color 4. add text size 5. remove load signature image 6. change the canvas size and corresponding no. of asterisk 7. display a counter on the bottom left corner and in black color 8. Add extensive comments 9. return a random no in integer */ let xDim = 1000; //畫布尺寸-寬度 let yDim = 600; //畫布尺寸-高度 let timer = 0; let speed = 100; //轉動速度,預設值為 100 let maxSentences = 77; //原始:77 let sentences = 0; let xPos = [1, 2, 3, 4, 5]; //原始:8 行 let yPos = [1, 2, 3, 4]; //原始:5 列 let xCtr = 0; let yCtr = 0; let waitTime = 10000; let itr = 0; // 迭代次數 let milliStart = 0; let currentMillis; let fillColor; function setup(){ createCanvas(xDim, yDim); background(240); /*將每個星號的 x 位置計算為以陣列索引值 [0] 開頭的陣列(xPos[])*/ for(let i = 0; i < xPos.length; i++) { xPos[i] = xPos[i] * (xDim / (xPos.length+1)); } /*將每個星號的 y 位置計算為以陣列索引值 [0] 開頭的陣列(yPos[])*/ for(let i = 0; i < yPos.length; i++) { yPos[i] = yPos[i] * (yDim / (yPos.length+1)); } fill(0); //左下角計數器的顏色 textAlign(LEFT, CENTER); text(itr, 10, yDim-30); //呈現計數器 fillColor = color( floor(random(0, 255)),floor(random(0, 255)),floor(random(0, 255)) ); } function draw(){ // millis 代表程式開始運行後所經過的毫秒數,就像 frameCount 一樣 currentMillis = floor(millis() - milliStart); if(currentMillis > timer){ push() translate(xPos[xCtr], yPos[yCtr]); //列與行 rotate(radians((360/8)* (millis()/speed))); //自身旋轉 timer = currentMillis + speed; //下一次迴圈的時間 textSize(12); fill(fillColor); /* 與以星號形式寫成的時間字串,而此字串永遠會從 0 開始。nf:將數字格式化為字串,並在前面加上零[http://p5js.org/reference/#/p5/nf] 小數點前 3 位,以及小數點後 0 位。 */ text(nf(currentMillis, 6), 3, 0); sentences++; if(sentences >= maxSentences){ //reach the max for each asterisk xCtr++; //move to next array //meet max cols, and need to go to next row if(xCtr >= xPos.length) { xCtr = 0; yCtr++; //下一列 /* 程式達到螢幕上所能呈現列數的最大值(亦即達到最大列數後);螢幕已滿 > 將一切重置,並更新計數器*/ if(yCtr >= yPos.length){ yCtr = 0; background(240); //計數器+1(迭代) itr++; pop(); //計數器顯示顏色 fill(0); //再次改變計數器顯示方式 text(itr, 10, yDim-30); //等待下此循環,會從第一個星號開始 let wait = floor(millis() + waitTime); while(millis() < wait){} //重置開始時間 milliStart = millis(); //重置計時器 timer = 0; push(); } } sentences = 0; fillColor = color( floor(random(0,255)),floor(random(0,255)),floor(random(0,255))); } pop(); //回復之前的狀態 } }

課堂練習

RunMe, https://aesthetic-programming.gitlab.io/book/p5_SampleCode/ch3_InfiniteLoops/

  1. 請閱讀上面的原始碼。程式碼註解小提醒:
  • // 表示單行註解
  • /* … */ 表示多行註解
  1. 請利用本章前面介紹的解碼方法,試著推測、實驗,並將您的想法與原始碼對照。
  • 推測:請描述您在螢幕上看到/體驗到的內容。
    • 螢幕上有什麼元素?
    • 螢幕上有多少個星號?它們的排列方式為何?
    • 有哪些東西在移動?它又是如何移動的?
    • 是什麼讓星號旋轉?程式何時會停止創造新的星號?
    • 您是否能在這個草稿碼中找到與時間相關的語法?
  • 實驗:更改一些程式碼的引數
    • 請嘗試對一些值做出更改(例如改動全域變數的值)
    • 有哪些新的語法和函式是您原本不知道的?(請在 p5.js 參考資料中查詢)
  • 對照:將推測中的元素對照到原始碼

陣列

想要更深入了解原始碼,只須再多學幾個基本的程式設計概念即可。首先是「陣列」的概念,我們通常將陣列理解為一串資料列表,而此概念也與之前的變數和資料類型等概念相關。若需要處理一大堆資料,例如一組字詞等,您便可以使用陣列一次解決,無需分別創造變數。舉例如下:

//範例
let words = [] //陣列 -> 從 0 開始
words[0] = "what";
words[1] = "are";
words[2] = "arrays";
console.log(words[2]); //輸出:陣列
console.log(words.length); //輸出:3

我們可以套用與之前使用變數時類似的結構:

  1. 宣告:想出一個用來儲存一個列表的值的陣列名稱。例如 let words = [] 便以符號 [] 來表示「words」被建構為一個陣列,但裡面有多少字是未知的,尚未在這行程式碼中指定。
  2. 初始化/指派:在上面的範例中,要儲存在引號裡的文字值有三個(這表明它們是「字串」String 的資料型態):「what」、「are」和「arrays」。由於陣列是一串值的列表,而這些值需要各別標識出來,因此我們會用方括號內的「陣列索引」來表示每筆資料在陣列中的位置。此索引以 [0] 為第一項,緊接著是第二項 [1],依此類推。因此 words[0] ="what" 就表示,陣列「words」的第一個索引項目的資料型態是字串,其值為「what」。
  3. 重複(使用)console.log() 函式是一個範例,說明如何取得和使用資料、如何在網頁主控台區域顯示資料,或者如何在畫布上繪圖。

語法 arrayname.length 是用於詢問陣列裡有多少個物件。

讓我們來看看以下《星號繪製》的範例:

let xPos = [1, 2, 3, 4, 5];
let yPos = [1, 2, 3, 4];

上面是一種稍微不同的陣列宣告方式。它將宣告和初始化/指派合併到同一行,用 let 這個詞來宣告 xPosyPos 為陣列名稱,同時將數值指派給陣列索引,兩個索引分別指的是第幾列和第幾行。我們可以用這種方式理解:程式需要知道在移動到下一行之前應該在螢幕上繪製多少個星號,以及何時重新啟動(星號會以能達到最大行數和列數的方式來填滿整個畫布)。

由於陣列索引值由 [0] 開使,因此每個索引的值都以如下方式來對應:

let xPos = [1,2,3,4,5];
xPos.length 為 5,表示該陣列中儲存了 5 個值:xPos[0] = 1, xPos[1] = 2, xPos[2] = 3, xPos[3] = 4, xPos[4] = 5。
let yPos = [1,2,3,4];
yPos.length 為 4,表示該陣列中儲存了 4 個值:ypos[0] = 1, yPos[1] = 2, yPos[2] = 3, yPos[3] = 4。

上述兩個陣列以 x 和 y 座標的形式來儲存各個星號的中心位置。

這裡也提供一些新增或刪除陣列索引的方法:

array.push(value)[14]
→ 在陣列尾端新增一個值。例:xPos.push(6) 會將索引擴展為 xPos[5] = 6
array.splice()[15]
→ 這會從陣列索引中刪除一個範圍,或刪除現有索引並以內含其他值的新索引替換。

條件陳述式

上一章對條件陳述式的討論會讓您更容易理解《星號繪製》的程式碼。我們依循程式中內建的條件結構(if-then),以便知道何時要從一個星號移動到下一個。

if(sentences >= maxSentences){ //達到某個星號所含句數的最大值
//移動到下一個星號並繼續; 
} 

變數 maxSentences 的值為 77(請參閱原始碼中的第 5 行),因此每個星號至多包含 77 個句子(以一串數字的形式組成的句子)。另外一個變數 sentences 則用來計算句子的數量,程式會檢查當前句子的數量是否已達到最大值。「如果」(if)星號包含的句子達到 77 個,「則」(then)移動到下一個星號,sentences 計數器將重置為零(第 84 行)並再次開始計數。該邏輯會在draw() 函式中的所有星號反覆運作。

迴圈

迴圈的核心概念,是提供讓您可以多次執行一個區塊的程式碼的方式。舉例來說,如須繪製一百條線,並讓這些線一一排序、彼此垂直,使用「line()」語法直接編寫出一百行程式碼當然是沒問題。

但「for-loop」則可以重複執行程式碼,若要繪製一百次直線,這種方式會藉由設定條件結構、計算已繪製的行數和行數的最大值來進行,非常快速有效率。同理,在這份草稿碼中,有一些元素需要重複運行,但同時這種運行又有一個終結點 (例如使用基於畫布的寬和高的 x 和 y 座標,精確計算每個星號的中心點)。知道畫布的行列數,便能知曉繪製每個星號中心點的值。

欲建構 for-loop,請詢問自己以下問題:

  • 有哪些事情/動作是您想按照某種順序或模式來重複的?
  • 更具體來說,條件式的結構是什麼?什麼時候要跳出迴圈?
  • 當此條件被滿足或不滿足時,您想要做什麼?

以下摘自《星號繪製》(第 20-29 行):

/*以一個 xPos[] 陣列來計算每個星號的 x 座標
 陣列的開始為 index[0]*/ 
for(leti=0; i<xPos.length; i++){ 
  xPos[i] = xPos[i] * (xDim / (xPos.length+1));
}
/*以一個 yPos[] 陣列來計算每個星號的 y 座標
  陣列的開始為 index[0]*/ 
 for(leti=0; i<yPos.length; i++){ 
  yPos[i] = yPos[i] * (yDim / (yPos.length+1)); 
 } 

請細看 for-loop 的結構:

根據圖 3.8 所示,for-loop 的組成元素如下

  1. 一個變數宣告和初始化:通常以 0 開頭
  2. 特定條件:滿足條件的標準
  3. 動作:當條件滿足時您想要讓什麼事發生
  4. 下一個迴圈:用於下一次迭代(通常為遞增/遞減)

上面這段程式碼示範了如何根據 x 和 y 座標來描述星號的位置(每個星號的中心點座標 [x, y])。由於全域變數中定義了 5 列(xPos)和 4 行(yPos),因此程式會需要知道確切座標。定位位置的整體公式,例如 xPos,就是將畫布的寬度除以水平方向的星號數後,再加上一個星號(見圖 3.9)。因此,程式碼可以理解為:以 0 為起點,為每次迭代計算 xPos[i]。另外,每次迭代都會讓星號計數加 1,直至達到一行中的最大星號數為止(i < xPos.length)。

圖 3.9 每個星號的 xPos

在教學中,我們描述了另一個使用 for-loop 的例子,以進一步闡明其用途並示範如何重複繪製物件。這個例子(見圖 3.10)繪製了 20 個圓,各與下一個圓相距 20 個像素。

圖 3.10 利用 for loop 在畫布上繪製 20 個圓

let x = 20; function setup() { createCanvas(420, 100); background(200); for (let i = 0; i < 20; i++) { ellipse(x, 45, 15, 15); x += 20; } }

這個簡單的圓繪製範例的關鍵是區域變數 i(見上面第 6 行,用於開始計算圓的數量:let i = 0;,以及設定應該要畫出多少個圓:i < 20,並計算每次圓增加的數量:i++)。全域變數 x 則是用來確定每個圓的位置(根據 x 軸或可以描述為距離的位置),並確保程式每次迭代都會增加 20 個像素:x+=20。透過這種方式,我們便能用 for-loop 來繪製多個圓,而無需寫出 20 行有固定 x 和 y 座標的程式碼。

「While loop」是另一種用於執行迭代的迴圈,其中語句會在條件為真的狀態下持續執行,一旦為假就終止迴圈。

舉例來說,《星號繪製》第 76 行中的 while(millis() < wait){} 是在告訴電腦,如果 millis()[16] 的值小於 wait 變數的值,則什麼也不做。millis() 是一種類似 frameCount(),與時間相關的語法,會回傳自程式啟動以來的毫秒數。一旦條件為假(即millis()不再小於 wait),就會跳出迴圈,程式便可繼續執行下一行。當所有星號都繪製完畢後,此範例是在程式的尾端,所有星號都繪製完成時,不過,程式需要等待一段時間才會重置(清除)畫布並重新開始。因此,這個 while-loop 有著暫停、凍結程式運行的作用(在開始和結束括號之間實際上沒有任何東西)。

While()

如前所述,只要指定的條件為真,迴圈就會持續執行一段程式碼。到了本章的結尾部分,我們似乎應該要強調一下,while-loop 和 for-loop 皆會基於已設定之條件,重複執行程式碼。我們可以把這個迴圈看作一個重複的「if」語句,它提供了一種挑戰線性時間傳統結構的良好方法,並展現了電腦如何以不同的方式利用時間。程式設計挑戰了許多我們對時間先入為主的看法,包括時間本身的組織結構、如何使用各種特別為時間設計的參數和條件,將當下透過算圖來呈現,就像「跳動者」的例子一樣。我們希望上述能讓您清楚明白,機器時間與人類時間是在不同的暫存器/範疇中運行,而全球網路基礎設施和即時運算的概念,更將這些時間進一步複雜化。

開始和結束一個給定的過程所代表的意義,成了一個哲學性的問題。在《電腦作為時間關鍵的媒介》(The Computer as Time-Critical Medium)一書中[17],沃夫岡・恩斯特(Wolfgang Ernst)釐清了時間對於電腦操作和執行任務在本體論上的重要性,劍指在程式設計的語言層面上,諸如可程式設計性、回饋和遞迴等等的關鍵問題,而我們希望本章提供的範例也能與他的論述產生共鳴。[18]恩斯特的範例,關乎在電腦中的「時間」如何被以不同的方式「計算」,例如時脈訊號等,對於這裡的討論來說,精確的技術細節至關重要。在討論軟體時,時間性這個議題經常被忽略,恩斯特提出的「微時間性」(micro-temporality)則聚焦程式設計的時間性,可說是相當實用的概念,此外,他也進一步檢視在時間的哲學中,技術或數學的層面如何被劃分為決定論的(deterministic)範疇而遭到忽視。[19]

迴圈則提供了另一種時間想像。恩斯特在一篇於研討會發表的論文〈⋯⋯另一種迴圈的永恆〉(… Else Loop Forever)中,將這個討論與「不及時性」(untimeliness)連結並予以延伸。[20]他援引圖靈運算的基礎,惡名昭彰的「停機問題」,談論一個給定所有輸入可能的電腦程式,是會結束運行或永久地持續運行下去。在他一九三六/三七年發表的文章〈論可計算數及其在判定問題上的應用〉中,圖靈斷言解決停機問題的通用演算法不可能存在,圖靈機的數學定義便是從這個論點延伸而來。[21]這個「可判定性」,或者恩斯特所說的「結束」,強調了更廣泛的演算法時間概念,以及電腦在無限迴圈中,永遠都預期自己是「永無止境」的。就傳輸條件的品質無法預測這點而言,「跳動者」圖示或許可說是個很好的比喻,動畫圖形以這種方式描繪出一種不確定感,而這樣的不確定感正是深層過程式微時間性的基礎。[22]

恩斯特指出,運算式的記錄與任何具備開頭、中段和結尾的傳統敘事相反,可以無止盡地重演,亦即作為一種「以時間為關鍵元素的狀態」,運算式紀錄「並沒有內在意義上的終結」。[23]可能「沒有幸福的結局」這樣的狀況也讓恩斯特得以詳細闡述新的時間結構,而這些結構亦不再與傳統敘事結構或「歷史終結」的終端邏輯相符。[24]我們的第一個跳動者範例便暗示了這種開始和結束的模糊。一台有限狀態機是否能在給定的時間內意識到自身的「有意識」狀態,以及這種機器是否需要有一種結束感才能正常工作,是圖靈對人工智慧的相關推想,而透過這些推想,時間的複雜性得到了進一步發展。很明顯,有限狀態機是以程序為基礎的,它們像發條一樣在時間上按線性序列操作個別事件,但正如恩斯特的提醒:「沒有一個自動程序可以為所有程式做決定,無論是它是否包含一個無限迴圈。[25]

參考馬丁・海德格(Martin Heidegger)的「存在與時間」(being-in-time)[26] 之說,以及人類對生命終結的認識(其中銘刻了「生為人類」這件事在時間上的意義),恩斯特表示:「人類在活著的同時,隱隱意識到自身的死亡已是過去的未來。[27]」這種迴圈式的終點延遲,在本體論上隨著運算的進行而加劇,而「存在的終結」作為人類和機器共有的時間關鍵條件,亦隨之漸次展開。姑且不對海德格哲學進行深入探討,上述這點對於迴圈相關討論的重要性,似乎反映出在生活中經歷過的時間是多麽複雜。即時編寫程式碼時,程式設計師也正與運行中的系統互動,而這個系統在等待新的程式語句的期間仍不會停止,同樣的,程式設計這件事本身,也在設法於此提供洞見和創造性的機會。[28]我們甚至可以開始推想,軟體如何製造出我們生活中經歷的時間,而不僅僅只是與之即時同步,而我們希望,本章中的兩個例子能幫助讀者從概念和技術層面來思考永恆、迴圈、條件和時間性之間的交集。我們甚至可以說,程式設計讓人們能夠透過一種以時間為關鍵要素的角度,理解技術如何在人的時間體驗中發揮重要作用,這不僅是在於我們如何為時間建立模型,更關乎如何創造新的開始與結束。

迷你習作:設計「跳動者」圖示

目標:

  • 透過設計「跳動者」圖示來反思數位文化中的時間性。
  • 實驗各種運算語法、動畫效果以及轉換。

更多靈感:

請看看其他跳動者相關作品,以及他人如何將想法情境化。

任務(RunMe):

使用迴圈和任一轉換功能來重新設計並編寫「動態」的跳動者程式。

在您的 ReadMe 檔案中可供思考的問題:

描述您的跳動者之設計概念與技術。

  • 您想探索和/或表達什麼?
  • 您在程式中使用了哪些與時間相關語法/函式?為什麼要以這種方式使用它們?時間是如何在運算中建構的(請參考閱讀材料和您自己的程式碼)?
  • 想想您在數位文化中遇到過的其中一個跳動者圖示,例如 YouTube 上的串流影片、Facebook 上載入最新動態消息,或等待付款交易,並思考跳動者所傳遞和/或隱藏的意義。我們如何以不同的方式描述這個圖示?

指定閱讀

延伸閱讀

註釋


  1. 迴圈背後的邏輯可以透過「下一句為真。前一句為假」這個自相矛盾的文字遊戲來說明。更多關於悖論、遞迴和奇怪的迴圈示例請見侯世達(Douglas R. Hofstadter)的《哥德爾、艾雪、巴哈:一條永恆的金帶》(Gödel, Escher, Bach: An Eternal Golden Braid, New York: Basic Books,1999)。 ↩︎

  2. 有關「G筆記」的描述,請參閱喬希亞・克莉薩(Joasia Krysa)的《愛達・勒芙蕾絲》(Ada Lovelace 100 Notes-100 Thoughts Documenta 13, Berlin: Hatje Cantz Verlag, 2011)。 ↩︎

  3. Eugene Eric Kim and Betty Alexandra Toole, "Ada and the First Computer," Scientific American 280, no. 5 (1999), 78. ↩︎

  4. 有趣的是,「跳動者」一詞原用以形容勃起的陰莖,帶有貶義,與本書第一章說明的「git」(原有「混帳」之意)有異曲同工之妙。 ↩︎

  5. 在生產力勞動時間與注意力經濟等更廣泛的脈絡下,我們可以在此針對螢幕保護程式作為一種文化形式進行諸多補充說明。亞歷珊卓・艾尼基納(Alexandra Anikina)在其博士論文《程序式電影》(Procedural Films, Goldsmiths, University of London, 2020)中有一個章節主題為螢幕保護程式的美學形式,並涉及空閒時間/睡眠及認知勞動相關討論;她的《慢性電影》(Chronic Films, 2017)演說可至 http://en.mieff.com/2017/alexandra_anikina 上觀看。另見拉斐爾・羅森達(Rafaël Rozendaal)在荷蘭新研究所(Het Nieuwe Instituut)的裝置藝術《睡眠模式:螢幕保護程式的藝術》(2017),https://hetnieuweinstituut.nl/en/press-releases/sleep-mode-art-screensaver ↩︎

  6. Winnie Soon, "Throbber: Executing Micro-temporal Streams," Computational Culture 7 (October 21, 2019), http://computationalculture.net/throbber-executing-micro-temporal-streams/. ↩︎

  7. 藝術家戈蘭・萊文(Golan Levin)在 The Coding Train 系列中帶來了一門關於模數運算子的線上課程,參見:https://www.youtube.com/watch?v=r5Iy3v1co0A. ↩︎

  8. Derek Robinson, "Function," in Matthew Fuller, ed. Software Studies, 101. ↩︎

  9. 為求不要偏離範例中的討論,我們僅提供兩種轉換相關語法。但除了 translate()rotate() 之外,還有其他轉換相關函式,如 scale()shearX()shearY() 等。請參閱 https://p5js.org/reference/#group-Transform. ↩︎

  10. https://p5js.org/reference/#/p5/frameCount. ↩︎

  11. https://p5js.org/reference/#/p5/push. ↩︎

  12. http://www.johnpbell.com/asterisk-painting/. ↩︎

  13. Hans Lammerant, "How humans and machines negotiate experience of time," in The Techno-Galactic Guide to Software Observation (Brussels: Constant, 2018), 88-98. ↩︎

  14. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push ↩︎

  15. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice ↩︎

  16. millis() 是一種 p5.js 語法,會回傳自啟動程式以來經過的毫秒數,類似 frameCount ,但是以毫秒為單位,請參見 https://p5js.org/reference/#/p5/millis ↩︎

  17. Wolfgang Ernst, Chronopoetics: The Temporal Being and Operativity of Technological Media (London: Rowman & Littlefield International, 2016), 63-95. ↩︎

  18. Ernst, Chronopoetics, 63. ↩︎

  19. 舉例來說,哲學家亨利・柏格森(Henri Bergson)在生活中經歷的「持續」時間,以及讓時間體驗變得扁平死板的「庸俗時間」(以時鐘計時的時間)之間進行了質性區分。請參見 Henri Bergson, Matter and Memory [1896] (New York: Zone Books, 1990)。 ↩︎

  20. Wolfgang Ernst, “‘… Else Loop Forever’. The Untimeliness of Media” (2009). 可至以下網址閱讀:https://www.medienwissenschaft.hu-berlin.de/de/medienwissenschaft/medientheorien/downloads/publikationen/ernst-else-loop-forever.pdf ↩︎

  21. Alan M. Turing, "On Computable Numbers, with an Application to the Entscheidungs problem," Proceedings of the London Mathematical Society 42 (1936/1937): 230–265. ↩︎

  22. Soon, "Throbber." ↩︎

  23. Ernst, "‘… Else Loop Forever’." ↩︎

  24. 「歷史的終結」引自法蘭西斯・福山(Francis Fukuyama)的《歷史之終結與最後的人》(The End of History and the Last Man, New York: Free Press, 1992),這本書點出了「後一九八九」時代蘇聯解體之後,西方自由民主的崛起。 ↩︎

  25. Ernst, "‘… Else Loop Forever’." ↩︎

  26. Martin Heidegger, Being in Time (1927). 本書實用易讀的大綱請參閱:https://plato.stanford.edu/entries/heidegger/#BeiTim ↩︎

  27. Ernst, "‘… Else Loop Forever’." ↩︎

  28. 參見之後將出版的 Alan Blackwell, Emma Cocker, Geoff Cox, Thor Magnussen, Alex McLean, Live Coding: A User's Manual (出版社和實際出版日期未知)。 ↩︎