Ink

Ink is a framework for 2D graphics in Go, focused on creative coding, and based on OpenGL.

The project is still young in many ways. There are many TODOs, bugs, and ugly APIs. I will rewrite and break and hopefully improve things over time.

There's code that doesn't have examples here. Check out the docs, the sketches directory, or just browse through the code.

Usage

You'll need Go installed. These docs assume the reader is familiar with programming in Go.

Ink relies on GLFW, which is built using CGO, so you'll probably need some libraries installed:

Linux: you'll need the build-essential, xorg-dev, and libglfw3-dev pacakges

Mac: you'll need xcode installed

Windows: untested

Install ink:

go get github.com/buchanae/ink

(I'm pretty sure that should work, but if it doesn't please let me know by filing a github issue. If nothing else, you can clone the code and run go install . from the root directory.)

Use the "ink" CLI to run a sketch file. This will open a window, draw the sketch, and watch for changes.

ink example.go

Hello, Triangle

The "hello, world" of graphics: a triangle with different colored vertices.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	// The "hello, world" of graphics:
	// a triangle with different colored vertices.
	t := Triangle{
		XY{0.2, 0.2},
		XY{0.8, 0.2},
		XY{0.5, 0.8},
	}

	s := gfx.Fill{Shape: t}.Shader()
	s.Set("a_color", []RGBA{
		Red, Green, Blue,
	})
	s.Draw(doc)
}

Basic Shapes

The "dd" package holds 2D geometry types. The "gfx" package holds operations like "Fill", "Stroke", etc.

package main

import (
	"github.com/buchanae/ink/color"
	"github.com/buchanae/ink/dd"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	shapes := []dd.Fillable{

		Rect{
			XY{0.1, 0.7},
			XY{0.3, 0.9},
		},

		Circle{
			XY:     XY{0.5, 0.5},
			Radius: 0.1,
		},

		Triangle{
			XY{.7, .1},
			XY{.8, .3},
			XY{.9, .1},
		},

		Ellipse{
			XY:   XY{.2, .2},
			Size: XY{.15, .1},
		},

		Quad{
			XY{0.65, 0.7},
			XY{0.9, 0.7},
			XY{0.85, 0.95},
			XY{0.7, 0.9},
		},
	}

	for _, s := range shapes {
		gfx.Fill{s, color.Blue}.Draw(doc)
	}
}

Rotate

Rotate a shape using OpenGL.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	r := Rect{
		XY{0.2, 0.2},
		XY{0.8, 0.8},
	}

	s := gfx.Fill{Shape: r}.Shader()
	s.Set("a_pivot", r.Center())
	s.Set("a_rot", 0.4)
	s.Set("a_color", color.Red)
	s.Draw(doc)
}

Paths

Draw paths using a Pen.

package main

import (
	"github.com/buchanae/ink/color"
	"github.com/buchanae/ink/dd"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	pen := &Pen{}
	pen.MoveTo(XY{0.1, 0.4})
	pen.Line(XY{0.1, 0.2})
	pen.Line(XY{0.1, -0.2})
	pen.Line(XY{-0.25, 0.125})
	pen.Line(XY{0.3, 0.0})
	pen.Close()

	pen.MoveTo(XY{0.7, 0.5})
	pen.QuadraticTo(XY{0.9, 0.6}, XY{0.7, 0.6})
	pen.QuadraticTo(XY{0.8, 0.5}, XY{0.9, 0.5})
	pen.Close()

	shapes := []dd.Strokeable{
		pen,

		Rect{
			XY{0.1, 0.7},
			XY{0.3, 0.9},
		},

		Circle{
			XY:     XY{0.5, 0.5},
			Radius: 0.1,
		},

		Triangle{
			XY{.7, .1},
			XY{.8, .3},
			XY{.9, .1},
		},

		Ellipse{
			XY:   XY{.2, .2},
			Size: XY{.15, .1},
		},

		Quad{
			XY{0.65, 0.7},
			XY{0.9, 0.7},
			XY{0.85, 0.95},
			XY{0.7, 0.9},
		},
	}

	for _, s := range shapes {
		gfx.Stroke{
			Shape: s,
			Color: color.Red,
			Width: 0.002,
		}.Draw(doc)
	}
}

Blue Noise

