SourceWidth and Height are not being respected in Trimmed Image Sheet

Hi Vince,

In a complete “slap forehead” moment, I may have a solution to your issue. Instead of using “display.newImageRect()”, use “display.newImage()”. Also, do not set the object’s width/height anytime after… leave it as it’s rendered.

I know you understand the basic differences between these 2 APIs, but here’s the catch: because (and only because) you’re pulling a frame from an image sheet, you should still get the proper “suffixed” image using “display.newImage()”, properly pulled from the appropriate default, @2x, and @4x sheets (or however you’ve set them up and named the suffixes).

When dealing with static images (files) and dynamic scaling, we always recommend “display.newImageRect()”, but in this case, assuming you’ve properly set up the image sheets with the “sheetContentWidth” and “sheetContentHeight” properties (it looks like you have), Corona should “know” the sheet to pull the image from and how to size it accordingly, without you ever needing to specify any width/height on the display side (merely in the sheet setup side).

Can you please test this out? My initial findings on this were not taking trimming into account (sourceX, sourceY, etc.) so perhaps it won’t work after all… but it’s worth a shot.

Brent

Hi Brent,

Thanks for taking the time to follow up on this. Can you explain a little further how this would give me an image of arbitrary width and height? From the docs, display.newImage doesn’t take width or height parameters so how do I get it to fit within whatever sized box I need?

Oh, perhaps I was wrong (not about my findings but about your project). I see you still need to position these objects in a tile-based manner, so you need to “assume” they’re 64x64, even after TexturePacker trims them.

Another potential idea though: have you considered using non-center anchor points for some objects? For example, if I look at “conveyor_2.png” in your sheet, TP is clearly going to trim off transparent space from the top and left. What you could do, then, is use “display.newImage()” as I mentioned above, which would give you a smaller image (say 50x50). Since you know that particular piece should be aligned with other pieces based on its bottom-right corner (since no trimming occurred on those sides), you could set the tile’s anchor to 1,1 and align that corner point along the bottom side of that “tile row” and the right edge along the right side of that tile row.

Hopefully that makes some sense. :slight_smile:

Hi Vince,

There is a fundamental problem in regards of untrimming the frames. The imagesheet is a single texture that resides in the texture memory. It’s a single object for many sprites on screen, hence the effectiveness. But because all frames are sitting together, you can’t manipulate the texture without dealing with the neighbor frames. There are two possible ways of how to untrim a frame when at the end you will have a single display object for each tile with correct width/height.

  1. Use display.newSnapshot() for each tile. With this method you create a new texture in memory for each tile and paste your trimmed frame into the canvas. But that method annihilates that texture memory bonus you’ve gained when you trimmed the frames. However, that method isn’t bad if you create only enough snapshots to fill up the screen and when the map is moved, you quickly swap the frames inside them and make an illusion of an endless map. That requires quite a bit of coding and has some performance hit, however that’s totally possible and can be quite effective in general, but it’s outside of the realm of desired solutions.

  2. Use display.newMesh() for each tile. That method doesn’t create a new texture in memory, but it produces much more triangles instead, again hitting the performance. You can create a 9-patch mesh and place the actual frame image into the central slice. Change central slice size according to the trimmed frame size. And leave surrounding slices with a blank region of the texture. That is done by manipulating the UV coordinates of the mesh’s vertices. A complex solution that requires quite a bit of coding as well.

Using display.newRect() and simply filling it up with the ImageSheetFill won’t work because that fill can’t scale or offset.

The only easy solution as I see it, is to have two display objects in a group for each tile. Make a transparent rect with correct tile size and put it above the image frame (newImage or newImageRect or newRect with ImageSheetPaint). That way you will have a group display object with correct width/height and you can offset the frame inside it.

As you can see, there is really no automatic&easy solution to untrim the frames by Corona itself. That is something for the developer to figure out how to achieve in a desired way.

So it seems like using trimmed frames in your case is not very suitable.

Thanks,

Sergey

@Sergey

Thanks for looking into it. I am sorry to hear that the newRect + imageSheetFill idea didn’t pan out.

I was thinking that it could be handled a bit easier, using math instead of trying to recreate the actual transparent pixels of the original untrimmed image. Similar to what Rob mentioned in a reply above. (Also that’s why I used “untrimmed” in quotes in my feedback request :slight_smile: ).

Anyway, this is what I had in mind and I made a simple function to illustrate the idea:

local sheetInfo = require("images.conveyor\_trimmed") local conveyorSheet = graphics.newImageSheet( "images/conveyor\_trimmed.png", sheetInfo:getSheet() ) local function getUntrimmedImage(parentGroup, sheet, frame, targetWidth, targetHeight)              local sheetFrame = sheetInfo.sheet.frames[frame]     local sourceWidth, sourceHeight = sheetFrame.sourceWidth, sheetFrame.sourceHeight     local actualWidth, actualHeight = sheetFrame.width, sheetFrame.height          local image = display.newImageRect(parentGroup, sheet, frame, actualWidth, actualHeight)          --Check if the frame is actually trimmed     if sourceWidth and sourceHeight and         (sourceWidth ~= actualWidth or sourceHeight ~= actualHeight) then                  local widthRatio = actualWidth / sourceWidth         local heightRatio = actualHeight / sourceHeight                  local scaleX = targetWidth / actualWidth         local scaleY = targetHeight / actualHeight                  image.width = actualWidth \* scaleX \* widthRatio         image.height = actualHeight \* scaleY \* heightRatio             end          return image end

Ideally this would be incorporated into the display.newImageRect() function and it would be done behind the scenes by Corona. Corona already detects if there is a sourceX and Y offset and automatically applies it, so I’m assuming there’s a way to do the same with detecting when the images have been trimmed by seeing if sheetInfo contains sourceWidth and height attributes.

This function works pretty well for keeping trimmed images proportional when using an arbitrary width and height, but there is an issue with sourceX and Y offsets. Either they are not being respected or I don’t understand how they are supposed to work (or TexturePacker is doing something weird), but my trimmed images are not aligned properly when placed in a grid. Here is a screenshot to illustrate what I mean. The upper left conveyor uses my untrim function, the lower right uses the actual untrimmed image sheet.

sVdw3yY.png

Also, I had to update sheetInfo.lua to rearrange the frames because TexturePacker likes to put frame 10 after frame 1. If you want the updated sheet I can PM it to you.

Hi Vince,

Good news. After I spotted a mistake in my code I was able to use display.newRect() and ImageSheetFill, I was wrong about it.

Sergey

local name = 'conveyor\_trimmed' local sheetInfo = require(name) local map = display.newGroup() map.x, map.y = display.contentWidth / 2, display.contentHeight / 2 local tileSize = 128 local mapSize = 4 for y = -mapSize / 2, mapSize / 2 do for x = -mapSize / 2, mapSize / 2 do -- Grid pattern local rect = display.newRect(map, x \* tileSize, y \* tileSize, tileSize, tileSize) rect:setFillColor(0, (x + y ) % 2 \* 0.5, 0.5) local frameIndex = math.random(1, 16) local frameInfo = sheetInfo.sheet.frames[frameIndex] local w, h = frameInfo.width, frameInfo.height local tile = display.newRect(map, x \* tileSize + frameInfo.sourceX - tileSize / 2, y \* tileSize + frameInfo.sourceY - tileSize / 2, w, h) tile.anchorX, tile.anchorY = 0, 0 tile.fill = {type = 'image', filename = name .. '.png'} tile.fill.scaleX, tile.fill.scaleY = sheetInfo.sheet.sheetContentWidth / w, sheetInfo.sheet.sheetContentHeight / h tile.fill.x = (frameInfo.x + w/2) / sheetInfo.sheet.sheetContentWidth - 0.5 tile.fill.y = (frameInfo.y + h/2) / sheetInfo.sheet.sheetContentHeight - 0.5 end end

19f72946be3846839a592b00118c4048.png

@Sergey

I think this is great step in the right direction. I took the liberty of adapting your code to accept any arbitrary width and height (which is the issue that spawned this thread).

