Animations
Every animation here runs from the same formula and the same numbers as the C code, and each one names the file those numbers come from.
A zrnui app does not call any of these. libzrnui has no animation API at commit ea16771: page changes, presses, switches and toasts animate by themselves, and the style decides how. Swivel, fade and the other named transitions are not in libzrnui. They exist in ZrnMetroCore (ztOS's own apps) and in the older Python SDK's animate().
| Where you are | What you get | How |
|---|---|---|
| zrn-sdk, libzrnui | Page change, press, switch, pivot, toast, busy, progress, fling | Automatic. No call, no way to change the numbers |
Older Python SDK, ztos | Its own turnstile and tilt, plus 13 named transitions | ztos.animate(widget, "swivel-in"). Some values differ: see below |
ZrnMetroCore (ztmetro/core) | 13 named transitions, the Swivel board, zmc_turn | zmc_transition(), zmc_board_open(). For apps in the ztOS tree, not an SDK |
Using these demos
Replay runs it again. The speed button cycles 1x, 0.25x and 0.1x. The slider shows any single millisecond. Demos start when you scroll to them. If your system asks for reduced motion, nothing starts by itself and each demo rests on its last frame, so you can step through with the slider.
- Same: durations, staggers, angles, pivots, opacity, easing formulas and the eye distance (a 34 degree field of view on a 740 pt tall screen,
gfx.c,zrnmetrogpu.c). A browser'sperspectiveis the same divide the shader does. - Different: the fonts and row layouts are plain CSS stand-ins, not Segoe UI through the SDF renderer. The real look is in the renders.
- Different: the demos draw in the Metro look only. DotMatrix runs the same motion through its lattice, as the real frames show.
- Different: phones here run at your browser's frame rate, not the S9's, and the C code samples its clock once per compositor frame.
- Different: libzrnui rotates each row's quads, while these demos rotate a whole row box. Each named-transition demo shows one rectangle in a 300 pt tall stage, while the eye distance stays that of a 740 pt screen.
- Not compared to the phone: these were compared by eye with frames from the laptop's virtual compositor, not with the S9's display.
The curves
Progress over time for each easing. libzrnui has three, in libzrnui/src/core.c: zui_ease_out is 1 - (1-t)^3, zui_ease_in is t^3, and zui_ease_emph is a cubic Bezier (0.2, 0.8, 0.2, 1). ZrnMetroCore rebuilds the firmware's named curves in zt_shape, with k = weight / 30: Exp is (e^(k t) - 1) / (e^k - 1) and Log is 1 - (e^(k (1-t)) - 1) / (e^k - 1). The firmware stores names and weights. The formula itself is a reconstruction, as the core's own comment says.
Page change: the turnstile libzrnui
Automatic on zui_push and zui_back, in both styles, with the same numbers. Runs 720 ms in all. Rows leave one at a time, 28 ms apart, each over 128 ms, turning 50 degrees about the left edge on zui_ease_in toward the viewer, with opacity 1 - u^2. New rows start at 330 ms, also 28 ms apart, each over 180 ms from 80 degrees behind the screen, on zui_ease_out. Going back, both run bottom up and the old rows turn the other way.
Source: page_xf, OUT_MS, IN_MS, STAGGER, GAP_AT in libzrnui/src/style-metro.c. The comment above them says 33 ms apart; the code that runs says 28.
Forward
Back
Real frames, from zrn run --headless in a virtual compositor, one run per picture. Frames the compositor delayed past the end are dropped, and the times are not exact: this shows the shape, while the numbers above come from the code. Metro, then DotMatrix.


Page change: ZrnMetroCore turnstile ztOS apps only
The built-in apps' own version, for comparison: rows 33 ms apart, 128 ms out on t^2, a 120 ms black gap, 180 ms in on 1 - (1-t)^3. The total depends on the row count. zmc_turn plays this on one page when its content changes. Source: ROW_OUT_MS, ROW_IN_MS, ROW_STAGGER_MS, GAP_MS and TURN_OUT_DEG in ztmetro/core/zrnmetrocore.c. Simplified here: the real one also holds the tapped row until last, and an app's first page arrives by a different curve (zmr_entrance, 28 ms stagger, 190 ms rows, 200 ms title), which is not shown.
Forward
Back
Swivel: the domino board ztOS apps only
The jump grid and the pickers. Each of the 28 tiles turns about a horizontal line through its own middle, in reading order, and each starts while the one before is still moving.
| Angle | Time | Curve | Visibility | |
|---|---|---|---|---|
| In | -45 to 0 degrees | 0.3 s | Log 200 | opacity 0 at 0 s, 1 at 0.01 s |
| Out | 0 to 60 degrees | 0.2 s | Exp 150 | opacity 1 until 0.199 s, 0 at 0.2 s |
These come from the Lumia 530 firmware's ANIMATIONS.UIX (SwivelTransitionIn at 0x3f29, SwivelTransitionOut at 0x3eb3), recorded in ztmetro/core/zrnboard.h as ZB_SWIVEL_*. The firmware fixes each tile's motion but not the stagger. The 12 ms step (ZB_STAGGER_MS) is ztOS's own choice, and so is using the same order going out. The source says which is which. Tile sizes are also from there and CONSTANTS.UIX: 99 wide with a 12 gap at 480, scaled by 0.75.
A picker adds fixed steps this demo leaves out: the box lit for 116 ms, the page gone in 50 ms, the surface in 33 ms (ZB_BOX_MS, ZB_PAGE_OUT_MS, ZB_PAGE_IN_MS).
Board in, 500 ms held here for viewing, then out
Every named transition ZrnMetroCore, older Python SDK
The table ztanims[] in ztmetro/core/zrnmetrocore.c, read from the Lumia 530's compiled Iris markup. The readerboard pair stays only for the motion catalogue in zrndevtest.
| Name | Axis | Degrees | Pivot | Other | ms | Curve |
|---|---|---|---|---|---|---|
turnstile-in | Y | -80 to 0 | left edge | no | 350 | Log 60 |
turnstile-out | Y | 0 to 50 | left edge | 1 to 0 | 128 | Exp 60 |
swivel-in | X | -45 to 0 | middle | no (shown from its start) | 300 | Log 200 |
swivel-out | X | 0 to 60 | middle | no (hidden at its end) | 200 | Exp 150 |
continuum-hide | X | 0 to 80 | top edge | 1 to 0, drops 120 pt | 120 | Exp 60 |
continuum-show | X | -50 to 0 | top edge | 0 to 1, scale 1.5 to 1, from -60 pt | 350 | Log 60 |
readerboard-in | X | -45 to 0 | top edge | 0 to 1 | 166 | Log 60 |
readerboard-out | X | 0 to 90 | top edge | 1 to 0 | 166 | Exp 60 |
slide-up-fade-in | none | none | none | 0 to 1, up from 24 pt | 240 | Log 150 |
rotate-90 | Z | 90 to 0 | middle | no | 167 | SCurve 7.5 |
fade-in | none | none | none | 0 to 1 | 350 | Log 150 |
fade-out | none | none | none | 1 to 0 | 150 | Exp 150 |
tile-flip-notify | X | -180, 35, -6, 0 | middle | no | 520 | Log 10, SCurve 10, linear |
turnstile-in
turnstile-out
swivel-in
swivel-out
continuum-hide
continuum-show
readerboard-in
readerboard-out
slide-up-fade-in
rotate-90
fade-in
fade-out
tile-flip-notify
Press and tilt libzrnui
Press a row in this demo: it follows your pointer. What is under the finger leans toward it by up to 14 degrees, but its far edge never lifts more than 40 pt, and it sinks 4.5 %. Rows tilt. Sliders and fields do not move, and a button fills instead. In takes 90 ms, and the release takes 200 ms (160 ms when the press is cancelled by a scroll), all on zui_ease_out. DotMatrix uses the same press_xf.
Source: press_xf, TILT_DEG, TILT_LIFT, PRESS_SINK in style-metro.c, timing in core.c. ZrnMetroCore shares the numbers (ZMC_TILT_*).
Tilt: press it
Small things libzrnui
| What | Numbers | Source |
|---|---|---|
| Switch block | 180 ms, zui_ease_out | zui_set_on, core.c |
| Pivot | 240 ms from where the finger left it, zui_ease_out. Past a quarter of a screen a swipe goes on to the next tab | zui_tabs_select, extras.c; core.c |
| Toast | 2600 ms: in over 200, out over the last 300. A bar drops 60 pt from the top | core.c, toast in style-metro.c |
| Busy, unknown progress | five 4 pt dots every 2600 ms, each 7.5 % later, on 0.5 + 4 (p - 0.5)^3 across the width | dots, style-metro.c |
| Fling | speed times 0.9972^ms, a further 0.8^(ms/16) past an end, stop under 0.02 pt/ms, then back to the end with a 70 ms time constant | core.c |
| Glide to the end of a chat | a 45 ms time constant, for up to 600 ms | zui_page_scroll_end, core.c |
A field's box turns from grey to white the moment it has focus. That change is not animated.
Switch
Pivot
Toast
Busy
Fling at 3.2 pt/ms
Older Python SDK: where its numbers differ older
ztos.animate(widget, name, repeat=False, on_done=None) plays a named transition over any Qt widget. Its table is TRANSITIONS in sdk/runtime/ztos/__init__.py. Most entries match the list above, but its swivel predates the firmware decode now in zrnboard.h:
| ZrnMetroCore (firmware) | Older Python SDK | |
|---|---|---|
swivel-in | -45 to 0, 300 ms, Log 200, no fade | -60 to 0, 300 ms, Log 200, fades 0 to 1 |
swivel-out | 0 to 60, 200 ms, Exp 150, no fade | 0 to 60, 199 ms, Exp 60, fades 1 to 0 |
| Press in, out | 90, 200 ms | 70, 160 ms, divided by the menu speed setting |
| Row out | 128 ms | 110 ms, divided by the menu speed setting |
Everything else that moves in that SDK is also scaled by ANIM_SPEED, which comes from a user setting, so its real durations vary.
Checked against zrn-sdk commit ea16771. Pictures are real renders of code that compiled. Source and proof.