Generate evenly spaced random points.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {

	r := Rect{
		A: XY{.1, .1},
		B: XY{.9, .9},
	}
	gfx.Fill{r, color.Lightgray}.Draw(doc)

	bn := rand.BlueNoise{
		Limit: 5050,
		Rect:  r,
	}
	xys := bn.Generate()

	for _, xy := range xys {
		gfx.Dot{XY: xy, Radius: 0.003}.Draw(doc)
	}
}

Grid

Grids are useful for laying out shapes in a grid pattern.

package main

import (
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {

	grid := Grid{Rows: 20, Cols: 20}
	palette := rand.Palette()

	for _, cell := range grid.Cells() {
		r := cell.Rect.Shrink(0.003)
		c := rand.Color(palette)
		gfx.Fill{r, c}.Draw(doc)
	}
}

Triangulation

Turn a set of points into triangles.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/tess"
)

func Ink(doc gfx.Doc) {

	xys := []XY{
		{0.2, 0.2},
		{0.2, 0.6},
		{0.4, 0.7},
		{0.9, 0.7},
		{0.3, 0.5},
		{0.5, 0.4},
		{0.4, 0.3},
	}

	tris := tess.Tesselate(xys)
	m := Triangles(tris)
	gfx.Fill{Shape: m, Color: Black}.Draw(doc)

	for _, xy := range xys {
		d := gfx.Dot{XY: xy, Color: Red, Radius: 0.005}
		d.Draw(doc)
	}
}

Gradient

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	gfx.Gradient{
		Rect: Rect{
			XY{0.2, 0.2},
			XY{0.8, 0.8},
		},
		A: Blue,
		B: Red,
	}.Draw(doc)
}

Blur

Gaussian blur.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	gfx.Fill{
		Shape: Rect{
			XY{0.2, 0.2},
			XY{0.8, 0.8},
		},
		Color: Blue,
	}.Draw(doc)

	gfx.Blur{
		Passes: 2,
		Source: doc,
	}.Draw(doc)
}

Noise

Generate perlin (simplex?) noise using OpenGL.

package main

import (
	"github.com/buchanae/ink/color"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {
	n := gfx.DefaultNoise
	n.Size = 30
	n.Color = color.Red
	n.Draw(doc)
}

Tweak

Tweak the vertices of a mesh, to give it some character.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {

	c := Circle{
		XY:       XY{0.5, 0.5},
		Radius:   0.3,
		Segments: 40,
	}
	m := c.Fill()
	m = rand.TweakMesh(m, 0.03)

	gfx.Fill{m, color.Red}.Draw(doc)
}

Image

Display an image (currently only PNG?)

package main

import "github.com/buchanae/ink/gfx"

func Ink(doc gfx.Doc) {
	img := doc.LoadImage("toshiro.png")
	img.Draw(doc)
}

Opacity

Testing opacity and blending (a tricky thing to get right, so it's probably wrong...)

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	fills := []gfx.Fill{
		{
			Rect{XY{0.3, 0.3}, XY{0.6, 0.6}},
			RGBA{1, 0, 0, 0.5},
		},
		{
			Rect{XY{0.4, 0.4}, XY{0.7, 0.7}},
			RGBA{1, 0, 0, 0.5},
		},

		{
			Rect{XY{0.4, 0.4}, XY{0.5, 0.5}},
			RGBA{0, 0, 0, 0},
		},
		{
			Rect{XY{0.2, 0.2}, XY{0.4, 0.4}},
			RGBA{0, 1, 0, 1},
		},
		{
			Rect{XY{0.2, 0.4}, XY{0.4, 0.6}},
			RGBA{0, 0, 1, 0.5},
		},
		{
			Rect{XY{0.1, 0.4}, XY{0.2, 0.6}},
			RGBA{1, 0, 0, 1},
		},
		{
			Rect{XY{0.6, 0.4}, XY{0.8, 0.6}},
			RGBA{1, 1, 0, 0.5},
		},
	}

	for _, f := range fills {
		f.Draw(doc)
	}
}

Instancing

Generate thousands of instances of the same shape efficiently using OpenGL.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	const N = 100000

	pos := make([]XY, N)
	rot := make([]float32, N)
	colors := make([]RGBA, N)
	palette := rand.Palette()

	for i := 0; i < N; i++ {
		pos[i] = rand.XYRange(0.1, 0.9)
		rot[i] = rand.Angle()
		colors[i] = rand.Color(palette)
	}

	doc.AddShader(&gfx.Shader{
		Vert:      gfx.DefaultVert,
		Frag:      gfx.DefaultFrag,
		Instances: N,
		Mesh:      RectWH(0.05, 0.05).Fill(),
		Attrs: gfx.Attrs{
			"a_pos":   pos,
			"a_rot":   rot,
			"a_color": colors,
		},
		Divisors: map[string]int{
			"a_pos":   1,
			"a_rot":   1,
			"a_color": 1,
		},
	})
}

