Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vector: add utility functions to draw Path #3150

Closed
4 of 11 tasks
sedyh opened this issue Nov 2, 2024 · 17 comments
Closed
4 of 11 tasks

vector: add utility functions to draw Path #3150

sedyh opened this issue Nov 2, 2024 · 17 comments

Comments

@sedyh
Copy link
Contributor

sedyh commented Nov 2, 2024

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • OpenBSD
  • Android
  • iOS
  • Nintendo Switch
  • PlayStation 5
  • Xbox
  • Web Browsers

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
Copy link
Owner

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
Copy link
Contributor Author

sedyh commented Nov 2, 2024

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
Copy link
Owner

Note to myself: #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
Copy link
Contributor Author

sedyh commented Nov 2, 2024

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

  • 3x3 size at first
  • 2x2 sub image, it will panic otherwise.
  • Filled with any color, it will draw nothing otherwise.

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

@hajimehoshi
Copy link
Owner

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
Copy link
Contributor Author

sedyh commented Nov 2, 2024

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
Copy link
Owner

hajimehoshi commented Nov 2, 2024

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?
  • I changed width/height arguments to x,y arguments for rectangles, along with the standard rectangle image.Rectangle.
  • I removed GeoM. If we want, we can introduce a new function to apply GeoM to the entire path.
  • I kept float32 for consistency with other vector functions.

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

@hajimehoshi
Copy link
Owner

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

@sedyh
Copy link
Contributor Author

sedyh commented Nov 2, 2024

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 hajimehoshi changed the title Add simple function to draw vector.Path vector: add utility functions to draw Path Nov 3, 2024
@hajimehoshi
Copy link
Owner

hajimehoshi commented Nov 3, 2024

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
Copy link
Owner

hajimehoshi commented Nov 3, 2024

  • We cannot support transparent colors so far. We can do it by using offscreens, but the cost is not free.
  • Blend might be tricky. Do we really want? This also requires offscreens in fact...

@sedyh
Copy link
Contributor Author

sedyh commented Nov 3, 2024

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

@hajimehoshi
Copy link
Owner

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

@hajimehoshi
Copy link
Owner

hajimehoshi commented Nov 6, 2024

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 #3153)

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

@hajimehoshi
Copy link
Owner

hajimehoshi commented Nov 9, 2024

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
Copy link
Contributor Author

sedyh commented Nov 9, 2024

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

@hajimehoshi
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants