mysvglibを作り始めた

if __name__ == "__main__":
    cont = Context()
    cont.initialize()
    cont.render_header()
    cont = cont.trans(300, 300)
    cont.circle(r=100)
    for i in range(8):
        cont.rot(degrees(45 * i)).trans(200).circle(r=50)

    cont.render_footer()

仕様を網羅したライブラリを作る気ははなから存在しないので、このライブラリの進捗は僕が何を描きたいと思うかに依存します Dependencies: やる気、描きたいもの



    cont.circle(r=100)
    cont.loop(
        8, lambda cont: cont.rot(degrees(45)),
        lambda cont: cont.trans(200).circle(r=50))

これでさっきと同じ図が出るようにした。


    cont.circle(**attrs(r=100, fill_opacity=0.5))
    cont.loop(
        8, lambda cont: cont.rot(degrees(45)),
        lambda cont: cont.trans(100).circle(
            **attrs(r=50, fill_opacity=0.5)))

んー、「**attrs」はさすがにダメかなぁ。


attrsをユーザに使わせるのはださいが、circleやsquareで毎回同じことをするのも実装としてださい。というわけで。

def attrs_decorator(f):
    def new_func(self, **kw):
        "replace key's hyphen: e.g. fill_rule -> fill-rule"
        new_kw = dict((key.replace("_", "-"), kw[key]) for key in kw)
        return f(self, **new_kw)
    return new_func

class Context(dict):
...
    @attrs_decorator
    def circle(self, **kw):
        kw["transform"] = mat2str(self["matrix"])
        attr_str = " ".join('%s="%s"' % p for p in kw.items())
        self["out"].write('''<circle %s />\n''' % attr_str)
        return self

こんな感じ。自分の持っている行列の情報からtransformを作るところとか他のエレメントと共通かもしれないな。まあ、それは他のエレメントができてからくくりだせばいいや。僕は円だけで十分。


あとは複数の命令をまとめる機能が欲しいわけかな。

まずこうして

    cont = cont.trans(300, 300).scale(100)
    cont.circle(fill_opacity=0.5)
    cont.loop(
        8, lambda cont: cont.rot(degrees(45)),
        lambda cont: (
            cont.trans(1)
            .scale(0.5)
            .circle(fill_opacity=0.5)))

この「cont.circle(fill_opacity=0.5)」を再利用可能な塊にしたい、と。

関数を取ってapplyして返すメソッドをcontextにつけてやればいいだけか。

class Context(dict):
...
    def apply(self, f):
        return f(self)
    def circles(cont):
        return cont.circle(fill_opacity=0.5)

    cont = cont.trans(300, 300).scale(100)
    cont.apply(circles)
    cont.loop(
        8, lambda cont: cont.rot(degrees(45)),
        lambda cont: (
            cont.trans(1)
            .scale(0.5)
            .apply(circles)))

これでさっきと同じ絵が生成された。そして

    def circles(cont):
        return (
            cont
            .circle(fill_opacity=0.5)
            .scale(0.5)
            .circle(fill_opacity=0.5)
            .scale(0.5)
            .circle(fill_opacity=0.5))

うん、めでたしめでたし。


    @recursion.max_depth(5)
    def circles(cont):
        return (
            cont
            .circle(fill_opacity=0.5)
            .scale(0.8)
            .trans(0.7)
            .apply(circles))

    cont = cont.trans(300, 300).scale(100)
    cont.apply(circles)

うん、ちゃんと5回で再起呼び出しが止まっている。


次は「だんだん薄くなる/濃くなる」をやろうかと思ったけど、それってfill-opacityの属性に干渉する必要があるんだな。Context Free Artではalphaの値がfill-opacityに加算で干渉するんだけども、この設計はいくらなんでもひどいと思うわけだ。で、乗算で干渉するのがただしいかって言うと、薄くなる方向はそれでいいのだけども、濃くなる方向もそれでいいのかというとちょっと疑問。

opacityって要するに前景色と背景色がx : 1-xなわけだから、xが0から1を動くときに比の値は0から無限大まで動くんだよね。前景色を「2倍に濃くする」と背景色が「2分の1に薄くなる」であってほしい。ということはどういうことか。opacityがxで、この「何倍濃くする」の値をmとすると、もともとx / (1-x) だったものをmx / (1-x)にするわけだ。比の値がそうなるように新しいxを決める。


でけた。

    @recursion.max_depth(15)
    def circles(cont):
        return (
            cont
            .circle(fill_opacity=0.5)
            .trans(0.7)
            .alpha(0.8)
            .apply(circles))


    cont.apply(circles)
    cont.trans(y=2).alpha(10).apply(circles)


上は濃さ0.5から0.8倍ずつ薄くなって行って、下は10倍濃い状態から0.8倍ずつ薄くなって行っている。0を下回って表示されなくなったり、1を超えて振り切れてしまったりしていたContext Free Artに比べるとだいぶよくなったんじゃなかろうか。少なくともアート作品を作る上では。


今日の結論

if __name__ == "__main__":
    cont = Context()
    cont.initialize()
    cont.render_header()
    cont = cont.trans(300, 300).scale(100)

    @recursion.max_depth(7)
    def circles(cont):
        return (
            cont
            .circle(fill_opacity=0.5)
            .loop(5, lambda cont: cont.rot(invr(5)),
                  lambda cont: cont.trans(1)
                  .alpha(1.2)
                  .scale(0.5)
                  .apply(circles)))

    cont.alpha(0.1).apply(circles)

    cont.render_footer()

これくらいの行数で出力できる。

ちなみにこのSVGは3メガバイトあって、20000個くらいの円がある。そろそろ画面プレビューを済ませた後、時間のかかるパラメータ設定で直接画像を出力、とかできるようにすべきかの。今日のところはこの辺で寝ることにする。続きは気がむいたとき。


TODO: 色!