Edited at

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


前置き

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

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


ドットアートへ変換

これは僕の過去3ヵ月分のvimを開いた回数なんですが

https://pixe.la/v1/users/wordijp/graphs/vim-pixela?mode=short

vim-pixela-fs8.png

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

dot-grass-fs8.png

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

dot-vim-fs8.png

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

layout-calendar-fs8.png

変換するプログラムはライブラリとして区切りを付けて公開してます、興味のある方はこちらです

(サイトとAPIも作成中ですがAdvent Calendarに間に合わなかった)


変換の仕組み

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

仕様を考える時に、なるべく汎用性を持たせてプログラムを変更することなく追加・修正できるように意識しました、Linuxのcowsayコマンドが表示するキャラクターをテキストで編集しているように本プログラムもそうしたいなと。

なので画像を用意し、それをドットアートに変換する方法を取りました、その際に色ごとに命令をセットする方法も選択できるようにして簡単な表示切り替えにも対応しています、カレンダーは来月になると12月と1月の表示にちゃんと切り替わるのです。


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

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

calendar-fs8.png

月表示では、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 ... 312月繰り返し ... {
...
} 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読み込みよりも遅くなる始末でした。


おわりに

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





  1. 色々思いついたら追加予定。