@twango/twiki 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/product-cache/action.yml +32 -0
- package/.github/actions/product-cache/index.mjs +450 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bun.lock +1701 -0
- package/cli/runtime.test.ts +420 -0
- package/cli/runtime.ts +385 -0
- package/cli/twiki.ts +305 -0
- package/package.json +138 -4
- package/site/_headers +7 -0
- package/site/assets/fonts/OFL.txt +93 -0
- package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
- package/site/assets/social-background.svg +38 -0
- package/site/bun.lock +1701 -0
- package/site/database/index.ts +7 -0
- package/site/database/schema.ts +95 -0
- package/site/drizzle.config.ts +7 -0
- package/site/migrations/0001_auth.sql +14 -0
- package/site/migrations/0002_site_access.sql +5 -0
- package/site/migrations/0003_github_username.sql +1 -0
- package/site/migrations/meta/0002_snapshot.json +432 -0
- package/site/migrations/meta/0003_snapshot.json +439 -0
- package/site/migrations/meta/_journal.json +27 -0
- package/site/package.json +120 -0
- package/site/src/app.d.ts +14 -0
- package/site/src/app.html +11 -0
- package/site/src/hooks.server.ts +12 -0
- package/site/src/lib/analytics-config.json +1 -0
- package/site/src/lib/analytics.ts +40 -0
- package/site/src/lib/auth-config.json +3 -0
- package/site/src/lib/components/SEO.svelte +113 -0
- package/site/src/lib/components/doc/AccessLink.svelte +27 -0
- package/site/src/lib/components/doc/Backlinks.svelte +59 -0
- package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
- package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
- package/site/src/lib/components/doc/DocPager.svelte +91 -0
- package/site/src/lib/components/doc/DocShell.svelte +115 -0
- package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
- package/site/src/lib/components/doc/LockedContent.svelte +29 -0
- package/site/src/lib/components/doc/NavTree.svelte +158 -0
- package/site/src/lib/components/doc/NoteActions.svelte +139 -0
- package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
- package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
- package/site/src/lib/components/doc/Sidebar.svelte +351 -0
- package/site/src/lib/components/doc/Toc.svelte +339 -0
- package/site/src/lib/components/doc/enhancements.ts +146 -0
- package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
- package/site/src/lib/components/doc/image-preview.ts +114 -0
- package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
- package/site/src/lib/components/embeds/mermaid.ts +353 -0
- package/site/src/lib/components/files/FileCode.svelte +81 -0
- package/site/src/lib/components/files/FileExplorer.svelte +74 -0
- package/site/src/lib/components/files/FileHistory.svelte +170 -0
- package/site/src/lib/components/files/FileIcon.svelte +28 -0
- package/site/src/lib/components/files/FileRmd.svelte +209 -0
- package/site/src/lib/components/files/FileTabs.svelte +180 -0
- package/site/src/lib/components/files/FileTree.svelte +67 -0
- package/site/src/lib/components/files/blame-gutter.ts +45 -0
- package/site/src/lib/components/files/code-view.ts +98 -0
- package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
- package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
- package/site/src/lib/components/graph/GraphView.svelte +100 -0
- package/site/src/lib/components/graph/graph-data.ts +137 -0
- package/site/src/lib/components/graph/graph-model.ts +103 -0
- package/site/src/lib/components/graph/index.ts +3 -0
- package/site/src/lib/components/graph/render-graph.ts +626 -0
- package/site/src/lib/components/popover/index.ts +1 -0
- package/site/src/lib/components/popover/link-popovers.ts +233 -0
- package/site/src/lib/components/popover/popover.css +82 -0
- package/site/src/lib/components/search/SearchButton.svelte +57 -0
- package/site/src/lib/components/search/SearchPalette.svelte +189 -0
- package/site/src/lib/components/search/index.ts +3 -0
- package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
- package/site/src/lib/components/search/pagefind-query.ts +22 -0
- package/site/src/lib/components/search/pagefind-url.ts +12 -0
- package/site/src/lib/components/search/search-state.svelte.ts +22 -0
- package/site/src/lib/components/ui/IconButton.svelte +30 -0
- package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
- package/site/src/lib/components/ui/index.ts +1 -0
- package/site/src/lib/config.ts +9 -0
- package/site/src/lib/content-license.ts +7 -0
- package/site/src/lib/file-history.ts +37 -0
- package/site/src/lib/file-icons.ts +27 -0
- package/site/src/lib/file-tabs.ts +72 -0
- package/site/src/lib/files.ts +73 -0
- package/site/src/lib/github-user.ts +70 -0
- package/site/src/lib/metadata.ts +166 -0
- package/site/src/lib/notebook-nav.ts +63 -0
- package/site/src/lib/publication.ts +1 -0
- package/site/src/lib/rmd.ts +156 -0
- package/site/src/lib/server/access-admin.ts +33 -0
- package/site/src/lib/server/content-model.ts +253 -0
- package/site/src/lib/server/content-provider.ts +266 -0
- package/site/src/lib/server/content.ts +277 -0
- package/site/src/lib/server/runtime-content.ts +34 -0
- package/site/src/lib/site-config.json +9 -0
- package/site/src/lib/social-image.ts +13 -0
- package/site/src/lib/styles/article.css +607 -0
- package/site/src/lib/styles/foundation.css +138 -0
- package/site/src/lib/styles/print.css +115 -0
- package/site/src/lib/styles/shell.css +182 -0
- package/site/src/lib/text-exports.ts +5 -0
- package/site/src/lib/types.ts +140 -0
- package/site/src/lib/utils.ts +6 -0
- package/site/src/routes/+error.svelte +16 -0
- package/site/src/routes/+layout.server.ts +10 -0
- package/site/src/routes/+layout.svelte +72 -0
- package/site/src/routes/+layout.ts +3 -0
- package/site/src/routes/404/+page.svelte +13 -0
- package/site/src/routes/[...slug]/+page.server.ts +24 -0
- package/site/src/routes/[...slug]/+page.svelte +180 -0
- package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
- package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
- package/site/src/routes/admin/access/+page.server.ts +53 -0
- package/site/src/routes/admin/access/+page.svelte +118 -0
- package/site/src/routes/graph.json/+server.ts +7 -0
- package/site/src/routes/index.xml/+server.ts +7 -0
- package/site/src/routes/layout.css +10 -0
- package/site/src/routes/robots.txt/+server.ts +9 -0
- package/site/src/routes/sitemap.xml/+server.ts +7 -0
- package/site/src/routes/tags/+page.server.ts +5 -0
- package/site/src/routes/tags/+page.svelte +38 -0
- package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
- package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
- package/site/static/favicon.png +0 -0
- package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
- package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
- package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
- package/site/static/fonts/Virgil.woff2 +0 -0
- package/site/svelte.config.js +71 -0
- package/site/tooling/LICENSE-quartz +25 -0
- package/site/tooling/adapter.js +55 -0
- package/site/tooling/assets.ts +323 -0
- package/site/tooling/benchmark.ts +191 -0
- package/site/tooling/build.ts +407 -0
- package/site/tooling/cache.ts +133 -0
- package/site/tooling/check-diagrams.ts +164 -0
- package/site/tooling/check-links.ts +86 -0
- package/site/tooling/check-targets.ts +139 -0
- package/site/tooling/compiler.ts +1386 -0
- package/site/tooling/deploy.ts +112 -0
- package/site/tooling/fingerprint.ts +81 -0
- package/site/tooling/lib/app-version.js +92 -0
- package/site/tooling/lib/application-asset-urls.ts +27 -0
- package/site/tooling/lib/application-cache.ts +269 -0
- package/site/tooling/lib/asset-hash-cache.ts +116 -0
- package/site/tooling/lib/autotag.ts +99 -0
- package/site/tooling/lib/build-assembly.ts +148 -0
- package/site/tooling/lib/build-workspace.ts +386 -0
- package/site/tooling/lib/cache-selection.ts +70 -0
- package/site/tooling/lib/cloudflare-prewarm.js +40 -0
- package/site/tooling/lib/comments.ts +32 -0
- package/site/tooling/lib/content-cache.ts +244 -0
- package/site/tooling/lib/content-urls.ts +50 -0
- package/site/tooling/lib/course-cache.ts +298 -0
- package/site/tooling/lib/course-files.ts +281 -0
- package/site/tooling/lib/currency.ts +33 -0
- package/site/tooling/lib/deployment-config.ts +222 -0
- package/site/tooling/lib/deployment-manifest.ts +274 -0
- package/site/tooling/lib/deployment-prepare.ts +325 -0
- package/site/tooling/lib/deployment-upload.ts +293 -0
- package/site/tooling/lib/dev-git-state.ts +130 -0
- package/site/tooling/lib/dev-history.ts +86 -0
- package/site/tooling/lib/dev-social-images.ts +38 -0
- package/site/tooling/lib/dev-state.ts +107 -0
- package/site/tooling/lib/edition-builds.ts +184 -0
- package/site/tooling/lib/edition-paths.js +29 -0
- package/site/tooling/lib/empty-tag-route.js +23 -0
- package/site/tooling/lib/file-history.ts +201 -0
- package/site/tooling/lib/file-icons.ts +93 -0
- package/site/tooling/lib/file-policy.ts +38 -0
- package/site/tooling/lib/file-search.js +13 -0
- package/site/tooling/lib/git-revisions.ts +41 -0
- package/site/tooling/lib/history-pool.ts +74 -0
- package/site/tooling/lib/history-worker.ts +22 -0
- package/site/tooling/lib/knit-rmd.R +38 -0
- package/site/tooling/lib/lastmod.ts +201 -0
- package/site/tooling/lib/markdown-files.ts +51 -0
- package/site/tooling/lib/ofm.ts +634 -0
- package/site/tooling/lib/output.ts +60 -0
- package/site/tooling/lib/product-cache.ts +1006 -0
- package/site/tooling/lib/project-config.ts +288 -0
- package/site/tooling/lib/public-assets.ts +38 -0
- package/site/tooling/lib/public-links.ts +56 -0
- package/site/tooling/lib/publishing.ts +156 -0
- package/site/tooling/lib/r-runtime.ts +257 -0
- package/site/tooling/lib/rebuild-queue.ts +25 -0
- package/site/tooling/lib/rmd-preflight.ts +166 -0
- package/site/tooling/lib/rmd-preview-cache.ts +93 -0
- package/site/tooling/lib/rmd-previews.ts +129 -0
- package/site/tooling/lib/runtime-data-modules.ts +30 -0
- package/site/tooling/lib/runtime-packaging.ts +321 -0
- package/site/tooling/lib/search-cache.js +165 -0
- package/site/tooling/lib/search-inputs.js +129 -0
- package/site/tooling/lib/slug.ts +377 -0
- package/site/tooling/lib/social-images.ts +210 -0
- package/site/tooling/lib/text-exports.ts +51 -0
- package/site/tooling/lib/visit-elements.ts +17 -0
- package/site/tooling/live-test.ts +138 -0
- package/site/tooling/pipeline.ts +103 -0
- package/site/tooling/prepare.ts +3 -0
- package/site/tooling/publish.ts +28 -0
- package/site/tooling/r-cache.ts +45 -0
- package/site/tooling/r-render-profile.json +7 -0
- package/site/tooling/search.js +94 -0
- package/site/tooling/verify-deployment.ts +109 -0
- package/site/tooling/vite-content.ts +240 -0
- package/site/tooling/wrangler-paths.js +25 -0
- package/site/tooling/wrangler.ts +19 -0
- package/site/tsconfig.json +16 -0
- package/site/vite.config.ts +40 -0
- package/site/worker/access.ts +57 -0
- package/site/worker/analytics.ts +100 -0
- package/site/worker/auth-routes.ts +78 -0
- package/site/worker/auth.ts +78 -0
- package/site/worker/content-runtime.ts +226 -0
- package/site/worker/framing.ts +4 -0
- package/site/worker/gate.ts +147 -0
- package/site/worker/index.ts +9 -0
- package/site/worker/login.ts +62 -0
- package/site/worker/publication.ts +44 -0
- package/site/worker/runtime.ts +104 -0
- package/site/worker/tsconfig.json +13 -0
- package/site/wrangler.jsonc +37 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faithful port of Quartz's graph renderer
|
|
3
|
+
* (quartz/components/scripts/graph.inline.ts @ repo root) to the SvelteKit
|
|
4
|
+
* site: pixi.js draws, d3-force simulates (and keeps simulating — drag
|
|
5
|
+
* reheats it via the alphaTarget pattern), d3-zoom/d3-drag handle
|
|
6
|
+
* interaction, @tweenjs/tween.js animates the hover highlights. Same stack,
|
|
7
|
+
* same parameters, same behavior.
|
|
8
|
+
*
|
|
9
|
+
* This module is only ever loaded via dynamic `import()` from
|
|
10
|
+
* GraphView.svelte, so pixi + d3 + tween stay out of the initial bundle.
|
|
11
|
+
*
|
|
12
|
+
* Intentional deviations from the original (all mechanical, not behavioral):
|
|
13
|
+
* - Data comes from /graph.json (display-form ids) instead of Quartz's
|
|
14
|
+
* contentIndex, and navigation goes through a callback (SvelteKit `goto`)
|
|
15
|
+
* instead of window.spaNavigate.
|
|
16
|
+
* - Link construction and neighbourhood traversal retain Quartz's observable
|
|
17
|
+
* ordering (d3-force seeds nodes by index); only final node lookup uses a Map
|
|
18
|
+
* instead of repeated Array.find calls.
|
|
19
|
+
* - Quartz re-renders on its custom `themechange` event; here GraphView
|
|
20
|
+
* re-runs renderGraph when `:root`'s class list changes (MutationObserver).
|
|
21
|
+
* - Pixi uses its stable WebGL backend instead of preferring experimental
|
|
22
|
+
* WebGPU; the rendered scene is otherwise the same and works on browsers
|
|
23
|
+
* without a usable WebGPU adapter.
|
|
24
|
+
* - The local graph anchors the page being read at the simulation origin so
|
|
25
|
+
* dense two-hop neighborhoods cannot push it outside the preview.
|
|
26
|
+
* - Cleanup also stops the d3 simulation timer (Quartz leaves it to decay).
|
|
27
|
+
*/
|
|
28
|
+
import { Group as TweenGroup, Tween as Tweened } from '@tweenjs/tween.js';
|
|
29
|
+
import { drag } from 'd3-drag';
|
|
30
|
+
import {
|
|
31
|
+
forceCenter,
|
|
32
|
+
forceCollide,
|
|
33
|
+
forceLink,
|
|
34
|
+
forceManyBody,
|
|
35
|
+
forceRadial,
|
|
36
|
+
forceSimulation,
|
|
37
|
+
type Simulation,
|
|
38
|
+
type SimulationLinkDatum,
|
|
39
|
+
type SimulationNodeDatum
|
|
40
|
+
} from 'd3-force';
|
|
41
|
+
import { select } from 'd3-selection';
|
|
42
|
+
import { zoom, zoomIdentity, type ZoomBehavior } from 'd3-zoom';
|
|
43
|
+
import { Application, Circle, Container, Graphics, Text } from 'pixi.js';
|
|
44
|
+
import { getVisited, type GraphConfig, type GraphData } from './graph-data';
|
|
45
|
+
import { canvasPosition, orderedGraphElements } from './graph-model';
|
|
46
|
+
|
|
47
|
+
type GraphicsInfo = {
|
|
48
|
+
color: string;
|
|
49
|
+
gfx: Graphics;
|
|
50
|
+
alpha: number;
|
|
51
|
+
active: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type NodeData = {
|
|
55
|
+
id: string;
|
|
56
|
+
text: string;
|
|
57
|
+
tags: string[];
|
|
58
|
+
} & SimulationNodeDatum;
|
|
59
|
+
|
|
60
|
+
type LinkData = {
|
|
61
|
+
source: NodeData;
|
|
62
|
+
target: NodeData;
|
|
63
|
+
} & SimulationLinkDatum<NodeData>;
|
|
64
|
+
|
|
65
|
+
type LinkRenderData = GraphicsInfo & {
|
|
66
|
+
simulationData: LinkData;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type NodeRenderData = GraphicsInfo & {
|
|
70
|
+
simulationData: NodeData;
|
|
71
|
+
label: Text;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type TweenNode = {
|
|
75
|
+
update: (time: number) => void;
|
|
76
|
+
stop: () => void;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Quartz reads its theme palette straight off :root (--secondary, --tertiary,
|
|
81
|
+
* --gray, --light, --lightgray, --dark, --bodyFont). This site's tokens
|
|
82
|
+
* (site/src/routes/layout.css) play the equivalent roles:
|
|
83
|
+
*
|
|
84
|
+
* --secondary (current node) → --color-accent
|
|
85
|
+
* --tertiary (visited node, tag ring) → accent softened toward muted
|
|
86
|
+
* --gray (unvisited node fill) → --color-muted
|
|
87
|
+
* --light (tag node fill) → --color-bg
|
|
88
|
+
* --lightgray (link lines) → --color-subtle
|
|
89
|
+
* --dark (label text) → --color-text
|
|
90
|
+
* --bodyFont → body font-family
|
|
91
|
+
*
|
|
92
|
+
* Colors are resolved through a probe element so any CSS color syntax the
|
|
93
|
+
* tokens use ends up as an rgb() string pixi can parse.
|
|
94
|
+
*/
|
|
95
|
+
function resolveThemeColors() {
|
|
96
|
+
const probe = document.createElement('span');
|
|
97
|
+
probe.style.position = 'absolute';
|
|
98
|
+
probe.style.visibility = 'hidden';
|
|
99
|
+
document.body.appendChild(probe);
|
|
100
|
+
const resolve = (expr: string): string => {
|
|
101
|
+
probe.style.color = '';
|
|
102
|
+
probe.style.color = expr;
|
|
103
|
+
return getComputedStyle(probe).color;
|
|
104
|
+
};
|
|
105
|
+
// channel-wise mix of two resolved rgb()/rgba() strings
|
|
106
|
+
const mix = (a: string, b: string, t: number): string => {
|
|
107
|
+
const pa = a.match(/[\d.]+/g)?.map(Number) ?? [0, 0, 0];
|
|
108
|
+
const pb = b.match(/[\d.]+/g)?.map(Number) ?? [0, 0, 0];
|
|
109
|
+
const c = [0, 1, 2].map((i) => Math.round((pa[i] ?? 0) * (1 - t) + (pb[i] ?? 0) * t));
|
|
110
|
+
return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;
|
|
111
|
+
};
|
|
112
|
+
const accent = resolve('var(--color-accent)');
|
|
113
|
+
const muted = resolve('var(--color-muted)');
|
|
114
|
+
const map = {
|
|
115
|
+
'--secondary': accent,
|
|
116
|
+
'--tertiary': mix(accent, muted, 0.4),
|
|
117
|
+
'--gray': muted,
|
|
118
|
+
'--light': resolve('var(--color-bg)'),
|
|
119
|
+
'--lightgray': resolve('var(--color-subtle)'),
|
|
120
|
+
'--dark': resolve('var(--color-text)'),
|
|
121
|
+
'--bodyFont': getComputedStyle(document.body).fontFamily
|
|
122
|
+
};
|
|
123
|
+
probe.remove();
|
|
124
|
+
return map;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function renderGraph(
|
|
128
|
+
graph: HTMLElement,
|
|
129
|
+
fullData: GraphData,
|
|
130
|
+
slug: string | undefined,
|
|
131
|
+
cfg: GraphConfig,
|
|
132
|
+
isGlobalGraph: boolean,
|
|
133
|
+
navigate: (id: string) => void
|
|
134
|
+
): Promise<() => void> {
|
|
135
|
+
const visited = getVisited();
|
|
136
|
+
graph.replaceChildren();
|
|
137
|
+
graph.dataset.graphReady = 'false';
|
|
138
|
+
|
|
139
|
+
const {
|
|
140
|
+
drag: enableDrag,
|
|
141
|
+
zoom: enableZoom,
|
|
142
|
+
scale,
|
|
143
|
+
repelForce,
|
|
144
|
+
centerForce,
|
|
145
|
+
linkDistance,
|
|
146
|
+
fontSize,
|
|
147
|
+
opacityScale,
|
|
148
|
+
removeTags,
|
|
149
|
+
showTags,
|
|
150
|
+
focusOnHover,
|
|
151
|
+
enableRadial,
|
|
152
|
+
centerCurrentNode
|
|
153
|
+
} = cfg;
|
|
154
|
+
const data = new Map(fullData.nodes.map((n) => [n.id, n]));
|
|
155
|
+
const tweens = new Map<string, TweenNode>();
|
|
156
|
+
const selected = orderedGraphElements(fullData, slug, {
|
|
157
|
+
depth: cfg.depth,
|
|
158
|
+
showTags,
|
|
159
|
+
removeTags
|
|
160
|
+
});
|
|
161
|
+
const nodes: NodeData[] = selected.nodeIds.map((url) => {
|
|
162
|
+
const text = url.startsWith('tags/') ? '#' + url.substring(5) : (data.get(url)?.title ?? url);
|
|
163
|
+
return {
|
|
164
|
+
id: url,
|
|
165
|
+
text,
|
|
166
|
+
tags: data.get(url)?.tags ?? []
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const nodeById = new Map(nodes.map((n) => [n.id, n]));
|
|
170
|
+
const graphData: { nodes: NodeData[]; links: LinkData[] } = {
|
|
171
|
+
nodes,
|
|
172
|
+
links: selected.links.map((l) => ({
|
|
173
|
+
source: nodeById.get(l.source)!,
|
|
174
|
+
target: nodeById.get(l.target)!
|
|
175
|
+
}))
|
|
176
|
+
};
|
|
177
|
+
const centeredNode = centerCurrentNode && slug !== undefined ? nodeById.get(slug) : undefined;
|
|
178
|
+
if (centeredNode) {
|
|
179
|
+
centeredNode.fx = 0;
|
|
180
|
+
centeredNode.fy = 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let width = graph.offsetWidth;
|
|
184
|
+
const previewHeight = Number.parseFloat(
|
|
185
|
+
getComputedStyle(graph).getPropertyValue('--graph-preview-height')
|
|
186
|
+
);
|
|
187
|
+
let height = Math.max(graph.offsetHeight, previewHeight || 1);
|
|
188
|
+
|
|
189
|
+
// we virtualize the simulation and use pixi to actually render it
|
|
190
|
+
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
|
191
|
+
.force('charge', forceManyBody().strength(-100 * repelForce))
|
|
192
|
+
.force('center', forceCenter().strength(centerForce))
|
|
193
|
+
.force('link', forceLink(graphData.links).distance(linkDistance))
|
|
194
|
+
.force('collide', forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3));
|
|
195
|
+
const radius = (Math.min(width, height) / 2) * 0.8;
|
|
196
|
+
if (enableRadial) simulation.force('radial', forceRadial(radius).strength(0.2));
|
|
197
|
+
|
|
198
|
+
// precompute style prop strings as pixi doesn't support css variables
|
|
199
|
+
const computedStyleMap = resolveThemeColors();
|
|
200
|
+
|
|
201
|
+
// calculate color
|
|
202
|
+
const color = (d: NodeData) => {
|
|
203
|
+
const isCurrent = d.id === slug;
|
|
204
|
+
if (isCurrent) {
|
|
205
|
+
return computedStyleMap['--secondary'];
|
|
206
|
+
} else if (visited.has(d.id) || d.id.startsWith('tags/')) {
|
|
207
|
+
return computedStyleMap['--tertiary'];
|
|
208
|
+
} else {
|
|
209
|
+
return computedStyleMap['--gray'];
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
function nodeRadius(d: NodeData) {
|
|
214
|
+
const numLinks = graphData.links.filter(
|
|
215
|
+
(l) => l.source.id === d.id || l.target.id === d.id
|
|
216
|
+
).length;
|
|
217
|
+
return 2 + Math.sqrt(numLinks);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let hoveredNodeId: string | null = null;
|
|
221
|
+
let hoveredNeighbours: Set<string> = new Set();
|
|
222
|
+
const linkRenderData: LinkRenderData[] = [];
|
|
223
|
+
const nodeRenderData: NodeRenderData[] = [];
|
|
224
|
+
function updateHoverInfo(newHoveredId: string | null) {
|
|
225
|
+
hoveredNodeId = newHoveredId;
|
|
226
|
+
|
|
227
|
+
if (newHoveredId === null) {
|
|
228
|
+
hoveredNeighbours = new Set();
|
|
229
|
+
for (const n of nodeRenderData) {
|
|
230
|
+
n.active = false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const l of linkRenderData) {
|
|
234
|
+
l.active = false;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
hoveredNeighbours = new Set();
|
|
238
|
+
for (const l of linkRenderData) {
|
|
239
|
+
const linkData = l.simulationData;
|
|
240
|
+
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
|
|
241
|
+
hoveredNeighbours.add(linkData.source.id);
|
|
242
|
+
hoveredNeighbours.add(linkData.target.id);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const n of nodeRenderData) {
|
|
249
|
+
n.active = hoveredNeighbours.has(n.simulationData.id);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let dragStartTime = 0;
|
|
255
|
+
let dragging = false;
|
|
256
|
+
|
|
257
|
+
function renderLinks() {
|
|
258
|
+
tweens.get('link')?.stop();
|
|
259
|
+
const tweenGroup = new TweenGroup();
|
|
260
|
+
|
|
261
|
+
for (const l of linkRenderData) {
|
|
262
|
+
let alpha = 1;
|
|
263
|
+
|
|
264
|
+
// if we are hovering over a node, we want to highlight the immediate neighbours
|
|
265
|
+
// with full alpha and the rest with default alpha
|
|
266
|
+
if (hoveredNodeId) {
|
|
267
|
+
alpha = l.active ? 1 : 0.2;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
l.color = l.active ? computedStyleMap['--gray'] : computedStyleMap['--lightgray'];
|
|
271
|
+
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
tweenGroup.getAll().forEach((tw) => tw.start());
|
|
275
|
+
tweens.set('link', {
|
|
276
|
+
update: tweenGroup.update.bind(tweenGroup),
|
|
277
|
+
stop() {
|
|
278
|
+
tweenGroup.getAll().forEach((tw) => tw.stop());
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderLabels() {
|
|
284
|
+
tweens.get('label')?.stop();
|
|
285
|
+
const tweenGroup = new TweenGroup();
|
|
286
|
+
|
|
287
|
+
const defaultScale = 1 / scale;
|
|
288
|
+
const activeScale = defaultScale * 1.1;
|
|
289
|
+
for (const n of nodeRenderData) {
|
|
290
|
+
const nodeId = n.simulationData.id;
|
|
291
|
+
|
|
292
|
+
if (hoveredNodeId === nodeId) {
|
|
293
|
+
tweenGroup.add(
|
|
294
|
+
new Tweened<Text>(n.label).to(
|
|
295
|
+
{
|
|
296
|
+
alpha: 1,
|
|
297
|
+
scale: { x: activeScale, y: activeScale }
|
|
298
|
+
},
|
|
299
|
+
100
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
tweenGroup.add(
|
|
304
|
+
new Tweened<Text>(n.label).to(
|
|
305
|
+
{
|
|
306
|
+
alpha: n.label.alpha,
|
|
307
|
+
scale: { x: defaultScale, y: defaultScale }
|
|
308
|
+
},
|
|
309
|
+
100
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
tweenGroup.getAll().forEach((tw) => tw.start());
|
|
316
|
+
tweens.set('label', {
|
|
317
|
+
update: tweenGroup.update.bind(tweenGroup),
|
|
318
|
+
stop() {
|
|
319
|
+
tweenGroup.getAll().forEach((tw) => tw.stop());
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderNodes() {
|
|
325
|
+
tweens.get('hover')?.stop();
|
|
326
|
+
|
|
327
|
+
const tweenGroup = new TweenGroup();
|
|
328
|
+
for (const n of nodeRenderData) {
|
|
329
|
+
let alpha = 1;
|
|
330
|
+
|
|
331
|
+
// if we are hovering over a node, we want to highlight the immediate neighbours
|
|
332
|
+
if (hoveredNodeId !== null && focusOnHover) {
|
|
333
|
+
alpha = n.active ? 1 : 0.2;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
tweenGroup.add(new Tweened<Graphics>(n.gfx, tweenGroup).to({ alpha }, 200));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
tweenGroup.getAll().forEach((tw) => tw.start());
|
|
340
|
+
tweens.set('hover', {
|
|
341
|
+
update: tweenGroup.update.bind(tweenGroup),
|
|
342
|
+
stop() {
|
|
343
|
+
tweenGroup.getAll().forEach((tw) => tw.stop());
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderPixiFromD3() {
|
|
349
|
+
renderNodes();
|
|
350
|
+
renderLinks();
|
|
351
|
+
renderLabels();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
tweens.forEach((tween) => tween.stop());
|
|
355
|
+
tweens.clear();
|
|
356
|
+
|
|
357
|
+
const app = new Application();
|
|
358
|
+
await app.init({
|
|
359
|
+
width,
|
|
360
|
+
height,
|
|
361
|
+
antialias: true,
|
|
362
|
+
autoStart: false,
|
|
363
|
+
autoDensity: true,
|
|
364
|
+
backgroundAlpha: 0,
|
|
365
|
+
preference: 'webgl',
|
|
366
|
+
resolution: window.devicePixelRatio,
|
|
367
|
+
eventMode: 'static'
|
|
368
|
+
});
|
|
369
|
+
graph.appendChild(app.canvas);
|
|
370
|
+
|
|
371
|
+
const stage = app.stage;
|
|
372
|
+
stage.interactive = false;
|
|
373
|
+
|
|
374
|
+
const labelsContainer = new Container<Text>({ zIndex: 3, isRenderGroup: true });
|
|
375
|
+
const nodesContainer = new Container<Graphics>({ zIndex: 2, isRenderGroup: true });
|
|
376
|
+
const linkContainer = new Container<Graphics>({ zIndex: 1, isRenderGroup: true });
|
|
377
|
+
stage.addChild(nodesContainer, labelsContainer, linkContainer);
|
|
378
|
+
|
|
379
|
+
for (const n of graphData.nodes) {
|
|
380
|
+
const nodeId = n.id;
|
|
381
|
+
|
|
382
|
+
const label = new Text({
|
|
383
|
+
interactive: false,
|
|
384
|
+
eventMode: 'none',
|
|
385
|
+
text: n.text,
|
|
386
|
+
alpha: 0,
|
|
387
|
+
anchor: { x: 0.5, y: 1.2 },
|
|
388
|
+
style: {
|
|
389
|
+
fontSize: fontSize * 15,
|
|
390
|
+
fill: computedStyleMap['--dark'],
|
|
391
|
+
fontFamily: computedStyleMap['--bodyFont']
|
|
392
|
+
},
|
|
393
|
+
resolution: window.devicePixelRatio * 4
|
|
394
|
+
});
|
|
395
|
+
label.scale.set(1 / scale);
|
|
396
|
+
|
|
397
|
+
let oldLabelOpacity = 0;
|
|
398
|
+
const isTagNode = nodeId.startsWith('tags/');
|
|
399
|
+
const gfx = new Graphics({
|
|
400
|
+
interactive: true,
|
|
401
|
+
label: nodeId,
|
|
402
|
+
eventMode: 'static',
|
|
403
|
+
hitArea: new Circle(0, 0, nodeRadius(n)),
|
|
404
|
+
cursor: 'pointer'
|
|
405
|
+
})
|
|
406
|
+
.circle(0, 0, nodeRadius(n))
|
|
407
|
+
.fill({ color: isTagNode ? computedStyleMap['--light'] : color(n) })
|
|
408
|
+
.on('pointerover', (e) => {
|
|
409
|
+
updateHoverInfo(e.target.label);
|
|
410
|
+
oldLabelOpacity = label.alpha;
|
|
411
|
+
if (!dragging) {
|
|
412
|
+
renderPixiFromD3();
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
.on('pointerleave', () => {
|
|
416
|
+
updateHoverInfo(null);
|
|
417
|
+
label.alpha = oldLabelOpacity;
|
|
418
|
+
if (!dragging) {
|
|
419
|
+
renderPixiFromD3();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
if (isTagNode) {
|
|
424
|
+
gfx.stroke({ width: 2, color: computedStyleMap['--tertiary'] });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
nodesContainer.addChild(gfx);
|
|
428
|
+
labelsContainer.addChild(label);
|
|
429
|
+
|
|
430
|
+
const nodeRenderDatum: NodeRenderData = {
|
|
431
|
+
simulationData: n,
|
|
432
|
+
gfx,
|
|
433
|
+
label,
|
|
434
|
+
color: color(n),
|
|
435
|
+
alpha: 1,
|
|
436
|
+
active: false
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
nodeRenderData.push(nodeRenderDatum);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const l of graphData.links) {
|
|
443
|
+
const gfx = new Graphics({ interactive: false, eventMode: 'none' });
|
|
444
|
+
linkContainer.addChild(gfx);
|
|
445
|
+
|
|
446
|
+
const linkRenderDatum: LinkRenderData = {
|
|
447
|
+
simulationData: l,
|
|
448
|
+
gfx,
|
|
449
|
+
color: computedStyleMap['--lightgray'],
|
|
450
|
+
alpha: 1,
|
|
451
|
+
active: false
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
linkRenderData.push(linkRenderDatum);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let currentTransform = zoomIdentity;
|
|
458
|
+
if (enableDrag) {
|
|
459
|
+
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
|
|
460
|
+
drag<HTMLCanvasElement, NodeData | undefined>()
|
|
461
|
+
.container(() => app.canvas)
|
|
462
|
+
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
|
|
463
|
+
.on('start', function dragstarted(event) {
|
|
464
|
+
if (!event.active) simulation.alphaTarget(1).restart();
|
|
465
|
+
event.subject.fx = event.subject.x;
|
|
466
|
+
event.subject.fy = event.subject.y;
|
|
467
|
+
event.subject.__initialDragPos = {
|
|
468
|
+
x: event.subject.x,
|
|
469
|
+
y: event.subject.y,
|
|
470
|
+
fx: event.subject.fx,
|
|
471
|
+
fy: event.subject.fy
|
|
472
|
+
};
|
|
473
|
+
dragStartTime = Date.now();
|
|
474
|
+
dragging = true;
|
|
475
|
+
})
|
|
476
|
+
.on('drag', function dragged(event) {
|
|
477
|
+
const initPos = event.subject.__initialDragPos;
|
|
478
|
+
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k;
|
|
479
|
+
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k;
|
|
480
|
+
})
|
|
481
|
+
.on('end', function dragended(event) {
|
|
482
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
483
|
+
if (event.subject === centeredNode) {
|
|
484
|
+
event.subject.fx = 0;
|
|
485
|
+
event.subject.fy = 0;
|
|
486
|
+
} else {
|
|
487
|
+
event.subject.fx = null;
|
|
488
|
+
event.subject.fy = null;
|
|
489
|
+
}
|
|
490
|
+
dragging = false;
|
|
491
|
+
|
|
492
|
+
// if the time between mousedown and mouseup is short, we consider it a click
|
|
493
|
+
if (Date.now() - dragStartTime < 500) {
|
|
494
|
+
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData;
|
|
495
|
+
navigate(node.id);
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
} else {
|
|
500
|
+
for (const node of nodeRenderData) {
|
|
501
|
+
node.gfx.on('click', () => {
|
|
502
|
+
navigate(node.simulationData.id);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let zoomController: ZoomBehavior<HTMLCanvasElement, NodeData> | undefined;
|
|
508
|
+
if (enableZoom) {
|
|
509
|
+
zoomController = zoom<HTMLCanvasElement, NodeData>()
|
|
510
|
+
.filter((event: Event) => {
|
|
511
|
+
// local graph only wheel-zooms with ctrl/meta held (Quartz checks
|
|
512
|
+
// for the .global-graph-container class; here it's a parameter)
|
|
513
|
+
if (event.type === 'wheel') {
|
|
514
|
+
if (isGlobalGraph || graph.dataset.expanded === 'true') return true;
|
|
515
|
+
const we = event as WheelEvent;
|
|
516
|
+
return we.ctrlKey || we.metaKey;
|
|
517
|
+
}
|
|
518
|
+
return !(event as MouseEvent).button;
|
|
519
|
+
})
|
|
520
|
+
.extent((): [[number, number], [number, number]] => [
|
|
521
|
+
[0, 0],
|
|
522
|
+
[width, height]
|
|
523
|
+
])
|
|
524
|
+
.scaleExtent([0.25, 4])
|
|
525
|
+
.on('zoom', ({ transform }) => {
|
|
526
|
+
currentTransform = transform;
|
|
527
|
+
stage.scale.set(transform.k, transform.k);
|
|
528
|
+
stage.position.set(transform.x, transform.y);
|
|
529
|
+
|
|
530
|
+
// zoom adjusts opacity of labels too
|
|
531
|
+
const scale = transform.k * opacityScale;
|
|
532
|
+
const scaleOpacity = Math.max((scale - 1) / 3.75, 0);
|
|
533
|
+
const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label);
|
|
534
|
+
|
|
535
|
+
for (const label of labelsContainer.children) {
|
|
536
|
+
if (!activeNodes.includes(label)) {
|
|
537
|
+
label.alpha = scaleOpacity;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
select<HTMLCanvasElement, NodeData>(app.canvas).call(zoomController);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Resize the existing Pixi canvas, keeping the simulation and zoom alive.
|
|
545
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
546
|
+
const nextWidth = graph.clientWidth;
|
|
547
|
+
const nextHeight = graph.clientHeight;
|
|
548
|
+
if (!nextWidth || !nextHeight || (nextWidth === width && nextHeight === height)) return;
|
|
549
|
+
const { x, y, k } = currentTransform;
|
|
550
|
+
const nextTransform = zoomIdentity
|
|
551
|
+
.translate(x + ((nextWidth - width) * (1 - k)) / 2, y + ((nextHeight - height) * (1 - k)) / 2)
|
|
552
|
+
.scale(k);
|
|
553
|
+
width = nextWidth;
|
|
554
|
+
height = nextHeight;
|
|
555
|
+
app.renderer.resize(width, height);
|
|
556
|
+
if (enableRadial) {
|
|
557
|
+
simulation.force('radial', forceRadial((Math.min(width, height) / 2) * 0.8).strength(0.2));
|
|
558
|
+
simulation.alpha(Math.max(simulation.alpha(), 0.15)).restart();
|
|
559
|
+
}
|
|
560
|
+
if (zoomController)
|
|
561
|
+
select<HTMLCanvasElement, NodeData>(app.canvas).call(zoomController.transform, nextTransform);
|
|
562
|
+
});
|
|
563
|
+
resizeObserver.observe(graph);
|
|
564
|
+
|
|
565
|
+
function drawFrame(time: number) {
|
|
566
|
+
for (const n of nodeRenderData) {
|
|
567
|
+
const { x, y } = n.simulationData;
|
|
568
|
+
const position = canvasPosition(x, y, width, height);
|
|
569
|
+
if (!position) continue;
|
|
570
|
+
n.gfx.position.set(position.x, position.y);
|
|
571
|
+
if (n.simulationData === centeredNode) {
|
|
572
|
+
graph.dataset.graphCurrentX = String(position.x);
|
|
573
|
+
graph.dataset.graphCurrentY = String(position.y);
|
|
574
|
+
}
|
|
575
|
+
if (n.label) {
|
|
576
|
+
n.label.position.set(position.x, position.y);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const l of linkRenderData) {
|
|
581
|
+
const linkData = l.simulationData;
|
|
582
|
+
l.gfx.clear();
|
|
583
|
+
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2);
|
|
584
|
+
l.gfx
|
|
585
|
+
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
|
|
586
|
+
.stroke({ alpha: l.alpha, width: 1, color: l.color });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
tweens.forEach((t) => t.update(time));
|
|
590
|
+
app.renderer.render(stage);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Draw once immediately. A backgrounded/throttled tab may defer
|
|
594
|
+
// requestAnimationFrame, but its graph preview should never remain blank.
|
|
595
|
+
drawFrame(performance.now());
|
|
596
|
+
graph.dataset.graphReady = 'true';
|
|
597
|
+
graph.dataset.graphNodes = String(nodeRenderData.length);
|
|
598
|
+
graph.dataset.graphLinks = String(linkRenderData.length);
|
|
599
|
+
|
|
600
|
+
let stopAnimation = false;
|
|
601
|
+
let animationFrame = 0;
|
|
602
|
+
function animate(time: number) {
|
|
603
|
+
if (stopAnimation) return;
|
|
604
|
+
drawFrame(time);
|
|
605
|
+
animationFrame = requestAnimationFrame(animate);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
animationFrame = requestAnimationFrame(animate);
|
|
609
|
+
let destroyed = false;
|
|
610
|
+
return () => {
|
|
611
|
+
if (destroyed) return;
|
|
612
|
+
destroyed = true;
|
|
613
|
+
stopAnimation = true;
|
|
614
|
+
resizeObserver.disconnect();
|
|
615
|
+
cancelAnimationFrame(animationFrame);
|
|
616
|
+
tweens.forEach((t) => t.stop());
|
|
617
|
+
tweens.clear();
|
|
618
|
+
simulation.stop();
|
|
619
|
+
// Pixi treats a boolean `true` renderer option as "remove the canvas AND
|
|
620
|
+
// release global resources". Local and global graphs are separate Pixi
|
|
621
|
+
// applications, so releasing the process-wide text texture pool while
|
|
622
|
+
// the other graph is alive corrupts its atlas. Remove this app's canvas,
|
|
623
|
+
// children and renderer without releasing resources shared by its peer.
|
|
624
|
+
app.destroy({ removeView: true, releaseGlobalResources: false }, { children: true });
|
|
625
|
+
};
|
|
626
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { linkPopovers } from './link-popovers';
|