mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 15:05:42 -05:00
perf(graph): initial canvas layout
include nodes and links drawn Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
This commit is contained in:
parent
d27c292736
commit
e00c1ac491
82
package-lock.json
generated
82
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.8",
|
||||||
"@napi-rs/simple-git": "0.1.16",
|
"@napi-rs/simple-git": "0.1.16",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
|
"pixi.js": "^8.3.3",
|
||||||
"preact": "^10.23.2",
|
"preact": "^10.23.2",
|
||||||
"preact-render-to-string": "^6.5.7",
|
"preact-render-to-string": "^6.5.7",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
@ -810,6 +812,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@ -838,6 +846,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tweenjs/tween.js": {
|
||||||
|
"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/cli-spinner": {
|
"node_modules/@types/cli-spinner": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cli-spinner/-/cli-spinner-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cli-spinner/-/cli-spinner-0.2.3.tgz",
|
||||||
@ -847,6 +861,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/css-font-loading-module": {
|
||||||
|
"version": "0.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
|
||||||
|
"integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3": {
|
"node_modules/@types/d3": {
|
||||||
"version": "7.4.3",
|
"version": "7.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
@ -1108,6 +1128,12 @@
|
|||||||
"@types/ms": "*"
|
"@types/ms": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/earcut": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
@ -1230,6 +1256,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
|
||||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@webgpu/types": {
|
||||||
|
"version": "0.1.44",
|
||||||
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz",
|
||||||
|
"integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||||
|
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
||||||
@ -2130,6 +2171,12 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/earcut": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/eastasianwidth": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
@ -2248,6 +2295,12 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/extend": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
@ -3112,6 +3165,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||||
},
|
},
|
||||||
|
"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/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
|
||||||
@ -4614,6 +4673,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
|
||||||
"integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="
|
"integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-svg-path": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||||
@ -4690,6 +4755,23 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pixi.js": {
|
||||||
|
"version": "8.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.3.3.tgz",
|
||||||
|
"integrity": "sha512-dpucBKAqEm0K51MQKlXvyIJ40bcxniP82uz4ZPEQejGtPp0P+vueuG5DyArHCkC48mkVE2FEDvyYvBa45/JlQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pixi/colord": "^2.9.6",
|
||||||
|
"@types/css-font-loading-module": "^0.0.12",
|
||||||
|
"@types/earcut": "^2.1.4",
|
||||||
|
"@webgpu/types": "^0.1.40",
|
||||||
|
"@xmldom/xmldom": "^0.8.10",
|
||||||
|
"earcut": "^2.2.4",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"ismobilejs": "^1.1.1",
|
||||||
|
"parse-svg-path": "^0.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/preact": {
|
"node_modules/preact": {
|
||||||
"version": "10.23.2",
|
"version": "10.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.23.2.tgz",
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@floating-ui/dom": "^1.6.8",
|
"@floating-ui/dom": "^1.6.8",
|
||||||
"@napi-rs/simple-git": "0.1.16",
|
"@napi-rs/simple-git": "0.1.16",
|
||||||
|
"@tweenjs/tween.js": "^25.0.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
|
"pixi.js": "^8.3.3",
|
||||||
"preact": "^10.23.2",
|
"preact": "^10.23.2",
|
||||||
"preact-render-to-string": "^6.5.7",
|
"preact-render-to-string": "^6.5.7",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
|
|||||||
@ -32,9 +32,9 @@ const defaultOptions: GraphOptions = {
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
scale: 1.1,
|
scale: 1.1,
|
||||||
repelForce: 0.5,
|
repelForce: 0.5,
|
||||||
centerForce: 0.3,
|
centerForce: 0.1,
|
||||||
linkDistance: 30,
|
linkDistance: 30,
|
||||||
fontSize: 0.6,
|
fontSize: 6,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
@ -44,11 +44,11 @@ const defaultOptions: GraphOptions = {
|
|||||||
drag: true,
|
drag: true,
|
||||||
zoom: true,
|
zoom: true,
|
||||||
depth: -1,
|
depth: -1,
|
||||||
scale: 0.9,
|
scale: 1.25,
|
||||||
repelForce: 0.5,
|
repelForce: 1,
|
||||||
centerForce: 0.3,
|
centerForce: 0.1,
|
||||||
linkDistance: 30,
|
linkDistance: 50,
|
||||||
fontSize: 0.6,
|
fontSize: 12,
|
||||||
opacityScale: 1,
|
opacityScale: 1,
|
||||||
showTags: true,
|
showTags: true,
|
||||||
removeTags: [],
|
removeTags: [],
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
import type { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||||
import * as d3 from "d3"
|
import * as d3 from "d3"
|
||||||
|
import * as PIXI from "pixi.js"
|
||||||
|
import * as TWEEN from "@tweenjs/tween.js"
|
||||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||||
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
|
||||||
|
|
||||||
@ -7,13 +9,32 @@ type NodeData = {
|
|||||||
id: SimpleSlug
|
id: SimpleSlug
|
||||||
text: string
|
text: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
|
||||||
|
label?: PIXI.Text
|
||||||
|
|
||||||
|
gfx?: PIXI.Graphics
|
||||||
|
alpha?: number
|
||||||
|
|
||||||
|
r?: number
|
||||||
|
active?: boolean
|
||||||
} & d3.SimulationNodeDatum
|
} & d3.SimulationNodeDatum
|
||||||
|
|
||||||
type LinkData = {
|
type LinkData = {
|
||||||
source: SimpleSlug
|
source: SimpleSlug
|
||||||
target: SimpleSlug
|
target: SimpleSlug
|
||||||
|
|
||||||
|
gfx?: PIXI.Graphics
|
||||||
|
alpha?: number
|
||||||
|
color?: string
|
||||||
|
|
||||||
|
active?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkNodes = Omit<LinkData, "source" | "target"> & {
|
||||||
|
source: NodeData
|
||||||
|
target: NodeData
|
||||||
|
} & d3.SimulationLinkDatum<NodeData>
|
||||||
|
|
||||||
const localStorageKey = "graph-visited"
|
const localStorageKey = "graph-visited"
|
||||||
function getVisited(): Set<SimpleSlug> {
|
function getVisited(): Set<SimpleSlug> {
|
||||||
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
||||||
@ -25,6 +46,18 @@ function addToVisited(slug: SimpleSlug) {
|
|||||||
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TweenNode = {
|
||||||
|
update: (time: number) => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let tweens = new Map<string, TweenNode>()
|
||||||
|
function animate(time: number) {
|
||||||
|
tweens.forEach((t) => t.update(time))
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
@ -55,8 +88,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
)
|
)
|
||||||
const links: LinkData[] = []
|
const links: LinkData[] = []
|
||||||
const tags: SimpleSlug[] = []
|
const tags: SimpleSlug[] = []
|
||||||
|
|
||||||
const validLinks = new Set(data.keys())
|
const validLinks = new Set(data.keys())
|
||||||
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
const width = graph.offsetWidth
|
||||||
|
|
||||||
for (const [source, details] of data.entries()) {
|
for (const [source, details] of data.entries()) {
|
||||||
const outgoing = details.links ?? []
|
const outgoing = details.links ?? []
|
||||||
|
|
||||||
@ -100,7 +135,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
if (showTags) tags.forEach((tag) => neighbourhood.add(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
// XXX: How does links got morphed into LinkNodes here?
|
||||||
|
// links => LinkData[], where as links.filter(l => neighbourhood.has(l.source) && neighbourhood.has(l.target)) => LinkNodes[]
|
||||||
|
const graphData: { nodes: NodeData[]; links: LinkNodes[] } = {
|
||||||
nodes: [...neighbourhood].map((url) => {
|
nodes: [...neighbourhood].map((url) => {
|
||||||
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
|
const text = url.startsWith("tags/") ? "#" + url.substring(5) : (data.get(url)?.title ?? url)
|
||||||
return {
|
return {
|
||||||
@ -109,242 +146,392 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
tags: data.get(url)?.tags ?? [],
|
tags: data.get(url)?.tags ?? [],
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
links: links.filter((l) => neighbourhood.has(l.source) && neighbourhood.has(l.target)),
|
links: links.filter(
|
||||||
|
(l) => neighbourhood.has(l.source) && neighbourhood.has(l.target),
|
||||||
|
) as unknown as LinkNodes[],
|
||||||
}
|
}
|
||||||
|
|
||||||
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
const computedStyleMap = new Map<string, string>()
|
||||||
.forceSimulation(graphData.nodes)
|
for (let i of [
|
||||||
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
"--secondary",
|
||||||
.force(
|
"--tertiary",
|
||||||
"link",
|
"--gray",
|
||||||
d3
|
"--light",
|
||||||
.forceLink(graphData.links)
|
"--lightgray",
|
||||||
.id((d: any) => d.id)
|
"--dark",
|
||||||
.distance(linkDistance),
|
"--darkgray",
|
||||||
)
|
"--bodyFont",
|
||||||
.force("center", d3.forceCenter().strength(centerForce))
|
]) {
|
||||||
|
computedStyleMap.set(i, getComputedStyle(graph).getPropertyValue(i))
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
}
|
||||||
const width = graph.offsetWidth
|
|
||||||
|
|
||||||
const svg = d3
|
|
||||||
.select<HTMLElement, NodeData>("#" + container)
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.attr("viewBox", [-width / 2 / scale, -height / 2 / scale, width / scale, height / scale])
|
|
||||||
|
|
||||||
// draw links between nodes
|
|
||||||
const link = svg
|
|
||||||
.append("g")
|
|
||||||
.selectAll("line")
|
|
||||||
.data(graphData.links)
|
|
||||||
.join("line")
|
|
||||||
.attr("class", "link")
|
|
||||||
.attr("stroke", "var(--lightgray)")
|
|
||||||
.attr("stroke-width", 1)
|
|
||||||
|
|
||||||
// svg groups
|
|
||||||
const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g")
|
|
||||||
|
|
||||||
// calculate color
|
// calculate color
|
||||||
const color = (d: NodeData) => {
|
const color = (d: NodeData) => {
|
||||||
const isCurrent = d.id === slug
|
const isCurrent = d.id === slug
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return "var(--secondary)"
|
return computedStyleMap.get("--secondary")
|
||||||
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
||||||
return "var(--tertiary)"
|
return computedStyleMap.get("--tertiary")
|
||||||
} else {
|
} else {
|
||||||
return "var(--gray)"
|
return computedStyleMap.get("--gray")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
|
|
||||||
function dragstarted(event: any, d: NodeData) {
|
|
||||||
if (!event.active) simulation.alphaTarget(1).restart()
|
|
||||||
d.fx = d.x
|
|
||||||
d.fy = d.y
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragged(event: any, d: NodeData) {
|
|
||||||
d.fx = event.x
|
|
||||||
d.fy = event.y
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragended(event: any, d: NodeData) {
|
|
||||||
if (!event.active) simulation.alphaTarget(0)
|
|
||||||
d.fx = null
|
|
||||||
d.fy = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const noop = () => {}
|
|
||||||
return d3
|
|
||||||
.drag<Element, NodeData>()
|
|
||||||
.on("start", enableDrag ? dragstarted : noop)
|
|
||||||
.on("drag", enableDrag ? dragged : noop)
|
|
||||||
.on("end", enableDrag ? dragended : noop)
|
|
||||||
}
|
|
||||||
|
|
||||||
function nodeRadius(d: NodeData) {
|
function nodeRadius(d: NodeData) {
|
||||||
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length
|
const numLinks = links.filter((l: any) => l.source.id === d.id || l.target.id === d.id).length
|
||||||
return 2 + Math.sqrt(numLinks)
|
return 2 + Math.sqrt(numLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectedNodes: SimpleSlug[] = []
|
function renderLinks(data: LinkNodes[]) {
|
||||||
|
tweens.get("link")?.stop()
|
||||||
|
const Group = new TWEEN.Group()
|
||||||
|
|
||||||
// draw individual nodes
|
data.forEach((l) => {
|
||||||
const node = graphNode
|
let alpha = 1
|
||||||
.append("circle")
|
if (currentNodeId) {
|
||||||
.attr("class", "node")
|
alpha = l.active ? 1 : 0.3
|
||||||
.attr("id", (d) => d.id)
|
|
||||||
.attr("r", nodeRadius)
|
|
||||||
.attr("fill", color)
|
|
||||||
.style("cursor", "pointer")
|
|
||||||
.on("click", (_, d) => {
|
|
||||||
const targ = resolveRelative(fullSlug, d.id)
|
|
||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
|
||||||
})
|
|
||||||
.on("mouseover", function (_, d) {
|
|
||||||
const currentId = d.id
|
|
||||||
const linkNodes = d3
|
|
||||||
.selectAll(".link")
|
|
||||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
|
||||||
|
|
||||||
if (focusOnHover) {
|
|
||||||
// fade out non-neighbour nodes
|
|
||||||
connectedNodes = linkNodes.data().flatMap((d: any) => [d.source.id, d.target.id])
|
|
||||||
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".link")
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style("opacity", 0.2)
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
|
||||||
.filter((d) => !connectedNodes.includes(d.id))
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style("opacity", 0.2)
|
|
||||||
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
|
||||||
.filter((d) => !connectedNodes.includes(d.id))
|
|
||||||
.nodes()
|
|
||||||
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
|
|
||||||
.forEach((it) => {
|
|
||||||
let opacity = parseFloat(it.style("opacity"))
|
|
||||||
it.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr("opacityOld", opacity)
|
|
||||||
.style("opacity", Math.min(opacity, 0.2))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
l.color = l.active ? computedStyleMap.get("--gray") : computedStyleMap.get("--lightgray")
|
||||||
// highlight links
|
Group.add(new TWEEN.Tween<LinkNodes>(l).to({ alpha }, 200))
|
||||||
linkNodes.transition().duration(200).attr("stroke", "var(--gray)").attr("stroke-width", 1)
|
|
||||||
|
|
||||||
const bigFont = fontSize * 1.5
|
|
||||||
|
|
||||||
// show text for self
|
|
||||||
const parent = this.parentNode as HTMLElement
|
|
||||||
d3.select<HTMLElement, NodeData>(parent)
|
|
||||||
.raise()
|
|
||||||
.select("text")
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.attr("opacityOld", d3.select(parent).select("text").style("opacity"))
|
|
||||||
.style("opacity", 1)
|
|
||||||
.style("font-size", bigFont + "em")
|
|
||||||
})
|
})
|
||||||
.on("mouseleave", function (_, d) {
|
|
||||||
if (focusOnHover) {
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".link").transition().duration(200).style("opacity", 1)
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node").transition().duration(200).style("opacity", 1)
|
|
||||||
|
|
||||||
d3.selectAll<HTMLElement, NodeData>(".node")
|
Group.getAll().forEach((tw) => tw.start())
|
||||||
.filter((d) => !connectedNodes.includes(d.id))
|
tweens.set("link", {
|
||||||
.nodes()
|
update: Group.update.bind(Group),
|
||||||
.map((it) => d3.select(it.parentNode as HTMLElement).select("text"))
|
stop() {
|
||||||
.forEach((it) => it.transition().duration(200).style("opacity", it.attr("opacityOld")))
|
Group.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLabels(data: NodeData[]) {
|
||||||
|
tweens.get("label")?.stop()
|
||||||
|
const Group = new TWEEN.Group()
|
||||||
|
|
||||||
|
data.forEach((n) => {
|
||||||
|
if (!n.label) return
|
||||||
|
if (currentNodeId === n.id) {
|
||||||
|
Group.add(
|
||||||
|
new TWEEN.Tween<PIXI.Text>(n.label).to(
|
||||||
|
{ alpha: 1, scale: { x: (1 / scale) * 1.5, y: (1 / scale) * 1.5 } },
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let alpha = n.active ? 0.8 : 0
|
||||||
|
Group.add(
|
||||||
|
new TWEEN.Tween<PIXI.Text>(n.label).to(
|
||||||
|
{ alpha, scale: { x: 1 / scale, y: 1 / scale } },
|
||||||
|
200,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const currentId = d.id
|
|
||||||
const linkNodes = d3
|
|
||||||
.selectAll(".link")
|
|
||||||
.filter((d: any) => d.source.id === currentId || d.target.id === currentId)
|
|
||||||
|
|
||||||
linkNodes.transition().duration(200).attr("stroke", "var(--lightgray)")
|
|
||||||
|
|
||||||
const parent = this.parentNode as HTMLElement
|
|
||||||
d3.select<HTMLElement, NodeData>(parent)
|
|
||||||
.select("text")
|
|
||||||
.transition()
|
|
||||||
.duration(200)
|
|
||||||
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
|
|
||||||
.style("font-size", fontSize + "em")
|
|
||||||
})
|
})
|
||||||
// @ts-ignore
|
|
||||||
.call(drag(simulation))
|
|
||||||
|
|
||||||
// make tags hollow circles
|
Group.getAll().forEach((tw) => tw.start())
|
||||||
node
|
tweens.set("label", {
|
||||||
.filter((d) => d.id.startsWith("tags/"))
|
update: Group.update.bind(Group),
|
||||||
.attr("stroke", color)
|
stop() {
|
||||||
.attr("stroke-width", 2)
|
Group.getAll().forEach((tw) => tw.stop())
|
||||||
.attr("fill", "var(--light)")
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// draw labels
|
function renderCurrentNode({
|
||||||
const labels = graphNode
|
nodeId,
|
||||||
.append("text")
|
focusOnHover,
|
||||||
.attr("dx", 0)
|
}: {
|
||||||
.attr("dy", (d) => -nodeRadius(d) + "px")
|
nodeId: string | null
|
||||||
.attr("text-anchor", "middle")
|
focusOnHover: boolean
|
||||||
.text((d) => d.text)
|
}) {
|
||||||
.style("opacity", (opacityScale - 1) / 3.75)
|
tweens.get("hover")?.stop()
|
||||||
.style("pointer-events", "none")
|
currentNodeId = nodeId
|
||||||
.style("font-size", fontSize + "em")
|
|
||||||
.raise()
|
|
||||||
// @ts-ignore
|
|
||||||
.call(drag(simulation))
|
|
||||||
|
|
||||||
// set panning
|
// NOTE: we need to create a new copy here
|
||||||
if (enableZoom) {
|
const connectedNodes: Set<SimpleSlug> = new Set()
|
||||||
svg.call(
|
|
||||||
|
graphData.links.forEach((l) => {
|
||||||
|
l.active = l.source.id === nodeId || l.target.id === nodeId
|
||||||
|
if (l.source.id === nodeId || l.target.id === nodeId) {
|
||||||
|
connectedNodes.add(l.source.id)
|
||||||
|
connectedNodes.add(l.target.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (nodeId) {
|
||||||
|
connectedNodes.add(nodeId as SimpleSlug)
|
||||||
|
}
|
||||||
|
const Group = new TWEEN.Group()
|
||||||
|
|
||||||
|
graphData.nodes.forEach((n) => {
|
||||||
|
if (!n.gfx) return
|
||||||
|
let alpha = 1
|
||||||
|
if (nodeId !== null) {
|
||||||
|
n.active = connectedNodes.has(n.id)
|
||||||
|
if (focusOnHover) alpha = connectedNodes.has(n.id) ? 1 : 0.2
|
||||||
|
if (n.id !== nodeId) {
|
||||||
|
Group.add(new TWEEN.Tween<PIXI.Graphics>(n.gfx, Group).to({ alpha }, 200))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n.active = false
|
||||||
|
Group.add(new TWEEN.Tween<PIXI.Graphics>(n.gfx, Group).to({ alpha }, 200))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
renderLabels(graphData.nodes)
|
||||||
|
renderLinks(graphData.links)
|
||||||
|
|
||||||
|
Group.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("hover", {
|
||||||
|
update: Group.update.bind(Group),
|
||||||
|
stop() {
|
||||||
|
Group.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tweens.forEach((tween) => tween.stop())
|
||||||
|
tweens.clear()
|
||||||
|
const app = new PIXI.Application()
|
||||||
|
await app.init({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
antialias: true,
|
||||||
|
autoStart: false,
|
||||||
|
autoDensity: true,
|
||||||
|
backgroundAlpha: 0,
|
||||||
|
preference: "webgpu",
|
||||||
|
resolution: window.devicePixelRatio,
|
||||||
|
eventMode: "static",
|
||||||
|
})
|
||||||
|
graph.appendChild(app.canvas)
|
||||||
|
|
||||||
|
const stage = app.stage
|
||||||
|
stage.interactive = false
|
||||||
|
stage.scale.set(1 / scale)
|
||||||
|
|
||||||
|
const nodesContainer = new PIXI.Container<PIXI.Graphics>({ zIndex: 1 })
|
||||||
|
const labelsContainer = new PIXI.Container<PIXI.Text>({ zIndex: 2 })
|
||||||
|
const linkGraphic = new PIXI.Graphics()
|
||||||
|
|
||||||
|
stage.addChild(nodesContainer, labelsContainer)
|
||||||
|
nodesContainer.addChild(linkGraphic)
|
||||||
|
|
||||||
|
const simulation: d3.Simulation<NodeData, LinkNodes> = d3
|
||||||
|
.forceSimulation(graphData.nodes)
|
||||||
|
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
||||||
|
.force("center", d3.forceCenter().strength(centerForce))
|
||||||
|
.force(
|
||||||
|
"link",
|
||||||
d3
|
d3
|
||||||
.zoom<SVGSVGElement, NodeData>()
|
.forceLink(graphData.links)
|
||||||
|
.id((d: any) => d.id)
|
||||||
|
.distance(linkDistance),
|
||||||
|
)
|
||||||
|
.force(
|
||||||
|
"collide",
|
||||||
|
d3.forceCollide((n) => nodeRadius(n)),
|
||||||
|
)
|
||||||
|
|
||||||
|
let currentNodeId: string | null = null
|
||||||
|
let currentNodeGfx: PIXI.Graphics | undefined
|
||||||
|
let dragStartTime = 0
|
||||||
|
|
||||||
|
graphData.nodes.forEach((n) => {
|
||||||
|
const nodeId = n.id
|
||||||
|
|
||||||
|
const label = new PIXI.Text({
|
||||||
|
text: n.text,
|
||||||
|
alpha: 0,
|
||||||
|
anchor: { x: 0.5, y: -0.5 },
|
||||||
|
style: {
|
||||||
|
fontSize,
|
||||||
|
fill: computedStyleMap.get("--dark"),
|
||||||
|
fontFamily: computedStyleMap.get("--bodyFont"),
|
||||||
|
},
|
||||||
|
resolution: window.devicePixelRatio,
|
||||||
|
})
|
||||||
|
label.scale.set(scale)
|
||||||
|
n.label = label
|
||||||
|
|
||||||
|
const gfx = new PIXI.Graphics({
|
||||||
|
interactive: true,
|
||||||
|
label: nodeId,
|
||||||
|
eventMode: "static",
|
||||||
|
hitArea: new PIXI.Circle(0, 0, nodeRadius(n)),
|
||||||
|
cursor: "pointer",
|
||||||
|
})
|
||||||
|
.circle(0, 0, nodeRadius(n))
|
||||||
|
.on("pointerover", () => {
|
||||||
|
tweens.get(nodeId)?.stop()
|
||||||
|
const tweenScale = { x: 1, y: 1 }
|
||||||
|
const tween = new TWEEN.Tween(tweenScale)
|
||||||
|
.to({ x: 1.5, y: 1.5 }, 100)
|
||||||
|
.onUpdate(() => {
|
||||||
|
gfx.scale.set(tweenScale.x, tweenScale.y)
|
||||||
|
})
|
||||||
|
.onStop(() => {
|
||||||
|
tweens.delete(nodeId)
|
||||||
|
})
|
||||||
|
.start()
|
||||||
|
tweens.set(nodeId, tween)
|
||||||
|
renderCurrentNode({ nodeId, focusOnHover })
|
||||||
|
})
|
||||||
|
.on("pointerdown", (e) => {
|
||||||
|
currentNodeGfx = e.target as PIXI.Graphics
|
||||||
|
})
|
||||||
|
.on("pointerup", () => {
|
||||||
|
currentNodeGfx = undefined
|
||||||
|
})
|
||||||
|
.on("pointerupoutside", () => {
|
||||||
|
currentNodeGfx = undefined
|
||||||
|
})
|
||||||
|
.on("pointerleave", () => {
|
||||||
|
tweens.get(nodeId)?.stop()
|
||||||
|
const tweenScale = {
|
||||||
|
x: gfx.scale.x,
|
||||||
|
y: gfx.scale.y,
|
||||||
|
}
|
||||||
|
const tween = new TWEEN.Tween(tweenScale)
|
||||||
|
.to({ x: 1, y: 1 }, 100)
|
||||||
|
.onUpdate(() => {
|
||||||
|
gfx.scale.set(tweenScale.x, tweenScale.y)
|
||||||
|
})
|
||||||
|
.onStop(() => {
|
||||||
|
tweens.delete(nodeId)
|
||||||
|
})
|
||||||
|
.start()
|
||||||
|
tweens.set(nodeId, tween)
|
||||||
|
renderCurrentNode({ nodeId: null, focusOnHover })
|
||||||
|
})
|
||||||
|
n.gfx = gfx
|
||||||
|
|
||||||
|
if (n.id.startsWith("tags/")) {
|
||||||
|
gfx.fill({ color: computedStyleMap.get("--light") }).stroke({ width: 0.5, color: color(n) })
|
||||||
|
} else {
|
||||||
|
gfx.fill(color(n)).stroke({ color: color(n) })
|
||||||
|
}
|
||||||
|
|
||||||
|
nodesContainer.addChild(gfx)
|
||||||
|
labelsContainer.addChild(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
graphData.links.forEach((l) => {
|
||||||
|
l.alpha = 1
|
||||||
|
l.color = computedStyleMap.get("--lightgray")
|
||||||
|
})
|
||||||
|
|
||||||
|
let currentTransform = d3.zoomIdentity
|
||||||
|
if (enableDrag) {
|
||||||
|
d3.select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
|
||||||
|
d3
|
||||||
|
.drag<HTMLCanvasElement, NodeData | undefined>()
|
||||||
|
.container(() => app.canvas)
|
||||||
|
.subject(() => {
|
||||||
|
// get the item in graphData such that item.gfx === currentNodeGfx
|
||||||
|
const target = graphData.nodes.filter((j) => j.gfx === currentNodeGfx)[0]
|
||||||
|
return target
|
||||||
|
})
|
||||||
|
.on("start", function dragstarted(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(1).restart()
|
||||||
|
event.subject.fx = event.subject.x
|
||||||
|
event.subject.fy = event.subject.y
|
||||||
|
event.subject.__initialDragPos = {
|
||||||
|
x: event.subject.x,
|
||||||
|
y: event.subject.y,
|
||||||
|
fx: event.subject.fx,
|
||||||
|
fy: event.subject.fy,
|
||||||
|
}
|
||||||
|
dragStartTime = Date.now()
|
||||||
|
})
|
||||||
|
.on("drag", function dragged(event) {
|
||||||
|
const k = currentTransform.k
|
||||||
|
const initPos = event.subject.__initialDragPos
|
||||||
|
const dragPos = event
|
||||||
|
event.subject.fx = initPos.x + (dragPos.x - initPos.x) / k
|
||||||
|
event.subject.fy = initPos.y + (dragPos.y - initPos.y) / k
|
||||||
|
})
|
||||||
|
.on("end", function dragended(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(0)
|
||||||
|
event.subject.fx = null
|
||||||
|
event.subject.fy = null
|
||||||
|
// Check for node click event here.
|
||||||
|
if (Date.now() - dragStartTime < 200) {
|
||||||
|
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
|
||||||
|
const targ = resolveRelative(fullSlug, node.id)
|
||||||
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (enableZoom) {
|
||||||
|
d3.select<HTMLCanvasElement, NodeData>(app.canvas).call(
|
||||||
|
d3
|
||||||
|
.zoom<HTMLCanvasElement, NodeData>()
|
||||||
.extent([
|
.extent([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[width, height],
|
[width, height],
|
||||||
])
|
])
|
||||||
.scaleExtent([0.25, 4])
|
.scaleExtent([0.25, 4])
|
||||||
.on("zoom", ({ transform }) => {
|
.on("zoom", ({ transform }) => {
|
||||||
link.attr("transform", transform)
|
currentTransform = transform
|
||||||
node.attr("transform", transform)
|
stage.scale.set(transform.k, transform.k)
|
||||||
|
stage.position.set(transform.x, transform.y)
|
||||||
|
|
||||||
const scale = transform.k * opacityScale
|
const scale = transform.k * opacityScale
|
||||||
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
|
||||||
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
const activeNodes = graphData.nodes
|
||||||
|
.filter((n) => n.active)
|
||||||
|
.flatMap((n) => n.label) as PIXI.Text[]
|
||||||
|
labelsContainer.children.forEach((label) => {
|
||||||
|
if (!activeNodes.includes(label)) {
|
||||||
|
label.alpha = scaleOpacity
|
||||||
|
}
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// progress the simulation
|
function animate() {
|
||||||
simulation.on("tick", () => {
|
graphData.nodes.forEach((n) => {
|
||||||
link
|
let { x, y, gfx, label, active } = n
|
||||||
.attr("x1", (d: any) => d.source.x)
|
if (!gfx || !x || !y || !label) return
|
||||||
.attr("y1", (d: any) => d.source.y)
|
gfx.position.set(x + width / 2, y + height / 2)
|
||||||
.attr("x2", (d: any) => d.target.x)
|
label.position.set(x + width / 2, y + height / 2)
|
||||||
.attr("y2", (d: any) => d.target.y)
|
gfx.zIndex = active ? 2 : 1
|
||||||
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
})
|
||||||
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
|
||||||
})
|
linkGraphic.clear()
|
||||||
|
graphData.links.forEach((l) => {
|
||||||
|
linkGraphic
|
||||||
|
.moveTo(l.source.x! + width / 2, l.source.y! + height / 2)
|
||||||
|
.lineTo(l.target.x! + width / 2, l.target.y! + height / 2)
|
||||||
|
.stroke({ alpha: l.alpha, width: 1, color: l.color })
|
||||||
|
})
|
||||||
|
app.renderer.render(stage)
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const slug = getFullSlug(window)
|
const slug = e.detail.url
|
||||||
|
addToVisited(simplifySlug(slug))
|
||||||
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
const container = document.getElementById("global-graph-outer")
|
const container = document.getElementById("global-graph-outer")
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
container?.classList.add("active")
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
function renderGlobalGraph() {
|
||||||
|
const slug = getFullSlug(window)
|
||||||
|
container?.classList.add("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGraph("global-graph-container", slug)
|
||||||
|
|
||||||
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
}
|
||||||
|
|
||||||
function hideGlobalGraph() {
|
function hideGlobalGraph() {
|
||||||
container?.classList.remove("active")
|
container?.classList.remove("active")
|
||||||
@ -356,15 +543,18 @@ function renderGlobalGraph() {
|
|||||||
removeAllChildren(graph)
|
removeAllChildren(graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
}
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
const globalGraphOpen = container?.classList.contains("active")
|
||||||
const slug = e.detail.url
|
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
addToVisited(simplifySlug(slug))
|
}
|
||||||
await renderGraph("graph-container", slug)
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
|
|
||||||
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
})
|
})
|
||||||
|
|||||||
@ -59,8 +59,8 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
height: 60vh;
|
height: 80vh;
|
||||||
width: 50vw;
|
width: 80vw;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and (max-width: $fullPageWidth) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user