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

Proposal: 3 properties to manage object positioning #997

Closed
englercj opened this issue Sep 24, 2014 · 34 comments
Closed

Proposal: 3 properties to manage object positioning #997

englercj opened this issue Sep 24, 2014 · 34 comments
Assignees
Milestone

Comments

@englercj
Copy link
Member

Proposal

I propose, there should be 2 properties on a DisplayObject: origin and pivot; and one extra property on a Sprite: anchor. These properties are defined below:

Origin

The origin would be a normalized point that describes where the position of the object is oriented. (0, 0) means the position of the object describes the top-left of the object, (1, 1) means the position describes the bottom-right of the object.

Pivot

The pivot would be a normalized point that describes the point at which the object rotates around. Again, (0, 0) is top-left, (1, 1) is bottom-right.

Anchor

For sprites, there is an extra property anchor. This is in addition to the previous properties that it inherits from DisplayObject. The anchor is a normalized point that describes where the top-left of the texture attaches to the object. (0, 0) is top-left of the texture is on the top-left of the object, (1, 1) is top-left of the texture is on the bottom-right of the object.

Conclusion

With these three properties you have the ability to do anything you need to with regard to positioning and rotating. It also reduces ambiguity that comes from having pivot be a pixel position, and anchor be normalized. Many issues arise from having the current implementation of pivot be a hybrid of the proposed origin and pivot and even more so from anchor being used to fill that gap. This solution clarifies the use of each property, removes ambiguity, and adds additional functionality.

@englercj englercj changed the title Proposal: 3 Clear properties to manage object positioning Proposal: 3 properties to manage object positioning Sep 24, 2014
@photonstorm
Copy link
Collaborator

👍 x 1000000 because we really need this.

1 question: Which point would the anchor attach to? origin, or an internal value?

@agamemnus
Copy link
Contributor

Hmm.

What are the pros/cons for normalized positions versus pixel positions? For me, pro: A pixel position is easier to understand, though (con) less portable.

"(1, 1) is top-left of the texture is on the bottom-right of the object."
I am bad with coordinates and can't picture that. :-(

@englercj
Copy link
Member Author

@photonstorm I said in the proposal that the anchor attaches the texture to the top-left of the object, but I am open to it attaching to the origin instead

@agamemnus I definitely disagree that a pixel position is more clear, remember we are talking about a world pixel position, not a local position (that is how it works now). A normalized position is portable, has to be normalized eventually anyway, and makes sense in this context (and what players are used to for anchor now).

@kittykatattack
Copy link

This is perfect! ✨

@wayfu
Copy link

wayfu commented Sep 25, 2014

I think that for some cases, a pixel position makes a lot more sense. Consider the use case of orbiting: the pivot is outside the object's bounds and in all probability attached to some sort of object. The proposed normalized pivot would necessitate determining that object's relative normalized position in respect to the orbiting object, which sounds like a huge mess to me.

Perhaps what's necessary is a range of helper functions to easily access global/local pixel values?

@krzysztof-o
Copy link
Contributor

From my experience pixel values are easier to understand but normalised values are easier and more convenient to use. Very often I set anchor to (0.5, 0.5) and I don't care about size of a sprite. My opinion is based on comparison between Starling (where you have pixel values) and PIXI.js.

@GoodBoyDigital
Copy link
Member

Hey peeps,

The main reason I have avoided using normalized values (outside of texture alignment) is that it requires knowing the dimensions of the object at all times. Calculating the widths / heights of all objects on the stage each frame would add an overhead to the rendering loop that would impact performance :/

I'm thinking we could rename pivot to origin?

@photonstorm
Copy link
Collaborator

@englercj I only asked where it'd align because if you adjust where the top-left of the object is, would it work in relation to that or the actual real un-modified top-left? :)

I much prefer working with normalised values, I just find them easier to deal with in our responsive games. Although I've lost count of the number of times new phaser devs have been puzzled by them :)

@agamemnus
Copy link
Contributor

Unless there are tens of thousands of sprites with pivots and so on, in my opinion, performance wouldn't really be an issue... it is just some multiplication... right?

@photonstorm
Copy link
Collaborator

