Skip to content

Animate a firing only where it can be seen - #9655

Open
kube wants to merge 3 commits into
claude/canvas-frame-and-adjacencyfrom
claude/canvas-firing-visibility
Open

Animate a firing only where it can be seen#9655
kube wants to merge 3 commits into
claude/canvas-frame-and-adjacencyfrom
claude/canvas-firing-visibility

Conversation

@kube

@kube kube commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Before this PR, playing or scrubbing a large net cost far more than the canvas draws. Every transition that fires animates its box, its bolt and each of its arcs, and the style engine resolves and paints each of those for as long as it runs. On a thousand-node net that is hundreds of animations in flight per frame, on nodes a few pixels across that nobody is looking at.

A firing now animates only where it can be seen: nodes and arcs off the side of the pane, and any net drawn small enough that a node covers a few pixels, skip the flash. The transitions in view still animate, and those animations are what the remaining frame time goes to when the canvas is zoomed in.

Scenario Before After
Scrub, 1000 nodes 13 fps 44 fps
Playback, 1000 nodes 16 fps 39 fps
Playback, 1000-node ring, fitted view, this head 9 fps, p95 250ms 38 fps, p95 125ms
Playback, 1000-node ring, zoomed to an arc, this head 10 fps, p95 283ms 13 fps, p95 233ms

Measured against the built website in headless Chromium, driving a ring net through a run, with frame intervals sampled from requestAnimationFrame; the last two rows against production builds of the base and this head with smoke-ring.mjs. Zoomed in, the transitions in view still animate, and those animations are what the remaining frame time goes to.

Before

9655-before.mp4

After

9655-after.mp4

Links

Changes

  • Firing animations run only on what is on screen

    A node's position and an arc's endpoints are projected through React Flow's transform in the effect that starts the animation.
    Reading the transform there rather than subscribing to it keeps a pan or a zoom from re-rendering every node.

  • Both transition styles share one firing animation hook

    useTransitionFiringAnimation holds the box and bolt animation once; the compact and classic nodes call it.

  • Firing animations stop below a zoom of 0.25

    Below it a compact transition is under 45px across, and its flash reads as a flicker rather than a firing.

Review fixes

  • Arc firing judged by the box its drawn path occupies

    getBBox() on the arc's path at firing time, so a curve sweeping past its endpoints animates where it is drawn.

Test coverage

  • firing-animation-visibility.test.ts:

    Projection through pan and zoom, the zoom floor, and arcs that run in either direction.

  • Existing @hashintel/petrinaut unit suite.

How to test

  • Open Petrinaut preview on Vercel
  • Menu > Load example > Supply Chain with Disruption
  • Press Play

    Expect transitions to flash and arcs to thicken as they fire

  • Zoom out until nodes are a few pixels across

    Expect no flashes, token counts and the timeline still moving

  • Zoom back in

    Expect flashes on the transitions in view

@kube kube self-assigned this Sep 11, 2026
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hash Ready Ready Preview Sep 11, 2026 3:46am UTC
petrinaut Ready Ready Preview Sep 11, 2026 3:46am UTC
petrinaut-docs Ready Ready Preview Sep 11, 2026 3:46am UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Sep 11, 2026 3:46am UTC

Request Review

@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team labels Sep 11, 2026
@kube
kube added this pull request to stack #9657 September 11, 2026 00:21
@kube
kube force-pushed the claude/canvas-firing-visibility branch from a8af603 to c29ba66 Compare September 11, 2026 02:05
@kube
kube marked this pull request as ready for review September 11, 2026 02:05
Copilot AI balanced review requested due to automatic review settings September 11, 2026 02:05
@cursor

cursor Bot commented Sep 11, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Visual-only optimization with no simulation or data changes; wrong visibility math would only skip or show flashes incorrectly.

Overview
Firing flash animations are now viewport-budgeted so large nets stay responsive during playback and scrubbing. Transition yellow flashes, lightning bolts, and arc stroke pulses still run for firings you can actually see; off-pane elements and anything drawn below zoom 0.25 skip Web Animations work while simulation state (tokens, timeline) stays the same.

Visibility is decided when a firing lands: React Flow’s pan/zoom is read once via useStoreApi (not subscribed), so panning/zooming does not re-render every node and arc. Nodes use absolute position; arcs use the drawn path’s getBBox() so curved spans count where they’re painted. Compact and classic transition nodes share a new useTransitionFiringAnimation hook (replacing duplicated inline logic).

Docs add a “What a firing looks like” section describing the culling behavior; unit tests cover pan, zoom floor, and arc bounding boxes.

Reviewed by Cursor Bugbot for commit 6f59a6d. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Default custom arcs can remain visible while their endpoint-only bounds incorrectly suppress firing animations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Optimizes Petrinaut simulation rendering by suppressing firing animations outside the visible viewport or below a zoom threshold.

Changes:

  • Adds viewport-aware visibility checks for transition and arc animations.
  • Shares transition animation logic across both node styles.
  • Adds tests, documentation, and a patch changeset.
File summaries
File Description
.changeset/canvas-firing-animation-budget.md Records the performance improvement.
docs/simulation.md Documents animation visibility behavior.
arc.tsx Gates arc firing animations by visibility.
classic-transition-node.tsx Uses the shared animation hook.
firing-animation-visibility.test.ts Tests viewport visibility rules.
firing-animation-visibility.ts Implements visibility calculations.
transition-node.tsx Uses the shared animation hook.
use-transition-firing-animation.ts Centralizes transition firing animation logic.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Every transition that fires animates its box, its bolt and each of its
arcs, and the style engine resolves and paints each of those animations
for as long as it runs. On a large net that is hundreds in flight at
once, costing more per frame than everything else the canvas does.

Most of that work lands where nobody is looking: on nodes off the side
of the pane, or on a net drawn small enough that the flash covers a few
pixels. Both are now skipped. On a thousand-node net a scrub goes from
13 to 44 frames per second, and playback from 16 to 39.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

3 participants