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.