Repository navigation
perf(web): render the mindwalk scenes on change instead of every frame - #18
Conversation
The verbatim port ran mindwalk's unconditional rAF loop, repainting forever while mounted. AGENTS.md: "No continuously repainting animations; they peg the GPU on high-refresh displays." A visible but idle scene must now cost nothing, which is stricter than pausing on a hidden tab — a parked, visible view is exactly the case the rule names. FrameLoop drives both scenes: an explicit invalidate() books one repaint for a discrete change, the draw callback's return value keeps the loop going while an animation is in flight, and a hidden tab or an off-screen host suspends both. Playback is the one continuous mode, gated behind a new `playing` prop. Auto-rotate is dropped. It is unbounded idle motion with no bounded form, so it cannot survive the rule; the firefly's sine pulse moves under the playback exception instead. DirLabelSet.ease now reports whether a fade is still easing, and the height/halo lerps report the same, so the loop knows when to stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The render-on-change work disabled `controls.autoRotate` outright, reading it as unbounded idle motion. It is narrower than that: mindwalk stops the drift permanently at the first user interaction and already skips it under prefers-reduced-motion, so it is bounded in the common case and unbounded only on a view you open and never touch. That case is still the one AGENTS.md names, but removing the effect gives up how the scene reads as 3D on arrival. Bound it instead: the drift plays and stops at whichever comes first, the first interaction or ATTRACT_DRIFT_MS. At autoRotateSpeed -0.5 that is roughly 18 degrees of parallax. Idle repaint after the window is still zero, since the frame loop already gates on whether the camera moved. Refs #17. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The original reasoning was right but the facts were incomplete. Mindwalk stops the drift permanently at the first interaction ( The drift now ends at whichever comes first: the first interaction, still mindwalk's own rule, or The idle measurement is unaffected. The frame loop already gates on whether the camera moved, so once the window closes the scene returns to zero draws on a parked, visible host. The only change is that the first six seconds after mount are animated — bounded, user-visible, and over on its own. Rejected alternative: scoping the drift to Typecheck and lint clean. The one lint warning in |
The section lists four deviations but opened by claiming one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The verbatim scene port landed mindwalk's unconditional
requestAnimationFrameloop, so a parked, visible 3D view repainted every frame forever — the caseAGENTS.mdnames: "No continuously repainting animations; they peg the GPU on high-refresh displays."A new
scene/frameLoop.tsdrives both scenes instead. The gate iscameraMoved || labelsEasing || lerpMoving || playbackRunning, using signals the scenes already had:OrbitControls.update()returns whether the camera moved, and the height lerps already tracked amovingflag.invalidate()is called from every data effect, fromresize, and from anOrbitControlschangelistener (which also coversfitViewandensureVisible). AnIntersectionObserverplusdocument.hiddengates tab-hidden and surface-inactive.Stayed on plain three.js. Nothing about suppressing frames needed react-three-fiber, and a rewrite would have discarded the verbatim baseline built in #9.
Measured, not inspected
mainIdle is zero, not merely low. Interaction is unchanged. The post-drag damping tail terminates —
dampingFactor = 0.08over roughly 100 frames, seconds on a GPU.Two deliberate deviations from verbatim
Both are recorded in
apps/web/src/mindwalk/README.mdand on the map.controls.autoRotateis disabled. Upstream drifts the camera on arrival and stops permanently at the first user interaction (TreeScene.tsx:123-127), already gated onprefers-reduced-motion. Bounded in the common case, unbounded if you open the view and never touch it — which is a realistic way to use a panel you glance at while an agent works. This is the one decision worth a second opinion; a middle option is to keep the drift but bound it by time or to hover/focus rather than removing it.The firefly's sine pulse runs only while playing. Both scenes take a new
playing?: booleanprop defaulting tofalse— plumbing for the future playback deck, and what makes the idle measurement meaningful today.Also found
Mindwalk's lerps are frame-count based rather than time based, so the intro animation's duration depends on frame rate — 30+ seconds under software rendering. A genuine upstream defect, recorded as post-port work alongside the three found in #16, not fixed here.
Not covered
No unit tests. The behaviour under test is "does this schedule a frame," which is a browser scheduling property; the evidence is the frame profile above rather than an assertion that would mostly restate the implementation.
Refs #17.
Model: Claude Opus 5 via Claude Code.
🤖 Generated with Claude Code
Note
Render mindwalk scenes on demand instead of every frame
FrameLoopclass that schedules frames only when invalidated or while animations are active, suspending when the host is off-screen or the tab is hidden.requestAnimationFrameloops inCitySceneandTreeScenewith adrawFramecallback that returns whether any animation is still in flight.ATTRACT_DRIFT_MSand stops on the first user interaction; previously it ran indefinitely.playingprop is true and settles when playback stops.DirLabelSet.easenow returns a boolean indicating whether any label opacity moved, so the frame loop can determine whether to continue.invalidate()to trigger a frame.Macroscope summarized d984a7e.