Skip to content

キャンバスとキーボード

仮想的な画素の格子と、知らせを待つのではなくこちらから尋ねるキーボード。

キャンバス

キャンバスは仮想的な画素の格子で、命令を一つずつ実行して塗っていきます。 格子を用意するのは Canvas(width, height) です。 Scale は、仮想的な画素一つを論理画素いくつ分で描くかを決めます。 64x40 のキャンバスに 6 を与えると、画面では 384x240 になります。 色に名前を付けるのは Palette で、Paint には描画命令を持つ *Painter が渡されます。

package main

import (
    "fmt"

    . "github.com/i2y/yokan/gomamochi"
)

var palette = []string{"#11111b", "#89b4fa", "#f38ba8", "#eeeeee", "#a6e3a1"}

type Sky struct {
    frame int
    x, dx int
}

func (s *Sky) tick() {
    s.frame += 1
    // The keyboard is read here, in the tick, never in a view.
    if KeyDown("left") {
        s.dx = -1
    }
    if KeyDown("right") {
        s.dx = 1
    }
    s.x += s.dx
    if s.x < 4 || s.x > 59 {
        s.dx = -s.dx
        s.x += s.dx
    }
}

func (s *Sky) View() Element {
    return Column(
        Canvas(64, 40).Scale(6).Background(0).Palette(palette...).Paint(func(p *Painter) {
            p.Rect(2, 2, 12, 6, 1)
            p.CircleOutline(34, 5, 4, 3)
            p.Line(2, 11, 61, 11, 2)
            p.Circle(s.x, 24, 3, 3)
            p.PixelText(2, 14, fmt.Sprintf("FRAME %d", s.frame), 3)
        }),
    ).Spacing(12).Padding(16)
}

func main() {
    app := &Sky{x: 30, dx: 1}
    Every(0.05, func() { app.tick() })
    Run(app, Title("canvas"))
}

色はどれも番号です。 パレットの何番目か、という意味の番号です。 ドット絵の道具はどれもこの作りなので、そうした道具のために書かれた描画のコードは、数字を書き換えずにそのまま移せます。 *Painter の命令は PixelLineRectRectOutlineCircleCircleOutlineTriangleTriangleOutlineSpritePixelText です。 引数はどれも仮想的な画素を表す整数で、省略できるものはありません。 Sprite は、画像、切り出す矩形、カラーキー(-1 ならすべての画素を写します)、二つの反転を、この順に取ります。 Paint のクロージャの中の繰り返しは、ただの繰り返しです。 その本体が塗ったものは、書いた場所でそのまま絵に加わります。

命令は要素ではありません。 塗ったものは、押すことも、テーマを変えることも、大きさを与えることも、動かすこともできません。 命令が意味を持つのは、それを書いたキャンバスの中だけです。 一コマは、ウィンドウを開かなくても取り出せます。 PIXIE_FRAMES=<dir> を与えると、スクリプトの一手ごとにキャンバスを PNG に書き出します。 絵を描くのは、ウィンドウが使うのと同じラスタライザです。 ギャラリーにある二つのゲームの録画も、こうして撮りました。

キーボード

問いは二つあり、答え方もそれぞれ違います。

「今どのキーが押されているか」は、コマを描くアプリのための問いです。 KeyDown("left") はキーが押されているあいだ真で、KeyPressed は押された瞬間に一度だけ、KeyReleased は離された瞬間に一度だけ真になります。 渡す名前はキー一つだけで、cmd+s のような組み合わせは書けません。 これらを読むのはタイマーの中だけで、ビューの中では読みません。 ビューでキーボードを読むと、ウィンドウとスクリプトで違う画面を描いてしまいます。 そうなると、ゲートは違う二つのアプリを比べることになります。

「今どのキーが押されたか」は組み合わせで、アプリが動き出す前に宣言しておいたハンドラに届きます。

package main

import (
    "fmt"

    . "github.com/i2y/yokan/gomamochi"
)

type Keys struct {
    count, saved int
    last         string
}

func (k *Keys) View() Element {
    return Column(
        Text(fmt.Sprintf("count: %d  saved: %d  last key: %s", k.count, k.saved, k.last)),
        Button("+1").OnClick(func() { k.count += 1 }),
    ).Spacing(8).Padding(12)
}

func main() {
    app := &Keys{last: "-"}
    MenuItem("Count", "Save", func() { app.saved = app.count })
    MenuItem("Count", "Clear", func() { app.count, app.saved = 0, 0 })
    Shortcut("cmd+s", func() { app.saved = app.count })
    OnKey(func(chord string) { app.last = chord })
    Run(app, Title("keys"))
}

Shortcut には、プラットフォームのつづりのままの組み合わせ(cmd+scmd+shift+r)を渡します。 MenuItem は同じハンドラを、宣言した順にアプリケーションのメニューバーに並べます。 OnKey にはどのキーも、押されたときの組み合わせのまま届きます。 OnFileDrop には、ウィンドウに落とされたファイルのパスが届きます。 スクリプトでは、key:cmd+s で押し、menu:Clear で選び、drop:<path> でファイルを落とします。