github.com/michalmitros/sketch

Simplified Go Ebiten wrapper made for fun


License
MIT
Install
go get github.com/michalmitros/sketch

Documentation

PkgGoDev Build Status codecov

sketch

A simplified, opinionated wrapper around Ebiten Go's 2D game engine designed for creative coding and quick visual experiments. Instead of managing the full Ebiten game loop yourself, you implement a single Sketchable interface and call Run. The library handles window setup, frame scheduling, and coordinate-system transforms so you can focus on drawing.

Sketch gives you a Screen with a high-level drawing API: lines, rectangles, circles, arcs, polygons, all with both stroked and filled version and automatic transform support (translate, rotate, scale, push/pull). Keyboard and mouse input are exposed as simple package-level functions as well as through the State passed to each frame. A vector sub-package provides 2D Cartesian and polar vector types for convenient geometry math.

Getting started

Implementing your own Sketch

Embed sketch.Sketch into a struct and override whichever methods you need:

package main

import (
	"image/color"
    "log"

	"github.com/MichalMitros/sketch"
	"github.com/MichalMitros/sketch/vector"
)

type BouncingBall struct {
    // Embedding the Sketch struct makes the BouncingBall a noop Sketchable which can be added to Run.
    // It is not required, but makes implementation easier as it provides default noop implementations for required functions.
	sketch.Sketch

	pos      vector.Vector
	velocity vector.Vector
	radius   float64
}

// Setup is called once before first Update() call.
// It's the best way of initializing the sketch if its parameters require data from the State, like screen size.
func (b *BouncingBall) Setup(state *sketch.State) error {
	b.pos = state.ScreenSize().Scale(0.5) // center of the screen, screen width / 2 and screen height / 2
	b.velocity = vector.New(3, 2)
	b.radius = 30
	return nil
}

// Update is called every frame.
func (b *BouncingBall) Update(state *sketch.State) error {
	// move the ball
	b.pos = b.pos.Add(b.velocity)

    // check if the ball is out of bounds
    w, h := state.ScreenSize().Values() // Values() returns the vector's components for easy access
	if b.pos.X-b.radius < 0 || b.pos.X+b.radius > w {
		b.velocity.X *= -1
	}
	if b.pos.Y-b.radius < 0 || b.pos.Y+b.radius > h {
		b.velocity.Y *= -1
	}

    // terminate the sketch if the escape key is pressed,
    // can be also done by passing WithTerminationKeys(sketch.KeyEscape) to Run
	if state.IsKeyPressed(sketch.KeyEscape) {
		return sketch.Termination
	}
	return nil
}

// Draw is used each frame to render it.
func (b *BouncingBall) Draw(screen *sketch.Screen) {
    // fill red circle at b.pos with radius b.radius and stroke width 2
	screen.FillCircle(b.pos, b.radius, 2, color.RGBA{255, 0, 0, 255})
}

func main() {
    // run the BouncingBall sketch with 800x600 resolution, a black background and the title "Bouncing Ball"
	if err := sketch.Run(800, 600, new(BouncingBall),
		sketch.WithWindowTitle("Bouncing Ball"),
		sketch.WithBackgroundColor(color.Black),
	); err != nil {
		log.Fatal(err)
	}
}

Options

Run accepts optional configuration via functional options:

Option Default Description
WithWindowTitle(title) "" Sets the window title
WithBackgroundColor(c) [240,240,240,255] Background color
WithResizing(enable) disabled Allows the window to be resized
WithAntyaliasing(enable) enabled Toggles anti-aliased rendering
WithRunnableOnUnfocused(enable) enabled Keeps running when unfocused
WithTerminationKeys(keys...) none Keys that terminate the sketch

Input

Keyboard

All standard keys are available as Key constants:

sketch.IsKeyPressed(sketch.KeySpace)

Mouse

Three package-level functions cover mouse input:

sketch.IsMouseButtonPressed(sketch.MouseButtonLeft)    // bool
sketch.CursorPosition()                                // vector.Vector
sketch.Scroll()                                        // (dx, dy float64)

Available button constants: MouseButtonLeft, MouseButtonRight, MouseButtonMiddle, and MouseButton0-MouseButton4.

Utility

