feat(constellarium): rebuild on pixi + d3-force — Obsidian's actual mechanics - #147
Merged
Conversation
…echanics
The old Constellarium ran cosmos.gl, a GPU particle simulator with its own
shader integrator. It could show 12k points, but no parameter tuning could
make it feel like Obsidian, because the feel is not a parameter: it is
d3-force's velocity-Verlet settle plus a handful of very specific
interaction behaviours. Researched against the canonical open replica
(Quartz v4 graph.inline.ts — pixi.js + d3-force, the same stack Obsidian
itself credits) and implemented its mechanics verbatim:
charge = forceManyBody().strength(-100 * repelForce) [repel 0.5]
center = forceCenter().strength(centerForce) [0.3]
link = forceLink().distance(linkDistance) [30]
collide = forceCollide(nodeRadius)
radius = 2 + sqrt(degree)
hover → non-neighbour nodes AND links tween to α0.2 over 200ms;
hovered label pops to α1 at 1.1x over 100ms
label → α = max((k · opacityScale − 1) / 3.75, 0) — the zoom fade law
drag → alphaTarget(1) reheat on grab; fx delta divided by zoom k;
release un-pins so the node re-floats
Plus the four Obsidian force sliders (center / repel / link force / link
distance) live on the running simulation, and a text-fade slider — the
"behaves like Obsidian" ask made literal.
Deviations, each deliberate and documented inline:
- node radius capped at 14: our hub entities reach degree 500+, which no
vault sees — uncapped they render as plates
- labels materialised for the top 400 by degree + on hover, not all 5k:
Obsidian-scale vaults are hundreds of notes; faint stars' labels are
zoom-faded to zero anyway, so this is visually indistinguishable
- deep tier 12k -> 5k: d3-force is CPU; Obsidian itself slows at that
scale, and the feel matters more here than raw count. The GPU full sky
still exists at /galaxy.
Kept: manuscript chrome, tier picker, seek, folio panel, the curated
12-hue warm community palette, community-bearing seeding.
Bundle 570kB -> 360kB (cosmos + webgl-device out, pixi + d3 in).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GobDsDKP6FE5hr747DPtcC
vicquick
enabled auto-merge (squash)
August 21, 2026 21:28
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Why cosmos.gl could never feel like Obsidian
The feel isn't a parameter. It's d3-force's velocity-Verlet settle plus a handful of very specific interaction behaviours, and cosmos.gl runs its own shader integrator. Tuning was never going to converge.
Research (stored in nobrainr, tags: research/obsidian-graph)
Obsidian core is closed, but the canonical open replica — Quartz v4
graph.inline.ts— uses pixi.js + d3-force and its mechanics are readable. Cross-checked against Obsidian's official Help (sliders, filters, groups) and the plugin ecosystem (Pixi.js WebGL mirrors the core's stack).The mechanics, implemented verbatim
forceManyBody().strength(-100 * repelForce), repel 0.5forceCenter().strength(0.3)forceLink().distance(30)+ strength sliderforceCollide(nodeRadius)2 + √degreeα = max((k·opacityScale − 1)/3.75, 0)— the exact zoom lawalphaTarget(1)reheat on grab;fx = init + Δ/k; release un-pins, node re-floatsPlus Obsidian's four force sliders live on the running sim + text-fade slider — "behaves like Obsidian" made literal.
Deliberate deviations (documented inline)
/galaxy.Kept
Manuscript chrome, tier picker, seek, folio, the curated 12-hue warm palette, community-bearing seeding.
Bundle 570kB → 360kB. Build passes.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GobDsDKP6FE5hr747DPtcC