Hex Color

Convert hex to a color.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {

	t := Triangle{
		XY{0.2, 0.2},
		XY{0.8, 0.2},
		XY{0.5, 0.8},
	}

	red := color.Hex(0xff0000)
	green := color.Hex(0x00ff00)
	blue := color.HexString("#0000ff")

	s := gfx.NewShader(t.Fill())
	s.Set("a_color", []color.RGBA{
		red, green, blue,
	})
	s.Draw(doc)
}

Stateful Context

Experimenting with adding a stateful "context" drawing API, since people are very familiar with this pattern and it can save some verbose lines of code.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
)

func Ink(doc gfx.Doc) {
	ctx := gfx.NewContext(doc)

	ctx.Clear(color.White)

	e := Ellipse{
		XY:       XY{.5, .5},
		Size:     XY{.3, .2},
		Segments: 100,
	}

	ctx.FillColor = color.Blue
	ctx.Fill(e)

	ctx.StrokeWidth = 0.005
	ctx.Stroke(e)
}

Circle Pack

Currently, a failed experiement with finding an efficient circle packing algorithm. Maybe some day I'll crack it.

package main

import (
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
	"github.com/buchanae/ink/voronoi"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	box := Rect{
		A: XY{.1, .1},
		B: XY{.9, .9},
	}
	bn := rand.BlueNoise{
		Limit:   5000,
		Rect:    box,
		Spacing: 0.02,
	}

	var xys []XY
	for _, xy := range bn.Generate() {
		if rand.Bool(0.3) {
			continue
		}
		xys = append(xys, xy)
	}

	for _, xy := range xys {
		gfx.Dot{XY: xy}.Draw(doc)
	}

	colors := rand.Palette()

	v := voronoi.New(xys, box)
	for _, cell := range v.Cells() {
		c := rand.Color(colors)
		c.A = 0.3

		for _, tri := range cell.Tris {
			gfx.Fill{tri, c}.Draw(doc)
		}

		for _, e := range cell.Edges {
			gfx.Stroke{
				Shape: e,
				Width: 0.002,
			}.Draw(doc)
		}
	}
}

Combos

Generate all 3x3 combos.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	center := XY{0.5, 0.5}
	grid := Grid{
		Rows: 32,
		Cols: 16,
		Rect: RectCenter(center, XY{.5, .97}),
	}
	sub := Grid{Rows: 3, Cols: 3}

	var bold []Strokeable
	var strokes []Strokeable

	for i, cell := range grid.Cells() {
		r := cell.Rect.Shrink(0.003)
		bold = append(bold, r)

		for j, sc := range sub.Cells() {
			sr := sc.Rect

			xr := Rect{
				A: r.Interpolate(sr.A),
				B: r.Interpolate(sr.B),
			}

			strokes = append(strokes, xr)

			// TODO interleaving a stroke
			//      causes all the batching to fail
			// TODO move these things to an examples
			//      folder demonstrating performance
			//      issues
			//doc.Shader(stk)

			mask := 1 << j
			if i&mask == mask {
				gfx.Fill{xr, color.Black}.Draw(doc)
			}
		}
	}

	for _, stk := range strokes {
		gfx.Stroke{
			Shape: stk,
			Width: 0.0002,
			Color: color.Black,
		}.Draw(doc)
	}

	for _, stk := range bold {
		gfx.Stroke{
			Shape: stk,
			Width: 0.0009,
			Color: color.Black,
		}.Draw(doc)
	}
}

Coqart Grid

Replica of a piece by Roger Coqart.

package main

