1. 程式人生 > >1.9 開始第一幅“碼繪”——掌握大殺器”迴圈“,一招搞定百千萬個懵逼臉

1.9 開始第一幅“碼繪”——掌握大殺器”迴圈“,一招搞定百千萬個懵逼臉

引言:重複與美

這一節裡,我們將要學到一項強大的技能——迴圈 有了迴圈,我們就能夠指揮計算機幫我們去做大量重複性的事情。 直覺上,提到“重複”,往往聯想到枯燥乏味。的確,從操作而言,重複性地做單調乏味地任務繪讓人感到煩躁不安。 然而,重複卻又是不可避免的。而且,很有有趣的東西也是通過重複操作實現的。 在繪畫中,重複性也體現得非常多。並且相當多的畫作都是通過“重複”來體現其趣味性。 為此,我們先賞析一些畫作。

埃舍爾


這一幅是荷蘭著名畫家埃舍爾的作品。 從它的結構可以明顯看出,這就是將平面空間分割成了重複的結構。實際上每一塊貼磚都是完全一樣的。
這幅作品在重複中增添了一定的變化,這種變化製造了強烈的趣味感,讓觀賞者總有一種好奇心,想去搞清楚它的規律。

這幅作品在重複中增加了尺度的變化,它實際上是一幅“分形“(fractals)圖案。

草間彌生

下面再看看怪婆婆草間彌生的作品。
這幅照片中,她的服飾/飾品/盒子/桌面/背景全都是她本人的標誌性圖案”波點“。 她作品的最大特色就是不斷重複的”波點”。然而,仔細觀察,其實每一樣樣物品上的波點都有所不同,波點的形狀/顏色/排列方式都有所變化。通過這些重複中的變化,將最簡單元素“點”構成了豐富的形態。 再看其它作品,雖然都是“波點”,都是重複,但卻樣樣不同!各有各的趣味。


梵高

上述兩位都是搞“圖案”的。圖案也是很容易聯絡上“重複”一詞。但是,在表意性繪畫中,重複也同樣重要。
我們再看幾幅梵高的作品。
仔細觀察它們,可以看出,梵高也是一位善用“重複”的大師。他喜歡用重複的圖案來填充平面區域,他喜歡用重複的筆觸來表現物件。 但是,他在重複的過程中,卻注入了強烈的即興表現。他的每一個筆觸都有所不同,其中既有他主觀的控制,也有他當時的情緒/下意識/動作的不確定性。

分形藝術

近幾十年來,還有一門專門運用”重複“的美術流派——分形藝術。 下面時幾幅分形藝術圖案。



另外附一個程式驅動的動圖: 仔細看這些圖片,發現它們的美感完全來自於重複,而且主要是兩種特徵:1.區域性和整體重複;2.重複中略有變化。

Craig Mullins

可別以為只有非寫實繪畫才喜歡重複。下面這位搞電腦繪畫的,風格方面主要時寫實的。但又在寫實中加入了強烈的寫意性。
觀察這幅巨集大的場景,可以發現,景觀結構都是重複的:道路/燈/遠處的建築物/建築物的細節部件等等。但在這種重複的有序結構中,整體上存在一種變化的節律,而且重複的細節結構也包含變化,例如每一個燈都略有不同。
在這一幅《狙擊手》作品中,也存在大量重複結構,而且他的筆觸其實也是重複的。同樣的,這些重複性中由融入了節律和變化,從而製造了極大的豐富性。

第四維上的重複

第四維即時間(根據愛因斯坦相對論),在時間上的重複,在傳統靜態繪畫中是不可能實現的,但用程式設計就可以做到,請看下面作品:

先看靜止狀態:




怎麼樣,很無聊吧。。。

下面再看看動起來的樣子:




是否感到突然就有了耐人尋味的趣味?

這麼比較,是希望讀者明白,可以將動態看作一種全新的“媒介”,它可以用於表現以往的靜態構成方式所無法表達的美感。

看了這麼多例子,相信大家也明白了,重複本身就是一項高階的繪畫手法,只要運用得當,可以製造出極大的豐富性/特殊的風格/引人入勝的趣味感。 這裡,筆者總結一下用重複製造“美”的基本法則: 雷同的重複導致無聊,變化的重複製造美。 然而,重複雖然很棒,但是要操作重複卻又是一件很惱火的事情。 好訊息是,現在有了計算機,有了這個”俯首甘為孺子牛“的勞模,我們就有了更好的駕馭”重複“的幫手了,計算機最擅長的就是幹這種重複的勞動。 為了讓計算機聽命於我們,我們必須要學會給他下命令,也就是我們即將要學會的強大技能——迴圈