sketch.FPS()           // current frames per second
sketch.TPS()           // current ticks per second
sketch.Fullscreen(b)   // toggle fullscreen
sketch.IsFullscreen()  // check fullscreen state
sketch.MonitorSize()   // size (as vector.Vector) of the primary monitor

State

State is passed to both Setup and Update. It carries:

state.ScreenSize()           // vector.Vector - current width and height

When resizing of the window is enabled, always read dimensions from State instead of caching them from Setup.


Screen

Screen is the drawing surface passed to Draw. Every shape method accepts screen-space coordinates and a color.Color.

Transforms

Push/pull a transformation stack to isolate coordinate changes:

screen.Push()			  // push a new transformation layer to easily isolate changes
screen.Translate(v)       // move origin
screen.Rotate(angle)      // rotate axes (radians)
screen.Scale(rate)        // uniform scale
screen.ScaleX(rate)       // scale only X
screen.ScaleY(rate)       // scale only Y
// ... draw shapes ...
screen.Pull()			  // pop the transformation layer to restore previous layer

Shapes

Stroked methods take a strokeWidth and color.Color; filled methods take only color.Color:

screen.Line(v1, v2, strokeWidth, color)
screen.Rectangle(pos, w, h, strokeWidth, color)
screen.FillRectangle(pos, w, h, color)
screen.Circle(center, radius, strokeWidth, color)
screen.FillCircle(center, radius, color)
screen.Arc(center, radius, startAngle, endAngle, strokeWidth, color)
screen.FillArc(center, radius, startAngle, endAngle, color)
screen.Shape(points, close, strokeWidth, color)
screen.FillShape(points, close, color)

Plus:

screen.Clear()            // fill with background color
screen.Fill(c)            // fill with arbitrary color
screen.At(x, y)           // sample pixel color
screen.Width() / screen.Height() / screen.Size()

Image

Image is a drawable 2D image type. Load from files, create blank canvases, or draw images onto each other with transform support.

Loading & Creating

img, err := sketch.ImageFromFile("path/to/image.png")   // GIF, JPEG, PNG
img, err := sketch.ImageFromFS(fsys, "path/to/image.png")
img := sketch.ImageFromStdImage(stdImg)
img := sketch.NewBlankImage(dim)          // transparent
img := sketch.NewFilledImage(dim, color)  // filled with color
img := sketch.NewWhiteImage(dim)          // white
img := sketch.NewBlackImage(dim)          // black

Drawing

Draw an image onto another using DrawOptions:

opts := sketch.DefaultDrawOptions()
opts.Pos = vector.New(100, 100)
opts.Anchor = vector.New(0.5, 0.5)  // center
opts.Scale = vector.New(2, 2)
opts.Rotation = math.Pi / 4
opts.Tint = color.RGBA{255, 200, 200, 255}
opts.Opacity = 0.8
img.Draw(dst, &opts)

img.DrawAt(dst, pos)  // shorthand

Plus:

img.Width() / img.Height() / img.Size()
img.At(v) / img.Set(v, c)
img.Fill(c) / img.Clear()
img.Clone()
img.SubImage(pos, size)
img.CopyTo(dst, srcPos, size, dstPos)

Drawing on Screen

screen.DrawImage(img, pos, size)

Example

img, _ := sketch.ImageFromFile("sprite.png")
opts := sketch.DefaultDrawOptions()
opts.Pos = vector.New(200, 150)
opts.Anchor = vector.New(0.5, 0.5)
opts.Scale = vector.New(2, 2)
opts.Rotation = math.Pi / 4
img.Draw(canvas, &opts)

Vector & Polar

The vector sub-package provides two types for 2D geometry.

Vector (vector.Vector) - Cartesian coordinates:

v := vector.New(x, y)
zero := vector.Zero()
x, y := v.Values()
v.Add(other)   | v.Sub(other)   | v.Mul(other)
v.Scale(s)     | v.Mag()        | v.SetMag(m)
v.Dist(other)  | v.Rotate(rad)  | v.Angle()
v.Polar()      // conversion to Polar

Polar (vector.Polar) - polar coordinates (angle in radians):

p := vector.NewPolar(radius, angle)
r, a := p.Values()
p.Rotate(rad)  | p.Add(other)  | p.Dist(other)
p.Vector()     // conversion to Cartesian