import (
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	ctx := gfx.NewContext(doc)

	center := XY{0.5, 0.5}
	grid := Grid{
		Rows: 25, Cols: 25,
		Rect: SquareCenter(center, 0.95),
	}

	lines := []Line{
		{XY{0, 0}, XY{1, 1}},
		{XY{0, 1}, XY{1, 0}},
		{XY{0, 0.5}, XY{1, 0.5}},
		{XY{0.5, 0}, XY{0.5, 1}},
		{XY{0, 0.5}, XY{0.5, 1}},
		{XY{0, 0.5}, XY{0.5, 0}},
		{XY{0.5, 0}, XY{1, 0.5}},
		{XY{0.5, 1}, XY{1, 0.5}},
	}

	for _, cell := range grid.Cells() {
		r := cell.Rect
		r = r.Shrink(0.005)

		p := float32(cell.Row) / float32(grid.Rows)
		n := int(math.Interp(1, 15, p))
		i := 0
		for i < n {
			l := lines[rand.Intn(len(lines))]
			c := r.Interpolate(l.A)
			d := r.Interpolate(l.B)

			ctx.Stroke(Line{c, d})

			i++
		}

		ctx.Stroke(r)
	}
}

Grayblocks

Just for fun.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
	"github.com/buchanae/ink/voronoi"
)

const (
	N       = 10
	Padding = 0.01
	Margin  = Padding * 2
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	for i := float32(0); i < N; i++ {
		split := rand.Range(.3, .7)
		bot := i/N + (Padding / 2)
		top := (i+1)/N - (Padding / 2)

		p := i / N
		c := RGBA{p, p, p, 1}

		// left
		ra := Rect{
			A: XY{Margin, bot},
			B: XY{split - (Padding / 2), top},
		}
		// right
		rb := Rect{
			A: XY{split + (Padding / 2), bot},
			B: XY{1 - Margin, top},
		}

		ca := VoronoiCells{
			Rect:    ra,
			Spacing: math.Interp(0.003, 0.03, i/N),
		}

		gfx.Fill{
			Shape: ca.Mesh(),
			Color: c,
		}.Draw(doc)

		gfx.Fill{
			Shape: rb,
			Color: c,
		}.Draw(doc)
	}
}

type VoronoiCells struct {
	Rect
	Spacing float32
}

func (vc VoronoiCells) Mesh() Mesh {
	bn := rand.BlueNoise{
		Rect:    vc.Rect,
		Spacing: vc.Spacing,
	}
	noise := bn.Generate()
	v := voronoi.New(noise, vc.Rect)

	var meshes []Mesh
	for _, e := range v.Edges() {
		meshes = append(meshes, e.Stroke(StrokeOpt{}))
	}
	return Merge(meshes...)
}

Hex Grid

Generating hexagons in a grid.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

const (
	Size = 10
	Z    = 0.015
)

func Ink(doc gfx.Doc) {
	palette := rand.Palette()
	grid := HexGrid{Size}
	cells := grid.Cells()

	for _, cell := range cells {
		col := rand.Color(palette)
		gfx.Fill{cell, col}.Draw(doc)
	}

	for _, cell := range cells {
		gfx.Stroke{
			Shape: cell,
			Color: Black,
			Width: 0.004,
		}.Draw(doc)
	}
}

Hobbs Split

Inspired by an essay from Tyler Hobbs.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	a := Triangle{
		B: XY{1, 0},
		C: XY{0, 1},
	}

	b := Triangle{
		A: XY{1, 1},
		B: XY{1, 0},
		C: XY{0, 1},
	}

	tris := recursive(8, a, b)
	p := rand.Palette()

	layer := doc.NewLayer()

	for _, t := range tris {
		gfx.Fill{t, rand.Color(p)}.Draw(layer)
	}

	for _, t := range tris {
		gfx.Stroke{
			Shape: t,
			Width: 0.0005,
			Color: color.Black,
		}.Draw(layer)
	}

	cir := Circle{
		XY:       XY{0.5, 0.5},
		Radius:   0.4,
		Segments: 100,
	}
	gfx.Cut{
		Shape:  cir.Fill(),
		Source: layer,
	}.Draw(doc)

	gfx.Stroke{
		Shape: cir,
		Width: 0.005,
		Color: color.Black,
	}.Draw(doc)

}

func recursive(depth int, tris ...Triangle) []Triangle {
	if depth == 0 {
		return nil
	}
	var out []Triangle
	for _, t := range tris {
		a, b := split(t)
		out = append(out, a, b)
		out = append(out, recursive(depth-1, a, b)...)
	}
	return out
}

