From c43b4ed788b3e8684a30176a964ad7ef05b0db77 Mon Sep 17 00:00:00 2001
From: vicquick <88567707+vicquick@users.noreply.github.com>
Date: Fri, 21 Aug 2026 23:28:01 +0200
Subject: [PATCH] =?UTF-8?q?feat(constellarium):=20rebuild=20on=20pixi=20+?=
=?UTF-8?q?=20d3-force=20=E2=80=94=20Obsidian's=20actual=20mechanics?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
Claude-Session: https://claude.ai/code/session_01GobDsDKP6FE5hr747DPtcC
---
dashboard/package-lock.json | 193 +++++-
dashboard/package.json | 10 +
dashboard/src/views/ConstellationView.vue | 690 +++++++++++++++-------
3 files changed, 661 insertions(+), 232 deletions(-)
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 42fd63e..4fd00ad 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -11,14 +11,20 @@
"@cosmos.gl/graph": "^3.4.1",
"@mdi/font": "^7.4.47",
"@sigma/node-border": "^3.0.0",
+ "@tweenjs/tween.js": "^25.0.0",
"@types/dompurify": "^3.0.5",
"axios": "^1.7.9",
+ "d3-drag": "^3.0.0",
+ "d3-force": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
"dompurify": "^3.4.2",
"graphology": "^0.25.4",
"graphology-layout-forceatlas2": "^0.10.1",
"marked": "^18.0.3",
"motion-v": "^2.2.1",
"pinia": "^2.3.0",
+ "pixi.js": "^8.20.0",
"sigma": "^3.0.0",
"three": "^0.172.0",
"vue": "^3.5.13",
@@ -26,6 +32,10 @@
"vuetify": "^3.10.0"
},
"devDependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-force": "^3.0.10",
+ "@types/d3-selection": "^3.0.11",
+ "@types/d3-zoom": "^3.0.8",
"@types/three": "^0.172.0",
"@vitejs/plugin-vue": "^5.2.0",
"typescript": "^5.7.0",
@@ -606,6 +616,12 @@
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
"license": "Apache-2.0"
},
+ "node_modules/@pixi/colord": {
+ "version": "2.9.6",
+ "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
+ "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA==",
+ "license": "MIT"
+ },
"node_modules/@probe.gl/env": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.1.tgz",
@@ -962,12 +978,63 @@
}
},
"node_modules/@tweenjs/tween.js": {
- "version": "23.1.3",
- "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
- "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "version": "25.0.0",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
+ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
@@ -977,6 +1044,12 @@
"@types/trusted-types": "*"
}
},
+ "node_modules/@types/earcut": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-3.0.0.tgz",
+ "integrity": "sha512-k/9fOUGO39yd2sCjrbAJvGDEQvRwRnQIZlBz43roGwUZo5SHAmyVvSFyaVVZkicRVCaDXPKlbxrUcBuJoSWunQ==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1012,6 +1085,13 @@
"meshoptimizer": "~0.18.1"
}
},
+ "node_modules/@types/three/node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -1276,9 +1356,17 @@
"version": "0.1.69",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
- "dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.14",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.14.tgz",
+ "integrity": "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/alien-signals": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
@@ -1403,6 +1491,20 @@
"node": ">=12"
}
},
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-format": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
@@ -1424,6 +1526,15 @@
"node": ">=12"
}
},
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
@@ -1574,6 +1685,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/earcut": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz",
+ "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
+ "license": "ISC"
+ },
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1679,6 +1796,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -1836,6 +1959,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/gifuct-js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
+ "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+ "license": "MIT",
+ "dependencies": {
+ "js-binary-schema-parser": "^2.0.3"
+ }
+ },
"node_modules/gl-bench": {
"version": "1.0.42",
"resolved": "https://registry.npmjs.org/gl-bench/-/gl-bench-1.0.42.tgz",
@@ -1965,6 +2097,18 @@
"node": ">=12"
}
},
+ "node_modules/ismobilejs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
+ "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==",
+ "license": "MIT"
+ },
+ "node_modules/js-binary-schema-parser": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+ "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==",
+ "license": "MIT"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2108,6 +2252,12 @@
"integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
"license": "MIT"
},
+ "node_modules/parse-svg-path": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.2.0.tgz",
+ "integrity": "sha512-Tf7FFIrguPKQwzD4pWnYkR2VOv3raoHeKED80Bm+BYHI3KxC8KsgsGC5+fSMzAGDA6UEk4bHvmi+RsjmL3khpg==",
+ "license": "MIT"
+ },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -2156,6 +2306,32 @@
}
}
},
+ "node_modules/pixi.js": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.20.0.tgz",
+ "integrity": "sha512-WL7+yhxgNnI87AbucMR2X7yXmi/XhhYA25w2wV1QaNv6WMByM02kSX2ryreSq38OavHytFD0zY0bX6Nqgj/WAA==",
+ "license": "MIT",
+ "workspaces": [
+ "examples",
+ "playground"
+ ],
+ "dependencies": {
+ "@pixi/colord": "^2.9.6",
+ "@types/earcut": "^3.0.0",
+ "@webgpu/types": "^0.1.69",
+ "@xmldom/xmldom": "^0.8.13",
+ "earcut": "^3.0.2",
+ "eventemitter3": "^5.0.1",
+ "gifuct-js": "^2.1.2",
+ "ismobilejs": "^1.1.1",
+ "parse-svg-path": "^0.2.0",
+ "tiny-lru": "^11.4.7"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/pixijs"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -2278,6 +2454,15 @@
"integrity": "sha512-6HMgMlzU97MsV7D/tY8Va38b83kz8YJX+BefKjspMNAv0Vx6dxMogHOrnRl/sbMIs3BPUKijPqDqJ/+UwJbIow==",
"license": "MIT"
},
+ "node_modules/tiny-lru": {
+ "version": "11.4.7",
+ "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz",
+ "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
diff --git a/dashboard/package.json b/dashboard/package.json
index 0cc5a46..362e09b 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -12,14 +12,20 @@
"@cosmos.gl/graph": "^3.4.1",
"@mdi/font": "^7.4.47",
"@sigma/node-border": "^3.0.0",
+ "@tweenjs/tween.js": "^25.0.0",
"@types/dompurify": "^3.0.5",
"axios": "^1.7.9",
+ "d3-drag": "^3.0.0",
+ "d3-force": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
"dompurify": "^3.4.2",
"graphology": "^0.25.4",
"graphology-layout-forceatlas2": "^0.10.1",
"marked": "^18.0.3",
"motion-v": "^2.2.1",
"pinia": "^2.3.0",
+ "pixi.js": "^8.20.0",
"sigma": "^3.0.0",
"three": "^0.172.0",
"vue": "^3.5.13",
@@ -27,6 +33,10 @@
"vuetify": "^3.10.0"
},
"devDependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-force": "^3.0.10",
+ "@types/d3-selection": "^3.0.11",
+ "@types/d3-zoom": "^3.0.8",
"@types/three": "^0.172.0",
"@vitejs/plugin-vue": "^5.2.0",
"typescript": "^5.7.0",
diff --git a/dashboard/src/views/ConstellationView.vue b/dashboard/src/views/ConstellationView.vue
index a209964..f8aa2a0 100644
--- a/dashboard/src/views/ConstellationView.vue
+++ b/dashboard/src/views/ConstellationView.vue
@@ -21,10 +21,42 @@
@click="setTier(i)"
>{{ t.label }}
+ ·
+
charting the heavens…
+
+
+