あけましておめでとうございます
年賀状をHaskellで
上の画像が今年の僕達の年賀状です。
実は、妻が最初アプリでデザインを考えていたのですが、 画像のダウンロードができず、Pixelmatorで作りなおそうという話がでました。 でも、これってHaskellでできるんじゃないの…?と思い、 試しにやってみることにしました。
5分の仕事が結局2日になったけど(笑)… 面白かったので、やり方を公開します。
概要
まず、上の画像をみて、デザインを考えます。
最初は、下記のやりかたでやろうと思っていました。
- 写真の上に、真っ白のレイヤーを載せる。
- その白いレイヤーから3つの三角形を切って、下の写真が見えるようになる。
- 最後にメッセージを追加する。
ただ、diagramsでは、レイヤーを作って、そのレイヤーから切る機能がなかった (というか、あるかもしれないけど、僕が分からなかったので)。
結局、「上の白いレイヤーから切る」という方法ではなく、 「そのまま5つの三角形を描く」という方法でやりました。
文章ではちょっと分かりづらいと思うので、絵で説明します。
この下にメッセージをつけます。
では、実際にどう作ったか振り返ります。
依存関係
まず、Haskellのライブラリをインストールします。
下記のcabal
設定でインストールしました。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
arithmoi
は最近llvm
のバージョンによってエラーになるときがあります。 その場合は、下記のコマンドでcabal install
してください。
|
フォントをsvg
に変換するため、fontforge
が必要です。 Macを使ってたので、まずXQuartzをインストールしました。 それから、Homebrewで、
|
を入力して、インストールします。
予備
LANGUAGE
プラグマは、意外と多くなりました。 まず、いつものOverloadedStrings
と、 diagramsのドキュメンテーションに推奨されるNoMonomorphismRestriction
。
|
|
NoMonomorphismRestriction
は使った方がいいと書いてあったので 入れましたが必要なかったので、結果的に使いませんでした。
フォントを設定するためのデータ構造では、 Functor
とTraversable
の関数を使いたいと思いました。
|
|
|
よっし!diagramsをインポートしよ!
|
|
あ、テキストも書きたいからSVGFontsも必要になるね…
|
|
でも、SVGFontsを使うため、フォントをttfからsvgに変換しないと…
そのため、システム的なモジュールも必要だね…
|
|
|
|
あ、きっとそのsvgファイルを調整しないとダメだろー
Data.Text
を使った方が最適…
|
|
|
|
え、さっきTraversable
って言わなかった?
|
|
まだだよー
|
|
まだだよー
|
終わり。
型定義
今回のプロジェクトは殆どの型はdiagramsで定義されています。
自分で定義をしたのは一つだけです。それは、フォントを設定するための型です。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
「なぜ多相型で定義する必要があるの?」と思うかもしれませんが、 フォントは自動的に.ttf
から.svg
に変換できるようにしたいので、 同じストラクチャーで、ロードする前のファイル名と、 準備ができた使える状態のフォントを入れたいと思います。
|
このnewtypeを使って、「フォントが変換された」と、型安全的に証明します。 (因みに最新版のSVGFontsではPreparedFont
は既に定義されているので、 最新版がHackageにアップされたらこれは必要なくなります。)
それでは、コード自体を始めましょう。
二等辺三角形
もう一度下の画像を見てみましょう。
この5つの三角形は全部二等辺三角形です! diagramsは正三角形を作る関数が定義されていますが、 二等辺三角形はないため、自分で定義する必要があります。
|
|
|
|
|
|
θ
は頂角、a
は高度(altitude)です。
(^-^)
、(^/)
はvector-spaceというパッケージで定義され、 角度やベクトル等を引算、割算ができるような関数です。 意味が分からないときは^
を消してみたら、だいたいの意味が見えてきます ((^-^)
→ (-)
、(^/)
→ (/)
)。
(&)
、(.~)
はlensのオペレーターです。 今からlensの説明しようとすると話が終わらないので、今日は省きます。
画像のレイアウトをするため、底辺の長さが必要なときがあります。 それを計算するために下記のユーティリティ関数を使います。
|
|
レイアウト
簡単に言うと、このデザインは「上に画像があって、その下にメッセージ」と説明できます。
とりあえず、フォントや写真はもう既にロードされていると見なしましょう。 そうすると、下記のようになります。
|
|
|
|
|
|
|
(===)
は、2つのDiagram
を上から下に並べる関数です。
分かりやすいでしょう! topImage
は上の画像、message
は下のメッセージ。 並べると、年賀状になると。
あとは、ここで使った値を定義するだけです。
|
|
|
|
|
|
|
|
|
|
まずは、写真や画像についての変数です。θ
の値を変更したら、三角形の形を変えられます。 photoHeight
は写真全体の高さですが、 imageWidth
はできた画像(緑のメイン三角形)の幅となります。
|
|
|
実はもとの写真では人物は真ん中ではなかったので、 細かいことになりますが、これで調整しています。
|
|
|
|
|
|
色が付いている三角形の画像をみると、ちょっと外側が汚いので、 inViewport
で、外の部分を消す関数を用意しました。
画像
画像自体も簡単に説明できます。まず、5つの三角形と合わせるため、 写真を切る必要があります。その切った写真の上に、三角形を載せていきます。
|
|
切るサイズは、写真の元の高さ✕緑の三角形の底辺の長さです。 それではちょっと幅が見えてしまうので、あと2ピクセルずつ、念の為に切ります。
|
|
|
|
|
|
|
|
|
|
triangles
は、左から右に言うと、
|
|
|
|
|
|
|
|
|
|
|
|
|
|
になります。この画像では順番は関係ないけれど、 場合によっては必要になりますね。 Diagram
を連結すると、 上から下の順番になります(edgeTriangleLeft
の下に bottomTriangleLeft
を描いて、その下に centralTriangle
を描く…という形)。
この画像の三角形を見ると2種類があります。 まずは、真ん中の3つの三角形。
輪郭のみを描いて、下にある写真が見える三角形ですね。
|
|
|
|
|
|
|
|
「なぜcenterXY
が必要か?」ですが、Diagramsはデフォルトでは 原点は高度から見る真ん中ではなく、重心に設定してあります。 二等辺三角形を鏡映すると、ずれてしまう。 centerXY
をしたら、簡単に鏡映できるようになります。
次は左と右の、真っ白の三角形です。
この三角形の頂角は真ん中の三角形の反対角度になっています。
|
高度は下の三角形の底辺の半分です。 この値はまた使うので変数に保存しておきましょう。
|
|
あとは色と90°の回転です。
|
|
|
|
|
|
|
|
|
|
outlineTriangle
とedgeTriangle
の2種類を定義できました。 これで5つの三角形が描けます。まずは真ん中の三角形です。 高度は写真と一緒です。その三角形を、y 軸に鏡映します。
|
|
あとは右側の下の三角形と真っ白の三角形。
|
|
|
|
|
|
|
|
|
|
最後は左側です。右側を、x 軸に鏡映するだけです。
|
|
|
|
画像の分はここまでです。
メッセージ
最初に「型はFonts
以外は定義しない」と言いましたが、 実は、メッセージを定義するため、今回下記のユーティリティー型を定義しました。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
この型はメッセージのDiagramを定義するための、一瞬の型なので、 絶対必要とは言えません。 正直、もともとこのプログラムを書いたときはこの型は使わず作りました。 ただ、あった方が絶対分かりやすいと思って、 ブログのためにちょっとリファクタリングしてみました。
MessagePart
はメッセージの一行です。 その一行はメッセージのテキスト(MsgText
)か、 何も表記しない、ただスペース開けるため(MsgSpace
)です。
この型について一つポイントがあります。 proportionalHeight
とproportionalWidth
は、「高さ」と「幅」の割合を意味します。 ただ、表記の仕方はそれぞれ違います。 proportionalWidth
の方は、全体の幅に対しての割合ー 例えば、幅の半分としたいなら1/2
と表記します。 一方、proportionalHeight
は、表記スペースに対して一行の大きさを決るため、表記スペースに対するの「割合の分子」のみ表記します。 分母は、全部のメッセージのproportionalHeight
の合計になるはずです。 結果、MessagePart
を並べれば、スペースを100%と使えていることになります。
この型があったらメッセージ自体は、ただMessagePart
のリストになります。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
では、このメッセージをどうやってDiagram
に変換しましょう?
|
|
|
MessagePart
を一つ一つdrawMsgPart
で描いて、 それからfoldr1 (===)
で上から下まで並べます。
|
|
|
|
MsgSpace
だったら、ただy 軸にスペースを開けます。 MsgText
だったらtext'
としてレンダーします。
getRealHeight
は、割合の分子から、実際の高さに変換する関数です。
|
|
|
|
text'
は、SVGFontsを使ってテキストをレンダーします。
|
|
|
|
|
|
|
|
|
|
不純なところ
今までの関数は純粋的に定義しました。 これからは、実世界と繋がっているIO
モナドを使って、 写真やフォントを準備するための関数を定義します。
写真は簡単です。DiagramsのloadImageEmb
関数を呼んで、 エラーが返されたらそのまま出力して停止します。
|
|
|
フォントはもうちょっと複雑なんです。
|
|
|
|
|
フォントは.ttf
から.svg
に変換しないと使えないんです。 変換されたフォントはsvg-fonts
というディレクトリーに出力します。 まず、そのディレクトリーがない場合は作成しないとダメです。
|
|
|
|
|
|
変換自体はfontforgeを使って行います。 もしもう既に変換されたフォントがあるならまた変換する必要はありません。
|
|
|
|
|
|
|
|
|
|
fontforgeが出すXMLはネームスペースに入っていますが、 SVGFontsがネームスペース無しのXMLしかサポートされていません。 下記の関数はネームスペース宣言を外します。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main
関数
最後に、上記の関数を結んでいくmain
関数です。
写真とフォントの準備をし、nengajou
の関数に渡してDiagram
を作成します。 pad
を使って枠を作ります。それから背景を白にしましょう。 最終、DiagramsのmainWith
関数を使ってコマンドラインインターフェースが出来上がります。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
さあ、これで今年の年賀状が完成しました!
今年も楽しみながら、関数型言語で面白いものを作って行きましょう!