func split(t Triangle) (Triangle, Triangle) {

	edges := t.Edges()
	lens := [3]float32{
		edges[0].SquaredLength(),
		edges[1].SquaredLength(),
		edges[2].SquaredLength(),
	}

	do := func(long, a, b Line) (Triangle, Triangle) {
		mid := long.Interpolate(rand.Range(0.3, 0.7))
		return Triangle{
				mid, a.A, a.B,
			}, Triangle{
				mid, b.A, b.B,
			}
	}

	switch {
	case lens[0] >= lens[1] && lens[0] >= lens[2]:
		return do(edges[0], edges[1], edges[2])
	case lens[1] >= lens[0] && lens[1] >= lens[2]:
		return do(edges[1], edges[0], edges[2])
	default:
		return do(edges[2], edges[0], edges[1])
	}
}

Molnar2

Inspired by Vera Molnar.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {

	grid := Grid{
		Rows: 15,
		Cols: 15,
		Rect: SquareCenter(gfx.Center, .8),
	}

	for _, cell := range grid.Cells() {
		const Z = 0.007
		const G = 0.005

		r := cell.Rect.Translate(XY{
			Y: rand.Range(-Z, Z),
		})
		grow := rand.Range(0, G)
		r.A.X -= grow
		r.B.X += grow

		col := Red
		col.A = 0.7
		gfx.Fill{r, col}.Draw(doc)
	}
}

Mosaic Sun

Trying to figure out mosaic styling.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
)

const (
	Gap   = 0.001
	Space = 0.0045
	Count = 25

	Start = 0.01
	Width = (0.5 - Start) / Count

	MinChord = 0.005
	MaxChord = 0.008

	JumpChance    = 0.2
	LightenChance = 0.4
	LightenAmt    = 0.2
	TweakChance   = 0.3
	TweakAmt      = -0.001
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	A := color.HexString("#ebc334")
	B := color.HexString("#0c79e8")

	gfx.Fill{
		Shape: Circle{
			XY:       XY{.5, .5},
			Radius:   Start - Gap,
			Segments: 5,
		},
		Color: A,
	}.Draw(doc)

	for i := float32(0); i < Count; i++ {

		rings := Rings{
			Offset: rand.Range(0, 3),
			Inner:  Start + i*Width,
			Outer:  Start + (i+1)*Width - Space,
			Gap:    Gap,
		}

		var min float32 = MinChord
		var max float32 = MaxChord

		min += i * 0.001
		max += i * 0.003
		max = math.Min(max, 0.05)

		chords := GenChords(rings.Inner, min, max)
		// TODO interpcolor isn't based on visual interpolation
		//      going form orange to blue goes through green
		col := color.Interpolate(A, B, i/Count)

		for _, in := range chords {
			rx := rings
			rx.From = in.From
			rx.To = in.To
			rx.Gap += rand.Range(0, 0.002)
			rx.Color = col

			if rand.Bool(JumpChance) {
				rx.Color = color.Interpolate(A, B,
					rand.Range(0, Count)/Count,
				)
			}

			if rand.Bool(LightenChance) {
				amt := rand.Range(-LightenAmt, LightenAmt)
				rx.Color = rx.Color.Lighten(amt)
			}
			rx.Draw(doc)
		}
	}
}

type Rings struct {
	Inner, Outer float32
	From, To     float32
	Offset       float32
	Gap          float32
	Color        color.RGBA
}

func (r Rings) Draw(doc gfx.Layer) {
	center := XY{.5, .5}
	inner := Circle{
		XY:     center,
		Radius: r.Inner,
	}
	outer := Circle{
		XY:     center,
		Radius: r.Outer,
	}
	from := r.Offset + r.From
	to := r.Offset + r.To
	innerGap := ChordAngle(r.Inner, r.Gap)
	outerGap := ChordAngle(r.Outer, r.Gap)
	quad := Quad{
		inner.XYFromAngle(from + innerGap),
		inner.XYFromAngle(to - innerGap),
		outer.XYFromAngle(to - outerGap),
		outer.XYFromAngle(from + outerGap),
	}
	if rand.Bool(TweakChance) {
		quad = rand.TweakQuad(quad, TweakAmt)
	}
	fill := gfx.Fill{quad, r.Color}
	fill.Draw(doc)
}

type Chord struct {
	From, To float32
}

