猫も杓子もCoffeeScriptの世の中、CoffeeScriptに手を出してしまったがために(JavaScriptの世に)帰らぬ人となりつつある人はたくさんいると思う。 僕も最近CoffeeScriptに手を染め五分で帰り途を見失ってしまった。
CoffeeScriptの編集にはujihisa氏のshadow.vimが便利だ。 shadow.vimを手短に紹介すると、main.js を開いて編集していると思ったら実はmain.js.shd(中身はCoffeeScript)を編集していて、気づいた時にはすでにmain.js.shdのコンパイル結果がmain.jsに保存されている、というvim-pluginである。
さて、JavaScriptを本格的に使うには、モジュールシステムを避けて通る事はできない。 グローバル変数を多用することも許されているJavaScriptでは、ライブラリによるオブジェクト空間の汚染は予測できない結果を引き起こしかねない。
モジュールシステムは未だ黎明期なので、本稿ではベストのものを選択するよりは、CoffeeScriptで書くクライアントサイド(以下CS)コードをサーバサイド(以下SS)でテストすることを念頭に、モジュールシステムの選び方を検討する、という方向で行こうと思う。 なお、ブラウザによる互換性は面倒くさいのでチェックしない。どうせやってることは一緒なのでいざとなれば解決可能な問題だし、各自で使用前にチェックしてください。
CommonJS
JavaScriptの処理系はちょっとリストアップするのが大変なほどある。
これらでばらばらのライブラリAPIを使われてはたまらないので、標準を決めようという事で、CommonJSと呼ばれる標準のようなものがあるらしい。 CS-APIではまだ存在する実装をまとめただけのような雰囲気もあり、CommonJS準拠だから選ぶ、ということにはならないが、 ありがたいことに、CommonJSに参加?しているプロジェクト一覧があり、これをベースに見ていくと楽できそうだ。
リストのうち、ブラウザ向けのものは
以下はリストには載ってないが見つけたやつ。
モジュールシステムについては、Modulesに関係する仕様と提案があり、これを参考にする。
Modules
おおまかに言うと、Modules/1.0では以下のようなrequireとexports変数を使ったオブジェクトのエクスポートを要求している。
// math.js
exports.add = function(x, y) { return x + y }
// increment.js
var add = require('math').add
exports.increment = function(val) {
return add(val, 1)
}
// main.js
require('increment').increment(2) //=> 3SSの処理系ではこれは広くサポートされているようだ。
Modules/AsynchronousDefinition (AMD)
CSでは、スクリプトの読み込み方法が限られている(scriptタグの追加かXHRによる読み込み)し、同期的読み込みを前提とした上記のような定義方法は実用的ではない。
そこでModules/Transport/* という名前でいくつかの提案がされている。
歴史的な経緯はよくわからないが、Modules/Transport/Aの実装であったRunJSはModules/Transport/Cの実装であるRequireJSへと名前を変え、
Modules/Transport/CはModules/AsynchronousDefinition(以下AMD)へと昇格?しているらしい。
AMDは以下のような形式で、他のものと比べて暗黙的な変数の導入がなく、スジがよいように思える。
これがデファクトになっていくのだろうか。
// define(moduleId?, deps?, factory function / object to be exported)
// math.js
define({
add: function(x, y) { return x + y }
})
// increment.js
define('increment', ['math'], function(math) {
return {
increment: function(val) {
return math.add(val, 1)
}
}
})CoffeeScriptとの相性もよい。これは今回は重要なポイントだ。参考までにCoffeeScript版も書いておく。
// increment.coffee
define ['math'], (math) ->
increment: (val) -> math.add(val, 1)Modules/1.xとの互換性を考慮した
// increment.js
define('increment', ['math', 'require', 'exports'], function(math, require, exports) {
// これより内側がModules/1.xの定義そのままで動く
exports.increment = function(val) {
return require('math').add(val, 1)
}
})といった書き方も用意されているようだ。
採用しているライブラリは
- RequireJS
- Yabble(partial)
- OzJS
- Nodules
- Dojo(1.6で試験的に導入)
- Teleport
- JSLocalnet
- PINF JS Loader(partial)
- backdraft
など。
Modules/Wrappings
//increment.js
module.declare(['math'], function(require, exports, module) {
return {
increment: function(val) {
return require('math').add(val, 1)
}
}
})あんまり違いがわからないが、ポイントはfactory関数の引数がrequire, exports, moduleの三つで固定されていることだろうか。その名前から、Modules/1.xとの互換性目的のものと思われる。
exportsを使ってもよいし、factory関数の返り値としてオブジェクトを返せばそれがそのままモジュールになる。
Modules/Async/A
AMDに対応したローダーの仕様はModules/Async/Aに書かれている。 といっても、Yabbleの実装(下記)をそのまま書いてあるだけのような気がするが。 対応ライブラリはYabbleのみしか書かれていない…え…
結局のところ、AMDはdefineの文法的形式が決まっているだけで、ローダにはまだ決定打がない。 コードを書く際はローダの呼び出しを最小限にするのが良さそうだ。
この辺がCommonJSの限界のようなので、あとは各々の実装を見ていくのがよさそうだ。
Yabble
// increment.js
require.define({
increment: function(require, exports, module) {
var math = require('math')
exports.increment = function(val) {
return math.add(val, 1)
}
}
}, ['math'])
// main.js
require.ensure(['increment'], function(require) {
require('increment').increment(2); // 3
});第一引数に依存するモジュールを、第二引数にcallbackを指定する。モジュールのロードが完了すると、callbackが呼ばれる。
テストコードも豊富でなかなか完成度の高いコードだ。
しかし、SSで使われることは想定されておらず、本体に手を入れずに使うことはできなさそう。
SSのテストコードでは利用する機能を搾って、同じインターフェースを持つ他のライブラリに丸投げする事が可能だ。
(例えばNodulesは、require.ensureを持っている)
Nodules
//main.js
require.ensure(['increment'], function(require) {
require('increment').increment(2); // 3
});$ node PATH_TO_NODULES/lib/nodules.js main.js
こんな感じで。
Nodulesの機能のこれはごく一部で、この他に
- HTTP上のjsをrequireする(そのためのrequire.ensure)
Packages/Mappingsの実装
など。
コマンドラインからの起動が必要なのはマイナスポイント。 あと、defineの第一引数を指定すると、ファイル名(フルパス)と違うと怒られた。
RequireJS
同じものがRequireJSではこうだ。
require(['increment'], function(increment) {
increment(2); // 3
});シンプルすぎて泣ける。かっこいい。 なお、defineは、一つのファイルに複数のモジュールを記述することを許しているがその場合は、
//libs.js
define('math', { … })
define('increment', ['math'], { … })
//main.js
require(['require', 'libs'], function(require) {
require('math').increment(2); // 3
});というように多段にしてrequireすればよい。
RequireJSはSSでの(node, rhinoに対応)実行も行えるようになっており、
$ PATH_TO_REQUIREJS/bin/x main.js
とすればいい。この時、engine-nativeのrequire関数はオーバロードされる。 ただこの方法だと、CoffeeScript中のrequireなどは置き換えられないため若干使いづらいし、そもそもrequireを汚染されるのは気持ちのいい物ではない。
これに関しては、wrapperを書く事で、通常のライブラリと同様読み込むことができるようだ。
// requirejs-node.js
(function() {
var read = require('fs').readFileSync
var run = require('vm').runInNewContext
var env = {
importScripts: function(url) {
run(read(url), env)
}
}
var code = read(require.resolve('require'))
run(code, env)
exports.require = env.require
exports.define = env.define
}).call()
// main.coffee
rjs = require 'requirejs-node'
rjs.require ['your_library'], (library) -> ...rjs.requireで読み込まれるライブラリ内ではrequire = rjs.require, define = rjs.define となる。
Dojo
Dojoでは1.6以降AMD形式のライブラリを提供していく予定らしい。 Loaderは提供しないので、今回は関係ない。
LoaderにはbdLoad / RequireJS を使えとのこと。
backdraft / bdLoad
backdraft frameworkの中のbdLoadというのがそれらしい。
- AMD-compliant / RequireJS compatible
- サイズが小さい
のが売りだそうだ。
RequireJSのテストコードがレポジトリに含まれていることから、RequireJSのよりよい実装と考えてもよさそうだ。
NodeJS用のインターフェースもデフォルトで用意されている。
上記RequireJSではwrapperを書いたが、bdLoadではデフォルトでほぼ同じ様になる。
// bdload-node.coffee
bdLoad = require('node').boot()
bdLoad.require
paths:
mylib: './mylib/lib'
bdLoad.require ['mylib/mylib'], (mylib) -> ...これはかなりいい。RequireJSを選ぶ理由はほぼない気がする。RequireJSはリファレンスマニュアルがかっこいい。
ちなみに、backdraftレポジトリをcloneしようとしたら、AAを用いたCAPTCHAのようなものが表示され、フォントが違うので読めなくてログインできない。CAPTCHAは見る価値あり。
OzJS
作者(dexteryy)はtudou.comのフロントエンジニアらしい。 スライドは中国語で詳しくはわからないが、かなり詳細に論じている様子が伺える。 他のライブラリに比べると圧倒的にサイズが小さいし、TUICompilerというのを使って静的に複数のスクリプトをまとめることができるようだ。
ただし、NodeJSからは使えなかった。
あとoz.defという名前で微妙にAMDと互換性がないので、気にする人はdefine = oz.defしてそっちを使うのがよいかもしれない。
// increment.js
var define = oz.def
oz.def('math', 'math.js')
define('increment', ['math'], function(math) {
return {
increment: function(val) {
return math.add(val, 1)
}
}
})
// main.js
oz.require(['increment'], function(increment) {
increment.increment(2) //=> 3
})FlyScript
Modules/Wrappingsを採用しているFlyScriptだが、何があったのかgithubのレポジトリが消えている。
作者の他のレポジトリも丸々ないので公開やめたのかな…
SeaJS
Modules/Wrappingsなのでこんな感じ。
//increment.js
define(['math'], function(require, exports, module) {
return {
increment: function(val) {
return require('math').add(val, 1)
}
}
})
//main.js
seajs.config({
base: './...',
alias: {
//...
}
})
seajs.use(['increment'], function(increment) {
increment.increment(2) //=> 3
})SSでは動かなかった。 Backbone.js系のライブラリを添付してくれていて助かるが、必ずmoduleId.jsを読みに行ってしまうみたい。 これは、production環境でファイルをまとめたい時などには困るだろう。 お手軽にBackbone.jsで遊ぶ専用か。
JSLocalnet
- ライセンスがGPLv3/プロプライエタリ
- XHRにしか対応してない
- IE8非対応
- コメントがスペイン語
- レポジトリがhg
- テストコード一切無し
と、異なる文化圏にあるようでちゃんと見るのやめました。
PINF JS Loader / bravojs
Modules/2.0!
Modules/Wrappingsとの違いがよく分からない…
未実装の部分が多いが、完成するとかなりの処理系(JetPack, NodeJS, browsers…) にまたがって共通のモジュールシステムを提供することになるようだ。
Modules/2.0提案用のリファレンス実装という色が濃く、unstable感が高いので今回はパス。
curl.js - updated on 2010/05/05
curl.jsはcujo platform用のAMDローダ。 以前まではdojoのローダを使っていたようだが、今はAMDを使うようになったamdブランチがあるようだ。 まだリリースされて一ヶ月とあまり情報もないので見逃していた。 SSでは動かない。
- 素直にAMDを実装
next,thenなど独自機能- ‘text!xxxx.text’ でテキストファイルをロード
- ‘js!xxxx.js’ でnon-AMDなjsをロード
といった感じ。特に最後の拡張は、AMD非対応jsが多い現状を考えると面白い。
作者のスライド(オススメ)によれば、AMDではrequireはThe Wild Westだという。
しかし、requireの呼び出しはコードの1%にも満たないので、もっとやれ!ということのようだ。
curl({
baseUrl: '...',
paths: ['.']
}, {})
require(['increment'], function(increment) {
increment.increment(2); // 3
});
// 別の書き方
require(['increment'], (function(increment) {
increment.increment(2); // 3
})
.next(['js!jquery.js!order', 'js!zoom.js!order'])
.then(function() {/* success */}, function(e) {/* error */});サイズ
CSで利用する際に気になるのはサイズだが各ライブラリの主要ファイルのサイズを以下の方法で調べた。
$ uglifyjs -nc --unsafe < xxx.js | wc -c
$ uglifyjs -nc --unsafe < xxx.js | gzip -c | wc -c
OzJSがもっとも小さく、RequireJSが最も大きいがその差は数倍程度の開きでしかなく、あまり大きな要素にはならないように感じた。 まとめの項に表を示す。
まとめ
CSのモジュール定義はAMDでほぼ確定、ローダは標準なしということで、 様々なモジュールローダを見て来たが、SS/CSで動くよう作られたライブラリはやはり少なかった。 現時点では、SS/CSで使うライブラリをわけるのが選択の幅を狭めない妥協点だ。
| library | license | server-side | size | gziped | nank |
| RequireJS | MIT / mBSD | ? | 16486 | 5788 | ★★★☆☆ |
| bdLoad | BSD | o | 11221 | 4714 | ★★★★★ |
| OzJS | MIT | x | 3143 | 1543 | ★★★★☆ |
| SeaJS | MIT | x | 5489 | 2319 | ★★☆☆☆ |
| Yabble | MIT | x | 6635 | 2468 | ★★★★☆ |
| bravojs | MIT | o | 8646 | 2671 | ★★★☆☆ |
| curl.js | MIT | x | 4627 | 2247 | ★★★★☆ |
| JSLocalnet | GPLv3 / Proprietary | x | - | - | - |
| FlyScript | MIT | - | 8594 | 3831 | - |
| Nodules | mBSD / AFL2.1 | o | - | - | - |
PINF JS Loader / bravojs の組み合わせは今回の様な用途を本気で目指したものであり、今後注視しておきたいが、今変化の直中であり手を出しづらい。
また、中国製のライブラリがいくつか(OzJS, SeaJS)見受けられ、中国でJavaScriptがキてるのが感じられる。 この組み合わせは強そうだ。
バランスがいいと感じたのはbdLoadであったが、さらに踏み込んで使いたい場合はOzJS / curl.js もよい選択だと思う。
おまけ
名前が挙ってたので調べたけど、関係なさそうなやつ。
Teleport
teleportは、browserでの開発にNodeJSのエコシステムを持ち込むものらしい。
$ npm init # package.json作成
$ npm link
$ vi index.html
$ teleport activate
とすることで、開発中のpackageにhttp://localhost:4747/PACKAGE_NAME/でアクセスできるようになる。
http://localhost:4747/teleport-dashboard/でnpm管理下のパッケージ一覧が見られる。
browser上でrequire('...')などとすると、npmパッケージをロードすることができる。
この時、Modules/1.x 形式のモジュールを、defineでくるんでCSに送信する。
また、require/define がすでに定義されている場合は、上書きしないような配慮がされている。 うまく開発プロセスに組み込めば、ブラウザによるテストも楽になりそう。
リリース時には、$ teleport bundle PACKAGE_NAME/app とすることで、必要なパッケージを展開?してくれるようだが、これはバグで動かなかった。
Transporter
Transporterでは、Modules/1.0モジュールを、様々なtransport形式でCSに提供するSSの仕組みを提供している。
CS-APIは、require.ensure / defineを使うようだが、非同期読み込みには対応しておらず、自分であらかじめscriptタグで読み込んでおく必要があるようだ。
利点がわからない…誰か教えて…
参考:
- CoffeeScript
- azer/jsbuild - GitHub
- vcs-localnet - CommonJS in browser-side / CommonJS en el navegador - Google Project Hosting
- pinf/loader-js - GitHub
- RequireJS
- jbrantly/yabble - GitHub
- bravojs - Modules/2.0 for the Browser - Google Project Hosting
- dexteryy/OzJS at master - GitHub
- Modules/Transport - CommonJS Spec Wiki
- CommonJS: JavaScript Standard Library
- Modules - CommonJS Spec Wiki
- Modules/Async/A - CommonJS Spec Wiki
- Head JS :: The only script in your HEAD
- SeaJS - 海纳百川,有容乃大。
- backdraft browser application framework - home
- ujihisa/shadow.vim - GitHub
- kriszyp/nodules - GitHub
- Unbeatable JavaScript Tools - The Dojo Toolkit
- Gozala/teleport - GitHub
- kriszyp/transporter - GitHub
- Dojo 1.6 Release Notes — The Dojo Toolkit - Reference Guide
- FlyScript - It’s a CommonJS Thing
- Modules/Wrappings - CommonJS Spec Wiki
- unscriptable/curl - GitHub
- curl.js – yet another AMD loader | Unscriptable.com
- cujo at amd from unscriptable/cujo - GitHub
- unscriptable/cujo - GitHub
- Using AMD loaders to write and manage modular javascript
前回のエントリから早一年半。
Atlasのベータプログラムもいつの間にかスタートして、心躍るネタを提供してくれるObjective-J/Cappuccino.
一方僕はVimScriptを改良した。
主な改善点は、
- []を自動で追加
- 代入による変数の型推測
- 定数を補完データベースに追加
Objective-CやObjective-Jでは、メッセージを続けて送る場合、[]を入れ子に書かないといけなくて、かなり苦痛なのだが、直前の対応する[の前に[を自動挿入することで、
[CPView alloc] init // ☟ [[CPView alloc] init]
という補完が行われるようになった。
Ruby勉強会@関西44の懇親会にて @cyross, @KazkiMatz さんらとSDLをWebSocketと組み合わせて…という話をしていておもしろそうだったので実装してみた。

青は@nanki, 赤は@ujm
- Ruby/SDLの画面描画を全て文字列としてシリアライズし、WebSocketで送信、Canvasに描画する。
- ブラウザ上で発生したイベントをWebSocketで送信、SDLのイベントモデルの中に還元してやる。
というのが基本的な仕組み。
# samples/sample1.rb
require 'rubygems'
gem 'rubysdl'
require 'sdl'
require 'websdl'
class TestFrame < Frame
attr_reader :mx, :my, :color
def initialize
@color = 0xff000000 | (1..3).inject(0){|r,i|r << 8 | rand(0xff)}
@mx = -100
@my = -100
end
def open_screen
SDL::Screen.open(640, 480, 32, SDL::HWSURFACE || SDL::DOUBLEBUF)
end
def mainloop(screen)
screen.fill_rect(0, 0, screen.w, screen.h, 0)
while event = SDL::Event.poll
@mx, @my = event.x, event.y if SDL::Event::MouseMotion === event
end
frames.each do |f|
screen.fill_rect(f.mx - 15, f.my - 15, 30, 30, f.color)
end
screen.flip
end
end
SDL = WebSDL if ARGV.first == 'web'
SDL.init(SDL::INIT_VIDEO)
SDL.run(TestFrame, :host => '0.0.0.0', :port => 3000)上記のようなコードで、
$ rsdl -Ilib samples/sample1.rb
ではSDLのウィンドウが起動し、
$ ruby -Ilib samples/sample1.rb web
でWebSDL版が起動するようになっている。
WebSDL版はsamples/test.html をWebSocket, Canvasに対応したブラウザで開く。
ただCanvasを使うだけでは芸が無いので、マルチユーザに対応、ユーザ間の情報共有にも対応してみた。
言うまでもなくProof of Conceptの段階であり、SDLの機能もdraw_rect, MouseMotion くらいしか実装していない。
また、0.1秒毎に更新しているのがかっこわるいが、適切なイベントに基づいた処理をすれば、パズルゲームやアドベンチャーゲームなど頻繁に画面更新のないものなら実用的なものができる可能性がある。 ウェブへぇボタンとかね。
問題点
一方、SDLのAPIは同期的なものもあり、SDL::Surface#get_pixel (画面上のピクセルの色を返す) などは現在の方法では実現が困難である。
参考:
某所で話題になったのでまとめ。
JavaScriptで、
200 << 24 => -939524096 200 * Math.pow(2, 24) =>3355443200
の結果が違うという話。
200 が 7~8bitなので、すぐに32bitの壁だとわかるが、これは仕様なのか、実装によるものなのか。
ECMA-262, p.76
- Let lref be the result of evaluating ShiftExpression.
- Let lval be GetValue(lref).
- Let rref be the result of evaluating AdditiveExpression.
- Let rval be GetValue(rref).
- Let lnum be ToInt32(lval).
- Let rnum be ToUint32(rval).
- Let shiftCount be the result of masking out all but the least significant 5 bits of rnum, that is, compute rnum & 0x1F.
- Return the result of left shifting lnum by shiftCount bits. The result is a signed 32-bit integer.
つまり、
(signed int32)(ToInt32(200) << (ToUint32(24) & 0x1F))
こういう感じになる。
結果は32bit符号付き整数なので、こうなるのは仕様。
参考:
選択した文字列もしくは現在のURLをQRコードにするbookmarklet
javascript:s=document.getSelection().toString();s=s.length?s:location;open("http://chart.apis.google.com/chart?cht=qr&chs=200x200&choe=UTF-8&chl="+encodeURIComponent(s),'_blank')Safariでしか試してない。 Google Chart API 便利。
参考:
Read full article 1721 comments
Canvasを使ったWii用ゲームを作ってみた (Kanasansoft Web Lab.)
Nintendo Wii に載っているOpera にはWii Remote APIなるものがあって、WiiリモコンとCanvasを使って色々できる。
うちには、OperaもWii のコントローラ(だけ)もあるのに、kanasan の力作が遊べないなんて!ありえない!
というわけで、Wii Remote API のふりをするコードを書いてみた。
WiiFake: emulate Wii Remote API on Mac OS X. from NANKI Haruo on Vimeo.
requires at runtime
- WiiRemote.framework from DarwinRemote Project
- Ruby
- Ruby/Cocoa
- Flash Player Plugin
How to use.
- Copy WiiRemote.framework to your ~/Library/frameworks/ directory.
- WiiFake を落としてくる。
- Wii リモコンの 1 2 keyを押したままサーバを起動する。
$ ruby server.rb - このbookmarkletを起動 WiiRemote bookmaklet
- 必要に応じてページの初期化処理を呼び出す。
(上記kanasanのページだと、javascript:initialize())
仕組み
bookmarklet で読み込んだ WiiProxy.swf から localhostに立てたRubyサーバに繋いで、JSON形式で受け取った情報を、javascriptの世界に渡しているだけです。
テスト
Safariでしかテストしてない;;
画質のテストのため、同じムービーをYoutubeにもアップロードしてみた。
参考:

しばらくはprototype.js や jQuery、時々はExt JS で楽しく遊んでいたけども、デスクトップアプリケーションのような物を作るのには力が不足していると感じていた。(機能が、ではなく、抽象化の程度が)
年初めくらいに cappuccinoというプロジェクトを見つけてきて、しばらくソースを眺めていたりしたのだけど、これはいい。 久々にピリピリ来るプロジェクト。
最近、iPhone SDK などでObjective-Cに触れて、Cocoa APIに慣れ親しんだせいもあるけど、それを差し引いても、よくできている。 興味と暇のある人は是非、調べてみて欲しい。
実際に、Keynote にしか見えないアプリケーション なんかもこれで作られている。
ちょっとソースを見ると、こんな調子だ。
<script type="text/javascript">
OBJJ_MAIN_FILE = "main.j";
</script>
<script src="Frameworks/Objective-J/Objective-J.js" type="text/javascript"></script>main.j というのが、Objective-J 言語で書かれていて、Objective-J.js を読み込むと、その場でJavaScriptにコンパイルされる。
元々、Objective-C は、Objective-Cのランタイム(Cの関数)とプリプロセッサだけでこしらえてある(と思う)のだが、その手の抜けた?実装とは裏腹に強力なオブジェクト指向機能を持っている。
今回、Objective-J がその手法をJavaScriptに対して適用したのを見て、この方法の持つシンプルさと力ににやにやせずにはいられない。(JavaScriptにOOを導入するとは!)
でも、Cocoa APIはフルスペリングでAPIが長いのでとても覚えられない。 IDEなどのコード補完機能がないと書く気が起こらない…ということで、vim の補完関数を作った。
github に置いてあるので、使うなり改良するなりしてください。
関係ないけど、Objective-J ではメソッドの定義に型を書くんだけど、コンパイルの時点で廃棄されている。(ので間違っててもコンパイルは通る) しかし、コード補完を作るにあたって、この型情報は非常に役に立った。
参考:
Rails pluginとして動く言語、purlを公開しました。
purl はクライアントサイドから柔軟な画像処理を行うために開発されました。 サムネイルの生成などを行うプラグインはいくつかありますが、そのどれよりも強力で汎用性、拡張性があります。
セットアップ
Rails2.1以上では、
$ script/plugin install git://github.com/nanki/purl.git
それ未満では、
$ git clone git://github.com/nanki/purl.git -- vendor/plugins/purl
gitが無い環境なら、githubのdownloadからファイルを落として、vendor/plugins/ に配置してください。
依存ライブラリ
- 必須: ImageMagick, rmagick
- あった方がいい: cairo, rcairo
その他
purl のデフォルトの環境では、画像の保存にデータベースを利用します。 imagesテーブルに、blob型のdataカラムを追加してください。
# app/models/image.rb
class Image < ActiveRecord::Base
attr_readonly :data
endその上で、ImageController#upload などに以下のように記述します。
# app/controllers/image_controller.rb
class ImageController < ApplicationController
def upload
Image.create(:data => params[:data].read)
end
endさらに、config/routes.rbを書き替え、purl が実行されるようにします。 書く場所は先頭に近い方がいいと思います。
# config/routes.rb
map.connect 'purl/:commands',
:controller => 'purl', :action => 'filter', :commands => /.*/# app/controllers/purl_controller.rb
class PurlController < ApplicationController
caches_page :filter
def filter
begin
result = Purl::StackMachine.new(
params[:commands], Purl::Purl.new).run
send_data(result[-2], :type => result[-1], :disposition => 'inline')
rescue => e
render :status => 500, :text => 'error'
end
end
endそんなこんなでセットアップは終わり。
言語
purl はURL上で記述されることを前提としています。 常にワンライナーであれ!
文法
purl は逆ポーランド記法を採用しています。 データと命令を書かれた順番にスタックマシンにプッシュしていき、命令が実行された結果がスタックマシン上につまれていきます。 命令列は最初に:で区切られ、数字としてパースできるものは数字へ、()で囲われた部分は文字列へと変換されます。
ポリシー
デフォルトで用意される全ての命令は、外部の値に依存しません。 つまり、命令列が決まれば実行結果は確定します。 このポリシーにより、全ての実行結果がキャッシュ可能になります。
サンプルコード
画像をそのまま表示
次の例では、load はスタック上に既にある数字1を消費して、ID=1のImage#dataをデータベースからロードして、スタック上にプッシュします。 to.png では、スタック上の画像データをPNG形式のバイト列に変換し、mimetypeとともにスタック上に積みます。
/purl/1:load:to.png

画像の形式を変換
次の例では、画像をGIF形式に変換しています。
/purl/1:load:to.gif

画像のリサイズ
次の例では、画像を100x100にリサイズしています。 リサイズ用の命令は、何種類か用意されており、高さを一定に保つ resize.heightや、上限を指定する、resize.upto などがあります。
/purl/1:load:100:100:resize:to.png

画像の合成
次の例ではふたつの画像を合成しています。
/purl/1:load:9:load:composite:to.png

次の例では順番を入れ替えています。
/purl/1:load:9:load:swap:composite:to.png

コメント
スタックに積んだ後すぐにpopすると、単純に無視されるので、コメントとして使えます。 キャッシュのライフサイクルコントロールなどに使えると思います。
/purl/1:load:(daily):pop:to.png
図形の描画
詳しくは解説しませんが、試験的にcairoの描画機能をベースにした図形描画命令を用意しています。 ctx命令で、指定したサイズのcairoコンテキストを準備し、xtc命令で実際にサーフェースへの描画を実行します。
/purl/80:80:ctx:40:40:moveto:40:40:40:-20:20:arc.ccw:1:0.3:0:rgb:fill:60:20:4:circle:0:0:0:rgb:fill:xtc:to.png

制限
実行結果をファイルとしてキャッシュする場合、ファイルシステムによる字数制限を受けます。
拡張
拡張は命令モジュールの追加という形で行います。 詳しくはソースコード見てください。
今後
- 無名関数的なもの。
- 外部から直接呼び出せない命令とか?
- エラーメッセージとか?
参考:
URLの#以降に/jsconsoe/#javascript:alert(123)といった形で保存する。
このままブックマークに保存すれば、書いている途中のスクリプトを次回ロードできる。
ブックマークした後に、#以前を削除すれば、そのままbookmarkletに。