Move assets without breaking the layout
Why a position sometimes jumps back after a resize, and the right way to move, animate and hide anything.
How the responsive system works
Every element from zStudio stores two sets of transforms: one for portrait and one for landscape. On every resize, zImporter does three things:
- Scales and centres the whole stage to fit the window.
- Re-applies the stored transform for the current orientation to every element.
- Moves anchored elements to their percentage of the window.
So the rule is simple: anything you change that isn't in the stored transform gets undone at the next resize. On a phone, that includes the address bar sliding away.
What survives a resize
| You write | What happens | Use instead |
|---|---|---|
mc.x = 100 | Kept, but only for the current orientation | Fine if that is what you want |
mc.scaleX = 2 | Kept, but only for the current orientation | Fine if that is what you want |
mc.scale.x = 2 | Undone | mc.scaleX = 2 |
mc.alpha = 0.5 | Undone | mc.setAlpha(0.5) |
mc.visible = false | Undone | mc.setVisible(false) |
mc.rotation = 1 | Kept for both orientations | Fine |
mc.x = 100 on an anchored element | Snaps back to its anchor | See recipe 4 |
Recipe 1: Move it for good
Write to both stored transforms, then re-apply. They can hold different values. This works for any stored value: x, y, scaleX, scaleY, rotation.
const coinIcon = stage.get('coinIcon') as ZContainer;
coinIcon.portrait.x = 120;
coinIcon.portrait.y = 900;
coinIcon.landscape.x = 40;
coinIcon.landscape.y = 300;
coinIcon.applyTransform();Recipe 2: Move it in this orientation only
coinIcon.x = 120; // the other orientation keeps its own positionRecipe 3: Animate it and come back
For a bounce, shake or pulse, the safest setup is a wrapper. In zStudio, put the artwork inside an inner container named content. The outer container keeps the layout, and you only ever animate the inner one.
coinIcon keeps its layout. Leave this one alone. └─ content animate this one └─ artwork
import { gsap } from 'gsap';
const content = coinIcon.get('content') as ZContainer;
const startY = content.y;
gsap.to(content, {
y: startY - 30,
duration: 0.15,
yoyo: true,
repeat: 1,
});coinIcon directly? Setting x or y writes into the stored transform for the current orientation. If the screen rotates mid-tween, the tween's values land in the wrong orientation and stay there.Recipe 4: Anchored elements
An anchored element is positioned as a percentage of the window, so setting its position has no lasting effect. To move it, change the anchor (values from 0 to 1). To animate it, use the wrapper from recipe 3.
const pauseBtn = stage.get('pauseBtn') as ZButton;
pauseBtn.portrait.anchorPercentage = { x: 0.9, y: 0.05 };
pauseBtn.landscape.anchorPercentage = { x: 0.95, y: 0.1 };
pauseBtn.applyTransform();Recipe 5: Your code owns the position
For a player character, a projectile, or anything your game logic moves every frame, opt out of the layout system. The element stays where you put it through any resize. It still scales with the stage.
const player = stage.get('player') as ZContainer;
player.resizeable = false;
// In your game loop
player.x += speed * delta;Recipe 6: Show, hide and fade
popup.setVisible(false); // both orientations, survives resizes
popup.setAlpha(0.5); // both orientations, survives resizesOne more trap
Timelines control their children. A child animated on a timeline gets its position from the timeline's frames, so your changes are overwritten. Move the timeline itself, or a container around it.