func GenChords(radius, min, max float32) []Chord {

	var out []Chord

	p := float32(0)
	for {
		length := rand.Range(min, max)
		// Protect against NaN
		// from Asin in ChordAngle
		if length >= radius {
			length = radius
		}
		ang := ChordAngle(radius, length)
		next := p + ang

		if next >= math.Pi*2 {
			out = append(out, Chord{
				From: p,
			})
			break
		}

		out = append(out, Chord{
			From: p,
			To:   next,
		})
		p = next
	}

	return out
}

func ChordAngle(radius, length float32) float32 {
	return 2 * math.Asin(length/(2*radius))
}

Nees

Inspired by George Nees.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
)

const (
	Width  = 16
	Height = 30
)

func Ink(doc gfx.Doc) {

	center := XY{.5, .5}
	grid := Grid{
		Rows: Height,
		Cols: Width,
		Rect: RectCenter(center, XY{.5, .9}),
	}

	for i, cell := range grid.Cells() {
		r := cell.Rect
		r = r.Shrink(0.002)

		row := i / Width
		row = Height - row - 5
		dr := float32(row) / Height
		dr = math.Clamp(dr, 0, 1)

		t := dr * 0.01
		r = r.Translate(rand.XYRange(-t, t))

		q := r.Quad()
		ang := rand.Range(-dr, dr)
		q = q.RotateAround(ang, r.Center())

		gfx.Stroke{
			Shape: q,
			Width: 0.001,
			Color: Black,
		}.Draw(doc)
	}
}

Noise 1D

1D noise line.

package main

import (
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

const (
	N       = 1000
	Scale   = 1.5
	Octaves = 40
	Shift   = 0.55
)

func Ink(doc gfx.Doc) {

	for i := 0; i < N; i++ {
		x := float32(i) / N

		h := octaves(x, 6)
		h -= 0.7
		h *= .2

		xy := XY{x, 0.5 + h}
		c := Circle{xy, 0.001, 10}
		s := gfx.NewShader(c.Fill())
		s.Draw(doc)
	}
}

func octaves(x float32, N int) float32 {
	var n float32
	var z float32 = 10
	var amp float32 = 1

	for j := 0; j < N; j++ {
		n += rand.Noise1(x*z) * amp
		amp *= 0.5
		z = z * 2
	}
	return n
}

Rotline

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	gfx.Clear(doc, White)

	grid := Grid{
		Rows: 40,
		Cols: 40,
		Rect: RectCenter(XY{.5, .5}, XY{.95, .95}),
	}
	col := Blue
	col.A = 0.7

	for _, cell := range grid.Cells() {
		r := cell.Rect.Shrink(0.001)
		size := r.Size()
		center := r.Center()
		a := XY{
			X: r.A.X + size.X/2,
			Y: r.A.Y,
		}
		b := XY{
			X: a.X,
			Y: r.B.Y,
		}
		rot := rand.Angle()

		gfx.Stroke{
			Shape: Line{
				a.RotateAround(rot, center),
				b.RotateAround(rot, center),
			},
			Color: col,
			Width: 0.0025,
		}.Draw(doc)
	}
}

Sand Spline

Inspired by Anders Hoff and Jared Tarbell. Not working the way I want yet. Splines need love.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

const (
	Lines  = 17
	N      = 30
	Passes = 20
	MinY   = 0.005
	MaxY   = 0.04

	MinX = -0.02
	MaxX = 0.02

	LineWidth = 0.003
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	redDot := gfx.Dot{Color: Red, Radius: 0.003}

	for k := 0; k < Lines; k++ {
		y := 0.1 + float32(k)*0.05

		for j := 0; j < Passes; j++ {
			var curves Path

			pt := XY{0.05, y}
			inc := XY{0.90 / N, 0}
			var ctrl XY
			var offset float32

			for i := 0; i < N; i++ {

				if i%2 == 0 {
					offset = rand.Range(MinX, MaxX)

					ctrl = pt.Add(XY{
						X: inc.X*0.5 + offset,
						Y: rand.Range(MinY, MaxY) * (float32(Lines-k-1) / Lines),
					})
				} else {
					ctrl = pt.Add(XY{
						X: inc.X*0.5 - offset,
						Y: pt.Y - ctrl.Y,
					})
				}

				curves = append(curves, Quadratic{
					A:    pt,
					B:    pt.Add(inc),
					Ctrl: ctrl,
				})

				rd := redDot
				rd.XY = ctrl
				//rd.Draw(doc)
				/*
					TODO want. go proposal
					redDot{
						XY: ctrl,
					}.Draw(doc)
				*/

				pt = pt.Add(inc)
			}

			c := Teal
			c.A = 0.3

			gfx.Stroke{
				Shape: curves,
				Width: LineWidth,
				Color: c,
			}.Draw(doc)
		}
	}
}

