- Download Go for your platform from here
- Create a new directory and switch to it
- Download the .go files from this Gist into it
- Run
$ go mod init mainthis will create a file called go.mod that's needed for the next step - Run
$ go mod tidythis will pull down the packages needed to run the program - Run
$ go run *.go - The result will be in inkstroke.png

Last active
September 12, 2026 16:50
-
-
Save jphsd/ee4fe6d918d23257c1eb83da8a3a388f to your computer and use it in GitHub Desktop.
Using graphics2d to simulate a water based ink stroke
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "flag" | |
| "math" | |
| "math/rand" | |
| stdimg "image" | |
| "image/draw" | |
| "github.com/disintegration/imaging" | |
| g2d "github.com/jphsd/graphics2d" | |
| "github.com/jphsd/graphics2d/color" | |
| "github.com/jphsd/graphics2d/image" | |
| ) | |
| // Demonstrate use of IrregularEllipse to draw variable lines | |
| func main() { | |
| bf := flag.Float64("b", 6, "blur radius") | |
| cf := flag.Bool("c", false, "show controls") | |
| ff := flag.Int("f", 0x30, "alpha floor") | |
| flag.Parse() | |
| cc, blur, floor := *cf, *bf, uint8(*ff&0xff) | |
| // Create image to write into | |
| width, height := 1000, 1000 | |
| img := image.NewRGBA(width, height, color.White) | |
| n := 10 | |
| dy := 1000 / float64(n+1) | |
| y := dy | |
| for range n { | |
| p1 := []float64{50, y} | |
| p2 := []float64{950, y} | |
| y += dy | |
| path, c, c1 := InkStroke(p1, p2, 20, 0) | |
| mask := WaterColorMask(g2d.NewShape(path), blur, floor) | |
| rect := mask.Bounds() | |
| g2d.DrawMask(img, rect, color.Orange, mask, rect.Min, draw.Over) | |
| if cc { | |
| pts := g2d.NewShape(g2d.Circle(p1, 2), g2d.Circle(p2, 2)) | |
| g2d.RenderColoredShape(img, pts, color.Red) | |
| pts = g2d.NewShape(g2d.Circle(c, 2), g2d.Circle(c1, 2)) | |
| g2d.DrawLine(img, p1, p2, g2d.BlackPen) | |
| g2d.RenderColoredShape(img, pts, color.Black) | |
| } | |
| } | |
| image.SaveImage(img, "inkstroke") | |
| } | |
| func InkStroke(p1, p2 []float64, mw, dw float64) (*g2d.Path, []float64, []float64) { | |
| dx, dy := p2[0]-p1[0], p2[1]-p1[1] | |
| th := math.Atan2(dy, dx) | |
| d := math.Hypot(dx, dy) | |
| mw /= 2 | |
| rx2 := rand.Float64() * 0.1 * d | |
| rx1 := d - rx2 | |
| rx1 += mw | |
| rx2 += mw | |
| ry1 := rand.Float64()*dw + mw | |
| ry2 := rand.Float64()*dw + mw | |
| disp := rand.Float64() | |
| t := (rx2 - mw) / d | |
| omt := 1 - t | |
| c := []float64{p1[0]*omt + p2[0]*t, p1[1]*omt + p2[1]*t} | |
| // c1 is only needed for display | |
| t = disp | |
| omt = 1 - t | |
| c1 := []float64{p2[0]*t + c[0]*omt, p2[1]*t + c[1]*omt} | |
| d1 := (rx1 - mw) * disp | |
| disp = d1 / rx1 | |
| return g2d.IrregularEllipse(c, rx1, rx2, ry1, ry2, disp, th), c, c1 | |
| } | |
| // Simulate a water color effect where the paint is full strength at the shape edge but blends with what's | |
| // underneath within the shape. | |
| // Makes a mask using the shape, blur radius and internal floor alpha value. | |
| func WaterColorMask(shape *g2d.Shape, blur float64, floor uint8) *image.Alpha { | |
| // imaging ignores image offset | |
| // and needs an image with the shape at least blur margin away from the edges | |
| blurm := int(blur + 0.5) | |
| // image/vector doesn't work with rectangles starting sub origin | |
| // so draw it by hand with the shape mask and DrawMask | |
| mask := shape.Mask() | |
| rect := mask.Bounds() | |
| imgm := stdimg.NewGray(image.Rectangle{ // image that can contain shape with margins | |
| image.Point{rect.Min.X - blurm, rect.Min.Y - blurm}, | |
| image.Point{rect.Max.X + blurm, rect.Max.Y + blurm}}) | |
| image.Fill(imgm, color.White) | |
| g2d.DrawMask(imgm, rect, color.Gray{floor}, mask, rect.Min, draw.Over) | |
| img2 := imaging.Blur(imgm, blur) // NRGBA | |
| // write img2 into a black image using shape mask and undoing the blurm offset | |
| img3 := stdimg.NewGray(rect) | |
| draw.DrawMask(img3, rect, img2, image.Point{blurm, blurm}, mask, rect.Min, draw.Over) | |
| //Return as Alpha image | |
| return image.GrayToAlpha(img3) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment