How to make a Thunkable Canvas fit any screen, resize sprites reliably, and position them with military precision

I’ve spent an unhealthy amount of time poking at Thunkable’s Canvas and Stage behaviour because I needed sprites to appear in exactly the same relative position on different phones, tablets, Live Test and exported apps.

The good news is that it can be done. The slightly less good news is that Canvas has a few… personality traits.

This is the process that ended up working reliably for me:

Step 1: Get the actual usable screen size

Before touching the Canvas, you need to know how large your app should actually be on the current device.

I already wrote a separate guide for this here:

That method calibrates a layout until its Computed Height matches the screen, then gives you the Set Height required to reproduce that size. I recommend putting this calibration routine on your app’s splash page.

Use that calibrated height for your Canvas. Do not assume that Canvas Height = Screen Computed Height

Thunkable Set Height and Computed Height are not the same thing..

Once calibration is complete, set:

Canvas Height = calibrated layout Set Height

Now we can deal with the Stage…

Step 2: Do not resize the Stage after the Canvas has loaded

This was the big discovery.

If you let the Canvas load and then change Stage Width or Height, Thunkable can do some very odd things with the Stage. In my testing it could suddenly shrink and move towards the top-left of the Canvas.

The exact same Stage dimensions worked perfectly when they were set before the Canvas loaded.

Fortunately, there is a way around this.

When the screen opens:

1. Hide the Canvas

2. Calculate the correct Stage dimensions

3. Set the Stage dimensions

4. Reveal the Canvas

The Canvas Loads event then fires after the Stage has already been configured.

I tested the lifecycle separately and found that a hidden Canvas did not fire Canvas Loads until it was made visible.

Very handy.

Step 3: Make the Stage aspect ratio match the device

I use a fixed logical Stage width of: Stage Width = 300

Then calculate the height from the current screen ratio:

Stage Height = 300 × (Screen Computed Height / Screen Computed Width)

So if a device reports:

Width = 411.43

Height = 914.29

then:

300 × (914.29 / 411.43) = 666.67

Your Stage becomes:

300 × 666.67

On a shorter/wider device the Stage height will be smaller.

On a tall phone it will be larger.

The important part is that the Stage coordinate system now has the same aspect ratio as the actual device.

In my project I currently use:

Stage Scaling Mode = stretch

Background Picture Resize Mode = stretch

The background therefore stretches to exactly fit the device. Yes, that can distort the background artwork slightly. For my use case that is intentional.

I would rather have a building doorway, pavement edge or parking space always represent a known position on the Stage than use a “cover” style background and have parts of the scene disappear off-screen. The sprites themselves do not need to be distorted. More on that below.

Step 4: Treat the Stage as your coordinate system

Once this is working, stop thinking in physical screen pixels.

Everything should be positioned relative to:

Stage Width

Stage Height

That means the same math works regardless of whether the player is using a phone, tablet or something with a particularly enthusiastic aspect ratio.

For example:

25% across the Stage:

X = Stage Width × 0.25

80% down the Stage:

Y = Stage Height × 0.80

This also makes it easy to position objects relative to your background artwork.

Suppose your background source image is 500 × 1000 pixels and a doorway is centred at:

X = 400

Y = 100

That doorway is:

80% across

10% down

So the Stage position becomes:

X = Stage Width × 0.80

Y = Stage Height × 0.10

You do not need the original image pixels again after converting them to percentages.

Step 5: Sprite positioning is centre-based

This one caught me out.

I created a test background with known target squares and tried to position a sprite over them.

I initially assumed the Move Sprite to X/Y block was positioning the sprite from its top-left corner, which is certainly how I interpreted Thunkable’s guidance. This turned out to be a barefaced lie.

In my testing, the X/Y coordinates used by the Move Sprite block correspond to the centre of the sprite.

So if you want the sprite centred at:

80% across

10% down

just use:

X = Stage Width × 0.80

Y = Stage Height × 0.10

No half-width adjustment required.

If you instead know the top-left corner where you want the sprite to appear, convert it to the centre:

X = desired left + Sprite Width / 2

Y = desired top + Sprite Height / 2

Step 6: Resizing sprites without distorting them

There are two slightly different situations here.

If you deliberately want a sprite to match a stretched part of the background, you can independently calculate its width and height as percentages of the Stage.

For example, I had a 50 × 50 target square on a 500 × 1000 background.

That represents:

10% of background width

5% of background height

So the matching sprite dimensions are:

Sprite Width = Stage Width × 0.10

Sprite Height = Stage Height × 0.05

That is useful for calibration. For actual characters, cars, collectibles, etc, I would normally preserve the original image aspect ratio

For example, say the original character PNG is:

200 px wide

400 px high

and I want it to occupy 15% of the Stage width.

Use:

Sprite Width = Stage Width × 0.15

Then preserve its original ratio:

Sprite Height = Sprite Width × (Original Image Height / Original Image Width)

So:

Sprite Height = Sprite Width × (400 / 200)

Sprite Height = Sprite Width × 2

That keeps the character correctly proportioned.

I generally prefer sizing game characters from Stage Width because horizontal crowding is usually the thing I care about most.

A tall phone then gives me more vertical room without mysteriously turning everybody into Stretch Armstrong.

You could equally choose height as the master dimension if that suits your app better.

The important bit is:

Pick one dimension.

Calculate the other from the image’s original aspect ratio.

Do not independently stretch both dimensions unless you actually want the sprite distorted.

Putting it all together:

My current sequence is basically:

App Opens

  • Run screen/layout calibration

Canvas Screen Opens

  • Hide Canvas

  • Flash up a loading screen layout on top of the canvas which acts like a curtain to hide any ugly positioning/resizing.

  • Set Canvas Height to calibrated height

  • Set Stage Width = 300

  • Set Stage Height = 300 × Screen Computed Height / Screen Computed Width

  • Reveal Canvas

Canvas Loads

  • Then after Canvas Loads, sprites can be resized and positioned using Stage coordinates.

For a character:

  • Sprite Width = Stage Width × desired percentage

  • Sprite Height = Sprite Width × original image aspect ratio

For positioning:

  • Sprite X = Stage Width × desired horizontal percentage

  • Sprite Y = Stage Height × desired vertical percentage

A few things I learned the hard way

  • Do not assume the Canvas designer size is the device size.

  • Do not assume Set Height and Computed Height mean the same thing.

  • Do not resize the Stage after Canvas Loads unless you enjoy surprise geometry.

  • Do not assume sprite X/Y means top-left.

  • Do not make the background “cover” the screen if you need landmarks in the artwork to correspond to predictable coordinates.

  • And definitely test this in an exported build, not only Thunkable Live. I found cases where something looked completely reasonable in Live and then the APK decided the left side of my Stage was optional.

The important trick is to establish the Canvas size and Stage aspect ratio before allowing the Canvas to load. Once that is done, the Stage becomes a surprisingly useful and predictable coordinate system.

I hope you all find this useful!

Grant

P.S. I’m a freelance Thunkable developer, so if you’ve found this post because your own app is doing something equally inexplicable and you’d rather pay someone else to lose sleep over it, feel free to PM me. I work on everything from general Thunkable development/debugging through to Firebase, Google Sheets, RevenueCat and app publishing.

Hi @grant.mccallum , thank you for sharing this.