用while迴圈來繪製懵逼臉的頭髮

迴圈有幾種寫法,最簡單的即while迴圈。 我們先直接用它來改造之前的drawConfuseFace()函式,用其實現重複繪製一簇頭髮:
   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
// 函式setup() : 準備階段function setup() {	// 建立畫布,寬度640畫素,高度480畫素	// 畫布的座標系統為,左上角座標為(0,0),	// x方向水平向右,y方向垂直向下,單位畫素	createCanvas(600,400);}// 函式draw():作畫階段function draw() {// 在數百哦位置畫懵逼臉		drawConfuseFace(mouseX,mouseY,200,0.4,0.2,0.3);}// 畫懵逼臉function drawConfuseFace(	posX, posY,  // 臉部中心位置	faceSize,  // 臉部尺寸	scaleMouth,  // 嘴巴尺度比例,相對於臉部尺寸	scaleLEye,  // 左眼尺度比例, 相對於臉部尺寸	scaleREye) // 右眼尺度比例, 相對於臉部尺寸{	//  -------------- 1 畫臉 --------------- 	fill(255);// 填充白色	ellipse(posX,posY,faceSize,faceSize);// 圓圈	//  -------------- 2 畫眼睛 --------------- 	// 2.1 計算眼睛相對於臉中心點的偏移量	var EyeOffsetX = 0.2 * faceSize; // 眼睛橫向偏移量為臉部尺寸的0.2倍	var EyeOffsetY = 0 * faceSize; // 眼睛縱向偏移量為臉部尺寸的0倍	// 2.2 計算眼睛尺寸	// 左右眼尺寸	var LEyeSize = faceSize * scaleLEye; 	var REyeSize = faceSize * scaleREye;	// 左右眼珠尺寸	var LIrisSize = LEyeSize * 0.4;	var RIrisSize = REyeSize * 0.4;	// 2.2 畫出左眼		fill(255);// 填充白色	ellipse(		posX-EyeOffsetX, // 臉的中心位置向左偏移EyeOffsetX		posY+EyeOffsetY, // 臉的中心位置向下偏移EyeOffsetY		LEyeSize,		LEyeSize);	// 2.3 畫出右眼		fill(255);// 填充白色	ellipse(		posX+EyeOffsetX,		posY+EyeOffsetY,		REyeSize,		REyeSize); 		// 5 左眼珠	fill(0);// 填充黑色		ellipse(		posX-EyeOffsetX, // 位置與左眼一樣		posY+EyeOffsetY,		LIrisSize, // 尺寸則採用比左眼小的尺寸		LIrisSize);	// 6 右眼珠	fill(0);// 填充黑色		ellipse(		posX+EyeOffsetX,		posY+EyeOffsetY,		RIrisSize,		RIrisSize);		//  -------------- 3 畫嘴巴 ---------------	// 3.1 計算嘴巴相對於臉部中心位置的偏移量	var MouthOffsetX = 0.0;	var MouthOffsetY = 0.3*faceSize;	// 3.2 計算嘴巴尺寸	var MouthWidth = faceSize * scaleMouth;	var MouthHeight = MouthWidth/2.0;	// 3.3 畫出嘴巴	fill(255); // 填充白色	ellipse(		posX + MouthOffsetX,		posY + MouthOffsetY,		MouthWidth,		MouthHeight);	//  -------------- 4 畫頭髮 --------------- 		var offsetX01 = -0.3;	// while(bool Condition){}:	// 當條件滿足時,就執行{}內容,然後再判斷()的條件	while(offsetX01<0.4) 	{		drawOneHair(posX,posY,faceSize,offsetX01);		// 警戒:必須要讓while()括號中的條件會變成false,否則,// 就會形成死迴圈,程式陷入永世輪迴,// 不斷地執行{}中的語句而無法退出。		offsetX01 += 0.1; // += 運算子用途:A+=B 等價於 A=A+B;	}	}// 繪製一根頭髮function drawOneHair(	faceX,faceY, // 臉的中心位置	faceSize, // 臉的尺寸	offsetXOnFaceSize) // 頭髮X座標的的偏移量,以臉部尺寸為單位尺寸{	// ------------- 1 計算尺寸和位置 ---------//	// 頭髮相對臉部中心的Y偏移量	var HairOffsetY = faceSize * 0.3;		// 計算X偏移量	var offsetX = offsetXOnFaceSize * faceSize;	// 頭髮長度	var HairLength = faceSize * 0.4;	// --------------- 2 畫頭髮 ---------------