hajimehoshi / ebiten

Ebitengine - A dead simple 2D game engine for Go
https://ebitengine.org
Apache License 2.0
11.12k stars 664 forks source link

vector: add utility functions to draw Path #3150

Closed sedyh closed 2 weeks ago

sedyh commented 3 weeks ago

Operating System

What feature would you like to be added?

It's pretty had for the novice user to draw custom shape without knowing some internal details.

I find myself copy-pasting the same two functions for all my projects:

var EmptySubImage = Empty()

func Empty() *ebiten.Image {
    emptyImage := ebiten.NewImage(3, 3)
    emptyImage.Fill(color.White)
    return emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image)
}

func Path(screen *ebiten.Image, path vector.Path, color color.Color, stroke ...float64) {
    op := &ebiten.DrawTrianglesOptions{
        FillRule:  ebiten.FillRuleFillAll,
        Filter:    ebiten.FilterLinear,
        AntiAlias: true,
    }
    vs, is := path.AppendVerticesAndIndicesForFilling(nil, nil)
    if len(stroke) > 0 {
        strokeOp := &vector.StrokeOptions{}
        strokeOp.Width = float32(stroke[0])
        vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, strokeOp)
    }
    r, g, b, a := color.RGBA()
    for i := range vs {
        vs[i].SrcX = 1
        vs[i].SrcY = 1
        vs[i].ColorR = float32(r) / float32(0xffff)
        vs[i].ColorG = float32(g) / float32(0xffff)
        vs[i].ColorB = float32(b) / float32(0xffff)
        vs[i].ColorA = float32(a) / float32(0xffff)
    }
    screen.DrawTriangles(vs, is, EmptySubImage, op)
}

Why is this needed?

To be able to just (same as text.Draw):

var path vector.Path
path.MoveTo(50, 55)
path.CubicTo(
    100,   
    50, 55, 
    105, 100,
    105,
)
vector.Draw(screen, path, colornames.Red, 5, true)

Personally, I don’t need more, but someone suggested options like these (similar to text.DrawOptions):

op := &vector.DrawOptions{}
op.GeoM.Translate(50, 50)
op.Stroke.Width(5)
hajimehoshi commented 3 weeks ago

I'm fine to add a utility functions, but

func Path(screen *ebiten.Image, path vector.Path, color color.Color, stroke ...float64) {

It is not good to use ... as an optional argument. I'd separate the functions like this:

func DrawFilledPath(dst *ebiten.Image, path vector.Path, color color.Color)
func StrokePath(dst *ebiten.Image, path vector.Path, color color.Color)

Also, would it be OK to fill paths only with a solid color? Also, do we need geometry matrix as an option?

sedyh commented 3 weeks ago

I'd separate the functions like this

Yeah, sure, looks good to me.

Also, would it be OK to fill paths only with a solid color?

Do you mean dashes or transparent color? The current api of vector utility functions dont have that so it should be ok without it by default. However, it would be very nice to have vector.PathDrawOptions.Alpha/FillMode for all of the functions, cause I had some cases where I need to set transparent color in the past for DrawFilledRect.

Also, do we need geometry matrix as an option?

I guess GeoM.Translate will be usefull in order to not to add x,y for every single path coord.

hajimehoshi commented 3 weeks ago

Note to myself: https://github.com/hajimehoshi/ebiten/issues/3124 might be related. In order to render a better edge, we would need a special shader IIUC. In this case, the utility functions we are adding might be a good wrapper to do this.

sedyh commented 3 weeks ago

Btw, could you remind me please, why it needs to be:

I found it somewhere near vector example and can't remember why its made this way.

hajimehoshi commented 3 weeks ago

Do you mean dashes or transparent color? The current api of vector utility functions dont have that so it should be ok without it by default.

I mean using an image with the path. I think just one color is fine along with the other existing utility functions. I suggest

type VectorOptions struct {
    GeoM ebiten.GeoM
    ColorScale ebiten.ColorScale
    Blend ebiten.Blend
    AntiAlias bool
}

func DrawFilledPath(dst *ebiten.Image, path vector.Path, op *VectorOptions)
func StrokePath(dst *ebiten.Image, path vector.Path, op *VectorOptions)

I might change the idea later. Let me think more.

2x2 sub image, it will panic otherwise.

The sub image size is 1x1.

I found it somewhere near vector example and can't remember why its made this way.

In order to prevent bleeding edges. See https://ebitengine.org/en/blog/subimage.html

sedyh commented 3 weeks ago

I might change the idea later. Let me think more.

Probably better to call it vector.DrawOptions like it was made with text.DrawOptions.

hajimehoshi commented 3 weeks ago

With deprecating the existing utility functions, what about these

type FillOptions struct {
    ColorScale ebiten.ColorScale
    Blend ebiten.Blend
    AntiAlias bool
}

func FillPath(dst *ebiten.Image, path vector.Path, op *FillOptions)
func FillCircle(dst *ebiten.Image, x, y, r float32, op *FillOptions)
func FillRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *FillOptions) // or width/height?

type StrokeOptions struct {
    StrokeWidth float32
    ColorScale ebiten.ColorScale
    Blend ebiten.Blend
    AntiAlias bool
}

func StrokePath(dst *ebiten.Image, path vector.Path, op *StrokeOptions)
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokeOptions)
func StrokeCircle(dst *ebiten.Image, x, y, r float32, op *StrokeOptions)
func StrokeRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokeOptions) // or width/height?

EDIT: Oops, there is already StrokeOptions. Let me revisit this.

hajimehoshi commented 3 weeks ago

I'll take a look tomorrow again as it is already midnight. Thanks,

sedyh commented 3 weeks ago

With deprecating the existing utility functions, what about these

Idk if we should make another options just for StrokeWidth. Having one option like vector.DrawOptions will also resolve the issue with already existing name.

hajimehoshi commented 3 weeks ago
type FillPathOptions struct {
    ColorScale ebiten.ColorScale
    AntiAlias bool
}

func FillPath(dst *ebiten.Image, path vector.Path, op *FillPathOptions)
func FillCircle(dst *ebiten.Image, x, y, r float32, op *FillPathOptions)
func FillRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *FillPathOptions) // or width/height?

type StrokePathOptions struct {
    StrokeOptions
    ColorScale ebiten.ColorScale
    AntiAlias bool
}

func StrokePath(dst *ebiten.Image, path vector.Path, op *StrokePathOptions)
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokePathOptions)
func StrokeCircle(dst *ebiten.Image, x, y, r float32, op *StrokePathOptions)
func StrokeRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokePathOptions) // or width/height?

EDIT: Removed Blend.

hajimehoshi commented 3 weeks ago
sedyh commented 3 weeks ago

We can skip them for now. It will be good addition in the future versions later.

hajimehoshi commented 3 weeks ago

Oh, the option has to take the method (EvenOdd vs NonZero)...

hajimehoshi commented 3 weeks ago
type FillRule int

const (
    FillRuleNonZero FillRule = FillRule(ebiten.FillRuleNonZero) // The value is derived from the ebiten package for compatibility.
    FillRuleEvenOdd FillRule = FillRule(ebiten.FillRuleEvenOdd)
)

type FillPathOptions struct {
    ColorScale ebiten.ColorScale
    AntiAlias bool
    FillRule FillRule
}

func FillPath(dst *ebiten.Image, path vector.Path, op *FillPathOptions)
func FillCircle(dst *ebiten.Image, x, y, r float32, op *FillPathOptions)
func FillRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *FillPathOptions) // or width/height?

type StrokePathOptions struct {
    StrokeOptions
    ColorScale ebiten.ColorScale
    AntiAlias bool
    FillRule FillRule
}

func StrokePath(dst *ebiten.Image, path vector.Path, op *StrokePathOptions)
func StrokeLine(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokePathOptions)
func StrokeCircle(dst *ebiten.Image, x, y, r float32, op *StrokePathOptions)
func StrokeRect(dst *ebiten.Image, x0, y0, x1, y1 float32, op *StrokePathOptions) // or width/height?

I'll add APIs like above later. The implementation might change later to allow better rendering like transparent colors (see https://github.com/hajimehoshi/ebiten/issues/3153)

EDIT: I realized that FillRule is not needed for utility functions. Maybe this should be simply ignored for e.g. StorkeLine.

hajimehoshi commented 2 weeks ago

Another suggestion would be like this:

type FillRule int

const (
    FillRuleNonZero FillRule = FillRule(ebiten.FillRuleNonZero) // The value is derived from the ebiten package for compatibility.
    FillRuleEvenOdd FillRule = FillRule(ebiten.FillRuleEvenOdd)
)

func DrawFilledPath(dst *ebiten.Image, path *vector.Path, clr color.Color, antialias bool, fillRule FillRule)
func StrokePath(dst *ebiten.Image, path *vector.Path, clr color.Color, antialias bool, op *StrokeOptions)

and leave the current existing APIs. DrawFilled is odd, but let's fix this in v3.

sedyh commented 2 weeks ago

In order not to wait for #3124 and #3153 before api v3? Probably fine.

hajimehoshi commented 2 weeks ago

Yeah, the situation might change later, so let's go a conservative way.