-
Notifications
You must be signed in to change notification settings - Fork 82
WIP Add GPU transitions and touch/pen support #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Stukova
wants to merge
25
commits into
main
Choose a base branch
from
feature/gpu-transitions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,271
−366
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
c5fd30e
feat: add GPU transitions and runtime simulation toggle
Stukova 050e19b
docs(stories): add point position transition example
Stukova 6b540da
docs(history): add history guidelines and /history Claude Code skill
Stukova 75b1a15
fix(transitions): only pause simulation for position transitions
Stukova 74e0567
fix(transitions): invalidate caches, gate hover, drop dead buffer ali…
Stukova c9d3f6a
docs(history): refresh gpu-transitions entry, add /history --update
Stukova b775c4c
fix(transitions): guard destroyed position textures
Stukova 45a12f4
fix(transitions): gate shouldAnimate on isPendingFor(Positions)
Stukova a826398
docs(history): add Codex /history skill and update related docs
Stukova bcbbdcb
docs(history): describe transitionDuration<=0 snap behavior more prec…
Stukova 579801d
fix(simulation): restore start() reheating semantics
Stukova 5006eea
fix(interactions): narrow transition hover and drag guards
Stukova fed8dcd
fix(transitions): tighten simulation/transition guards
Stukova 6256b66
fix(transitions): prevent negative progress by sampling time in step()
Stukova cd32f59
chore(transitions): minor doc and code tweaks
Stukova b8f993a
docs(history): refresh gpu-transitions entry; skip history-only commits
Stukova 635b6f4
3.0.0-beta.9
Stukova bdac751
fix(transitions): refresh tracked positions during interpolation
Stukova f65b746
docs(transitions): clarify onTransitionEnd interrupt sources
Stukova 2ca5348
fix(transitions): avoid leaking target into current position textures
Stukova 399b000
3.0.0-beta.10
Stukova c4b93e1
feat(interactions): touch and pen support via pointer events
Stukova 95dd97e
docs(history): add touch-input entry
Stukova 04eaf8c
fix(interactions): clear right-click flag on pointerup
Stukova 3f8e643
refactor(interactions): consolidate canvas event listeners
Stukova File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| --- | ||
| name: history | ||
| description: Draft a new history entry, or update the most recent one with `--update`. Use after a meaningful commit or set of commits. Provide the "why" as an argument — /history the reason this change was made. Use /history --update to extend the latest entry without prompting. | ||
| allowed-tools: Bash(git *) Bash(date *) Bash(mkdir *) Bash(ls *) Read Glob Edit Write | ||
| --- | ||
|
|
||
| Draft a history entry and save it to `history/` following this repo's conventions. | ||
|
|
||
| If `$ARGUMENTS` starts with `--update` (or `-u`), follow the **Update mode** steps. Otherwise follow **New entry** steps. | ||
|
|
||
| ## New entry | ||
|
|
||
| 1. Run `git log --oneline -15` to see recent commits and identify which ones this entry covers. | ||
| 2. Run `git show --stat` on the relevant commit(s) to see what changed. | ||
| 3. Read `history/PROMPT.md` — it contains the full writing instructions. Follow them. | ||
| 4. Read 1–2 recent files under `history/` to match their tone and structure. | ||
| 5. The **why** is: `$ARGUMENTS`. If empty, ask the user for it before proceeding — do not skip or guess. | ||
| 6. Draft the entry. Use today's date for the filename. Use `## h2` sections for substantial entries, inline bold labels for small ones. | ||
| 7. If any commit hashes are uncertain or not yet merged, leave them as `<!-- TODO -->`. | ||
| 8. Create the year directory if needed (`history/YYYY/`), then write the file to `history/YYYY/YYYY-MM-DD-topic.md`. | ||
| 9. Show the user the saved file path and content. Ask them to review before committing. | ||
|
|
||
| ## Update mode (`--update` / `-u`) | ||
|
|
||
| Use this when recent commits extend or correct a topic that already has an entry — do not create a new file, do not ask for a "why". | ||
|
|
||
| 1. Locate the latest entry: `ls history/*/ | tail` and pick the file with the most recent `YYYY-MM-DD-…` filename. That's the "latest topic". | ||
| 2. Run `git log --oneline -15` to see recent commits. The commits to add are everything since the **Commits** line in that entry (those hashes already documented). If the entry's hashes don't exist (squashed during merge), use the new commits that touch the same area. | ||
| 3. Run `git show --stat` on the new commit(s) to see what changed. | ||
| 4. Read the existing entry in full so updates stay consistent with its structure and tone. | ||
| 5. Update the entry in place with `Edit`: | ||
| - Append the new commit hashes to the **Commits** line (replace stale/squashed hashes if needed). **Skip commits that only touch `history/`** — entries that exist solely to refine the history doc itself shouldn't be listed as commits the entry "covers". | ||
| - Revise sections affected by the new commits (behavior matrices, migration notes, examples, API names) so the doc reflects the current state — not an addendum tacked on the end. | ||
| - Keep the original filename and date — the file represents the topic, not the latest commit. | ||
| - Anything left ambiguous by the commit messages: leave a `<!-- TODO: ... -->` rather than guessing. | ||
| 6. Show the user the file path and a summary of what changed (which sections, which commits added). Ask them to review before committing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| --- | ||
| name: history | ||
| description: Draft a new history entry, or update the most recent one with `--update`. Use after a meaningful commit or set of commits. Provide the "why" as an argument — /history the reason this change was made. Use /history --update to extend the latest entry without prompting. | ||
| allowed-tools: Bash(git *) Bash(date *) Bash(mkdir *) Bash(ls *) Read Glob Edit Write | ||
| --- | ||
|
|
||
| Draft a history entry and save it to `history/` following this repo's conventions. | ||
|
|
||
| If `$ARGUMENTS` starts with `--update` (or `-u`), follow the **Update mode** steps. Otherwise follow **New entry** steps. | ||
|
|
||
| ## New entry | ||
|
|
||
| 1. Run `git log --oneline -15` to see recent commits and identify which ones this entry covers. | ||
| 2. Run `git show --stat` on the relevant commit(s) to see what changed. | ||
| 3. Read `history/PROMPT.md` — it contains the full writing instructions. Follow them. | ||
| 4. Read 1–2 recent files under `history/` to match their tone and structure. | ||
| 5. The **why** is: `$ARGUMENTS`. If empty, ask the user for it before proceeding — do not skip or guess. | ||
| 6. Draft the entry. Use today's date for the filename. Use `## h2` sections for substantial entries, inline bold labels for small ones. | ||
| 7. If any commit hashes are uncertain or not yet merged, leave them as `<!-- TODO -->`. | ||
| 8. Create the year directory if needed (`history/YYYY/`), then write the file to `history/YYYY/YYYY-MM-DD-topic.md`. | ||
| 9. Show the user the saved file path and content. Ask them to review before committing. | ||
|
|
||
| ## Update mode (`--update` / `-u`) | ||
|
|
||
| Use this when recent commits extend or correct a topic that already has an entry — do not create a new file, do not ask for a "why". | ||
|
|
||
| 1. Locate the latest entry: `ls history/*/ | tail` and pick the file with the most recent `YYYY-MM-DD-…` filename. That's the "latest topic". | ||
| 2. Run `git log --oneline -15` to see recent commits. The commits to add are everything since the **Commits** line in that entry (those hashes already documented). If the entry's hashes don't exist (squashed during merge), use the new commits that touch the same area. | ||
| 3. Run `git show --stat` on the new commit(s) to see what changed. | ||
| 4. Read the existing entry in full so updates stay consistent with its structure and tone. | ||
| 5. Update the entry in place with `Edit`: | ||
| - Append the new commit hashes to the **Commits** line (replace stale/squashed hashes if needed). **Skip commits that only touch `history/`** — entries that exist solely to refine the history doc itself shouldn't be listed as commits the entry "covers". | ||
| - Revise sections affected by the new commits (behavior matrices, migration notes, examples, API names) so the doc reflects the current state — not an addendum tacked on the end. | ||
| - Keep the original filename and date — the file represents the topic, not the latest commit. | ||
| - Anything left ambiguous by the commit messages: leave a `<!-- TODO: ... -->` rather than guessing. | ||
| 6. Show the user the file path and a summary of what changed (which sections, which commits added). Ask them to review before committing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| <!-- suggested path: history/2026/2026-04-22-gpu-transitions.md --> | ||
|
|
||
| # GPU transitions for positions and attributes | ||
|
|
||
| **Commits:** c5fd30e, 75b1a15, 74e0567, b775c4c, 45a12f4, 579801d, 5006eea, fed8dcd, 6256b66, cd32f59 | ||
|
|
||
| ## Why | ||
|
|
||
| We wanted Cosmos to: | ||
|
|
||
| - Smoothly animate point positions, colors, and sizes (and link colors and widths) from one state to another, instead of snapping. | ||
| - Switch the simulation on and off at runtime, and actually free its GPU resources when it's off so apps that don't need layout don't pay for it. | ||
|
|
||
| ## Transitions | ||
|
|
||
| New module `src/modules/Transition/` — a single state machine that tracks every animated property (positions, point colors/sizes, link colors/widths) in one shared cycle. | ||
|
|
||
| New config: | ||
|
|
||
| ```ts | ||
| transitionDuration: 800, // ms; 0 or less = no animation | ||
| transitionEasing: TransitionEasing.CubicInOut, | ||
| onTransitionStart?: () => void | ||
| onTransition?: (progress: number) => void | ||
| onTransitionEnd?: (interrupted: boolean) => void | ||
| ``` | ||
|
|
||
| **First render after init.** Position setters never animate — there's no prior state to interpolate from. The auto-pause rule is also skipped. Attribute setters fire transition callbacks, but since there's no prior attribute data either, source equals target and the result is a visual snap. | ||
|
|
||
| **Auto-pause (position transitions only).** When `render()` sees a pending **position** transition with `transitionDuration > 0` and a running simulation (and it's not the first render), the simulation pauses before the transition starts and `onSimulationPause` fires. The simulation **stays paused after the transition ends** — `setPointPositions()` signals the user wants to explore a specific layout, not have forces immediately pull nodes away from it. Call `unpause()` to resume explicitly. Color and size transitions don't compete with force updates and never pause the simulation. | ||
|
|
||
| **Hover and drag during transitions.** Hover detection is skipped only during point size transitions — point hover picking reads target point sizes rather than interpolated sizes, so hit-testing would otherwise mismatch the geometry currently on screen. Position and link hover continue to use the interpolated current positions. Drag start is blocked during point position **and** point size transitions: starting a drag mid-position-transition would mean grabbing a point whose on-screen location is still moving under the cursor, and mid-size-transition the hit area would mismatch the visible point. | ||
|
|
||
| **Cache invalidation.** Position and centroid caches are invalidated each frame during a position transition so `getTrackedPointPositionsMap()` / `getTrackedPointPositionsArray()` and centroid getters return the interpolated values, not stale post-transition targets — important when the simulation is paused and there's no other refresh driving cache turnover. | ||
|
|
||
| **`fitView` during transition.** `fitView()` and `fitViewByPointIndices()` frame the target positions (`graph.pointPositions`), not the interpolated positions currently on screen. | ||
|
|
||
| ## Simulation toggle | ||
|
|
||
| `enableSimulation` is now runtime-switchable via `setConfig` or `setConfigPartial` (it's **not** in `preserveInitOnlyFields`): | ||
|
|
||
| ```ts | ||
| graph.setConfigPartial({ enableSimulation: true }) | ||
| graph.setConfigPartial({ enableSimulation: false }) | ||
| ``` | ||
|
|
||
| - `false → true`: creates simulation modules and GPU resources, fires `onSimulationStart`. If a transition is mid-flight, it's interrupted first (`onTransitionEnd(true)`) and the simulation starts from the current mid-animation positions. Any **queued but not yet started** position transition is also dropped, so the next `render()` doesn't immediately auto-pause the simulation it just enabled. | ||
| - `true → false`: stops the simulation, destroys simulation-only modules and GPU resources, fires `onSimulationEnd`. Any active transition keeps playing — its state is untouched. | ||
|
|
||
| `start()` and `unpause()` only interrupt a transition when **positions** are animating; an active color/size cycle keeps running. Repeated `start(alpha)` calls now reheat the simulation by resetting `alpha` and `simulationProgress`, but `onSimulationStart` only fires on a stopped/paused → running transition, not on every reheat. | ||
|
|
||
| ## Behavior matrix | ||
|
|
||
| All rows assume a setter ran (e.g. `setPointPositions`) so a transition is **pending** when `render()` fires. `enableSimulation` = simulation on/off, `transitionDuration` = transition duration. Rows describe the **second and later** renders — on the first render, position setters always snap (see "First render after init" above). | ||
|
|
||
| ### Initial state at `render()` | ||
|
|
||
| Each row is the state when `render()` fires. The last two columns show what happens if you flip each config via `setConfigPartial` from that state. Auto-pause applies to **position** transitions only — color/size transitions never pause the simulation. | ||
|
|
||
| | `enableSimulation` + `transitionDuration` | Behavior | `enableSimulation` switch | `transitionDuration` switch | | ||
| |---|---|---|---| | ||
| | `false` + `≤0` (no simulation, no transition) | Snap. No simulation, no transition cycle. | `→ true`: starts simulation, creates modules and resources, fires `onSimulationStart`. | `→ >0`: next animation uses the new duration. | | ||
| | `false` + `>0` (no simulation, transition) | Animate. No simulation to pause. | `→ true`: interrupts the transition (`onTransitionEnd(true)`), then starts simulation from current positions, fires `onSimulationStart`. | `→ ≤0`: the next update + render cycle snaps immediately instead of animating. If a transition is already running, the next render frame interrupts it with `onTransitionEnd(true)`. | | ||
| | `true` + `≤0` (simulation, no transition) | Snap. Simulation keeps running. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ >0`: next animation uses the new duration. | | ||
| | `true` + `>0` (simulation, **position** transition) | Animate. **Simulation auto-pauses and stays paused after** the transition ends; `onSimulationPause` fires. Call `unpause()` to resume. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: the next update + render cycle snaps immediately instead of animating. `start()` only changes simulation state. If a transition is already running, the next render frame interrupts it with `onTransitionEnd(true)`; `onSimulationEnd` still only fires if the simulation is later stopped or finishes. | | ||
| | `true` + `>0` (simulation, **color/size** transition) | Animate. Simulation keeps running — color/size transitions don't compete with forces. | `→ false`: stops simulation, destroys simulation-only resources, fires `onSimulationEnd`. | `→ ≤0`: the next update + render cycle snaps immediately instead of animating. `start()` only changes simulation state. If a transition is already running, the next render frame interrupts it with `onTransitionEnd(true)`; `onSimulationEnd` still only fires if the simulation is later stopped or finishes. | | ||
|
|
||
| ## Migration | ||
|
|
||
| The new `transitionDuration` config defaults to `800` ms, so calling `setPointPositions(...); render()` after the first render will now animate instead of snap — and if the simulation is running, it will auto-pause for the duration of the animation **and stay paused afterwards**. Call `graph.unpause()` to resume forces. | ||
|
|
||
| To keep the old snap behavior, set `transitionDuration: 0` in your config: | ||
|
|
||
| ```ts | ||
| new Graph(el, { transitionDuration: 0, ... }) | ||
| ``` | ||
|
|
||
| Or disable it only for specific programmatic updates: | ||
|
|
||
| ```ts | ||
| graph.setConfigPartial({ transitionDuration: 0 }) | ||
| graph.setPointPositions(newPositions) | ||
| graph.render() | ||
| graph.setConfigPartial({ transitionDuration: 800 }) // restore if needed | ||
| ``` | ||
|
|
||
| ## Example | ||
|
|
||
| `src/stories/transition/` (Storybook: **Examples / Transitions → Point Transition**) — a 200k-point cloud sampled from Bryullov's *Horsewoman* (1832) that auto-loops between the picture layout and a sequence of tile scatters. Demonstrates `transitionDuration`, `TransitionEasing`, and the `onTransitionStart` / `onTransition` / `onTransitionEnd` callbacks in a self-contained, runnable setup. | ||
|
|
||
| ## Future work | ||
|
|
||
| - **Separate timelines per property.** One clock runs all animations today, so a new one cuts the previous off. Goal: independent timelines per property (or per setter call). | ||
| - **Mid-animation attribute updates.** Point and link attribute transitions reuse their source/target buffers when topology stays the same, but smoothing updates that arrive in the middle of an already-running attribute transition is still a known edge case. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.