Sand Stroke

Inspired by Jared Tarbell.

package main

import (
	"log"
	"time"

	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
)

const (
	Lines   = 20
	Strokes = 20
	N       = 1000
	// percentage of W
	// only works for small N
	Padding = 0.00
	W       = (1 / float32(N)) * (1 - Padding)

	// min/max y-axis starting position
	MinY = 0.1
	MaxY = 0.9

	D    = 0.004
	B    = 0.3
	MaxD = 0.2
	M    = 0.4
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()
	palette := rand.Palette()

	start := time.Now()
	ys := make([]float32, Lines)
	for i := range ys {
		ys[i] = rand.Range(MinY, MaxY)
	}

	for j := 0; j < Strokes; j++ {

		y := ys[rand.Intn(len(ys))]
		dy := rand.Range(0.01, 0.1)
		color := rand.Color(palette)

		for i := 0; i < N; i++ {
			x := float32(i) / N

			dy += rand.Range(-D, D)
			dy = math.Clamp(dy, 0, MaxD)

			xy := XY{x, y}
			wh := XY{W, dy}
			r := RectCenter(xy, wh)

			s := gfx.NewShader(r.Fill())
			sc := color
			sc.A = 1 - dy/B - M
			s.Set("a_color", sc)
			s.Draw(doc)
		}
	}

	log.Printf("run time: %s", time.Since(start))
}

Fast Sand Stroke

Fast version of Sand Stroke, because it relies more on OpenGL/GPU.

package main

import (
	"image"
	colorlib "image/color"

	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/math"
	"github.com/buchanae/ink/rand"
)

const (
	Lines   = 3
	Strokes = 10
	N       = 300
	W       = 1 / float32(N)

	Amp = 1000

	// min/max y-axis starting position
	MinY = 0.1
	MaxY = 0.9

	D           = 0.0040
	MaxD        = 0.2
	AlphaOffset = -0.3
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()
	palette := rand.Palette()

	lines := make([]float32, Lines)
	for i := range lines {
		lines[i] = rand.Range(MinY, MaxY)
	}

	for j := 0; j < Strokes; j++ {

		y := lines[rand.Intn(len(lines))]

		heights := make([]float32, N)
		h := rand.Range(0.01, 0.1)
		for i := range heights {
			h += rand.Range(-D, D)
			h = math.Clamp(h, 0, MaxD)
			heights[i] = h
		}

		img := makeHeightMap(heights)
		heightmap := doc.NewImage(img)

		s := &gfx.Shader{
			Name: "Stroke",
			Vert: gfx.DefaultVert,
			Frag: Frag,
			Mesh: Rect{
				XY{0, y - MaxD},
				XY{1, y + MaxD},
			}.Fill(),
			Attrs: gfx.Attrs{
				"u_heightmap":    heightmap,
				"u_color":        rand.Color(palette),
				"u_alpha_offset": float32(AlphaOffset),
			},
		}
		s.Draw(doc)
	}
}

const Frag = `
#version 330 core

uniform vec4 u_color;
uniform sampler2D u_heightmap;
uniform float u_alpha_offset;

in vec2 v_uv;
out vec4 color;

void main() {
	float h = texture(u_heightmap, v_uv).r;
	float d = abs(v_uv.y - 0.5);
	float a = step(d, h) * (1-h*2) + u_alpha_offset;
	color = vec4(u_color.rgb, a);
}
`

// TODO want to easily create a texture without
//      involving the "image" library
func makeHeightMap(heights []float32) *image.Gray {
	r := image.Rect(0, 0, len(heights), 1)
	img := image.NewGray(r)

	for i, h := range heights {
		img.SetGray(i, 0, colorlib.Gray{
			Y: uint8(h * Amp),
		})
	}
	return img
}

Squiggle Grid

Inspired by George Nees.

package main

import (
	"github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	grid := Grid{Rows: 10, Cols: 10}

	for _, cell := range grid.Cells() {
		r := cell.Rect
		bnd := r.Shrink(0.013)

		current := bnd.Interpolate(rand.XYRange(0.1, 0.9))
		pen := &Pen{}
		i := 0
		horizontal := false

		pen.MoveTo(current)

		for i < 20 {
			var add XY

			if horizontal {
				add.X = rand.Range(-0.2, 0.2)
			} else {
				add.Y = rand.Range(-0.2, 0.2)
			}

			next := current.Add(add)
			if !bnd.Contains(next) {
				continue
			}

			pen.LineTo(next)
			current = next
			horizontal = !horizontal
			i++
		}

		pen.Close()

		gfx.Stroke{
			Shape: pen,
			Width: 0.001,
			Color: color.Black,
		}.Draw(doc)
	}
}

Tribox2

Just for fun.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

const (
	ShrinkRect   = 0.003
	RandPoint    = 0.03
	CircleRadius = 0.05
	ShouldStroke = false
	StrokeWidth  = 0.001
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	grid := Grid{Rows: 10, Cols: 10}
	colors := rand.Palette()

	l2 := doc.NewLayer()
	mask := doc.NewLayer()

	for _, cell := range grid.Cells() {
		r := cell.Rect.Shrink(ShrinkRect)
		q := r.Quad()
		p := rand.XYInRect(r.Shrink(RandPoint))

		tris := Triangles{
			{q.A, q.B, p},
			{q.B, q.C, p},
			{q.C, q.D, p},
			{q.D, q.A, p},
		}

		for _, t := range tris {
			s := gfx.NewShader(t.Fill())
			s.Set("a_color", rand.Color(colors))
			s.Draw(l2)
		}

		if ShouldStroke {
			gfx.Stroke{
				Shape: tris,
				Width: StrokeWidth,
				Color: White,
			}.Draw(l2)
		}

		gfx.Fill{
			Shape: Circle{
				XY:       r.Center(),
				Radius:   CircleRadius,
				Segments: 40,
			},
			Color: Black,
		}.Draw(mask)
	}

	gfx.Mask{
		Rect:   gfx.Fullscreen,
		Source: l2,
		Mask:   mask,
	}.Draw(doc)
}

Tribox3

Just for fun.

package main

import (
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
)

const (
	ShrinkRect = 0.019
	RandPoint  = 0.03
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	grid := Grid{Rows: 10, Cols: 10}
	colors := rand.Palette()

	for _, cell := range grid.Cells() {
		r := cell.Rect.Shrink(ShrinkRect)
		q := r.Quad()
		p := rand.XYInRect(r.Shrink(RandPoint))
		p = r.Center()

		//a := q.A.Add(XY{0.009, 0})
		q = rand.TweakQuad(q, 0.005)

		tris := Triangles{
			{q.A, q.B, p},
			{q.B, q.C, p},
			{q.C, q.D, p},
			{q.D, q.A, p},
		}

		for _, t := range tris {
			gfx.Fill{t, rand.Color(colors)}.Draw(doc)
		}
	}
}

Voronoi

Generating a voronoi mesh.

package main

import (
	. "github.com/buchanae/ink/color"
	. "github.com/buchanae/ink/dd"
	"github.com/buchanae/ink/gfx"
	"github.com/buchanae/ink/rand"
	"github.com/buchanae/ink/voronoi"
)

func Ink(doc gfx.Doc) {
	rand.SeedNow()

	box := Rect{
		A: XY{.1, .1},
		B: XY{.9, .9},
	}

	var initial []XY

	for i := 0; i < 20; i++ {
		p := float32(i) / 20
		xy := box.Interpolate(XY{p, p})
		initial = append(initial, xy)
	}

	noise := rand.BlueNoise{
		Limit:   450,
		Spacing: 0.05,
		Initial: initial,
		Rect:    box,
	}.Generate()

	v := voronoi.New(noise, box)

	colors := []RGBA{
		Blue, Yellow, Green, Black, Purple,
	}

	tris := v.Triangulate()

	for _, t := range tris {
		c := rand.Color(colors)
		c.A = 0.3
		gfx.Fill{
			Shape: t,
			Color: c,
		}.Draw(doc)
	}

	gfx.Stroke{
		Shape: Triangles(tris),
		Width: 0.001,
		Color: Black,
	}.Draw(doc)
}

There's more that doesn't have examples here. Check out the docs, the sketches directory, or just browse through the code.