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.
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/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) }
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 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) }
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) } }
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) } }
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) } }
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) } }
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) }
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) }
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 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) }
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) }
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) } }
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, }, }) }
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) }
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) }
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) } } }
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) } }
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) } }
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...) }
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) } }
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]) } }
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) } }
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)) }
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) } }
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 }
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) } }
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) } } }
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 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 }
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) } }
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) }
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) } } }
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.