faiface/pixel

How to create vector paths?

Closed this issue · 3 comments

I would like to create vector paths, more complex than just circles and rectangles I used to create with IMDraw, how to create a custom shape with a contour composed of arcs and lines?

To be more precise, I'm looking for something similar to Qt's qpainterpath, on which we can add arcs and lines.

I did it! \o/

I used svgo to create a svg from basic instructions, and oksvg to rasterize it in an image.
I'm sharing a simple working example, since I'm new to Go please let me know if any improvements can be added. ;)
Also, do you think that this code deserve a place in the Pixel wiki? I suppose that I'm not the only one that wanted to do this kind of stuff.

package main

import (
	"fmt"
	"io"
	"image"

	"github.com/ajstarks/svgo"
	"golang.org/x/image/colornames"
	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	. "github.com/srwiley/rasterx"
	. "github.com/srwiley/oksvg"
)

func drawSvg(writer io.Writer) {
	fmt.Println("drawSvg: creating svg image in writer...")
	width := 400
	height := 400
	canvas := svg.New(writer)
	canvas.Start(width, height)
	canvas.Circle(width/2, height/2, 200)
	canvas.End()
	fmt.Println("drawSvg: svg image created in writer.")
}

func loadSvg(svgStream io.Reader) (pixel.Picture, error) {
	fmt.Println("loadSvg: loading svg image from reader...")
	svg, err := ReadIconStream(svgStream, WarnErrorMode)
	if err != nil {
		return nil, err
	}
	w, h := int(svg.ViewBox.W), int(svg.ViewBox.H)
	fmt.Printf("loadSvg: image size = %vx%v\n", w, h)
	img := image.NewRGBA(image.Rect(0, 0, w, h))

	scannerGV := NewScannerGV(w, h, img, img.Bounds())
	raster := NewDasher(w, h, scannerGV)
	svg.Draw(raster, 1.0)

	image := pixel.PictureDataFromImage(img)
	fmt.Println("loadSvg: image loaded.")
	return image, nil
}

func startApp() {
	config := pixelgl.WindowConfig {
		Title: "Shapes drawing example",
		Bounds: pixel.R(0, 0, 600, 600),
	}
	win, err := pixelgl.NewWindow(config)
	if err != nil {
		panic(err)
	}

	fmt.Println("startApp: creating pipe...")
	var reader, writer = io.Pipe()
	fmt.Println("startApp: pipe created.")

	go func() {
		drawSvg(writer)
		writer.Close()
	}()

	pic, err := loadSvg(reader)
	if err != nil {
		panic(err)
	}

	sprite := pixel.NewSprite(pic, pic.Bounds())
	win.Clear(colornames.Greenyellow)
	sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))

	for !win.Closed() {
		win.Update()
	}
}

func main() {
	pixelgl.Run(startApp)
}

Even better: instead of drawing in svg then reading this svg, I draw directly in a graphic context with the draw2d package.

Less code and I didn't benchmarked it but it's also probably faster.

package main

import (
	"image"
	"math"

	"golang.org/x/image/colornames"
	"github.com/faiface/pixel"
	"github.com/faiface/pixel/pixelgl"
	"github.com/llgcode/draw2d/draw2dimg"
)

const (
	window_width, window_height int = 400, 400
)

func drawSvg() image.Image {
	radius := 150
	cx, cy := float64(window_width) / 2, float64(window_height) / 2
	img := image.NewRGBA(image.Rect(0, 0, window_width, window_height))
	gc := draw2dimg.NewGraphicContext(img)
	gc.BeginPath()
	gc.SetFillColor(image.Black)
	gc.MoveTo(cx, cy)
	gc.ArcTo(cx, cy, float64(radius), float64(radius), 0, math.Pi*2)
	gc.Fill()
	return img
}

func startApp() {
	config := pixelgl.WindowConfig {
		Title: "Shapes drawing example",
		Bounds: pixel.R(0, 0, float64(window_width), float64(window_height)),
	}
	win, err := pixelgl.NewWindow(config)
	if err != nil {
		panic(err)
	}

	image := pixel.PictureDataFromImage(drawSvg())
	sprite := pixel.NewSprite(image, image.Bounds())
	win.Clear(colornames.Greenyellow)
	sprite.Draw(win, pixel.IM.Moved(win.Bounds().Center()))

	for !win.Closed() {
		win.Update()
	}
}

func main() {
	pixelgl.Run(startApp)
}

image

Gonna go ahead and close this as you figured it out :) Cool solution!