VTAB1008 and Letterbox ... not working?

I have a customer that using a VTAB1008 which has a screen resolution of 1024 x 768 … which is the same as the older iPads.

My config is set:

content = {

        width = 320,

        height = 480, 

        scale = “letterBox”,

using this math to find the effective width and height of the screen

local minVisibleY = display.screenOriginY

local maxVisibleY = display.viewableContentHeight + -1 * minVisibleY

local minVisibleX = display.screenOriginX

local maxVisibleX = display.viewableContentWidth + -1 * minVisibleX

local screenHeight = maxVisibleY - display.screenOriginY

local screenWidth  = maxVisibleX - display.screenOriginX

On an iPad i get an effective screenHeight x screenWidth to work with of 480 x 360 

This user is seeing screenWidth of 388 leading me to believe that the letterBox setting in my config are not being respected.  I do not know what screenHeight is being seen on this device as my knowledge is coming from an error report that lacks that detail.  But i am sure that the app believes that the width is 388 … which is the closest even number to 768 / 2 without going over. 

Is there any chance that letterBox is not being handled properly on this device?

Hi @mslack,

It’s hard to determine what’s happening without a more detailed error report. Can you get any reports from this user when testing the other display size APIs like “contentHeight”, “actualContentHeight”, etc.?

Brent

Android’s bottom navigation bar steals screen real-estate.  That is, it’s not overlaid on top of your app and instead makes your app window height smaller.  Corona will scale your app to fit the width and height of the app window, and since it’s not at the aspect ratio that you think it is, it will show letterbox bars.

To make things more interesting, Android’s bottom navigation bar moves with the orientation of the device.  Unlike iPad, the width and height of your app are not simply swapped when changing from portrait to landscape and vice-versa.  Since the bottom bar move with your app, the aspect ratio of your app will completely change.  For a letterboxed app, this means that you’ll see thin bars at one orientation and thicker bars at another.  If anything, this proves that Corona’s scaling works correctly… although not everyone expects the scale of their app to change dynamically, but that’s just how it works on Android.  Corona provides a “resize” event to detect when this happens in the case where the screen is resized/rescaled without an orientation change.

Any case, I hope this helps!

The app is in portrait orientation

Well what i do know is that the actualContentWidth is 388.  So given a footprint of 320x480 we can compute the scale factor to be about 2.0 if everything is working properly.

388 x 1.979 = 768 … aka the width of the device.  

thus the ActualContentHeight would be:

480 x 1.979 = 950

… thus 1024(screen width) - 950 =74 

So i guess what you are saying Joshua is that  if 74 pixels would be used by the android system for control, etc we would have an effective screen of 768 x 950

which would give us the scale factor of 1.979 … and so 320 * 1.979 = 633    then 768 - 633 = 135

135 / 1.979 = 68 extra pixels back in original format thus the 388 actualContentWidth.

So … it all makes sense to me now.  Seems a lot of screen real estate for android to take away but …

No now the real run … we really need dynamic masks at least for rectangular area.  I have tried numerous times to build masks dynamically and when they get saved they become off by one pixel and then they don’t work on some devices. It all works fine when we are building masks for scale factors that are whole numbers but when you get a scale factor that is like 1.979… and you go to save the dynamic mask you have created (keeping in mind that the mask needs to be created and the unscaled reference size) you do not get exact results that you are expecting when the image goes to disk.

…  I think that this is the real issue that i need help with then … as i cannot anticipate every possible screen size.

Thanks a bunch

m

How are you creating your masks dynamically?

Is your app actually generating a grayscale bitmap file?

As you may already know, a grayscale image’s width needs to be a multiple of 4 pixels in order to be rendered correctly in OpenGL.  This is known as byte packing alignment.  If it’s short, then your image will appear skewed onscreen.

You can use properties “display.contentScaleX” and “display.contentScaleY” to convert content coordinates to pixels.  Converting to pixels would look like this…

   local widthInPixels = myObject.contentWidth / display.contentScaleX

   local heightInPixels = myObject.contentHeight / display.contentScaleY

As you’ve noted, you’ll likely get a decimal pixel value for your width and height.  So, you’ll need to round up to the next whole number using Lua’s math.ceil() function like this…

   widthInPixels = math.ceil( widthInPixels )

Next, you’ll need to round up to the next multiple of 4.  You can do this with the Lua modulo ‘%’ operator.

   widthInPixels = widthInPixels + (4 - (widthInPixels % 4))

If you do the above, you can calculate a valid pixel width and height that you can use for the mask you need to generate.

So, does this help?

here is how i am generating a mask … works fine for iphone and iphone4 but go to a tablet where the scale factor is not a simple multiple and it never saves the file right … always off by a pixel which causes the mask to be broken and not work.

Maybe you can see where i am going wrong.  The following code works just fine on iphone and iphone4 … you get a nice 20 x 20 image masking the requested 12 x 12 region.

But you take it to the iPad simulator or other with a fractional scale factor and even though it prints out that the size of the image is 20 x 20 just prior to saving it to disk …  the image on disk is 19 x 19 on disk and the mask is not centered anymore.

This has vexed me every time i pick this code back up saying today …!

Cheers,

m

function generateMaskFile( width, height ) local filename = "mask-"..width.."x"..height .. ".png" -- --Adjust height and width to a multiple of 4 -- as per requirement given by Corona -- if (width % 4 ~= 0) then width = width + (4 - (width % 4)) end if (height % 4 ~= 0) then height = height + (4 - (height % 4)) end local scaledWidth = display.contentScaleX \* width local scaledHeight = display.contentScaleY \* height print("Masking file missing for screen size: " .. width .. " / " .. height .. "\ndisplay.actualContentWidth = \n" .. display.actualContentWidth .. "\ndisplay.actualContentHeight = " .. display.actualContentHeight ) local realWidth = width + 8 local realHeight = height + 8 -- create the mask group and the surrounding black area local maskgroup = display.newGroup() local invisible = display.newRect(maskgroup, 0, 0, realWidth, realHeight) invisible:setFillColor(0, 0, 0) print("Rect Height and width: " .. invisible.width .. " / " .. invisible.height) local visible = display.newRect(maskgroup, 0, 0, width, height) invisible:setReferencePoint(display.CenterReferencePoint) visible:setReferencePoint(display.CenterReferencePoint) invisible.x,invisible.y = 0 , 0 visible.x,visible.y = 0, 0 maskgroup.x,maskgroup.y = display.contentCenterX, display.contentCenterY maskgroup.xScale = display.contentScaleX maskgroup.yScale = display.contentScaleY print("maskgroup width/height: " .. maskgroup.width .. " / " .. maskgroup.height) display.save( maskgroup, filename, system.TemporaryDirectory ) maskgroup:removeSelf() end generateMaskFile(12, 12)

You shouldn’t use display.save() to create the mask because…

  1. It creates a 32-bit color image instead of an 8-bit grayscale image.

  2. This function will round down to the nearest pixel for left and top most bounds and round up to the nearest pixel on the right and bottom bounds.  It’ll never give you the pixel precision that you want.

It would be better to generate the grayscale bitmap yourself via file IO.  That is the surest way to get the pixel precision that you need.  But it would likely be simpler to create mask image files at different resolutions, load the appropriate one, and then scale it to fit.

Hi @mslack,

It’s hard to determine what’s happening without a more detailed error report. Can you get any reports from this user when testing the other display size APIs like “contentHeight”, “actualContentHeight”, etc.?

Brent

Android’s bottom navigation bar steals screen real-estate.  That is, it’s not overlaid on top of your app and instead makes your app window height smaller.  Corona will scale your app to fit the width and height of the app window, and since it’s not at the aspect ratio that you think it is, it will show letterbox bars.

To make things more interesting, Android’s bottom navigation bar moves with the orientation of the device.  Unlike iPad, the width and height of your app are not simply swapped when changing from portrait to landscape and vice-versa.  Since the bottom bar move with your app, the aspect ratio of your app will completely change.  For a letterboxed app, this means that you’ll see thin bars at one orientation and thicker bars at another.  If anything, this proves that Corona’s scaling works correctly… although not everyone expects the scale of their app to change dynamically, but that’s just how it works on Android.  Corona provides a “resize” event to detect when this happens in the case where the screen is resized/rescaled without an orientation change.

Any case, I hope this helps!

The app is in portrait orientation

Well what i do know is that the actualContentWidth is 388.  So given a footprint of 320x480 we can compute the scale factor to be about 2.0 if everything is working properly.

388 x 1.979 = 768 … aka the width of the device.  

thus the ActualContentHeight would be:

480 x 1.979 = 950

… thus 1024(screen width) - 950 =74 

So i guess what you are saying Joshua is that  if 74 pixels would be used by the android system for control, etc we would have an effective screen of 768 x 950

which would give us the scale factor of 1.979 … and so 320 * 1.979 = 633    then 768 - 633 = 135

135 / 1.979 = 68 extra pixels back in original format thus the 388 actualContentWidth.

So … it all makes sense to me now.  Seems a lot of screen real estate for android to take away but …

No now the real run … we really need dynamic masks at least for rectangular area.  I have tried numerous times to build masks dynamically and when they get saved they become off by one pixel and then they don’t work on some devices. It all works fine when we are building masks for scale factors that are whole numbers but when you get a scale factor that is like 1.979… and you go to save the dynamic mask you have created (keeping in mind that the mask needs to be created and the unscaled reference size) you do not get exact results that you are expecting when the image goes to disk.

…  I think that this is the real issue that i need help with then … as i cannot anticipate every possible screen size.

Thanks a bunch

m

How are you creating your masks dynamically?

Is your app actually generating a grayscale bitmap file?

As you may already know, a grayscale image’s width needs to be a multiple of 4 pixels in order to be rendered correctly in OpenGL.  This is known as byte packing alignment.  If it’s short, then your image will appear skewed onscreen.

You can use properties “display.contentScaleX” and “display.contentScaleY” to convert content coordinates to pixels.  Converting to pixels would look like this…

   local widthInPixels = myObject.contentWidth / display.contentScaleX

   local heightInPixels = myObject.contentHeight / display.contentScaleY

As you’ve noted, you’ll likely get a decimal pixel value for your width and height.  So, you’ll need to round up to the next whole number using Lua’s math.ceil() function like this…

   widthInPixels = math.ceil( widthInPixels )

Next, you’ll need to round up to the next multiple of 4.  You can do this with the Lua modulo ‘%’ operator.

   widthInPixels = widthInPixels + (4 - (widthInPixels % 4))

If you do the above, you can calculate a valid pixel width and height that you can use for the mask you need to generate.

So, does this help?

here is how i am generating a mask … works fine for iphone and iphone4 but go to a tablet where the scale factor is not a simple multiple and it never saves the file right … always off by a pixel which causes the mask to be broken and not work.

Maybe you can see where i am going wrong.  The following code works just fine on iphone and iphone4 … you get a nice 20 x 20 image masking the requested 12 x 12 region.

But you take it to the iPad simulator or other with a fractional scale factor and even though it prints out that the size of the image is 20 x 20 just prior to saving it to disk …  the image on disk is 19 x 19 on disk and the mask is not centered anymore.

This has vexed me every time i pick this code back up saying today …!

Cheers,

m

function generateMaskFile( width, height ) local filename = "mask-"..width.."x"..height .. ".png" -- --Adjust height and width to a multiple of 4 -- as per requirement given by Corona -- if (width % 4 ~= 0) then width = width + (4 - (width % 4)) end if (height % 4 ~= 0) then height = height + (4 - (height % 4)) end local scaledWidth = display.contentScaleX \* width local scaledHeight = display.contentScaleY \* height print("Masking file missing for screen size: " .. width .. " / " .. height .. "\ndisplay.actualContentWidth = \n" .. display.actualContentWidth .. "\ndisplay.actualContentHeight = " .. display.actualContentHeight ) local realWidth = width + 8 local realHeight = height + 8 -- create the mask group and the surrounding black area local maskgroup = display.newGroup() local invisible = display.newRect(maskgroup, 0, 0, realWidth, realHeight) invisible:setFillColor(0, 0, 0) print("Rect Height and width: " .. invisible.width .. " / " .. invisible.height) local visible = display.newRect(maskgroup, 0, 0, width, height) invisible:setReferencePoint(display.CenterReferencePoint) visible:setReferencePoint(display.CenterReferencePoint) invisible.x,invisible.y = 0 , 0 visible.x,visible.y = 0, 0 maskgroup.x,maskgroup.y = display.contentCenterX, display.contentCenterY maskgroup.xScale = display.contentScaleX maskgroup.yScale = display.contentScaleY print("maskgroup width/height: " .. maskgroup.width .. " / " .. maskgroup.height) display.save( maskgroup, filename, system.TemporaryDirectory ) maskgroup:removeSelf() end generateMaskFile(12, 12)

You shouldn’t use display.save() to create the mask because…

  1. It creates a 32-bit color image instead of an 8-bit grayscale image.

  2. This function will round down to the nearest pixel for left and top most bounds and round up to the nearest pixel on the right and bottom bounds.  It’ll never give you the pixel precision that you want.

It would be better to generate the grayscale bitmap yourself via file IO.  That is the surest way to get the pixel precision that you need.  But it would likely be simpler to create mask image files at different resolutions, load the appropriate one, and then scale it to fit.