It's quite a lot of calculations: https://github.com/GoodBoyDigital/pixi.js/blob/dev/src/pixi/display/Sprite.js#L163-L234 - every frame, for every object. I'd imagine the depth of the display list is almost as important as the size in this case, as everything propagates.

@GoodBoyDigital
Copy link
Member

@photonstorm is correct :( we would need to essentially transform all children of the an object to its local coord system and then measure each object to calculate the min max values. Then we would need to transform them again to the global coord system.

@tleunen
Copy link

tleunen commented Sep 25, 2014

What about a global setting deciding which way the developer wants? If it doesn't add too much work.
This is just an idea to make everyone happy, but not sure it will be easy for new developers to understand how Pixi works.

@bokanist
Copy link

I don't catch the difference between Orgin and pivot. I think we need only two points.

  • one point inside the clip : let's call it origin, pivot or hotspot
  • one point inside it's parent : let's call it position
    then align them.
    @GoodBoyDigital : I think this feature will be used mainly for positioning objects in HUD / Menus / Interface. There's no rotation involved... so we could optimize a lot.

I'll try to implement a new kind of DisplayObject, a Frame, that is invisible, but serve as a guide to position siblings.

We should get rid of thinking in pixel. We are in the era of mixed display format, mixed resolution, mixed PPI (retina display). If we want to easely manage textures and change texture set according to resolution/PPI/bandwith of target we have to work in normalized values. Like GPU does. Modern engines like UBIart Framework are floating point based.
I can't beleive I'm saying that now ! After spending years focusing on delivering pixel perfect apps and website.

Time changes

@darionco
Copy link

@GoodBoyDigital: The way other frameworks solve the performance issue is by offloading the responsibility of maintaining sizes updated to the programmer.

The basic idea is to expose a size property of the display object, this size property is used to calculate the transformation matrix of the object and it can only be updated by the programmer or in certain specific events; for example:

  • Sprites are created with a size corresponding to the texture size.
  • When the scale of the object is modified, the size is modified as well (this is pixi.js' current behaviour)
  • The size property is updated manually by the programmer, as in object.size.width = 200
  • The programmer calls an updateSize function

It's important to note that the function to update the size and the functions to get the bounds are not the same, there are cases in which the programmer might wish for an object to maintain a size while moving the object's children arbitrarily, think of elliptical orbiting for example, the effect could be achieved as follows:

  • Create a circle, updateSize and set its pivot to 0.5, 0.5; we'll call this planet
  • Create a smaller circle moon, updateSize, set its anchor to 0.5, 0.5, its position to 0, planet.width, and child it to planet
  • In your update loop, rotate planet and use Math.sin(planet.rotation) multiplied by some offset to change the x position of moon

That will effectively make moon seem like it is following an elliptical orbit around planet, if the size property was updated automatically, therefore changing the position of the planet's pivot that was set to 0.5, 0.5, this effect would be a lot harder to achieve.

Another advantage of this approach is that DisplayObjectContainer becomes some sort of a "canvas within the canvas" in the sense that you could create a container, set its size, position, rotation and so on (maybe even mask it) and then start adding children to it treating it as your work space, making it easier to manage different spaces within your app and also opening the possibility to some cool effects involving rotations.

The biggest issues with this approach are (1) that it increases the amount of work needed to setup your objects and (2) that if it is more convenient for an object's size to be updated constantly now the programmer needs to keep track of which object is that and manually update its size every frame.

To solve issue (1) we need to solve issue (2) first. In real use cases most objects will not need to have their sizes updated every frame because most objects are sprites or simple objects, with that assumption in mind then we can create an autoUpdateSize property in the DisplayObject, something like this:

/**
 * Given that there is a 'size' property in the DisplayObject that contains a width and a height,
 * it could point to a cached transformation matrix.
 * As discussed in the comment, an 'updateSize' function would also be needed, its only goal is to
 * calculate and set the object's current size, including all its children.
 */
DisplayObject.prototype.__updateTransform = function (args)
{
    /* calculate the object's transformation matrix taking into account the 'size' property */
}

DisplayObject.prototype.__updateTransformAutoUpdateSize = function (args)
{
    this.updateSize(); /* as discussed above */
    this.__updateTransform(args); /* regular transform operation, calculated using the new size */
}

/* =============== */
/**
 * Optional extra level of indirection.
 * Solves instances in which third party objects override "updateTransform"
 */
DisplayObject.prototype.__updateTransformImp = DisplayObject.prototype.__updateTransform; /* default value */
DisplayObject.prototype.updateTransform = function (args)
{
    /* just forward the function call to the current "updateTransform" implementation */
    this.__updateTransformImp(args);
}
/* =============== */

Object.defineProperty(PIXI.DisplayObject.prototype, 'autoUpdateSize', {
    get: function() {
        return  (this.__updateTransformImp === DisplayObject.prototype.__updateTransformAutoUpdateSize);
    },
    set: function(value) {
        if (value)
        {
            this.__updateTransformImp = DisplayObject.prototype.__updateTransformAutoUpdateSize;
        }
        else
        {
            this.__updateTransformImp = DisplayObject.prototype.__updateTransform;
        }
    }
});

Obviously the code would have to be cleaned up and really thought through, I wrote this implementation from the top of my head.

Once we have an autoUpdateSize property implemented, issue (1) is solved by simply setting the default values for the DisplayObject to have pivot, anchor and origin = (0, 0), autoUpdateSize = false; and the size property to (0, 0) or the inferred size of the object (sprites, for example, infer a size form the texture rect they represent); this way, the behaviour and performance are kept as they currently are in pixi.js while allowing programmers to use size, pivot, anchor, origin and configure objects to have their size automatically updated (at a performance cost) if needed.

With all that said, this might not be the best solution to the problem, but I believe it should be considered.

@GoodBoyDigital
Copy link
Member

Nice! Thanks for sharing this @darionco ! I Will get back to ya soon with my thoughts! 👍

@wojciak
Copy link
Contributor

wojciak commented Nov 4, 2014

I'm maybe weird but I actually like how the pivot property works now... :P
So for example if I want to make a falling object stick to a wheel it's falling on (after calculating the collision) I do:

// align the falling object with the wheel   
sprite.y = wheel.y;
sprite.x = wheel.x;

// sprite and wheel anchor's is (0.5,0.5) so I make sure that the sprite "sticks out"
sprite.pivot.y = wheel.height/2 + sprite.height;

// I align the sprite to a proper angle based on where he fell on
var rotation = (wheel.x - sprite.x) / (wheel.y - sprite.y);
sprite.rotation = Math.atan(-rotation);

And I don't really see the point of making it different, could it be less code/more readable?

@jonaslund
Copy link

+1
Would be great to have a normalised pivot point for graphics, as the current pivot solution is very clunky to work with. Alternatively, adding an anchor to the graphics would solve my issues.

@Arduinology
Copy link

+1, pivot is awkward to use and I have yet to get it working, an origin is a must in my opinion.

@englercj englercj self-assigned this Jan 7, 2015
@englercj englercj added this to the v3.0 milestone Jan 7, 2015
@Arduinology
Copy link

As a follow up to this @englercj you suggested (0,0) to be top left and (1,1) to be bottom right for the origin, what if I have a map and I want to zoom in on whatever the cursor is pointed to. This would not work with such low granularity. What about a pixel based origin?

e.g. 500 x 500 rectangle, if I point to an object at 250x and 375y the origin would be (250,375). From there a scale transformation would respect that origin and zoom as intended.

@agamemnus
Copy link
Contributor

In Javascript, numbers are 64-bit floating-point IEEE numbers. The precision limits are the same regardless of the scale.

Anyway, in terms of map zoom, you can handle this by setting a scale and offset value on a DisplayObjectContainer.

@Arduinology
Copy link

@agamemnus stage.scale = 5; has no effect on the stage scale.

@agamemnus
Copy link
Contributor

My bad. I was calling it a stage in my code but it's actually a DisplayObjectContainer.

@Arduinology
Copy link

@agamemnus see that is what I have done but there is no offset. I am having to set the pivot and then the scale of the DisplayObjectContainer, this gets complicated though. As you zoom in 0,0 of the mouse is no longer 0,0 of the DOC, it is now 125, 125 depending on how much you zoomed in. I am having issues accounting for this numerical transformation.

@agamemnus
Copy link
Contributor

I see. Maybe my code snippet can help you. offset_x and offset_y are the .x and .y values of the container.

 function jigsaw_onscroll (evt) {
  evt.preventDefault ()
  if (draw_area.controls_locked) return
  var xy = getXY (evt, action_area)
  var x = xy[0]
  var y = xy[1]
  var n = 1 + Math.abs(wheelDistance (evt)) / 4.5
  var zoom_factor = (getWheelData(evt) > 0) ? n : 1 / n
  jigsaw_doscroll (zoom_factor, x, y)

  // Returns +1 for a single wheel roll "up" and -1 for a single roll "down".
  function wheelDistance (evt) {
   var w = evt.wheelDelta, d = evt.detail
   if (!d) return w / 120                    // IE / Safari / Chrome.
   if (w) return w / d / 40 * d > 0 ? 1 : -1 // Opera.
   return -d / 3                             // Firefox.
  }
 }

 function jigsaw_doscroll (zoom_factor, x, y) {
  var old_scale = game_instance.zoom.scale
  x /= content_div.content_scale
  y /= content_div.content_scale
  var scale = old_scale * zoom_factor
  if (zoom_factor > 1) {
   if (scale > game_instance.zoom.scalemax) scale = game_instance.zoom.scalemax
  } else {
   if (scale < game_instance.zoom.scalemin) scale = game_instance.zoom.scalemin
  }

  var offset_x = (game_instance.zoom.offset_x - x) * (scale / game_instance.zoom.scale) + x
  var offset_y = (game_instance.zoom.offset_y - y) * (scale / game_instance.zoom.scale) + y

  game_instance.zoom.scale = scale
  game_instance.zoom.offset_x = offset_x
  game_instance.zoom.offset_y = offset_y
 }

@Arduinology
Copy link

What does the getXY() function look like?

Also what is your content_div? is that an actual HTML div?

Lastly it seems that you don't apply these variables to a DisplayObjectContainer, can you elaborate. Would I just do myDOC.position.set(offset_x, offset_y); and myDOC.scale.set(scale, scale);?

@agamemnus
Copy link
Contributor

function getXY (evt, target) {
 if (typeof target == "undefined") target = evt.target
 var rect = target.getBoundingClientRect(); return [evt.clientX - rect.left, evt.clientY - rect.top]
}

content_div is a centered <div> element that has CSS scaling applied to it. The canvas is attached to content_div.

You can see it in action here (Chrome only atm): http://tinyurl.com/llpn4hn

@Arduinology
Copy link

Eh, I can see it working for you but am still unsure about many things like getBoundingClientRect(); and action_area, maybe I am over complicating this.

@agamemnus
Copy link
Contributor

container.scale = scale
container.x = offset_x
container.y = offset_y

rect.left and rect.top are typically 0, I think. The important part is:

  var offset_x = (game_instance.zoom.offset_x - x) * (scale / game_instance.zoom.scale) + x
  var offset_y = (game_instance.zoom.offset_y - y) * (scale / game_instance.zoom.scale) + y
  game_instance.zoom.scale = scale
  game_instance.zoom.offset_x = offset_x
  game_instance.zoom.offset_y = offset_y

@Arduinology
Copy link

@agamemnus I ended up getting it, code isn't the prettiest right now but it works. I will post my solution once I have things looking decent.

@Arduinology
Copy link

@agamemnus and everyone here, you can see my implementation as a stand alone repo over here: https://github.com/Arduinology/Pixi-Pan-and-Zoom

@agamemnus
Copy link
Contributor

Great. Don't forget to add a license text or file (MIT is typical) so people can potentially use it.

@Arduinology
Copy link

Thanks, added.

@GoodBoyDigital
Copy link
Member

Closing this one peeps. This seems like it could make a nice plugin for v3 though if anyone fancies the task! Cheers! 👍

@lock
Copy link

lock bot commented Feb 26, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@lock lock bot locked and limited conversation to collaborators Feb 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests