在 Flutter 裡頭用小烏龜畫圖

zonble
7 min readFeb 2, 2020

對於某個時代的孩子來說,學電腦跟學程式是同一件事情,因為打開電腦之後能夠做的每件事情大概都需要點程式。

對一個家境小康的台北孩子來說,那樣的年代大概是:爸媽看的電視連續劇一直出現潘迎紫,西門町的服飾店擺滿了當時因為電影而流行的飛行夾克,然後會出現一堆讓爸媽省去帶小孩時間的所謂的科學營隊告訴你 — 如果這次沒有看到哈雷彗星,下次要等七十六年之後,所以害得一個十歲孩子一整年都在把玩星座圖,而哈雷彗星靠近地球的那時候,台北下了一整個月的雨。

在哈雷彗星不能打發小孩之後,又開始有人說資訊科技是世界的未來,作為未來的主人翁,小朋友一定要提早學會電腦,所以家長就可能拿到一台人家淘汰的 Apple ][,買了一套馮定國錄製的,大概叫做「快快樂樂學 BASIC」之類的錄影帶,開機之後就直接進入 BASIC 的互動模式,小朋友就可以照著錄影帶的內容用迴圈以及 PRINT 語法在螢幕上面用星號排列出聖誕樹 — 後來想想,還是不能理解這種程式到底有什麼用途,還有為什麼 BASIC 會拿問號當做 PRINT 指令 — 好了,反正孩子有玩具了。

但這樣恐怕就太小看孩子的好奇心了。他知道,家裡雖然撿來了一台 Apple ][,但是卻沒有一起撿來 Apple ][ 搭配的磁帶機,別人可以把錄音帶上的程式載入到 Apple ][ 裡頭,這樣就可以在綠底黑字的螢幕上玩起「決戰富士山」,他也不特別羨慕,畢竟當時已經很多地方都有阿羅士遊樂器甚至紅白機。

他也的確會去玩那些電視遊樂器,下課後會在一些裝有紅白機的柑仔店流連,但他心裡,始終念念不忘的是 — 上次被帶去南海路上的博物館時,所看到的彩色電腦。多年後他才知道,那種電腦也是蘋果出品,叫做 Apple ][ gs,在上面可以用小烏龜畫圖,用的程式語言,叫做 Logo,而畫出來的東西,叫做 Turtle Graphics

小烏龜把畫圖變成了多麼單純的事情啊!

畫圖,就是前進跟轉彎,你只需要這兩件事情,就可以畫出所有的圖形。你想要畫一條線,就是叫小烏龜前進,你想要畫一條斜線,就是叫小烏龜轉彎再前進,方形就是把「轉九十度再前進」重複四次,想要小烏龜畫個圓,就是把「轉一度再前進」重複三百六十次。小烏龜一步一步,把圖形畫了出來,這樣繪圖的步驟,他可以看上一整天 — 那是真正純粹的電腦圖像之美。

多年之後,人們似乎逐漸淡忘了Logo 還有小烏龜,這年頭孩子學程式也不會從 BASIC 或是 Logo 開始,而是在 Scratch 把玩一隻會直立、而且長相怎麼看都很獵奇的貓。不過,網路上仍然到處都是資源。

人類搞出了 Web Assembly,在瀏覽器裡頭就可以模擬 Apple ][ gs ,所以只要打開瀏覽器,連上 Internet Archive,就可以直接執行當年這孩子所看到的 Logo Writer,也可以下載 DOS 版本的 Logo Writer(雖然絕大多數人去 Internet Archive 應該是為了玩毀滅戰士或是波斯王子)。還有一大堆像是 UCB LogoMSWLogo 之類的後續版本。

還有無數的線上版本,像是:

然後,他看到 Flutter 裡頭還沒有人實作一套 Turtle Graphics,在今年元旦假期的時候,就動手寫了一套。他也不知道誰會使用這套 package,想起來在工作上也的確用不到。不過,這就是為了

— 情懷嘛。

flutter_turtle 這個 package 裡頭基本上不打算寫一套 Logo 的直譯器,而是在 Dart 語言上面做一套 DSL。畢竟,這是一個為了情懷的 side project,因此就寫一個完整的直譯器…想起來就很累。

我大概想做的事情是:基本上你還是在寫 Dart,不過,可以把以前在 Logo 裡頭用來實際畫圖的 function,像是前進、轉彎,在 Dart 裡頭有對應的東西可以呼叫,然後轉換成一些指令,可以在 CustomPaint Widget 裡頭畫圖。

至於其他的部份,就盡量讓 Dart 本身提供:像是你要前進、轉彎多少,總是會用到各種數字運算,你可能也會需要用到亂數,這些都用 Dart 的就好。需要用到顏色,那還是用 dart-ui 裡頭提供的就好。

Dart 本身缺乏用來製作 DSL 的語法,比較能夠用來當做 DSL 用的方法,大概就是把一系列繼承自某個 class 的 subclass 變成一個 List 、method chaining 或是 cascade 等等。後來的選擇是第一種,所以原本在 Logo 裡頭,可能會寫成這樣:

LEFT 10
FORWARD 10

在 Dart 裡頭寫成一個這樣的 List

[Left(10), Forward(10)];

寫著寫著才想到,我需要把一些資訊傳遞給這些我當做 function 使用的物件,就像是在完整的程式語言當中,在呼叫 function 的時候,往往會需要知道一些在目前 scope 中所需要的變數。結果就變成了這樣:

[Left((Map map)=>10), Forward((Map map)=>10)];

怎麼看…都很醜。不過先湊合著用。哪天還有需要在 Dart 裡頭做一套 DSL 的時候,再看看有沒有更漂亮的作法。

接下來就是把指令裡頭描述的圖形畫出來。

CustomPaint 裡頭有一個負責實際負責怎麼繪圖的 class,要實作 CustomPainter,裡頭可以使用 CanvasPaint 等物件。一開始的想法很簡單,就是在我自 CustomPainter 實作的物件中,用 Canvas 的各種 method 畫圖,像是 Forward 就對應到 Canvas 的 drawLine。找了幾個線上的 Logo 程式範例,大概可以把該畫的圖給畫出來。

不過怎麼看都還是不對勁。這麼做基本上就是一次把圖畫完,在這個 Widget 出現在畫面上的時候,就直接看到最後的結果,可是當你用小烏龜畫圖,你預期的就不是這樣的東西。小烏龜從來就不該飛奔終點,你需要過程,你想要看到的是烏龜如何奮力地旋轉與移動,當年你看到的是這樣的烏龜,現在也應該如此,只有這樣的烏龜,可以讓你看上一整天。

Flutter 裡頭有不少 class 是可以拿來做動畫的,像是 AnimatedBuilder、TweenAnimationBuilder、AnimationController…等等,絕大多數的動畫,是讓某個 Widget 的某些屬性,在一段時間之內,從某個值到某個值之間發生變化,像是淡入就是不透明度從 0 到 1 的變化,移動就是位置從某點到某點的變化,至於變色、縮放等等效果,就是顏色與尺寸的變化。

拿這樣的東西用在我們的烏龜上,比較嚴格的作法應該是我們要算出烏龜移動的總長度,然後把動畫中的 timing function 對應到烏龜移動了多少,想起來…還是有點麻煩。所以用了一個偷懶的方法:我們去計算畫這張圖到底用了多少個繪圖指令,然後根據動畫中的時間位置,決定要丟多少個指令給 Canvas 處理。

偷懶的結果就是,動畫的過程還是跟你想的不太一樣,如果你的圖形就只是畫一條直線,那你會想像烏龜應該慢慢移動,把這條線畫完,不過我們的作法反而是這一筆就直接畫完了。不過,至少我們有動畫。

這裡有一些範例,是一個放在 GitHub Pages 的 Flutter Web app,當中有目前 flutter_turtle 可以畫出來的圖形,大概都是一些從線上找到的 Logo 程式翻譯過來的。

至於怎樣用 GitHub Pages 放置 Flutter Web app,之前也寫了一篇說明。程式碼放在 GitHub 上。

--

--

zonble
zonble

Written by zonble

XDDDD - eXtreme Due Date Driven Development

Responses (1)