The crash launched him. Rotating 66 degrees about a pivot inside the
sprite, shearing him through the middle and scaling him about the same
point left him diagonal in mid-air, elongated, nowhere near the ground —
a stunt, not a trip.
It is now a face-plant and a skid: 26 degrees, a 21px drop so the snout
reaches the grass, and 26px of forward slide along the ground with the
dust coming up. The shear is gone entirely. The pivot is the snout at the
grass line rather than a point inside the body.
The other half of looking airborne was the frozen frame: stopping a run
cycle leaves whatever mid-stride pose the gif was on, legs extended. He
now lands on frame 4, the one with the legs tucked under him.
Clearing the field and getting across now says WIN, with the tally, the
way eating it says GAME OVER. Before, a won lap just ended and the next
one opened with a doge sitting down, which reads as nothing having
happened.
And the scene comes in three amounts, because a form should not have to
compete with a dog: History runs the whole thing, Receive is a doge
sitting in the field, Send is money blowing past and nothing else. One
QML file, gated by mode; the beat machine only runs where there is a run.
Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
The +1s raining out of an empty sky came from a scene running on a tab
that had never been shown. An unshown QQuickWidget is 0px wide, and at
0px every coin sits at x=0, so the doge "collected" the whole field in
one step; the pickup pops from that phantom lap were still mid-flight
when the tab was finally selected and the real width arrived. Their
`SequentialAnimation on opacity { running: <bound> }` was cut off by the
width change, which leaves the property wherever it stopped — so a +1
hung in the air for the rest of the lap, next to a coin nobody had
taken.
Both halves are fixed:
* SceneWidget parks its scene on hide and starts a fresh lap on show,
and the QML refuses to run at all until it has been laid out. A
background tab now costs nothing, too.
* Every one-shot flourish (+1, bark, dust, GAME OVER) is started and
reset from an explicit handler, so an interrupted lap cannot strand
one on screen.
The rest is the ambient scene earning its keep:
* The face-plant has weight. He tips over his front paws — pivoting on
the paw at the grass line, not the middle of the sprite, which used
to swing his snout through the ground — squashes at the nose, bends
through the body, throws dust off the impact, and lies there
breathing. GAME OVER now lands a beat later so the crash reads on
its own, and the lap ends shortly after instead of running out the
clock on a wreck.
* He has opinions: HRNMMROOF, BONK, SNOOT.EXE and friends on a
wipeout; MUCH COIN, AMAZE, VERY WIN when he clears the field, with a
victory hop to go with it.
* More variety per lap: zero to three bills at their own speeds and
heights, a 1-in-3 fat +5 coin with a halo, a doge sitting in the
field during the empty beat, drifting clouds, a soft corona on the
sun, blinking stars and the odd shooting star at night, birds by
day, and a run speed that is no longer always eight seconds.
Verified by rendering scene.qml headless (qml6 + xvfb, frame grabs) at
the real widget sizes, and by driving two scenes through a QTabWidget to
watch the hidden-tab case before and after.
Claude-Session: https://claude.ai/code/session_01VLQ5DsgKv23osGANoFT3zF
Every tab was running the identical eight seconds forever. Three changes:
- Each lap rolls its own timing: 3-5s of empty field, and the money now enters
on its own delay at its own pace instead of shadowing the doge every time.
- Roughly one lap in six the doge eats it partway down the field: he tips
nose-first onto the grass, GAME OVER comes up, and the coins he never reached
are still sitting there. Costs no new art — it is the running sprite pivoted
on its own feet.
- A `variant` on SceneWidget picks the sky: dusk (History, lock screen), dawn
(Receive), night (Send, with stars and a moon instead of the sun).
The cycle is now a timer-driven state machine rather than one SequentialAnimation.
That is not stylistic: a lap that rolls its own durations has to assign them
somewhere, and assigning a duration to a child of a *running* SequentialAnimation
makes the group recompute and re-enter its own ScriptAction until the JS stack
blows. Qt flags the binding form of the same mistake as a binding loop.
Verified by headless qml6 render: a forced trip lap, and a clean run with no QML
warnings.
Reusable SceneWidget (QQuickWidget host) loads scene.qml — dusk sky, tiled
grass, running doge, bouncing WOW coins (slowed per feedback), drifting
money — and feeds it Config::reducedMotion so it freezes to a still when
that's set. First placement: a banner atop the unlock form. Scene rendered
& verified at banner + screensaver aspects via local qml6 harness. Assets
from wow-lite-wallet (friend's). Home banner / idle screensaver / per-tab
idle to follow on the same SceneWidget.