This is a quick and dirty introduction to Ink: a framework for 2D graphics in Go, focused on creative coding, and based on OpenGL.
Disclaimer: Although I've been working towards Ink for years, the project is still very 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 more that doesn't have examples here. Check out the docs, the sketches directory, or just browse through the code.
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
The "hello, world" of graphics: a triangle with different colored vertices.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.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.NewShader(t) s.Set("a_color", []RGBA{ Red, Green, Blue, }) s.Draw(doc) }
The "dd" package holds 2D geometry types. The "gfx" package holds operations like "Fill", "Stroke", etc.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { shapes := []gfx.Meshable{ 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 a shape using OpenGL.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { r := Rect{ XY{0.2, 0.2}, XY{0.8, 0.8}, } s := gfx.NewShader(r) s.Set("a_pivot", r.Center()) s.Set("a_rot", 0.4) s.Set("a_color", color.Red) s.Draw(doc) }
Draw paths using a Pen.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.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 := []gfx.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{ Target: s, Color: color.Red, Width: 0.002, }.Draw(doc) } }
Generate evenly spaced random points.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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) } }
Grids are useful for laying out shapes in a grid pattern.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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) } }
Turn a set of points into triangles.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/tess" ) func Ink(doc *app.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.NewShader(m).Draw(doc) for _, xy := range xys { d := gfx.Dot{XY: xy, Color: Red, Radius: 0.005} d.Draw(doc) } }
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { gfx.Gradient{ Rect: Rect{ XY{0.2, 0.2}, XY{0.8, 0.8}, }, A: Blue, B: Red, }.Draw(doc) }
Gaussian blur.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { gfx.Fill{ Mesh: Rect{ XY{0.2, 0.2}, XY{0.8, 0.8}, }, Color: Blue, }.Draw(doc) gfx.Blur{ Passes: 2, Source: doc, }.Draw(doc) }
Generate perlin (simplex?) noise using OpenGL.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { n := gfx.DefaultNoise n.Size = 30 n.Color = color.Red n.Draw(doc) }
Tweak the vertices of a mesh, to give it some character.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.Doc) { c := Circle{ XY: XY{0.5, 0.5}, Radius: 0.3, Segments: 40, } m := c.Mesh() m = rand.TweakMesh(m, 0.03) gfx.Fill{m, color.Red}.Draw(doc) }
Display an image (currently only PNG?)
package main import ( "github.com/buchanae/ink/app" ) func Ink(doc *app.Doc) { img := doc.LoadImage("toshiro.png") img.Draw(doc) }
Testing opacity and blending (a tricky thing to get right, so it's probably wrong...)
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.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) } }
Generate thousands of instances of the same shape efficiently using OpenGL.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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), 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, }, }) }
Change the window size from the sketch code.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.Doc) { doc.Config.Window.Width = 300 doc.Config.Window.Height = 300 t := Triangle{ XY{0.2, 0.2}, XY{0.8, 0.2}, XY{0.5, 0.8}, } s := gfx.NewShader(t) s.Set("a_color", []RGBA{ Red, Green, Blue, }) s.Draw(doc) }
Convert hex to a color.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.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) s.Set("a_color", []color.RGBA{ red, green, blue, }) s.Draw(doc) }
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/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" ) func Ink(doc *app.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) }
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/app" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" "github.com/buchanae/ink/voronoi" ) func Ink(doc *app.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 { s := gfx.NewShader(tri) s.Set("a_color", c) s.Draw(doc) } for _, e := range cell.Edges { gfx.Stroke{ Target: e, Width: 0.002, }.Draw(doc) } } }
Generate all 3x3 combos.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.Doc) { rand.SeedNow() gfx.Clear(doc, color.White) 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 []gfx.Strokeable var strokes []gfx.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.NewShader(xr).Draw(doc) } } } for _, stk := range strokes { gfx.Stroke{ Target: stk, Width: 0.0002, Color: color.Black, }.Draw(doc) } for _, stk := range bold { gfx.Stroke{ Target: stk, Width: 0.0009, Color: color.Black, }.Draw(doc) } }
Replica of a piece by Roger Coqart.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/math" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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) } }
Just for fun.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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{ Mesh: ca.Mesh(), Color: c, }.Draw(doc) gfx.Fill{ Color: c, Mesh: rb, }.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...) }
Generating hexagons in a grid.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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{ Target: cell, Color: Black, Width: 0.004, }.Draw(doc) } }
Inspired by an essay from Tyler Hobbs.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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{ Target: 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, Source: layer, }.Draw(doc) gfx.Stroke{ Target: 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]) } }
Inspired by Vera Molnar.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.Doc) { gfx.Clear(doc, White) 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) } }
Trying to figure out mosaic styling.
package main import ( "github.com/buchanae/ink/app" "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 *app.Doc) { rand.SeedNow() A := color.HexString("#ebc334") B := color.HexString("#0c79e8") gfx.Fill{ Mesh: 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)) }
Inspired by George Nees.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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{ Target: q, Width: 0.001, Color: Black, }.Draw(doc) } }
1D noise line.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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) 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 }
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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() l := Line{a.Rotate(rot, center), b.Rotate(rot, center)} gfx.Stroke{ Target: l, Color: col, Width: 0.0025, }.Draw(doc) } }
Inspired by Anders Hoff and Jared Tarbell. Not working the way I want yet. Splines need love.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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{ Target: curves, Width: LineWidth, Color: c, }.Draw(doc) } } }
Inspired by Jared Tarbell.
package main import ( "log" "time" "github.com/buchanae/ink/app" . "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 *app.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) 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 version of Sand Stroke, because it relies more on OpenGL/GPU.
package main import ( "image" colorlib "image/color" "github.com/buchanae/ink/app" . "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 *app.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}, }, 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 }
Inspired by George Nees.
package main import ( "github.com/buchanae/ink/app" "github.com/buchanae/ink/color" . "github.com/buchanae/ink/dd" "github.com/buchanae/ink/gfx" "github.com/buchanae/ink/rand" ) func Ink(doc *app.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{ Target: pen, Width: 0.001, Color: color.Black, }.Draw(doc) } }
Just for fun.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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) s.Set("a_color", rand.Color(colors)) s.Draw(l2) } if ShouldStroke { gfx.Stroke{ Target: tris, Width: StrokeWidth, Color: White, }.Draw(l2) } gfx.Fill{ Mesh: Circle{ XY: r.Center(), Radius: CircleRadius, Segments: 40, }, Color: Black, }.Draw(mask) } gfx.Mask{ Rect: gfx.Fullscreen, Source: l2, Mask: mask, }.Draw(doc) }
Just for fun.
package main import ( "github.com/buchanae/ink/app" . "github.com/buchanae/ink/color" . "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 *app.Doc) { rand.SeedNow() gfx.Clear(doc, White) 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) } } }
Generating a voronoi mesh.
package main import ( "github.com/buchanae/ink/app" . "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 *app.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{ Mesh: t, Color: c, }.Draw(doc) } gfx.Stroke{ Target: 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.