Skip to content

Instantly share code, notes, and snippets.

@jphsd
Last active September 12, 2026 16:50
Show Gist options
  • Select an option

  • Save jphsd/ee4fe6d918d23257c1eb83da8a3a388f to your computer and use it in GitHub Desktop.

Select an option

Save jphsd/ee4fe6d918d23257c1eb83da8a3a388f to your computer and use it in GitHub Desktop.
Using graphics2d to simulate a water based ink stroke
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)
}

How To Run

  • 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 main this will create a file called go.mod that's needed for the next step
  • Run $ go mod tidy this will pull down the packages needed to run the program
  • Run $ go run *.go
  • The result will be in inkstroke.pnginkstroke
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment