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

Add AnchorPos struct and functions #252

Merged
merged 9 commits into from
Jan 27, 2021
Merged

Conversation

roipoussiere
Copy link
Contributor

@roipoussiere roipoussiere commented Aug 10, 2020

This PR adds the concept of anchors to align elements.

It adds:

  • type Anchor: a vector used to define anchors, such as Center, Top, TopRight, etc.;
  • func (Anchor) String() string: returns the string representation of an anchor.;
  • func (*Rect) AnchorPos(anchor Anchor) Vec: returns the absolute position of the given anchor of the Rect.;
  • edit: func (*Rect) AnchorTo(Anchor): updates the Rect position to align it on the given anchor.
  • edit: func (Rect) AlignedTo(Anchor) Rect: returns the rect moved to be aligned on the given anchor.
  • edit: func (Matrix) Aligned(Rect, anchorPos AnchorPos) Matrix: moves everything to align the given rectangle on the given anchor position.
  • edit: func (*Text) AlignedTo(pixel.Anchor) *Text: returns the text moved by the given anchor.
  • edit: func (Anchor) Opposite() Anchor: returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).

Example code

edit: Update with last modifications.

package main

import (
	"image/color"
	"fmt"
	"github.com/faiface/pixel"
	"github.com/faiface/pixel/imdraw"
	"github.com/faiface/pixel/pixelgl"
	"golang.org/x/image/colornames"
	"github.com/faiface/pixel/text"
	"golang.org/x/image/font/basicfont"
)

const (
	winWidth   float64 = 600
	winHeight  float64 = 450
	padding    float64 = 170
	rectWidth, rectHeight float64 = 90, 90
	minX, minY float64 = padding, padding
	maxX, maxY float64 = winWidth - padding, winHeight - padding
	cX, cY     float64 = winWidth/2, winHeight/2
)

var dotsPos map[pixel.Anchor]pixel.Vec = map[pixel.Anchor]pixel.Vec{
	pixel.Center: pixel.V(cX, cY),
	pixel.Top: pixel.V(cX, maxY),
	pixel.TopRight: pixel.V(maxX, maxY),
	pixel.Right: pixel.V(maxX, cY),
	pixel.BottomRight: pixel.V(maxX, minY),
	pixel.Bottom: pixel.V(cX, minY),
	pixel.BottomLeft: pixel.V(minX, minY),
	pixel.Left: pixel.V(minX, cY),
	pixel.TopLeft: pixel.V(minX, maxY),
}
func drawRectanglesAndDots(win *pixelgl.Window) {
	imd := imdraw.New(nil)
	for anchor, vect := range dotsPos {
		rect := pixel.R(vect.X, vect.Y, vect.X + rectWidth, vect.Y + rectHeight).AlignedTo(anchor)
		drawRect(imd, rect, colornames.Gray)
		drawDot(imd, vect, colornames.Red)
	}
	imd.Draw(win)
}

func drawLabels(win *pixelgl.Window) {
	const scale float64 = 2
	basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
	for anchor, vect := range dotsPos {
		basicTxt := text.New(vect, basicAtlas)
		basicTxt.Color = colornames.Yellow
		fmt.Fprintln(basicTxt, anchor.String())
		basicTxt.AlignedTo(anchor).Draw(win, pixel.IM.Scaled(basicTxt.Orig, scale))
	}
}

func drawRect(imd *imdraw.IMDraw, rect pixel.Rect, color color.Color) {
	imd.Color = color
	imd.Push(rect.Min, rect.Max)
	imd.Rectangle(0)
}

func drawDot(imd *imdraw.IMDraw, vect pixel.Vec, color color.Color) {
	imd.Color = color
	imd.Push(vect)
	imd.Ellipse(pixel.V(3, 3), 0)
}

func run() {
	cfg := pixelgl.WindowConfig{
		Title:  "AnchorPos examples",
		Bounds: pixel.R(0, 0, winWidth, winHeight),
		VSync:  true,
	}
	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	win.Clear(colornames.Lightgray)
	drawRectanglesAndDots(win)
	drawLabels(win)

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

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

Result

image

More to come

edit: done!

The modifications added in this first commit are required for other functions that I plan to add as well in this PR, named AnchorTo:

- func (r *Rect) AnchorTo(anchor AnchorPos): updates the Rect position to align it on the given anchor.
- func (r *Text) AnchorTo(anchor AnchorPos): updates the Text position to align it on the given anchor.
- etc. At this point it's easy to add implementation for many other objects (Circle, Image, etc.)

Example usage:

basicTxt := text.New(vect, basicAtlas)
basicTxt.AnchorTo(AnchorPos.Center)

Here, basicTxt will be centered relative to the vect position. With AnchorPos.Left, it will be centered verticaly but not horizontally, etc.

This is helpful to position and align elements.

I would like to get your feedback before to continue. ;)

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 10, 2020

In this second commit, I added:

  • func (r *Rect) AnchorTo(anchor AnchorPos): updates the Rect position to align it on the given anchor.

Following the code sample shared above, I updated padding to 50, removed blue dots, and slightly modified the drawRectangle() in order to change the rectangle anchor:

func drawRectangles(win *pixelgl.Window) {
	imd := imdraw.New(nil)
	for anchor, vect := range dotsPos {
		rect := pixel.R(vect.X, vect.Y, vect.X + rectWidth, vect.Y + rectHeight)
		rect.AnchorTo(anchor)
		drawRect(imd, rect, colornames.Gray)
	}
	imd.Draw(win)
}

Which gives:

Screenshot_20200810_172323

Note that labels are not correctly positioned since Text.AnchorTo() is not yet implemented.

To be discussed: Maybe func (r Rect) AnchorTo(anchor AnchorPos) Rect better? It allows chained functions, like rect := pixel.R(a, b, c, d).AnchorTo(anchor).

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 10, 2020

Concerning func (r *Rect) AnchorTo(anchor AnchorPos), there is clearly a strange behavior that I don't understand.

I'm not able to update the text position in any way: I tried to change its Orig, bounds, or mat attributes but the position remains the same...

edit: hmm, just got it, I have to work with the transformation matrix when calling basicTxt.Draw().

@roipoussiere
Copy link
Contributor Author

I finally fixed this by applying a matrix transformation instead of trying to move a text with a AnchorTo function.

So in this third commit, I added:

  • func (m Matrix) Aligned(rect Rect, anchorPos AnchorPos) Matrix: moves everything to align the given rectangle on the given anchor position.

Following the code defined in first comment, I removed the squares and blue dots, updated padding to 150, and modified the drawLabels() function to use the Aligned matrix transformation:

func drawLabels(win *pixelgl.Window) {
	basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
	for anchor, vect := range dotsPos {
		basicTxt := text.New(vect, basicAtlas)
		basicTxt.Color = colornames.Gray
		fmt.Fprintln(basicTxt, anchor.String())
		basicTxt.Draw(win, pixel.IM.Aligned(basicTxt.Bounds(), anchor))
	}
}

which gives:

image

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 11, 2020

Well, I drop the idea of adding a matrix transformation since it only applies a translation and Matrix.Moved() should be used for this purpose.

Instead, I modified AnchorPos in order to return the relative position of the anchor. In this way it can be passed into a Moved matrix transformation.

I also transformed func (r *Rect) AnchorTo(anchor AnchorPos) into func (rect Rect) AlignedTo(anchorPos AnchorPos) Rect, in order to be more coherent with func (r Rect) Moved(delta Vec) Rect that returns a Rect.

The new drawLabels() function, with an additional scale factor for aesthetics.

func drawLabels(win *pixelgl.Window) {
	const scale float64 = 2
	basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
	for anchor, vect := range dotsPos {
		basicTxt := text.New(vect, basicAtlas)
		basicTxt.Color = colornames.Gray
		fmt.Fprintln(basicTxt, anchor.String())
		alignment := basicTxt.Bounds().AnchorPos(anchor).Scaled(scale)
		basicTxt.Draw(win, pixel.IM.Scaled(basicTxt.Orig, scale).Moved(alignment))
	}
}

image

@faiface
Copy link
Owner

faiface commented Aug 11, 2020

Hey, this is aiming towards something very useful, anchoring is pretty hard right now.

But, I’m skeptical about the necessity of the AnchorPos struct. What about adding a Rect.Anchor(Vec) Vec method, where the Vec has values between 0 and 1 and you define global variables for different anchors (just like you have), but they’d be of type Vec.

@bcvery1
Copy link
Contributor

bcvery1 commented Aug 11, 2020

No tests?

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 11, 2020

But, I’m skeptical about the necessity of the AnchorPos struct. What about adding a Rect.Anchor(Vec) Vec method, where the Vec has values between 0 and 1 and you define global variables for different anchors (just like you have), but they’d be of type Vec.

I think it's less intuitive. With Rect.Anchor(Vec) Vec defined as is, it's not clear for me what this function does and how to use it. With a AnchorPos type as argument, it's getting clearer be cause I understand that I'm supposed to put an anchor here.

Is there a specific reason to avoid the AnchorPos struct?

edit:

No tests?

I prefer to get feedback before, many things may change my the meantime. ;) I will also update documentation as well when this PR will be in a stable state.

@faiface
Copy link
Owner

faiface commented Aug 11, 2020

My reason for avoiding AnchorPos is that it makes it seem like the anchor is some special kind of an object, when it’s really not.

But I see your point. Though I also think that a simple documentation comment can clear up a lot.

However, if I were to go for a new, special type, I’d probably just go for:

type Anchor Vec

Which makes it clear that Anchor is just a special-named vector and no one should expect it getting more fields (as it could happen with a struct).

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 11, 2020

However, if I were to go for a new, special type, I’d probably just go for:

type Anchor Vec

Which makes it clear that Anchor is just a special-named vector and no one should expect it getting more fields (as it could happen with a struct).

That makes sense. This last commit makes Anchor a type Vec instead of a struct.

And here is the example code:

func drawRectanglesAndDots(win *pixelgl.Window) {
	imd := imdraw.New(nil)
	for anchor, vect := range dotsPos {
		rect := pixel.R(vect.X, vect.Y, vect.X + rectWidth, vect.Y + rectHeight).AlignedTo(anchor)
		drawRect(imd, rect, colornames.Gray)
		drawDot(imd, vect, colornames.Red)
	}
	imd.Draw(win)
}

func drawLabels(win *pixelgl.Window) {
	const scale float64 = 2
	basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
	for anchor, vect := range dotsPos {
		basicTxt := text.New(vect, basicAtlas)
		basicTxt.Color = colornames.Blue
		fmt.Fprintln(basicTxt, anchor.String())
		offset := basicTxt.Bounds().AnchorPos(anchor).Scaled(scale)
		basicTxt.Draw(win, pixel.IM.Scaled(basicTxt.Orig, scale).Moved(offset))
	}
}

image

I'm particularly happy with the elegance of rect.AlignedTo(anchor).

@roipoussiere
Copy link
Contributor Author

I'm particularly happy with the elegance of rect.AlignedTo(anchor).

Ok, I'm trying to do the same with Text.

I added the anchor property in Text struct:

anchor pixel.Anchor

and created a new Text function that simply updates this property and returns the Text to allow chained expressions:

func (txt *Text) AlignedTo(anchor pixel.Anchor) *Text {
	txt.anchor = anchor
	return txt
}

Then in Text.DrawColorMask(), I update the transformation matrix using the anchor property:

if matrix != txt.mat {
	txt.mat = matrix
	txt.dirty = true
} else {
	offset := txt.Bounds().AnchorPos(txt.anchor)
	txt.mat = txt.mat.Moved(offset)
}

As you can see for now I only apply the transformation if no matrix is passed. So that works for this:

basicTxt.AlignedTo(anchor).Draw(win, pixel.IM)

But the AlignedTo() is ignored if a transformation is applied like this:

basicTxt.AlignedTo(anchor).Draw(win, pixel.IM.Scaled(basicTxt.Orig, scale))

I'm lack of knowledge in affine transformations to update the matrix by moving it to offset defined above. Any help?

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 11, 2020

Bingo! What I was looking for was the Matrix.Chained() function. Now Text.AlignTo() works like a charm:

func drawLabels(win *pixelgl.Window) {
	const scale float64 = 2
	basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
	for anchor, vect := range dotsPos {
		basicTxt := text.New(vect, basicAtlas)
		basicTxt.Color = colornames.Blue
		fmt.Fprintln(basicTxt, anchor.String())
		basicTxt.AlignedTo(anchor).Draw(win, pixel.IM.Scaled(basicTxt.Orig, scale))
	}
}

image

At this point I'm beginning to be satisfied of this PR, do you have other feedback to do? If not I can start to add unit tests and documentation.

@roipoussiere
Copy link
Contributor Author

roipoussiere commented Aug 12, 2020

I just added 2 more commits:

The first one fix the anchors positions, that was inverted. Indeed, if the red dot is the origin, by something.AlignTo(pixel.Right), we would except that this something will be on the right of the dot:

image

The second commit adds a new function:

  • func (Anchor) Opposite() Anchor: returns the opposite position of the anchor (ie. Top -> Bottom; BottomLeft -> TopRight, etc.).

Since I already use my Pixel fork for my personal project, I can say that it is useful in many cases.

@roipoussiere
Copy link
Contributor Author

last commit: fix anchor position on some fonts (see #255)

@niklas-karlsson
Copy link

Any updates? Useful stuff in here, I would like to use it for a UI system Im currently building.

@delp
Copy link
Contributor

delp commented Jan 27, 2021

@niklas-karlsson @roipoussiere Sorry for the long delay, been pretty busy at day job. I'll take a look at this one as well.

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

Successfully merging this pull request may close these issues.

5 participants