高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

EaselJS新手上路

前言

我一直都沒有忘記我是個閃客(Flasher)的身份,不過近年來因為貪圖紅寶石(Ruby)的漂亮,所以同時也把它一併收到工具箱裡,當做自己謀生的工具之一。

也許你會好奇,Flash/AS寫得好好的,幹嘛學人家用什麼HTML5+JavaScript? 問的好! 簡單的答案是:因為我高興;複雜一點的答案是:我本來就喜歡一些冷門的玩具,而且AS的部份我覺得目前學的能力暫時夠混飯吃了,想來試點別的。雖然HTML5的規格都還沒標準化,瀏覽器的支援程度也都不太一樣,但如果要等到它真的紅的時候再進場,大概也輪不到我了,所以趁專案的空檔來研究一下,若學有所成的話,之後也可以試著引進到自己或客戶的專案裡。

Canvas是HTML5新進的東西,如果以Flash/AS的角度來看,它有點像是Bitmap或是BitmapData,不過操作起來可不像在AS裡那麼簡單。感謝我心目中的偶像 – Grant Skinner大神開發了一個叫做EaselJS的framework,可以讓Canvas的操作變得簡單許多,而且有Flash-like的API可以用,讓Flash/AS Developer可以比較無痛的上手。衝著這點以及GS大神的名號,於是選擇了它來當做我的第一個入門framework。

首先就要來面臨一個最現實的問題了:瀏覽器的支援程度(或該說是客戶電腦的瀏覽器是不是支援?)。這在Flash Player上幾乎沒這個困擾,但在HTML5就不是這麼回事了。目前比較新版本的瀏覽器,例如Firefox、Safari、Chrome以及Opera幾個大廠牌的支援都還算OK。不過我想那些不是重點,你我關注的還是那老兵不死的IE!! 版本新一點的IE也許還OK,但至於舊一點的.. 這個嘛,我想這部份的細節就讓大家自行體會了,對於這個問題,我只能說「I DON’T CARE」。

安裝

基本上不用什麼特別的安裝,到EaselJS的github repo上把壓縮檔下載回來,解壓縮後把lib資料夾裡的easel.js拿出來,放到適當的目錄就能用了。EaselJS是open source的,而且還是MIT授權,所以你可以放心的使用。在這個當下取得的release版本是0.3.2。

上路之前

在開始之前,建議最好對HTML以及JavaScript有些基礎的認識,不然可能容易迷路。接下來,在EaselJS裡有幾個主要的類別你也會需要花點時間了解一下:

DisplayObject體系

DisplayObject

寫過AS3的朋友應該不陌生這個名詞(如果覺得陌生的話,不要跟別人說你會寫AS3喔),它是所有可視物件(例如Bitmap、Shape、Text等)的最上層的父類別,定義一些基本屬性及方法,例如X、Y座標、放大縮小、旋轉、透明度等,更多細節請參閱文件DisplayObject章節

Stage

閃客們,有回家的感覺了嗎? 這個在AS3就是整個場景的最上層(root),但如果你從上往下看的話,也可以說是最下層的物件。Stage把Canvas包起來,然後在呼叫Stage的tick()update()的時候會render被包起來的Canvas。不過跟AS3不同的是,在AS3裡Stage只會有一個,在EaselJS可以有一個以上。細節請參閱文件Stage章節,或是看看底下的程式範例。

Container

這個在AS3是DisplayObjectContainer,在這邊名字變短了,但功能是差不多的,就是可以把一些DisplayObject包起來,以方便操作;當然你想要像在Flash裡面的一樣的一層包一層的巢狀結構也是可以的。在AS3裡常用的addChildaddChildAtremoveChildgetNumChildren等方法,以及碰撞偵測hitTest也都是在這個類別定義的。細節請見Container章節

Bitmap

拿來裝圖片跟影片用的,請參閱文件Bitmap章節

BitmapSequence

這個可能會比較陌生一些了,你可以暫時先把它想像成跟Flash的時間軸差不多的東西,這個類別定義了在Flash裡很常用的gotoAndPlay()gotoAndStop(),也許之後透過一些實作會更清楚它是幹嘛的。細節請參閱文件BitmapSequence章節

Shape

底下會提到的Graphics類別其實是沒辦法把向量圖形直接畫到Canvas上的,得透過Shape類別的包裝,並放到Stage上才行。細節請見文件Shape章節,或見底下的範例。

Text

看名字就知道它是用來處理文字的類別,請參閱文件Text章節

附上一個我自己畫的DisplayObject類別關係圖:

image

其它類別

Graphics

提供了方便的API,讓你可以畫一些向量圖形用的,這個類別還滿重要的,詳細內容請參閱文件Graphics章節

SpriteSheet

在Flash裡如果我們要做一個角色在跑步的MovieClip是很容易的,大概就是勞請設計師幫忙畫在跑步中的各個動作圖,然後使用關鍵影格,把各張圖片包在MovieClip就行了。這邊GS利用SpriteSheet來達到類似的功能,透過這個類別,你可以把一連串的圖片包成一個SpriteSheet來連續播放,看起來就有類似的動畫效果。細節請參閱文件SpriteSheet章節

Ticker

有點類似在Flash/AS裡ENTER_FRAME的東西,讓你可以在每個frame/interval做一些事情,細節請參閱Ticker章節

整體來看,EaselJS的類別不到20個,建議花點時間看一下文件或原始碼,如果你曾經或現在是個Flash Developer、自認AS3還算ok,也許EaselJS對你來說也不會太難上手的。 來寫code吧

講再多沒來個範例一定沒感覺的,來做個會旋轉的正方形好了(我知道這範例還滿無聊的,沒比Hello World強到哪裡去,but just for demo purpose):

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
<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <title>EaselJS Demo 001</title>
  <script type="text/javascript" src="easel.js"></script>
  <style type="text/css" media="screen">
    #my_canvas{
      background-color: gray;
    }
  </style>
  <script type="text/javascript">
    var stage;
    var box;
    var box_width = 50;

    function init()
    {
      var canvas = document.getElementById("my_canvas");
      stage = new Stage(canvas);

      var graphics = new Graphics();
      graphics.setStrokeStyle(3);
      graphics.beginStroke(Graphics.getRGB(255,255,255,0.8));
      graphics.drawRect(0, 0, box_width, box_width);

      box = new Shape(graphics);
      box.regX = box.regY = box_width / 2;
      box.x = canvas.width / 2;
      box.y = canvas.height / 2;

      stage.addChild(box);
      stage.update();

      Ticker.setFPS(30);
      Ticker.addListener(this);
    }

    function tick()
    {
      box.rotation += 4;
      stage.update();
    }
  </script>
</head>
<body onload="javascript:init();">
<canvas width="300" height="300" id="my_canvas"></canvas>
</body>
</html>

效果如下:

或按這裡檢視效果。

HTML的部份就不多解釋,JavaScript的部份則是把剛才下載回來的easel.js給引進來,然後在body的onload的地方呼叫自己寫的init()。不一定要這樣做,甚至你想把jQuery也引進來用也沒問題,要注意的是,因為這裡僅是做個示範,所以這個簡單的範例沒有特別檢查該瀏覽器是否支援Canvas。

上面這個範例需要說明的大概就是init()裡的那幾行程式碼了:

1
2
var canvas = document.getElementById("my_canvas");
stage = new Stage(canvas);

取得canvas,並把canvas用stage包起來。

1
2
3
4
var graphics = new Graphics();
graphics.setStrokeStyle(3);
graphics.beginStroke(Graphics.getRGB(255,255,255,0.8));
graphics.drawRect(0, 0, box_width, box_width);

利用Graphics類別把方塊畫出來,Graphics的method幾乎都是會回傳Graphics的instance回來,也就是說上面幾行例如setStrokeStyle()或是drawRect()等方法是可以直接串起來寫的。

1
2
3
4
box = new Shape(graphics);
box.regX = box.regY = box_width / 2;
box.x = canvas.width / 2;
box.y = canvas.height / 2;

因為Graphics畫完即使加到stage上,也還是看不到(因為它不是DisplayObject,請見上面的類別圖),所以你需要再用個Shape類別把它包起來,待會要把它放到stage裡。這裡的regX跟regY指的是這個shape的註冊點。如果你不知道註冊點是什麼,你可以試著把設定註冊點那行程式碼拿掉,再看看旋轉效果如何就知道了。

1
2
stage.addChild(box);
stage.update();

呼叫addChild()把剛剛建立的box加到stage上,再呼叫update()來把上面的可視物件給render到canvas上。只有在stage叫呼update()或tick()方法的時候,canvas才會重畫。事實上,當stage呼叫update()的時候,會呼叫所有被這個stage包起來的instance的tick()方法。

1
2
Ticker.setFPS(30);
Ticker.addListener(this);

利用Ticker來設定FPS(每秒30格),並加上listener。

1
2
3
4
5
function tick()
{
  box.rotation += 4;
  stage.update();
}

在每個interval把box轉動4度,然後重畫canvas。這個function一定要取名做tick()嗎? 別的名字不行嗎? 我們在前面請Ticker去監聽,Ticker在每個intereval就會去呼叫被監聽的物件上的tick()方法,所以你如果取做別的名字是沒用的。

如果你是AS3的閃客,應該不會覺得太難上手,如果不是,我想上面這程式也沒多難懂才是。

結論

  1. 目前EaselJS仍是alpha版的,即使是GS大神非常神,但還是不敢保證在使用過程中不會遇到些神奇的狀況。我個人覺得以入門來說,這套用起來算是不會太難,況且GS大神自己也用這套framework幫微軟做了款守城遊戲(Pirates Love Daisies),而且當初用的還只是0.1版的EaselJS。(我真的覺得像這樣可以吃自己做的狗食超棒!)
  2. 我沒有評估過或使用過市面上所有類似的framework,不確定在效能或功能上EaselJS是不是最佳選擇,不過目前看起來效能不至於到非常差,而且入門門檻對閃客來說也不算高。如果各位有能力,也可以直接到EaselJS的github repo去fork一份原始碼來研究看看,說不定也能幫忙貢獻一些心力。
  3. 目前網路上找得到的相關資料似乎不太多,所以官方文件將會是你最好的朋友,最好也跟原始碼做一下朋友。

之後應該還會再實做幾個例子,順便再來幾篇文章,來分享一些更進階的心得。

參考文件:

在Ruby如何找到方法的定義?

剛好昨天有朋友問到,就順便筆記一下。

Ruby官方文件很多,但有時對入門的朋友來說,常會遇到「我想看看XX方法的詳細用法,我該找哪個類別或模組的文件?」的問題。簡單的說,「常翻文件」就是這個問題最好的答案啦,除此之外,你也可以使用”Object#method“來找。

例如我想要知道puts這個很常用的方法是定義在哪邊的:

1
print method(:puts)   # <Method: Object(Kernel)#puts>

它會回傳一個Method類別的實體,並且寫著Object(Kernel)#puts,表示這個puts方法是被定義在Object的(事實上是在Kernel,透過Mixin混到Object裡的)

回傳的這個Method類別實體其實也可以直接呼叫,例如:

1
2
my_puts = method(:puts)
my_puts.call("Hello Ruby")

另外在寫Rails的時候,有時候想要知道某個好用的方法或helper是放在哪邊,想看看原始碼是怎麼寫的,也可以透過這個方式(Ruby 1.9限定):

1
2
3
require 'active_support/all'
puts 10.day.method(:ago).source_location
# ["/Users/eddie/.rvm/gems/ruby-1.9.2-p180/gems/activesupport-3.0.9/lib/active_support/core_ext/numeric/time.rb", 63]

Method#source_location會回傳一個陣列,裡面放著檔名以及在第幾行。

參考資料:

Ruby API – Method類別

在Titanium Studio使用CoffeeScript來寫app

既然CoffeeScript寫出來的東西就是一般的JavaScript,之前就在猜想是不是也可以把CoffeeScript用在Titanium Studio裡,果然,有善心人士用Python寫了給Titanium Studio的外掛。

安裝

首先,你需要在你電腦裡安裝好CoffeeScript的相關工具,詳情可參考這篇

相關的工具安裝完成之後,接著請到 https://github.com/billdawson/ti_coffee_plugin 下載壓縮檔,解壓縮之後把plugin.py這個檔案放到Titanium Studio的plugin資料夾。以我的電腦來看是放在這個資料夾:

/Library/Application Support/Titanium/plugins/ti_coffee_plugin/1.0/plugin.py

如果資料夾不存在的話,就自己手動建吧,檔案放進去之後就算安裝完成了。

使用

在每個Titanium Studio的project底下,應該都可以找到一個”tiapp.xml“的檔案:

image

找到它之後,請在最下面加上這段:

<plugins>
    <plugin version="1.0">ti_coffee_plugin</plugin>
</plugins>

image

接下來,因為我們要建一個app.coffee來產生app.js,所以原來的app.js就可以先丟掉,然後在Resources資料夾裡建一個叫app.coffee的檔案:

image

接著就是打開app.coffee來寫code啦,如果大家懶得寫,可以直接剪貼這段直接貼上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Titanium.UI.backgroundColor = 'white'

win1 = Titanium.UI.createWindow(
  backgroundColor: 'white'
  title: 'CoffeeScript Demo'
)

label1 = Titanium.UI.createLabel(
  color: '#999'
  text: 'Hello CoffeeScript'
  font:
      fontSize:30
      fontFamily: 'Helvetica Neue'
  textAlign: 'center'
  width:'auto'
)

win1.add label1
win1.open()

打完收工,按下build按鈕,耐心等一下畫面..

image

同時你應該也會發現在Resources資料夾裡自動長出了app.js,這個就是神奇的plugin幫我們在build的時候,自動幫我們完成從編譯js檔的工作。

為什麼好好的JavaScript要改用CoffeeScript來寫?

大家先看看程式碼的畫面:

image

主要是CoffeeScript的語法簡單、清楚很多,例如像是物件可以用YAML的方式來寫。所以像label1裡的參數本來是一層包一層,可以改用更清楚的YAML式的寫法(不過這個多少有個人觀感,說不定有的人覺得原來用大括號的比較清楚)。

好寫是好寫,但也不是完全沒缺點,大家自己看畫面應該會發現到,例如原本Titanium Studio有syntax highlight改用CoffeeScript就沒了,當然code hint就別想了(不知道會不會有善心人士也把這個功能補完),不然就是先到有支援CoffeeScript的編輯器裡先寫好,再回來Titanium Studion裡進行編譯也是一招。

所以各位客倌就自己取捨囉,供大家參考。

CoffeeScript OOP篇

在JavaScript裡寫OOP有好幾種方式,例如你可以使用prototype的方式,也可以使用function或物件的方式來寫,不過其實不管哪一種,類別的程式碼寫起來、看起來都不夠直覺(當然也部份是因為我對JavaScript沒這麼熟)。還好在CoffeeScript寫類別是相當容易,而且程式碼是很清楚、容易閱讀的,來看個例子:

1
2
3
4
5
6
7
class Animal
  constructor: (name, age) ->
      this.name = name
      this.age = age

animal = new Animal("eddie", 18)
console.log animal

編譯結果:

1
2
3
4
5
6
7
8
9
10
var Animal, animal;
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  return Animal;
})();
animal = new Animal("eddie", 18);
console.log(animal);

看到了嗎? 用CoffeeScript來定義類別真的簡單多了。特別注意一下constructor(建構子)的部份,這邊的:

1
this.name = name

因為在CoffeeScript裡@符號代表this,所以可以簡寫成:

1
@name = name

整個很有Ruby味道。又,因為建構子用的this.name = name這樣指定動作太常見,所以:

1
2
3
4
class Animal
  constructor: (name, age) ->
      this.name = name
      this.age = age

可以更簡化成:

1
2
class Animal
  constructor: (@name, @age) ->

方法

那如果要加其它的function到類別裡要怎麼做?

1
2
3
4
5
6
7
8
class Animal
  constructor: (@name, @age) ->

  say_hello: (something) ->
      console.log "Hello, #{something}"

animal = new Animal("eddie", 18)
animal.say_hello("CoffeeScript")

編譯結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Animal, animal;
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  Animal.prototype.say_hello = function(something) {
      return console.log("Hello, " + something);
  };
  return Animal;
})();
animal = new Animal("eddie", 18);
animal.say_hello("CoffeeScript");

編譯出來的程式碼已經有點開始長了..

繼承

在CoffeeScript裡,繼承使用extends這個關鍵字:

1
2
3
4
5
6
7
8
9
10
11
12
class Animal
  constructor: (@name, @age) ->
  say_hello: (something) ->
      console.log "Hello, #{something}"

class Human extends Animal
  walk: ->
      console.log "I can walk with my foots!"

eddie = new Human("eddie", 18)
eddie.say_hello "CoffeeScript"
eddie.walk()

編譯結果:

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
var Animal, Human, eddie;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
  for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
  function ctor() { this.constructor = child; }
  ctor.prototype = parent.prototype;
  child.prototype = new ctor;
  child.__super__ = parent.prototype;
  return child;
};
Animal = (function() {
  function Animal(name, age) {
      this.name = name;
      this.age = age;
  }
  Animal.prototype.say_hello = function(something) {
      return console.log("Hello, " + something);
  };
  return Animal;
})();

Human = (function() {
  __extends(Human, Animal);
  function Human() {
      Human.__super__.constructor.apply(this, arguments);
  }
  Human.prototype.walk = function() {
      return console.log("I can walk with my foots!");
  };
  return Human;
})();
eddie = new Human("eddie", 18);
eddie.say_hello("CoffeeScript");
eddie.walk();

看到編譯出來的JavaScript程式碼了嗎? 跟CoffeeScript的程式碼行數以及複雜度比起來,我想誰比較容易寫應該一目了然了。

在OOP的繼承體系裡,子類別會自動繼承自父類別的方法,如果兒子覺得老爸的方法不好用,兒子也可以自己定義一個來用,以上面的例子來說:

1
2
3
4
class Human extends Animal
  say_hello: (something) ->
      console.log "Hi, I am #{@name}"
      super something

你可以在子類別定義同名的function,以來取代或補充父類別的方法,如果在override的方法裡要存取父類,則是使用關鍵字super

有興趣的話,大家可以自己試著編譯看看這時候的程式碼是不是已經有點看不太懂了,但CoffeeScript的部份應該看起來還算滿清楚的。