Skip to content

The canvas and the keyboard

A grid of virtual pixels, and keys read as a device rather than waited for.

The canvas

A canvas is a grid of virtual pixels, painted command by command. Canvas(width, height) opens the grid; Scale says how many logical pixels one virtual pixel takes, so a 64x40 canvas at six is 384x240 on screen; Palette names the colors; and Paint is handed a painter with the commands.

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"))
}

Every color is a number: the index of a color in the palette. That is how tools for pixel art work, so drawing code written for one moves here with its numbers unchanged. The commands on the painter are Pixel, Line, Rect, RectOutline, Circle, CircleOutline, Triangle, TriangleOutline, Sprite and PixelText; every argument is a whole virtual pixel, and every argument is written — Sprite takes the sheet, the rectangle to cut, a color key (-1 copies every pixel) and two flips, in that order. A loop inside the paint closure is the ordinary loop: what its body paints joins the frame where it stands.

The commands are not elements. Nothing painted can be clicked, themed, sized or animated, and a command means nothing outside the canvas it was written in. A frame can be had without a window: PIXIE_FRAMES=<dir> writes a PNG of the canvas after every step of a script, drawn by the same rasterizer the window uses, which is how the two games' recordings in the gallery were made.

The keyboard

Two different questions, answered two different ways.

What is being held right now is for an app that draws frames. KeyDown("left") is true while the key is held, KeyPressed once when it goes down, KeyReleased once when it comes up; the name is the key alone, never a chord. Read them in a timer, never in a view — a view that read the keyboard would draw one thing in a window and another under a script, and the gate would be comparing two different apps.

What was just pressed is a chord, and reaches the app as a handler declared before it runs:

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 takes a chord spelled the way the platform spells it (cmd+s, cmd+shift+r), MenuItem puts the same handler in the application's menu bar in the order declared, OnKey is told every key as the chord it was, and OnFileDrop is told the path of a file dragged onto the window. A script presses one with key:cmd+s, picks one with menu:Clear, and drops a file with drop:<path>.