Lotechnica

Technical writings by Nathan Shafer

AnchorSprite: Setting an anchor point on any Sprite

Having done a lot of work with matrices in Gideros recently, during a discussion at the Gideros forums it occurred to me that with the Matrix class that I recently covered, I could possibly address an oft-requested feature of Gideros. The problem is that in Gideros, all Sprite objects have an anchor point of [0,0] that you can't change. So all transformations such as rotation and scaling are relative to the top left point of the Sprite. The one exception to this is with Bitmap objects, where there is a setAnchorPoint() method. But this doesn't help for other Sprite based objects, such as TextFields or Shapes.

So I whipped together a class called AnchorSprite, which when added to your project, adds a setAnchorPoint() and getAnchorPoint() method to every Sprite object and all objects that inherit from Sprite. Further, bowerandy had the great idea to make this support completely inactive until you set an anchor, that way you could avoid any overhead on sprites that still had the default anchor point.

So I'm going to explain a little bit about how this class works. I'm going to skip over most of the matrix stuff since I already covered that in detail.

Intercepting Sprite transform methods

The first step is to intercept all calls to transform methods of the Sprite class. This includes setPosition(), setScale(), setRotation() as well as all the other specific ones, setX(), setScaleY(), etc. We also need to intercept set() itself. We have to do this, because we don't want the Sprite class to actually change anything. Instead, we just take the desired changes and save them in private member variables. For example, here is how we intercept set().

-- Intercept Sprite's set and get functions for position, scale and rotation
function AnchorSprite:set(key, value)
    if value then
        if key == "x" then
           self._positionX = value
        elseif key == "y" then
           self._positionY = value
        elseif key == "scaleX" then
           self._scaleX = value
        elseif key == "scaleY" then
           self._scaleY = value
        elseif key == "rotation" then
           self._rotation = value
        else
           self._anchorBackup.set(self, key, value)
        end
        self:_applyTransforms()
    end
end

You'll notice that this is in the AnchorSprite class and not the Sprite class just yet. We don't want to intercept it until an anchor point is set, which I'll show in a bit.

So just to show another example, this is what our intercepted setPosition() method will look like.

function AnchorSprite:setPosition(x, y)
    self:set("x", x)
    self:set("y", y)
end

Applying the transforms

The thing to notice is that we're not doing anything with the values being passed in, just storing them in private member variables. This is because we need to basically keep a stack of transforms in memory, so that we can apply them all whenever any of them change. This is done in the _applyTransformsmethod, which is called every time set() is called.

-- New function to apply all transforms whenever anything changes
function AnchorSprite:_applyTransforms()

This function will do all the calculations needed to apply the transforms. It starts from scratch with a brand new Matrix.

    -- Create a new identity matrix
    local matrix = Matrix.new()

We then apply that new matrix to the sprite, thus resetting it back to completely default location, rotation and scale. This is so we can get an accurate width and height, so we know how to apply the anchor, which is specified as a percentage of the dimensions of the Sprite. We could forgo this and just have the anchor set as a specific pixel location, but I chose to do it this way to mirror the way that the Bitmap:setAnchorPoint() works. Plus it means you can change the dimensions of the Sprite and always know that the anchor point will be in the place you expect without having to calculate the anchor point yourself.

 -- Zero ourselves out so we can get accurate width and height
 self:setMatrix(matrix)

 -- Calculate the actual position of the anchor
 local anchorOffsetX = self._anchorPointX * self:getWidth()
 local anchorOffsetY = self._anchorPointY * self:getHeight()

The next set of operations apply our transformations to the Matrix, effectively concatenating them. They should all be pretty self explanatory if you've read my matrix overview. In short, each is in turn concatenated onto the matrix much like characters can be concatenated together to form a string. First we set the position, then rotation and scale.

 -- set position
 matrix:translate(self._positionX, self._positionY)

 -- concatenate rotation and scale matrices
 matrix:rotate(self._rotation)
 matrix:scaleX(self._scaleX)
 matrix:scaleY(self._scaleY)

And finally the magic part, which is to offset by the negative of the anchor point.

 -- concatenate offset to new origin in modified coordinate space
 matrix:translate(-anchorOffsetX, -anchorOffsetY)

Since we concatenate the anchor point translate last, it is applied to the new coordinate space that is a result of the previous concatenations, and not based on the original coordinate space. Thus if you have an anchor point of [.5,.5] and rotate clockwise 45 degrees, the last translate will move the whole sprite along that 45 degree angle, or basically straight up. If you were to try this with the original Sprite:setPosition, it would move based on the original coordinate space, and so end up moving the sprite up and to the left, which is not what you wanted.

Lastly we apply our built-up matrix with setMatrix(), thus causing all the transforms to get applied. This will affect the current sprite as well as any children in the scene graph.

 -- Apply the new matrix
 self:setMatrix(matrix)

This is our finished _applyTransforms() function.

-- New function to apply all transforms whenever anything changes
function AnchorSprite:_applyTransforms()
    -- Create a new identity matrix
    local matrix = Matrix.new()

    -- Zero ourselves out so we can get accurate width and height
    self:setMatrix(matrix)

    -- Calculate the actual position of the anchor
    local anchorOffsetX = self._anchorPointX * self:getWidth()
    local anchorOffsetY = self._anchorPointY * self:getHeight()

    -- set position
    matrix:translate(self._positionX, self._positionY)

    -- concatenate rotation and scale matrices
    matrix:rotate(self._rotation)
    matrix:scaleX(self._scaleX)
    matrix:scaleY(self._scaleY)

    -- concatenate offset to new origin in modified coordinate space
    matrix:translate(-anchorOffsetX, -anchorOffsetY)

    -- Apply the new matrix
    self:setMatrix(matrix)
end

Optimization

So the last bit is to make this code only get put into place when an anchor point is set. This way, after you add AnchorPoint.lua to your project, it doesn't do anything to any Sprites by default. Only those that you set an anchor point for with setAnchorPoint will start to intercept the transform methods, thus resulting in less overhead. This is why our functions so far have been in the AnchorSprite class. First step is to add the setAnchorPoint() method to the Sprite class along with our private member variables for storing the transforms.

Sprite._anchorSupport = false
Sprite._anchorBackup = {}
Sprite._anchorPointX = 0
Sprite._anchorPointY = 0
Sprite._positionX = 0
Sprite._positionY = 0
Sprite._scaleX = 1
Sprite._scaleY = 1
Sprite._rotation = 0

-- New functions in Sprite for setting the anchor
function Sprite:setAnchorPoint(x, y)
    self._anchorPointX = x
    self._anchorPointY = y or x

    if x ~= 0 or y ~= 0 and not self._anchorSupport then
    self:_enableAnchorSupport()
    elseif x == 0 and y == 0 and self._anchorSupport then
    self:_disableAnchorSupport()
    end
end

As you can see, this mainly calls a function called _enableAnchorSupport() that does all the work of modifying the Sprite class accordingly.

function Sprite:_enableAnchorSupport()
    self._anchorSupport = true

    -- Get current values
    self._positionX = self:getX()
    self._positionY = self:getY()
    self._scaleX = self:getScaleX()
    self._scaleY = self:getScaleY()
    self._rotation = self:getRotation()

    -- Override Sprite functions with AnchorSprite versions
    for k,v in pairs(AnchorSprite) do
        if type(v) == "function" then
            --print("AnchorSprite", k, v)
            if self[k] then
                -- Backup existing function
                self._anchorBackup[k] = Sprite[k]
            end

            -- Replace with AnchorSprite version
            self[k] = v
        end
    end

    self:_applyTransforms()
end

This initializes our transform variables, then does a bit of magic of modifying the Sprite class itself. If you're unfamiliar with how Lua works, the thing to remember is that almost everything in Lua is a table. All classes are just a table, and values of that table can be variables or references to functions. So after backing up the original reference in _anchorBackup (a table), we just replace the reference in Sprite's table with the one in AnchorSprites table. So now functions like AnchorSprite:_applyTransforms from earlier are now Sprite:_applyTransforms. The methods in AnchorSprite are never meant to be called while they're in AnchorSprite, and would actually throw weird errors. We make backups so that if the anchor is reset to [0,0], we can called _disableAnchorSupport() and return Sprite to the way it was.

Result

In the end, the result is that to the user, all we've done is added Sprite:setAnchorPoint() and Sprite:getAnchorPoint(), and everything just works. A sample project is available to see how it can be implemented and to show the results. To use it, simply include AnchorSprite.lua and Matrix.lua into your project.

Of course, this is hopefully just a stopgap solution until Gideros builds this functionality in. Until then, this is one way to get around the limitation. Let me know if you find it useful.

Share this:

Comments

comments powered by Disqus