mcpbeat Sign in

Hoist The Flag Not The Animation Agent Skill

Share the boolean that drives a fade, never the tween that runs it — each look derives its own curve, so one can go asymmetric (fast in, slow out) without changing how the other feels; covers why a symmetric linear fade over a bright backdrop makes container-backed controls read as the wrong colour, and why the shared animated value must stay published anyway. Use when buttons look lighter or darker than their neighbours only while something is fading, when one visual style needs a different timing from another, or before pulling an `animateFloatAsState` up into a shared state holder.

1k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill hoist-the-flag-not-the-animation

The instruction itself

3 sections, as written by the author

Hoist the flag, not the animation

A shell that owns "are the overlay controls showing?" is right. A shell that owns "…and here is the

alpha, already tweened" has quietly made a design decision on behalf of every look that will ever

render.

// adapted — derived inside the look, from the shell's boolean
val controlsAlpha by animateFloatAsState(
    targetValue = if (state.showControls) 1f else 0f,
    animationSpec = tween(durationMillis = if (state.showControls) 180 else 500, easing = LinearEasing),
)

Traps

**A symmetric linear fade has a long half-blended phase, and half-blended is a colour, not a

fade.** Over a bright backdrop, a control with a filled container composites the backdrop *through*

its own container for hundreds of milliseconds. Next to a row that never fades — a header, say — it

reads as a lighter button rather than an appearing one, and it is reported as a colour bug: "the top

bar buttons are darker than the ones below". Foreground-only elements (plain text, untinted icons)

do not show this, which is why one look can ship the shared curve happily and the next cannot.

Fix it with an asymmetric curve, not a different colour. Fading *in* fast (here 180 ms) makes the

blended frames effectively invisible; fading *out* keeps the original relaxed duration so nothing

about the dismissal changes. Both steady states are untouched, so nothing else in the look moves.

One animation, duration chosen from the target — not two animations. Write the direction into

the spec of the single animateFloatAsState, as above. Two animateFloatAsState calls swapped by an

if is a different thing: the one that takes over starts from *its* current value, not from what was

on screen, so the swap jumps.

Keep publishing the shared tweened value. The look that wanted the original curve still reads it;

a look that derives its own simply ignores it. Deleting the field because "the new style does not use

it" breaks the old style, and a doc comment claiming a field is unused is not evidence — grep for the

consumers (screen-shell-content-split).

Publish the flag as a plain boolean, not as a flow or an animation state. It has to be readable

by both looks in the same recomposition and comparable in an if; anything richer makes the second

look's derivation depend on the first look's mechanism.

Do not "fix" it by editing the shared curve. Changing 500 ms to 180 ms in the shell changes how

the original look feels, which nobody asked for, and the change is invisible in review because the

diff is one number in a file neither look owns. The asymmetric curve belongs to the look that needs

it.

The same rule covers easing, delay and spring stiffness. Anything that decides *how* a change is

perceived is presentation. What the shell may own is the state machine around the flag — the

auto-hide timer, the tap that toggles it, whether a scroll cancels it — because those must behave

identically no matter which look renders.

Verifying it

  • The shell publishes both a boolean and its own tweened value, and only one look reads the tween:
   grep -rn --include='*.kt' --exclude-dir=build "controlLayoutAlpha\|showControlLayout" . \
     | grep -v "^.*: \* "

Expect the shell to compute and hand over both, the state holder to declare both, and the counts

of consumers to differ — that asymmetry is the pattern, not a leak.

  • Every locally derived fade, so each one can be checked for a reason:
   grep -rn --include='*.kt' --exclude-dir=build -A6 "animateFloatAsState(" . \
     | grep -B2 "durationMillis = if"
  • By eye, and this is the test the symmetric curve fails: put a filled-container button and a

never-fading one side by side over a bright moving backdrop, then toggle the controls repeatedly

while watching the *middle* of the transition rather than its ends. If the fading button passes

through a lighter shade of its own container colour, the curve is exposing the composite.

How to use it

Copy the folder

Take maxrave-dev/hoist-the-flag-not-the-animation from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.