local name = 'images/conveyor\_trimmed' local sheetInfo = require("images.conveyor\_trimmed") local imageSheet = graphics.newImageSheet( "images/conveyor\_trimmed.png", sheetInfo:getSheet() ) local map = display.newGroup() map.x, map.y = display.contentWidth / 2, display.contentHeight / 2 local targetWidth = 90 local targetHeight = 90 local mapSize = 4 for y = -mapSize / 2, mapSize / 2 do for x = -mapSize / 2, mapSize / 2 do -- Grid pattern local rect = display.newRect(map, x \* targetWidth, y \* targetHeight, targetWidth, targetHeight) rect:setFillColor(0, (x + y ) % 2 \* 0.5, 0.5) rect.anchorX, rect.anchorY = .5, .5 local frameIndex = math.random(1, 16) local frameInfo = sheetInfo.sheet.frames[frameIndex] local sourceWidth, sourceHeight = frameInfo.sourceWidth, frameInfo.sourceHeight local actualWidth, actualHeight = frameInfo.width, frameInfo.height local widthRatio = actualWidth / sourceWidth local heightRatio = actualHeight / sourceHeight local frameScaleX = targetWidth / actualWidth local frameScaleY = targetHeight / actualHeight local targetSourceWidthRatio = targetWidth / sourceWidth local targetSourceHeightRatio = targetHeight / sourceHeight local w = actualWidth \* frameScaleX \* widthRatio local h = actualHeight \* frameScaleY \* heightRatio local tile = display.newRect(map, x \* targetWidth + (frameInfo.sourceX \* targetSourceWidthRatio) - targetWidth / 2, y \* targetHeight + (frameInfo.sourceY \* targetSourceHeightRatio) - targetHeight / 2, w, h) tile.anchorX, tile.anchorY = 0, 0 tile.fill = {type = "image", filename = name .. ".png"} tile.fill.scaleX = sheetInfo.sheet.sheetContentWidth / actualWidth tile.fill.scaleY = sheetInfo.sheet.sheetContentHeight / actualHeight tile.fill.x = (frameInfo.x + actualWidth/2) / sheetInfo.sheet.sheetContentWidth - 0.5 tile.fill.y = (frameInfo.y + actualHeight/2) / sheetInfo.sheet.sheetContentHeight - 0.5 end end

Here are a couple examples of it in action:

HxNU6T1.png?1

Tb5uWu6.png?1

Here it is with a completely arbitrary width and height. It should work for untrimmed frames that are rectangular too, not just squares.

PryaSYI.png?1

I tried wrapping it up in a nice function that would let you pass in a graphics.imageSheet to take advantage of the imageSheet fill, but that presented issues. Once again, the sourceX and Y offsets don’t seem to work properly in Corona (or I really don’t understand how they’re supposed to work). Here is an example of how it turned out. Notice the red rectangles/squares are where the tiles should be positioned. The white dots are positioned at the exact tile.x and tile.y, yet the tiles themselves are not positioned there.

V3VQe2a.png?1

As you can see this isn’t exactly an obvious or trivial solution for us devs to come up with on our own. So what are the chances that this can be incorporated into the SDK? Perhaps by adding a new function call like display.newUntrimmedImageRect().

There are a few extra points to consider.

  1. The width and height of the tile are the trimmed values. I think there should be untrimmedWidth and untrimmedHeight attributes as well that represent the target width and height that you pass into the function. That way you can do things like this when you’re trying to place images on screen:

    local x = tile.x + tile.untrimmedWidth

  2. I don’t know how the anchor point needing to be in the upper left corner will affect other use cases

  3. I also don’t know how the sourceX and Y offsets will be incorporated when you manipulate the x and y values outside of the function call. For example, would tile.x = 0 place the tile at the screenOrigin or screenOrigin + sourceX offset?

Hey Vince.

For arbitrary tile width/height you just need to scale the sprite and sourceX, sourceY values, if I understood you correctly.

Does this work for you?

local name = 'conveyor\_trimmed' local sheetInfo = require(name) local map = display.newGroup() map.x, map.y = display.contentWidth / 2, display.contentHeight / 2 local tileW, tileH = 64, 96 local mapSize = 4 for y = -mapSize / 2, mapSize / 2 do for x = -mapSize / 2, mapSize / 2 do -- Grid pattern local rect = display.newRect(map, x \* tileW, y \* tileH, tileW, tileH) rect:setFillColor(0, (x + y ) % 2 \* 0.5, 0.5) local frameIndex = math.random(1, 16) local frameInfo = sheetInfo.sheet.frames[frameIndex] local w, h = frameInfo.width, frameInfo.height local sx, sy = tileW / frameInfo.sourceWidth, tileH / frameInfo.sourceHeight local tile = display.newRect(map, x \* tileW + frameInfo.sourceX \* sx - tileW / 2, y \* tileH + frameInfo.sourceY \* sy - tileH / 2, w, h) tile:scale(sx, sy) tile.anchorX, tile.anchorY = 0, 0 tile.fill = {type = 'image', filename = name .. '.png'} tile.fill.scaleX, tile.fill.scaleY = sheetInfo.sheet.sheetContentWidth / w, sheetInfo.sheet.sheetContentHeight / h tile.fill.x = (frameInfo.x + w/2) / sheetInfo.sheet.sheetContentWidth - 0.5 tile.fill.y = (frameInfo.y + h/2) / sheetInfo.sheet.sheetContentHeight - 0.5 end end

3c4b732219304a04aa3136d43fcb938b.png

Good call! Not sure why I made it more complicated than necessary. Can someone from the Corona staff chime in on the points I made in my previous post though? Namely: Can this be implemented in the core SDK? And why does sourceX and sourceY not work as expected?

Vince, actually this is already implemented in display.newImage(), you can use this function instead of display.newRect(). However, there is a small bug that creates these misalignments and visible gaps between the tiles. Using newRect is a workaround for now. The bug is being worked on.

And sourceX, sourceY do work as expected, they show offset from the upper left corner of the untrimmed image to the trimmed version inside it.

If it wasn’t for the bug, your code inside the loop could look like this:

local frameIndex = math.random(1, 15) local frameInfo = sheetInfo.sheet.frames[frameIndex] local sx, sy = tileW / frameInfo.sourceWidth, tileH / frameInfo.sourceHeight local tile = display.newImage(map, imageSheet, frameIndex) tile.x, tile.y = x \* tileW, y \* tileH tile:scale(sx, sy)

Have you looked at the screenshots that I posted previously which show that Corona is not interpreting the sourceX and Y properly? The tiles are all misaligned. See this screenshot for a better illustration. It displays the tile first, then a white rect below it depicting where the tile should be according to the sourceX of that frame in sheetInfo. The tile in the upper left corner is placed at x = 0, yet the image itself extends past the line. I don’t even know what’s going on with the endcap pieces along the second vertical red line. They are so far off from the intended offset.

sjOmcM5.png

Here is the code to reproduce:

local black = display.newRect(0 + display.screenOriginX, 0, display.actualContentWidth, display.actualContentHeight) black.anchorX, black.anchorY = 0, 0 black:setFillColor(0) local line1 = display.newLine(0,0, 0,display.actualContentHeight) line1.width = 3 line1:setStrokeColor(1,0,0) local line2 = display.newLine(display.actualContentWidth\*.25,0, display.actualContentWidth\*.25,display.actualContentHeight) line2.width = 3 line2:setStrokeColor(1,0,0) local line3 = display.newLine(0 + display.screenOriginX, 20, display.actualContentWidth, 20) line3.width = 3 line3:setStrokeColor(1,0,0) local sheetInfo = require("images.conveyor\_trimmed") local conveyorSheet = graphics.newImageSheet( "images/conveyor\_trimmed.png", sheetInfo:getSheet() ) for i=8, 12 do local w, h = sheetInfo.sheet.frames[i].width, sheetInfo.sheet.frames[i].height local sourceX = sheetInfo.sheet.frames[i].sourceX local tile = display.newImageRect(conveyorSheet, i, w, h) tile.anchorX, tile.anchorY = 0, 0 tile.x = line1.x tile.y = line3.y + (i-8) \* 260 local rect = display.newRect(0, 0, w, h) rect.anchorX, rect.anchorY = 0, 0 rect.x = line1.x + sourceX rect.y = tile.y + tile.height + 20 rect:setFillColor(1) end for i=12, 16 do local w, h = sheetInfo.sheet.frames[i].width, sheetInfo.sheet.frames[i].height local sourceX = sheetInfo.sheet.frames[i].sourceX local tile = display.newImageRect(conveyorSheet, i, w, h) tile.anchorX, tile.anchorY = 0, 0 tile.x = line2.x tile.y = line3.y + (i-12) \* 260 local rect = display.newRect(0, 0, w, h) rect.anchorX, rect.anchorY = 0, 0 rect.x = line2.x + sourceX rect.y = tile.y + tile.height + 20 rect:setFillColor(1) end

I’ve modified your piece of code to show the proper relation to sourceX.

I hope that example can make it clear.

local black = display.newRect(0 + display.screenOriginX, 0, display.actualContentWidth, display.actualContentHeight) black.anchorX, black.anchorY = 0, 0 black:setFillColor(0) local line1 = display.newLine(0,0, 0,display.actualContentHeight) line1.width = 3 line1:setStrokeColor(1,0,0) local line2 = display.newLine(display.actualContentWidth\*.25,0, display.actualContentWidth\*.25,display.actualContentHeight) line2.width = 3 line2:setStrokeColor(1,0,0) local line3 = display.newLine(0 + display.screenOriginX, 20, display.actualContentWidth, 20) line3.width = 3 line3:setStrokeColor(1,0,0) local sheetInfo = require("conveyor\_trimmed") local conveyorSheet = graphics.newImageSheet( "conveyor\_trimmed.png", sheetInfo:getSheet() ) for i=8, 16 do local frame = sheetInfo.sheet.frames[i] local x, y if i \< 12 then x, y = line1.x, (i - 8) \* 260 else x, y = line2.x, (i - 12) \* 260 end -- True tile size, "untrimmed" local back = display.newRect(0, 0, frame.sourceWidth, frame.sourceHeight) back.anchorX, back.anchorY = 0, 0 back.x = x back.y = line3.y + y back:setFillColor(0, 0.2, 0.2) -- Trimmed tile object with sourceX, sourceY already taken into account local tile = display.newImage(conveyorSheet, i) tile.x = back.x + frame.sourceWidth / 2 -- offset because of different anchor point tile.y = back.y + frame.sourceHeight / 2 local rect = display.newRect(0, 0, frame.width, frame.height) rect.anchorX, rect.anchorY = 0, 0 rect.x = back.x + frame.sourceX -- white "shadow" of the tile, showing that sourceX is correct rect.y = back.y + frame.sourceHeight rect:setFillColor(1) end

88d66c0a2e9e4d99aa7164b8308e5ec2.png

See, this doesn’t make sense to me. Your example requires the anchor point for tile to be at .5, .5. If you change the anchor point to  be 0,0 and remove the offset like so:

local tile = display.newImage(conveyorSheet, i) tile.anchorX, tile.anchorY = 0, 0 --set anchor to upper left corner tile.x = back.x -- remove offset tile.y = back.y + frame.sourceHeight / 2 local rect = display.newRect(0, 0, frame.width, frame.height) rect.anchorX, rect.anchorY = 0, 0 rect.x = back.x + frame.sourceX -- white "shadow" of the tile, showing that sourceX is correct rect.y = back.y + frame.sourceHeight rect:setFillColor(1)

Then I would expect the tile and white rect to line up on the left edge, especially when sourceX = 0. But they don’t. The result looks like the screenshot from my previous post. Why is that?

Because x,y of the tile already contains sourceX, sourceY offset and Corona’s default anchor point is center.

If you want to use the 0,0 anchor, you would need to add an additional offset.

local tile = display.newImage(conveyorSheet, i) tile.anchorX, tile.anchorY = 0, 0 --set anchor to upper left corner tile.x = back.x + frame.sourceWidth / 2 - frame.width / 2 tile.y = back.y + frame.sourceHeight / 2 - frame.height / 2

Alternatively, you can use the center anchor for all objects, but you would need to convert sourceX from being TopLeft based to Center based.

-- True tile size, "untrimmed" local back = display.newRect(0, 0, frame.sourceWidth, frame.sourceHeight) back.x = x back.y = line3.y + y back:setFillColor(0, 0.2, 0.2) -- Trimmed tile object with sourceX, sourceY already taken into account local tile = display.newImage(conveyorSheet, i) tile.x = back.x tile.y = back.y local rect = display.newRect(0, 0, frame.width, frame.height) rect.x = back.x + frame.sourceX - frame.sourceWidth / 2 + tile.width / 2 rect.y = back.y + frame.sourceHeight rect:setFillColor(1)

Makes sense now?

Yes I think I understand better now. Thanks

Anyone?

What happens if you try and use display.newImage() instead?

Hi Rob, using display.newImage yields the same result. Setting square dimensions for a rectangular piece still causes it to be stretched out. Either way, if I used newImage I would lose out on the benefits of dynamic content scaling that newImageRect provides.

It seems that Corona accounts for positioning of images with trimmed transparent pixels but doesn’t account for the actual dimensions. But what’s the point of having sourcewidth and height if it only affects the position? Corona should do the math behind the scenes to produce an image with the same proportions of the untrimmed image relative to the sourcewidth and height. Otherwise, how else are you supposed to create imagesheets for tiled assets? I can leave the images untrimmed but that takes up unnecessary texture memory.

Can you put together a simple test case that demonstrates the problem?

Hi Vince,

I don’t understand this statement:

“The images are trimmed (rectangles and other shapes) in TexturePacker but are technically 64x64.”

How can a “rectangle” be “technically 64x64” but not be a square? I would need to see your image sheet to understand what you’re attempting to do. This may be a legitimate Corona issue, but many of these cases stem from an improper output setting from TexturePacker.

Best regards,

Brent