BETA

Pixelaの草をドットアートに変換する

投稿日:2018-12-26
最終更新:2018-12-26
※この記事は外部サイト(https://qiita.com/wordijp/items/48c790e96b...)からのクロス投稿です

前置き

これはcommit以外の数値でも草を生やせる、PixelaというAPIサービスに可能性を感じた僕が何か面白い事ができないかを模索した記録である。

Pixelaでは日々の数値の記録全般に対しての可能性を秘めている、すでにたくさんの実用例が公開されてるなか、自分は表現方法にもっとバリエーションが欲しいなと思ったわけで、絵で表現してみてはどうだろうと考えました。

ドットアートへ変換

これは僕の過去3ヵ月分のvimを開いた回数なんですが
https://pixe.la/v1/users/wordijp/graphs/vim-pixela?mode=short

程よく茂ってますね、これが実際の草に変換されたり

vimの開いた回数と一目で分かるようにvimアイコンにしたり

カレンダー表記にしたりできます^1

変換するプログラムはライブラリとして区切りを付けて公開してます、興味のある方はこちらです
(サイトとAPIも作成中ですがAdvent Calendarに間に合わなかった)

変換の仕組み

これだけだとなんなので、変換にあたっての特筆点を解説します。

仕様を考える時に、なるべく汎用性を持たせてプログラムを変更することなく追加・修正できるように意識しました、Linuxのcowsayコマンドが表示するキャラクターをテキストで編集しているように本プログラムもそうしたいなと。
なので画像を用意し、それをドットアートに変換する方法を取りました、その際に色ごとに命令をセットする方法も選択できるようにして簡単な表示切り替えにも対応しています、カレンダーは来月になると12月と1月の表示にちゃんと切り替わるのです。

月表示の切り替えについて

画像はこのように複数レイヤーを持っています。

月表示では、1月の時だけ表示する色で1月を書き、2月の時だけ表示する色で2月を、と12ヵ月分を異なる色でレイヤー別に重ねており、プログラム側では1月表示色に差し掛かった時に、今が1月なら表示、という処理を実装しています。

下記の全文があるソースへ

// layout/layout_psdparser.go  
package layout  
// WriteSvgString -- SVGとして書き出す  
func (d Data) WriteSvgString(svgs graph.Data, w io.Writer) {  
    s := svgo.New(w)  

    ... 背景表示 ...  

    now := time.Now()  
    thisYear := now.Year()  
    thisMonth := int(now.Month())  
    thisDay := 1  

    ... 先月色用の初期化 ...  

    for _, x := range d.Place.Elems {  
        if x.Rgb.Equal(rgbThisMonthDays) {  
            // 今月色グループ内の該当日を表示(カレンダーの日にち部分)  
            rgb, ok := func() (color.RGB8, bool) {  
                for _, y := range svgs.Elems {  
                    if y.Date.Equal(thisYear, thisMonth, thisDay) {  
                        return y.Rgb, true  
                    }  
                }  

                return color.RGB8{}, false  
            }()  
            if ok {  
                rect(s, x.XY, rgb)  
            }  

            thisDay++  
        } else if x.Rgb.Equal(rgbPrevMonthDays) {  
            ... 先月色グループ内の該当日を表示 ....  
        } else if x.Rgb.Equal(rgbMonth1) {  
            if thisMonth == 1 {  
                rect(s, x.XY, x.Rgb)  
            }  
        } else if x.Rgb.Equal(rgbMonth2) {  
            if thisMonth == 2 {  
                rect(s, x.XY, x.Rgb)  
            }  
        } else if ... 3月~12月繰り返し ... {  
            ...   
        } else {  
            log.Printf("unknown rgb: %s xy(len:%d [0]:%d %d)", x.Rgb.ToColorCode(), len(x.XY), x.XY[0].X, x.XY[0].Y)  
        }  
    }  

    s.End()  
}  
func rect(s *svgo.SVG, xy []point, rgb color.RGB8) {  
    for _, xy := range xy {  
        s.Rect(int(xy.X)*10, int(xy.Y)*10, 9, 9, fmt.Sprintf("fill=\"%s\"", rgb.ToColorCode()))  
    }  
}  

ドット数の違いへの対応について

Pixelaから受け取るSVGでは、変換後の合計ドット数に明らかに足りません、これは単純に合計ドット数へとスケーリングして対応しています。

該当ソースへ

// dot/dot_colorlevel.go  
package dot  

// 配列aの合計がtotalになるようにスケールする  
// @return (スケール後の配列, スケール値)  
func scaleArray(a []int, total int) (scaleA []int, scale float32) {  
    sum := numeric.Sumi(a)  
    if total == 0 || sum == 0 {  
        return  
    }  

    scaleA = make([]int, len(a), len(a))  

    add := 0  
    for {  
        scale = float32(total+add) / float32(sum)  

        scaleTotal := 0  
        for i, x := range a {  
            scaleA[i] = int(float32(x) * scale)  
            scaleTotal += scaleA[i]  
        }  

        if scaleTotal > total {  
            // ここに来る?  
            log.Printf("warn: sum(%d) to scaleTotal(%d) > total(%d)", sum, scaleTotal, total)  
        }  
        if scaleTotal >= total {  
            break  
        }  

        add += total - scaleTotal  
    }  

    return  
}  

隣接する同色とのグルーピングについて

こちらは有名な塗りつぶしアルゴリズム(Flood Fill)を利用しています。

該当ソースへ

// layout/layout_psdparser.go  
package layout  

func collectByFloodFill(x, y int, img image.Image, b image.Rectangle, memo *[]bool, mx, my, H, W int) (elem DataPlaceElement, ok bool) {  
    if (*memo)[mx+my] {  
        return elem, false  
    }  

    c := img.At(x, y)  
    rgb := color.RGB8Model.Convert(c).(color.RGB8)  
    if rgb.Equal(rgbConnector) {  
        return elem, false  
    }  

    rec(&elem, rgb, x, y, img, b, memo, mx, my, H, W)  

    elem.Rgb = rgb  
    return elem, true  
}  
func rec(elem *DataPlaceElement, parentRgb color.RGB8, x, y int, img image.Image, b image.Rectangle, memo *[]bool, mx, my, H, W int) {  
    if (*memo)[mx+my] {  
        return  
    }  

    c := img.At(x, y)  
    rgb := color.RGB8Model.Convert(c).(color.RGB8)  
    if rgb.Equal(parentRgb) {  
        (*elem).XY = append((*elem).XY, point{X: int16(x), Y: int16(y)})  
        (*memo)[mx+my] = true  
    } else if rgb.Equal(rgbConnector) {  
        // 通り道  
        (*memo)[mx+my] = true  
    } else {  
        return  
    }  

    if x > b.Min.X {  
        rec(elem, parentRgb, x-1, y, img, b, memo, mx-1, my, H, W)  
    }  
    if x < b.Max.X-1 {  
        rec(elem, parentRgb, x+1, y, img, b, memo, mx+1, my, H, W)  
    }  
    if y > b.Min.Y {  
        rec(elem, parentRgb, x, y-1, img, b, memo, mx, my-W, H, W)  
    }  
    if y < b.Max.Y-1 {  
        rec(elem, parentRgb, x, y+1, img, b, memo, mx, my+W, H, W)  
    }  
}  

読み込みの高速化にあたって

PSDファイルを読み込みのたびにパースするのは、明らかに遅くなるだろうなと思ったので、パース後データをシリアライズして保存・読み込みも出来るようにしました、案の定読み込んでデシリアライズする方が20倍ほど高速になりました。

BenchmarkParseLayoutPsd-4            300           3944347 ns/op  
BenchmarkLoadLayoutData-4          10000            213763 ns/op  

シリアライズ・デシリアライズにはmsgpackを選択しました、encoding/gobが代表的かとは思いますが、ソースを見ると内部でreflectをガンガン使ってて、ベンチマークをとってもPSD読み込みよりも遅くなる始末でした。

おわりに

サイトなるはやで完成させたい

技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

@wordijpの技術ブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!