From dd4b1cd6047ca76c2d3711b33238aa7387194714 Mon Sep 17 00:00:00 2001 From: Aaron Pham Date: Thu, 15 Aug 2024 03:33:30 -0400 Subject: [PATCH] chore(graph): add canvas element to avoid rerendering glitch Signed-off-by: Aaron Pham --- quartz/components/Graph.tsx | 14 +-- quartz/components/scripts/graph.inline.ts | 107 ++++++++++------------ 2 files changed, 53 insertions(+), 68 deletions(-) diff --git a/quartz/components/Graph.tsx b/quartz/components/Graph.tsx index 83536b8d9..48765696e 100644 --- a/quartz/components/Graph.tsx +++ b/quartz/components/Graph.tsx @@ -34,7 +34,7 @@ const defaultOptions: GraphOptions = { repelForce: 0.5, centerForce: 0.1, linkDistance: 30, - fontSize: 6, + fontSize: 8, opacityScale: 1, showTags: true, removeTags: [], @@ -44,10 +44,10 @@ const defaultOptions: GraphOptions = { drag: true, zoom: true, depth: -1, - scale: 1.25, - repelForce: 1, - centerForce: 0.1, - linkDistance: 50, + scale: 1.1, + repelForce: 0.5, + centerForce: 0.3, + linkDistance: 30, fontSize: 12, opacityScale: 1, showTags: true, @@ -64,7 +64,7 @@ export default ((opts?: GraphOptions) => {

{i18n(cfg.locale).components.graph.title}

-
+ {
-
+
) diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts index 127c96864..2a527a556 100644 --- a/quartz/components/scripts/graph.inline.ts +++ b/quartz/components/scripts/graph.inline.ts @@ -2,7 +2,7 @@ import type { ContentDetails } from "../../plugins/emitters/contentIndex" 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 } from "./util" import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path" type NodeData = { @@ -59,11 +59,11 @@ function animate(time: number) { requestAnimationFrame(animate) async function renderGraph(container: string, fullSlug: FullSlug) { + const canvas = document.getElementById(container) as HTMLCanvasElement | null + if (!canvas) return + const slug = simplifySlug(fullSlug) const visited = getVisited() - const graph = document.getElementById(container) - if (!graph) return - removeAllChildren(graph) let { drag: enableDrag, @@ -78,7 +78,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { removeTags, showTags, focusOnHover, - } = JSON.parse(graph.dataset["cfg"]!) + } = JSON.parse(canvas.dataset["cfg"]!) const data: Map = new Map( Object.entries(await fetchData).map(([k, v]) => [ @@ -89,8 +89,6 @@ async function renderGraph(container: string, fullSlug: FullSlug) { const links: LinkData[] = [] const tags: SimpleSlug[] = [] const validLinks = new Set(data.keys()) - const height = Math.max(graph.offsetHeight, 250) - const width = graph.offsetWidth for (const [source, details] of data.entries()) { const outgoing = details.links ?? [] @@ -151,6 +149,24 @@ async function renderGraph(container: string, fullSlug: FullSlug) { ) as unknown as LinkNodes[], } + const simulation: d3.Simulation = d3 + .forceSimulation(graphData.nodes) + .force("charge", d3.forceManyBody().strength(-100 * repelForce)) + .force("center", d3.forceCenter().strength(centerForce)) + .force( + "link", + d3 + .forceLink(graphData.links) + .id((d: any) => d.id) + .distance(linkDistance), + ) + .force( + "collide", + d3.forceCollide((n) => nodeRadius(n)), + ) + + const width = canvas.offsetWidth + const height = Math.max(canvas.offsetHeight, 250) const computedStyleMap = new Map() for (let i of [ "--secondary", @@ -162,7 +178,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { "--darkgray", "--bodyFont", ]) { - computedStyleMap.set(i, getComputedStyle(graph).getPropertyValue(i)) + computedStyleMap.set(i, getComputedStyle(canvas).getPropertyValue(i)) } // calculate color @@ -182,7 +198,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { return 2 + Math.sqrt(numLinks) } - function renderLinks(data: LinkNodes[]) { + function renderLinks(data: LinkNodes[], currentNodeId?: string | null) { tweens.get("link")?.stop() const Group = new TWEEN.Group() @@ -204,7 +220,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { }) } - function renderLabels(data: NodeData[]) { + function renderLabels(data: NodeData[], currentNodeId?: string | null) { tweens.get("label")?.stop() const Group = new TWEEN.Group() @@ -237,15 +253,10 @@ async function renderGraph(container: string, fullSlug: FullSlug) { }) } - function renderCurrentNode({ - nodeId, - focusOnHover, - }: { - nodeId: string | null - focusOnHover: boolean - }) { + function renderCurrentNode(props: { nodeId: string | null; focusOnHover: boolean }) { + const { nodeId, focusOnHover } = props + tweens.get("hover")?.stop() - currentNodeId = nodeId // NOTE: we need to create a new copy here const connectedNodes: Set = new Set() @@ -257,9 +268,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { connectedNodes.add(l.target.id) } }) - if (nodeId) { - connectedNodes.add(nodeId as SimpleSlug) - } + const Group = new TWEEN.Group() graphData.nodes.forEach((n) => { @@ -277,8 +286,8 @@ async function renderGraph(container: string, fullSlug: FullSlug) { } }) - renderLabels(graphData.nodes) - renderLinks(graphData.links) + renderLabels(graphData.nodes, nodeId) + renderLinks(graphData.links, nodeId) Group.getAll().forEach((tw) => tw.start()) tweens.set("hover", { @@ -295,6 +304,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { await app.init({ width, height, + canvas: canvas, antialias: true, autoStart: false, autoDensity: true, @@ -303,11 +313,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) { 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({ zIndex: 1 }) const labelsContainer = new PIXI.Container({ zIndex: 2 }) @@ -316,25 +324,9 @@ async function renderGraph(container: string, fullSlug: FullSlug) { stage.addChild(nodesContainer, labelsContainer) nodesContainer.addChild(linkGraphic) - const simulation: d3.Simulation = d3 - .forceSimulation(graphData.nodes) - .force("charge", d3.forceManyBody().strength(-100 * repelForce)) - .force("center", d3.forceCenter().strength(centerForce)) - .force( - "link", - d3 - .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 currentHoverNodeId: string | undefined let dragStartTime = 0 + let dragging = false graphData.nodes.forEach((n) => { const nodeId = n.id @@ -362,7 +354,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { }) .circle(0, 0, nodeRadius(n)) .on("pointerover", () => { - if (!currentNodeGfx) { + if (!dragging) { tweens.get(nodeId)?.stop() const tweenScale = { x: 1, y: 1 } const tween = new TWEEN.Tween(tweenScale) @@ -379,16 +371,16 @@ async function renderGraph(container: string, fullSlug: FullSlug) { } }) .on("pointerdown", (e) => { - currentNodeGfx = e.target as PIXI.Graphics + currentHoverNodeId = e.target.label }) .on("pointerup", () => { - currentNodeGfx = undefined + currentHoverNodeId = undefined }) .on("pointerupoutside", () => { - currentNodeGfx = undefined + currentHoverNodeId = undefined }) .on("pointerleave", () => { - if (!currentNodeGfx) { + if (!dragging) { tweens.get(nodeId)?.stop() const tweenScale = { x: gfx.scale.x, @@ -430,11 +422,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) { d3 .drag() .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 - }) + .subject(() => graphData.nodes.find((n) => n.id === currentHoverNodeId)) .on("start", function dragstarted(event) { if (!event.active) simulation.alphaTarget(1).restart() event.subject.fx = event.subject.x @@ -446,20 +434,20 @@ async function renderGraph(container: string, fullSlug: FullSlug) { fy: event.subject.fy, } dragStartTime = Date.now() + dragging = true }) .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 + event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k + event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k }) .on("end", function dragended(event) { if (!event.active) simulation.alphaTarget(0) event.subject.fx = null event.subject.fy = null + dragging = false // Check for node click event here. - if (Date.now() - dragStartTime < 200) { + if (Date.now() - dragStartTime < 100) { 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())) @@ -539,12 +527,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { function hideGlobalGraph() { container?.classList.remove("active") - const graph = document.getElementById("global-graph-container") if (sidebar) { sidebar.style.zIndex = "unset" } - if (!graph) return - removeAllChildren(graph) } async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {