Design Archive
中EN

Getting 124 light pillars to line up before they rise

A 3D map carried 124 light pillars, and in the first loop there was never a moment with every pillar standing at once. Stretching the full-height hold from 1.5 to 3.5 seconds, and making mouse interaction settle from the current state, gave the screen a moment you can read.

L.10 / 14
CATEGORY
Study
YEAR
2026
STATUS
Note
TOOLS
figma / mcp / motion

entryWrite-up

A data screen had a 3D map in the middle: 31 provinces, each carrying 4 light pillars in different colors for 4 indicators, 124 in all. The origin province, where the platform is based, showed an open detail card; every other province showed only its name. The static design was done, and the next step was to make the pillars move. A screen like this hangs on a wall all day: with no one looking, it has to feel alive; when someone walks up to read the numbers, they have to be legible. The first idea was for the 4 pillars in each province to rise one after another with a time offset, then loop, but that idea had not been tested.

Pillar height is the data, so if the pillars keep rising and falling, the height you read is mostly not the real value; and with a three-second cycle, a few rounds turn into mechanical breathing. Each of four options became an interactive mock-up. A was a staggered global loop, with heights mostly not real. B rose once on entry and then idled with light sweeps, with weak motion. C picked one province every few seconds to rise again after entry, which is complex to build. D highlighted one color every few seconds after entry, but same-color pillars in the east were too dense. A was kept, because the first impression of a big screen needs overall rhythm, and reading can be left to interaction; one more requirement was added: provinces should be offset from each other too.

Interactive mock-ups of the four options; the data is random.

With 124 pillars moving at once, and the eastern provinces already packed together, the screen gets messy easily. Three orders became slider mock-ups: a ripple out from the origin province, a west-to-east sweep, and random phases. The ripple from the origin province won, because that province is the lead, and a wave pushing out from it shows where to look at a glance. The offset has two layers. Within a province, the 4 pillars start in color order, 0.15 seconds apart; the next one starts before the previous one reaches the top, like steps. Between provinces, the delay is proportional to the distance from the origin province, continuous rather than banded, with the farthest province 1.6 seconds late, so provinces at similar distances rise almost together.

Two layers of offset: steps within a province, a ripple between provinces.

The design had two pillar styles, 248 pillar layers in all, and placing keyframes by hand would mean starting over after every change. Instead, scripts ran inside the design file through Figma’s MCP interface and wrote the keyframes in one by one. Every pillar’s timing came from the same formula, so a changed parameter could be recomputed across the whole map.

One round went by too fast to see. Playing two cycles back to back and asking how long the gap between rounds was showed that a single pillar held full height for only 1.5 seconds, while the ripple took more than 1.6 seconds to reach the farthest province: the origin was already falling before the west reached the top, and there was never a moment with every pillar standing at once. Consecutive rounds also overlapped end to start, with no room to breathe. Three rhythms became mock-ups for comparison: the old rhythm, a four-phase rhythm, and four-phase with an outside-in retreat. Four-phase won: only the full-height hold went from 1.5 to 3.5 seconds, everything else stayed, and 0.6 seconds of rest went between rounds. The whole cycle came to 7.35 seconds: rising outward from 0 to 2.75, all at full height from 2.75 to 4.2, falling outward from 4.2 to 6.75, and rest from 6.75 to 7.35.

The four-phase rhythm of a 7.35-second cycle; the green band is when every pillar stands at full height.

The first idea for the mouse entering the map was to play the animation once and stop. But if the mouse arrives while every pillar is at full height, a replay has to clear to zero and grow again, and someone who wants to read the data first sees the screen collapse. So the response became state-based rather than a timeline replay: each pillar settles from its current height to full height in 0.4 seconds, with no stagger; pillars at full height stay put, falling ones rise back, and ones already down rise from the base.

Entering mid-fall: a replay collapses to 0 first; settling in place reaches full height for every pillar after 0.4 seconds.

Hovering a province dims the pillars of the others and pops the detail card for that province; dimming was tried at 40% first, looked too dark in the simulation, and became 60%. When the mouse leaves the map, the loop does not resume at once but waits 1.5 seconds, so a pointer wobbling along the map edge does not keep retriggering it; since every pillar is at full height on leaving, rejoining at the rise phase would mean clearing to zero first, so the loop rejoins at the fall phase.

Interaction states: respond by state instead of replaying the timeline.

Reviewers and developers needed to see the real behavior, but the first interaction simulation entered during the rise phase, which did not show the problem. So it was replayed in Figma with a mouse pointer asset, entering instead while some pillars in the second cycle were falling, then hovering the origin province, and starting a new round 1.5 seconds after leaving, played in full. At the moment of entry, roughly 51 pillars per layer were at full height, 55 were falling and 18 were down, and all three cases settled at once; the settling logic was also made to work for any phase. Because Figma’s video export is unreliable on long timelines, the screenshots of the four interaction states come from an offline recreation built with the same parameters.

An offline recreation built with the same parameters: the four interaction states.

Each province had a static base, later replaced with a ripple status ring component made earlier: a ring texture with two ripples spreading outwards, on its own 2-second loop. The ripple had to keep its own loop, with opacity following the province’s pillars. The first version faded the base in and out with the pillars but kept the ring itself, so the map would not look empty during the rest; after seeing it, this changed to no base whenever the pillars are not showing, with the whole base appearing and disappearing with the pillars: it fades in as the pillars start to rise and is fully gone just as the last one comes down.

Building it handled four specific problems. Vertical scaling pivots on the center by default, so a shortening pillar floats off its base; the fix is a downward offset equal to half the pillar height times one minus the scale. Some provinces’ motion crossed the end of the timeline, so the easing curve was split at the end, with the first half at the tail and the second half at the head, keeping the first and last frames identical so the loop does not jump. Figma does not allow keyframes on layers inside a component instance, so the ripple ring was placed in an instance and then detached; the cost is that these 31 bases will not follow later changes to the component’s style. Video export failed often on timelines longer than 7 seconds, so day-to-day exports are done by hand, and the loop footage in this article is one cycle exported from a temporary copy. The rules also call the pillars only by color, cyan, pink, yellow and green, never by indicator, because the indicator names may change.

Scaling alone lifts the pillar off its base; scale plus offset keeps the bottom on the base.

The deliverables were a loop artboard and an interaction simulation artboard in Figma, each with a note card, and a motion spec for developers with state machine pseudocode; a developer only needs to implement one pillar’s animation and one delay formula to reproduce all 124, with no need to check keyframes pillar by pillar. The 7.35-second cycle set earlier is a sum: 1.6 plus 0.45 plus 0.7 plus 3.5 plus 0.5 plus 0.6; after the last pillar finishes falling, 0.6 seconds remain, which is exactly when the next round starts. For data screen motion, ask when the data can be read before asking whether it looks good: the change here was a longer full-height hold, which bought a readable moment with every pillar at full height. Motion is described with parameters and formulas, not keyframes, and developers get the same formulas, so the design and the code match. Interaction takes the current height as the start of the tween, because users reaching in at any moment is the normal case, and that makes every entry smooth. Rhythm is tested in interactive mock-ups before going into the design tool: many rhythm problems show up after two clicks on a mock-up, faster than reworking keyframes in the design tool. Names follow what things look like, not what they mean to the business, so the rules stay the same when the indicators change.

processBefore & after

Parameters

One pillar
rise 0.7s (fast then slow) · hold 3.5s · fall 0.5s (slow then fast) · anchored at the base
Within a province
cyan → pink → yellow → green · 0.15s apart · first up, first down
Between provinces
delay proportional to distance from the origin · farthest 1.6s
Cycle
7.35s · rise 0–2.75s · all high 2.75–4.2s · fall 4.2–6.75s · rest 6.75–7.35s
Province label
fades in and out with the province’s first pillar
Base
ripple ring keeps its 2s loop · whole base shows and hides with its province’s pillars
Entering the map
every element settles from its current state to full in 0.4s · no stagger · full-height pillars stay · falling ones reverse · fallen ones grow from the base
Hover
other provinces dim to 60% over 0.25s · detail card scales to 105%
Leaving the map
hold full height · after 1.5s rejoin the loop at the fall phase

Formulas

text
delay  = distance to origin province ÷ max distance × 1.6s
offset = pillar height ÷ 2 × (1 − scale)
cycle  = 1.6 + 0.45 + 0.7 + 3.5 + 0.5 + 0.6 = 7.35s

resultTakeaways

Finding
With the full-height hold stretched from 1.5s to 3.5s, the 7.35s cycle gains a 1.45s window where every pillar stands at full height; when the mouse enters, each pillar settles from its current height in 0.4s instead of collapsing and regrowing.
Parameters
0.15s between pillars in a province · delay between provinces = distance ÷ max distance × 1.6s · hover dims the rest to 60% · after leaving, wait 1.5s and rejoin at the fall phase
Open
To take keyframes, the ripple rings were detached from their component, so the 31 bases no longer follow component updates; Figma's video export fails often on timelines longer than 7 seconds.

logChangelog

2026-10
First entry

Related experiments

note
L.06(STUDY)2026

One font size, one line height

typefigmavariables
note
L.07(STUDY)2026

Centering icons and Chinese text for real

typefonttoolsfigma+1
note
L.01(STUDY)2026

(1) Choosing a base model

chest-xraypytorchrad-dino+1
← LabL.10 / 14
1 / 1

100%