Embed EpiphanyGraph biosphere viewer

This commit is contained in:
Meta Vens 2026-05-18 04:00:07 +01:00
commit f23e4891f7
17 changed files with 2223 additions and 529 deletions

View file

@ -5,7 +5,9 @@ societies. It is a nested web of memory-bearing contracts. A cell, wound, herd,
route, reef, breeding ground, and nation can all be political at their own scale
without becoming the same kind of mind.
<div class="zyphos-biosphere-graph"></div>
<link rel="stylesheet" href="/static/epiphany-graph/assets/viewer.css" />
<div class="zyphos-epiphany-graph-root"></div>
<script type="module" src="/static/epiphany-graph/assets/viewer.js"></script>
## Scale Bands

View file

@ -0,0 +1 @@
function r(o){throw new Error('Could not dynamically require "'+o+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}export{r as c};

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
html,body,#root{min-height:100%;margin:0}body{background:radial-gradient(circle at 72% 12%,rgba(255,174,88,.16),transparent 24%),radial-gradient(circle at 10% 18%,rgba(142,223,176,.16),transparent 28%),linear-gradient(180deg,#03070d,#07110f 52%,#091712);color:#effcf8;font-family:Ubuntu,Segoe UI,sans-serif}.zyphos-epiphany-graph-app{box-sizing:border-box;min-height:100vh;padding:16px}.zyphos-epiphany-graph-app *{box-sizing:border-box}.zyphos-graph-error{white-space:pre-wrap;color:#fecaca;background:#450a0a99;border:1px solid rgba(248,113,113,.45);border-radius:16px;padding:16px}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zyphos Fractal Eusocial Web</title>
<script type="module" crossorigin src="./assets/viewer.js"></script>
<link rel="modulepreload" crossorigin href="./assets/react.js">
<link rel="modulepreload" crossorigin href="./assets/elk.js">
<link rel="stylesheet" crossorigin href="./assets/viewer.css">
</head>
<body>
<div class="zyphos-epiphany-graph-root"></div>
</body>
</html>

View file

@ -13,3 +13,19 @@ The site follows the same deployment pattern as `AetheriaLore`:
- custom domain: `zyphos.gamecult.org`
DNS should point `zyphos.gamecult.org` at GitHub Pages, not Yggdrasil.
## EpiphanyGraph Embed
The fractal biosphere graph uses a small React/Vite embed around the neighboring
`EpiphanyGraph` viewer source. Quartz does not build this bundle during Pages
deploys, so rebuild the static assets before committing graph embed changes:
```powershell
cd "E:\Projects\Eusocial Interbeing\site\epiphany-graph-embed"
npm install
npm run build
```
The build writes deployable assets into
`Eusocial Interbeing/static/epiphany-graph/`, which Quartz then copies into the
site output.

1
site/epiphany-graph-embed/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules/

1724
site/epiphany-graph-embed/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
{
"name": "@gamecult/zyphos-epiphany-graph-embed",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build --base ./"
},
"dependencies": {
"@vitejs/plugin-react": "^5.1.1",
"elkjs": "^0.11.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"typescript": "^5.9.3",
"vite": "^7.2.4"
},
"devDependencies": {}
}

View file

@ -0,0 +1,318 @@
import React from "react"
import { flushSync } from "react-dom"
import ReactDOM from "react-dom/client"
import { EpiphanyGraphViewer } from "../../../../EpiphanyGraph/web/epiphany-graph-viewer/src/lib/EpiphanyGraphViewer"
import type { EpiphanyGraphsState } from "../../../../EpiphanyGraph/web/epiphany-graph-viewer/src/lib/types"
import "./styles.css"
type GraphErrorBoundaryState = {
error: Error | null
}
class GraphErrorBoundary extends React.Component<React.PropsWithChildren, GraphErrorBoundaryState> {
state: GraphErrorBoundaryState = { error: null }
static getDerivedStateFromError(error: Error) {
return { error }
}
render() {
if (this.state.error) {
return (
<pre className="zyphos-graph-error">
{this.state.error.message}
{"\n"}
{this.state.error.stack}
</pre>
)
}
return this.props.children
}
}
const zyphosGraphState: EpiphanyGraphsState = {
architecture: {
nodes: [
{
id: "memorioplast",
title: "Memorioplast Substrate",
status: "verified",
purpose: "Mutable memory endosymbionts make memory exchange cellular before it becomes culture.",
mechanism: "Cells store, edit, and transmit local state through symbiotic memory organelles.",
metaphor: "The biosphere's smallest treaty office.",
},
{
id: "tissue-boundaries",
title: "Tissue Boundary Politics",
status: "active",
purpose: "Immune and repair systems decide what counts as self enough to enter a body.",
mechanism: "Wounds, grafts, gut traffic, and root sheaths meter memory compatibility by channel.",
metaphor: "Customs inspection conducted by flesh.",
},
{
id: "organism-contracts",
title: "Organism Contracts",
status: "active",
purpose: "Mobile and rooted beings remain semi-independent while serving larger memory economies.",
mechanism: "Prismwake mats, threadwings, glassbacks, burden flowers, and lantern trees exchange food, route memory, warning, and sanction.",
metaphor: "Individuals with several employers and no clean resignation path.",
},
{
id: "colony-routes",
title: "Colony and Route Bodies",
status: "active",
purpose: "Colonies and logistics corridors become stable organs of larger ecological memory.",
mechanism: "Lattice ants, candle fungal roads, and choir reefs read local history and sell or gift transit.",
metaphor: "Roads that remember your manners.",
},
{
id: "archive-commons",
title: "Archives and Commons",
status: "active",
purpose: "Long-lived archives and reproductive commons hold continuity beyond mobile lifetimes.",
mechanism: "Mother trees guard deep memory while breeding grounds bind caretakers, infants, symbionts, and law.",
metaphor: "Library, nursery, court, and immune organ pretending to be a place.",
},
{
id: "native-nations",
title: "Native Nation Scale Interbeings",
status: "verified",
purpose: "Multispecies territorial bodies manage boundary permeability as foreign policy.",
mechanism: "Archival, migratory, fungal-road, reef, and wetland polities admit or reject traffic by channel.",
metaphor: "A border that is alive enough to be offended.",
},
{
id: "imperial-clausiform",
title: "Imperium airavense clausum",
status: "verified",
purpose: "Artificial quarter-continent superorganism with strict boundaries and tuned autonomy.",
mechanism: "The Biological Memetic Engine authors an impermeable topology where all permitted variation serves imperial goals.",
metaphor: "A utility function wearing ancestral skin.",
},
],
edges: [
{
id: "cell-to-tissue",
source_id: "memorioplast",
target_id: "tissue-boundaries",
kind: "scales",
label: "scales",
mechanism: "Cellular memory compatibility becomes immune and repair policy.",
},
{
id: "tissue-to-organism",
source_id: "tissue-boundaries",
target_id: "organism-contracts",
kind: "bounds",
label: "bounds",
mechanism: "Bodies inherit boundary habits from tissue-scale memory law.",
},
{
id: "organism-to-route",
source_id: "organism-contracts",
target_id: "colony-routes",
kind: "routes",
label: "routes",
mechanism: "Repeated exchanges stabilize into trails, colonies, and diagnostic circuits.",
},
{
id: "route-to-archive",
source_id: "colony-routes",
target_id: "archive-commons",
kind: "connects",
label: "connects",
mechanism: "Routes connect breeding grounds, mother archives, shelters, licks, and corpse gardens.",
},
{
id: "archive-to-nation",
source_id: "archive-commons",
target_id: "native-nations",
kind: "scales",
label: "scales",
mechanism: "Archives and commons become durable territorial agency.",
},
{
id: "empire-attacks-native",
source_id: "imperial-clausiform",
target_id: "native-nations",
kind: "invades",
label: "invades",
mechanism: "Imperial topology tries to overwrite negotiated permeability with uniform control.",
},
],
},
dataflow: {
nodes: [
{
id: "memory-state",
title: "Mutable Memory State",
status: "verified",
purpose: "Local experience becomes transferable biological state.",
mechanism: "Endosymbionts store route, injury, trust, hunger, and taboo emphasis.",
metaphor: "Evidence that can breed.",
},
{
id: "compatibility-filter",
title: "Compatibility Filter",
status: "active",
purpose: "Organisms decide which traffic may cross which boundary.",
mechanism: "Permeability differs for nutrient, symbiont, memory, reproductive, and military channels.",
metaphor: "Diplomacy as membrane behavior.",
},
{
id: "contract-ledger",
title: "Contract Ledger",
status: "active",
purpose: "Repeated exchange accumulates into debt, trust, warning, and sanction.",
mechanism: "Mats, roads, reefs, flowers, and herds remember who fed, cheated, healed, or lied.",
metaphor: "Ecology keeping receipts.",
},
{
id: "route-authority",
title: "Route Authority",
status: "active",
purpose: "Transit becomes governance when places open, close, tax, or reroute movement.",
mechanism: "Threadwings, fungal roads, lattice ants, and seasonal corridors carry permission.",
metaphor: "Infrastructure with opinions.",
},
{
id: "sovereign-permeability",
title: "Sovereign Permeability",
status: "verified",
purpose: "At nation scale, boundary policy becomes foreign policy.",
mechanism: "Native nations apply different rules to each traffic channel instead of one flat border.",
metaphor: "A treaty made of valves.",
},
{
id: "imperial-uniformity",
title: "Imperial Uniformity",
status: "verified",
purpose: "The Empire replaces negotiated ecology with controlled memetic structure.",
mechanism: "It allows exactly the autonomy needed for productivity and rejects uncontrolled memory traffic.",
metaphor: "Harmony with a knife behind it.",
},
],
edges: [
{
id: "state-filter",
source_id: "memory-state",
target_id: "compatibility-filter",
kind: "filtered_by",
label: "filtered by",
mechanism: "Memory is useful only if a boundary admits or rewrites it.",
},
{
id: "filter-ledger",
source_id: "compatibility-filter",
target_id: "contract-ledger",
kind: "records",
label: "records",
mechanism: "Accepted or rejected traffic becomes remembered precedent.",
},
{
id: "ledger-route",
source_id: "contract-ledger",
target_id: "route-authority",
kind: "authorizes",
label: "authorizes",
mechanism: "Debts and trust decide which roads, herds, and reefs open.",
},
{
id: "route-sovereignty",
source_id: "route-authority",
target_id: "sovereign-permeability",
kind: "scales",
label: "scales",
mechanism: "Route decisions aggregate into nation-scale boundary regimes.",
},
{
id: "uniformity-overwrites",
source_id: "imperial-uniformity",
target_id: "sovereign-permeability",
kind: "overwrites",
label: "overwrites",
mechanism: "The Empire treats local permeability as disorder to be corrected.",
},
],
},
links: [
{
architecture_node_id: "memorioplast",
dataflow_node_id: "memory-state",
relationship: "memory substrate",
},
{
architecture_node_id: "tissue-boundaries",
dataflow_node_id: "compatibility-filter",
relationship: "boundary policy",
},
{
architecture_node_id: "organism-contracts",
dataflow_node_id: "contract-ledger",
relationship: "organism-scale accounting",
},
{
architecture_node_id: "colony-routes",
dataflow_node_id: "route-authority",
relationship: "transit governance",
},
{
architecture_node_id: "native-nations",
dataflow_node_id: "sovereign-permeability",
relationship: "foreign policy",
},
{
architecture_node_id: "imperial-clausiform",
dataflow_node_id: "imperial-uniformity",
relationship: "artificial topology",
},
],
}
function App() {
return (
<main className="zyphos-epiphany-graph-app">
<GraphErrorBoundary>
<EpiphanyGraphViewer
state={zyphosGraphState}
title="Zyphos Fractal Eusocial Web"
/>
</GraphErrorBoundary>
</main>
)
}
function mountZyphosGraph() {
document.querySelectorAll(".zyphos-epiphany-graph-root").forEach((element) => {
if (!(element instanceof HTMLElement)) {
return
}
if (element.dataset.ready === "true" && element.childElementCount > 0) {
return
}
const root = ReactDOM.createRoot(element)
flushSync(() => {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
})
element.dataset.ready = "true"
})
}
function scheduleZyphosGraphMount() {
mountZyphosGraph()
window.setTimeout(mountZyphosGraph, 50)
window.setTimeout(mountZyphosGraph, 300)
window.setTimeout(mountZyphosGraph, 1000)
window.setTimeout(mountZyphosGraph, 2000)
}
document.addEventListener("nav", scheduleZyphosGraphMount)
document.addEventListener("DOMContentLoaded", scheduleZyphosGraphMount)
scheduleZyphosGraphMount()

View file

@ -0,0 +1,34 @@
html,
body,
#root {
min-height: 100%;
margin: 0;
}
body {
background:
radial-gradient(circle at 72% 12%, rgba(255, 174, 88, 0.16), transparent 24%),
radial-gradient(circle at 10% 18%, rgba(142, 223, 176, 0.16), transparent 28%),
linear-gradient(180deg, #03070d 0%, #07110f 52%, #091712 100%);
color: #effcf8;
font-family: "Ubuntu", "Segoe UI", sans-serif;
}
.zyphos-epiphany-graph-app {
box-sizing: border-box;
min-height: 100vh;
padding: 16px;
}
.zyphos-epiphany-graph-app * {
box-sizing: border-box;
}
.zyphos-graph-error {
white-space: pre-wrap;
color: #fecaca;
background: rgba(69, 10, 10, 0.6);
border: 1px solid rgba(248, 113, 113, 0.45);
border-radius: 16px;
padding: 16px;
}

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Zyphos Fractal Eusocial Web</title>
</head>
<body>
<div class="zyphos-epiphany-graph-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -0,0 +1,39 @@
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { fileURLToPath } from "node:url"
const viewerEntry = fileURLToPath(new URL("./viewer.html", import.meta.url))
const reactPath = fileURLToPath(new URL("./node_modules/react", import.meta.url))
const reactDomPath = fileURLToPath(new URL("./node_modules/react-dom", import.meta.url))
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
react: reactPath,
"react-dom": reactDomPath,
},
},
server: {
host: "127.0.0.1",
port: 4182,
},
build: {
outDir: "../../Eusocial Interbeing/static/epiphany-graph",
emptyOutDir: true,
rollupOptions: {
input: {
viewer: viewerEntry,
},
output: {
manualChunks: {
react: ["react", "react-dom"],
elk: ["elkjs/lib/elk.bundled.js"],
},
entryFileNames: "assets/[name].js",
chunkFileNames: "assets/[name].js",
assetFileNames: "assets/[name][extname]",
},
},
},
})

View file

@ -3,7 +3,6 @@ import * as Plugin from "./quartz/plugins"
import { QuartzEmitterPlugin } from "./quartz/plugins/types"
import { Fragment, h } from "preact"
import { zyphosHeroScript } from "./quartz/components/ZyphosHeroScript"
import { zyphosBiosphereGraphScript } from "./quartz/components/ZyphosBiosphereGraphScript"
const ZyphosFavicons: QuartzEmitterPlugin = () => ({
name: "ZyphosFavicons",
@ -37,22 +36,6 @@ const ZyphosHeroResources: QuartzEmitterPlugin = () => ({
}),
})
const ZyphosBiosphereGraphResources: QuartzEmitterPlugin = () => ({
name: "ZyphosBiosphereGraphResources",
async *emit() {},
async *partialEmit() {},
externalResources: () => ({
js: [
{
script: zyphosBiosphereGraphScript,
loadTime: "afterDOMReady",
contentType: "inline",
spaPreserve: true,
},
],
}),
})
const config: QuartzConfig = {
configuration: {
pageTitle: "Zyphos",
@ -149,7 +132,6 @@ const config: QuartzConfig = {
Plugin.Static(),
ZyphosFavicons(),
ZyphosHeroResources(),
ZyphosBiosphereGraphResources(),
Plugin.NotFoundPage(),
Plugin.CustomOgImages(),
],

View file

@ -1,312 +0,0 @@
import { QuartzComponent, QuartzComponentConstructor } from "./types"
export const zyphosBiosphereGraphScript = String.raw`
function bootZyphosBiosphereGraph() {
const mounts = document.querySelectorAll(".zyphos-biosphere-graph")
if (!mounts.length) return
const graph = {
nodes: [
{ id: "memorioplast", title: "Memorioplast", layer: "cellular", x: 0, y: 0, size: 30, purpose: "Mutable memory endosymbiont. Stores, edits, and transmits local cellular experience." },
{ id: "cell_congress", title: "Cell Congress", layer: "cellular", x: -170, y: -70, size: 24, purpose: "Cell clusters negotiate repair, immunity, hunger, and inherited response." },
{ id: "mirror_amoebae", title: "Mirror Amoebae", layer: "microbial", x: 170, y: -70, size: 22, purpose: "Copy and replay nearby memory states in wounds, ponds, stomachs, and root sheaths." },
{ id: "immune_liturgy", title: "Immune Liturgy", layer: "tissue", x: 0, y: -190, size: 24, purpose: "Boundary rituals inside flesh. Decides which memories count as self enough." },
{ id: "prismwake_mats", title: "Prismwake Mats", layer: "organism", x: -330, y: -170, size: 27, purpose: "Wetland sheets that pay gentle grazers with sugar and punish overfeeding." },
{ id: "lantern_trees", title: "Lantern Trees", layer: "organism", x: 330, y: -170, size: 28, purpose: "Umbros-facing canopies that light eclipse routes and tax passage through pulses." },
{ id: "threadwings", title: "Threadwing Couriers", layer: "organism", x: -380, y: 20, size: 24, purpose: "Bird-analogues moving pollen, gut symbionts, route memory, and grudges." },
{ id: "glassbacks", title: "Glassback Grazers", layer: "organism", x: -260, y: 170, size: 26, purpose: "Heat-sharing herd bodies whose translucent plates leak social and immune truth." },
{ id: "lattice_ants", title: "Lattice Ants", layer: "colony", x: 30, y: 190, size: 23, purpose: "Temporary body-bridges and ground-reading memory circuits." },
{ id: "candle_roads", title: "Candle Fungal Roads", layer: "colony", x: 300, y: 120, size: 27, purpose: "Dim mycelial routes that connect shelters, groves, licks, and breeding commons." },
{ id: "choir_reefs", title: "Choir Reefs", layer: "colony", x: 430, y: -20, size: 26, purpose: "Aquatic colonies that negotiate through current, pressure, shelter, and song." },
{ id: "burden_flowers", title: "Burden Flowers", layer: "organism", x: -120, y: 300, size: 22, purpose: "Mobile plant-analogues that carry immune and emotional rumor on traveler hides." },
{ id: "mother_archives", title: "Mother Archives", layer: "archive", x: 230, y: -330, size: 31, purpose: "Long-lived tree networks that hold ancestry, precedent, debt, disaster, and law." },
{ id: "breeding_grounds", title: "Breeding Grounds", layer: "commons", x: -230, y: 360, size: 31, purpose: "Sacred reproductive commons where infants, caretakers, infirm adults, and ecological partners hold continuity." },
{ id: "viator_nations", title: "Viator Nations", layer: "nation", x: -520, y: 280, size: 34, purpose: "Migratory route interbeings whose territory travels through herds, bloom corridors, and debt." },
{ id: "archival_nations", title: "Archival Nations", layer: "nation", x: 120, y: -500, size: 35, purpose: "Mother-tree centered native polities with porous food borders and guarded deep memory." },
{ id: "fungal_nations", title: "Fungal Road Nations", layer: "nation", x: 540, y: 260, size: 34, purpose: "Logistics organisms whose hospitality is accounting with teeth." },
{ id: "reef_nations", title: "Reef Nations", layer: "nation", x: 620, y: -190, size: 33, purpose: "Waterway polities whose borders follow larvae, current, mineral taste, and storm memory." },
{ id: "native_patchwork", title: "Native Patchwork", layer: "continent", x: -210, y: -540, size: 40, purpose: "Three quarters of the Airawa continent: many sovereign interbeings grown by local negotiation." },
{ id: "imperium", title: "Imperium airavense clausum", layer: "empire", x: 520, y: -500, size: 42, purpose: "Artificial quarter-continent clausiform being with impermeable borders and tuned autonomy." },
{ id: "memetic_engine", title: "Biological Memetic Engine", layer: "technology", x: 470, y: -650, size: 29, purpose: "Engineered organisms, rituals, and signal payloads that industrialize coercion through trusted memory channels." },
{ id: "saaueia_mesh", title: "Sa'auei'a Reciprocal Mesh", layer: "civilization", x: -570, y: 470, size: 36, purpose: "Mobile family units moving through remembered ecologies where cooperation must be earned repeatedly." },
],
edges: [
["memorioplast", "cell_congress", "stores"],
["memorioplast", "mirror_amoebae", "copies"],
["cell_congress", "immune_liturgy", "regulates"],
["mirror_amoebae", "immune_liturgy", "confuses"],
["immune_liturgy", "prismwake_mats", "bounds"],
["immune_liturgy", "lantern_trees", "bounds"],
["prismwake_mats", "glassbacks", "feeds"],
["glassbacks", "burden_flowers", "carries"],
["burden_flowers", "breeding_grounds", "reports"],
["threadwings", "prismwake_mats", "pollinates"],
["threadwings", "lantern_trees", "routes"],
["threadwings", "mother_archives", "carries"],
["lattice_ants", "candle_roads", "diagnoses"],
["lattice_ants", "glassbacks", "reads"],
["candle_roads", "breeding_grounds", "connects"],
["candle_roads", "fungal_nations", "scales"],
["choir_reefs", "reef_nations", "scales"],
["mother_archives", "archival_nations", "scales"],
["glassbacks", "viator_nations", "scales"],
["breeding_grounds", "saaueia_mesh", "anchors"],
["viator_nations", "saaueia_mesh", "treaties"],
["archival_nations", "native_patchwork", "member"],
["viator_nations", "native_patchwork", "member"],
["fungal_nations", "native_patchwork", "member"],
["reef_nations", "native_patchwork", "member"],
["memetic_engine", "imperium", "authors"],
["imperium", "native_patchwork", "invades"],
["imperium", "archival_nations", "denies"],
["imperium", "fungal_nations", "seals"],
["imperium", "saaueia_mesh", "misreads"],
["native_patchwork", "saaueia_mesh", "possible envoys"],
],
}
const layerColors = {
cellular: "#8edfb0",
microbial: "#b9f6ca",
tissue: "#73c6e3",
organism: "#ffae58",
colony: "#f7d06d",
archive: "#a7f3d0",
commons: "#f9a8d4",
nation: "#c4b5fd",
continent: "#efefff",
empire: "#ff6b6b",
technology: "#f97316",
civilization: "#7dd3fc",
}
mounts.forEach((mount) => {
if (mount.dataset.ready === "true") return
mount.dataset.ready = "true"
const shell = document.createElement("div")
shell.className = "zyphos-graph-shell"
const toolbar = document.createElement("div")
toolbar.className = "zyphos-graph-toolbar"
const title = document.createElement("div")
title.className = "zyphos-graph-title"
title.innerHTML = "<strong>Fractal Eusocial Web</strong><span>Memory, matter, boundary, and sovereignty channels.</span>"
const controls = document.createElement("div")
controls.className = "zyphos-graph-controls"
const zoomIn = graphButton("+", "Zoom in")
const zoomOut = graphButton("-", "Zoom out")
const reset = graphButton("Reset", "Reset view")
controls.append(zoomIn, zoomOut, reset)
toolbar.append(title, controls)
const body = document.createElement("div")
body.className = "zyphos-graph-body"
const viewport = document.createElement("div")
viewport.className = "zyphos-graph-viewport"
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
svg.setAttribute("role", "img")
svg.setAttribute("aria-label", "Interactive graph of Zyphos biosphere relationships")
viewport.append(svg)
const detail = document.createElement("aside")
detail.className = "zyphos-graph-detail"
body.append(viewport, detail)
shell.append(toolbar, body)
mount.replaceChildren(shell)
const edgeLayer = document.createElementNS("http://www.w3.org/2000/svg", "g")
const nodeLayer = document.createElementNS("http://www.w3.org/2000/svg", "g")
svg.append(edgeLayer, nodeLayer)
let selected = { kind: "node", id: "memorioplast" }
let transform = { x: 0, y: 0, scale: 0.78 }
let dragging = null
function worldToScreen(point) {
return {
x: point.x * transform.scale + transform.x,
y: point.y * transform.scale + transform.y,
}
}
function setTransform(next) {
transform = next
nodeLayer.setAttribute("transform", "translate(" + transform.x + " " + transform.y + ") scale(" + transform.scale + ")")
edgeLayer.setAttribute("transform", "translate(" + transform.x + " " + transform.y + ") scale(" + transform.scale + ")")
}
function fit() {
const rect = viewport.getBoundingClientRect()
const xs = graph.nodes.map((node) => node.x)
const ys = graph.nodes.map((node) => node.y)
const minX = Math.min.apply(null, xs) - 130
const maxX = Math.max.apply(null, xs) + 130
const minY = Math.min.apply(null, ys) - 130
const maxY = Math.max.apply(null, ys) + 130
const scale = Math.max(0.42, Math.min(1.08, Math.min(rect.width / (maxX - minX), rect.height / (maxY - minY))))
setTransform({
scale,
x: (rect.width - (minX + maxX) * scale) / 2,
y: (rect.height - (minY + maxY) * scale) / 2,
})
}
function render() {
edgeLayer.replaceChildren()
nodeLayer.replaceChildren()
const lookup = new Map(graph.nodes.map((node) => [node.id, node]))
graph.edges.forEach((edge, index) => {
const source = lookup.get(edge[0])
const target = lookup.get(edge[1])
if (!source || !target) return
const line = document.createElementNS("http://www.w3.org/2000/svg", "line")
line.setAttribute("x1", source.x)
line.setAttribute("y1", source.y)
line.setAttribute("x2", target.x)
line.setAttribute("y2", target.y)
line.setAttribute("class", selected.kind === "edge" && selected.id === index ? "selected" : "")
line.dataset.edgeIndex = String(index)
line.addEventListener("click", (event) => {
event.stopPropagation()
selected = { kind: "edge", id: index }
render()
})
edgeLayer.append(line)
const label = document.createElementNS("http://www.w3.org/2000/svg", "text")
label.setAttribute("x", (source.x + target.x) / 2)
label.setAttribute("y", (source.y + target.y) / 2 - 8)
label.setAttribute("class", "zyphos-graph-edge-label")
label.textContent = edge[2]
edgeLayer.append(label)
})
graph.nodes.forEach((node) => {
const group = document.createElementNS("http://www.w3.org/2000/svg", "g")
group.setAttribute("transform", "translate(" + node.x + " " + node.y + ")")
group.setAttribute("class", selected.kind === "node" && selected.id === node.id ? "zyphos-graph-node selected" : "zyphos-graph-node")
group.addEventListener("click", (event) => {
event.stopPropagation()
selected = { kind: "node", id: node.id }
render()
})
const aura = document.createElementNS("http://www.w3.org/2000/svg", "circle")
aura.setAttribute("r", node.size + 12)
aura.setAttribute("fill", layerColors[node.layer] + "24")
group.append(aura)
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle")
circle.setAttribute("r", node.size)
circle.setAttribute("fill", layerColors[node.layer] || "#8edfb0")
group.append(circle)
const text = document.createElementNS("http://www.w3.org/2000/svg", "text")
text.setAttribute("y", node.size + 22)
text.setAttribute("text-anchor", "middle")
text.textContent = node.title
group.append(text)
nodeLayer.append(group)
})
updateDetail()
setTransform(transform)
}
function updateDetail() {
if (selected.kind === "edge") {
const edge = graph.edges[selected.id]
const source = graph.nodes.find((node) => node.id === edge[0])
const target = graph.nodes.find((node) => node.id === edge[1])
detail.innerHTML = "<p class='zyphos-graph-kicker'>Selected relation</p><h2>" + edge[2] + "</h2><p><strong>" + source.title + "</strong> to <strong>" + target.title + "</strong></p><p>This edge marks one channel of memory, matter, permission, coercion, or obligation. It is a live ecological contract, not a decorative arrow.</p>"
return
}
const node = graph.nodes.find((item) => item.id === selected.id) || graph.nodes[0]
const incoming = graph.edges.filter((edge) => edge[1] === node.id).length
const outgoing = graph.edges.filter((edge) => edge[0] === node.id).length
detail.innerHTML = "<p class='zyphos-graph-kicker'>" + node.layer + "</p><h2>" + node.title + "</h2><p>" + node.purpose + "</p><dl><dt>Incoming</dt><dd>" + incoming + "</dd><dt>Outgoing</dt><dd>" + outgoing + "</dd></dl>"
}
function zoomAt(factor, clientX, clientY) {
const rect = viewport.getBoundingClientRect()
const x = clientX == null ? rect.width / 2 : clientX - rect.left
const y = clientY == null ? rect.height / 2 : clientY - rect.top
const nextScale = Math.max(0.24, Math.min(2.6, transform.scale * factor))
const worldX = (x - transform.x) / transform.scale
const worldY = (y - transform.y) / transform.scale
setTransform({
scale: nextScale,
x: x - worldX * nextScale,
y: y - worldY * nextScale,
})
}
svg.addEventListener("wheel", (event) => {
event.preventDefault()
zoomAt(event.deltaY < 0 ? 1.12 : 1 / 1.12, event.clientX, event.clientY)
}, { passive: false })
svg.addEventListener("pointerdown", (event) => {
if (event.target !== svg) return
dragging = { x: event.clientX, y: event.clientY, tx: transform.x, ty: transform.y }
svg.setPointerCapture(event.pointerId)
})
svg.addEventListener("pointermove", (event) => {
if (!dragging) return
setTransform({
...transform,
x: dragging.tx + event.clientX - dragging.x,
y: dragging.ty + event.clientY - dragging.y,
})
})
svg.addEventListener("pointerup", () => {
dragging = null
})
svg.addEventListener("click", (event) => {
if (event.target === svg) {
selected = { kind: "node", id: "memorioplast" }
render()
}
})
zoomIn.addEventListener("click", () => zoomAt(1.18))
zoomOut.addEventListener("click", () => zoomAt(1 / 1.18))
reset.addEventListener("click", fit)
window.addEventListener("resize", fit)
render()
fit()
})
}
function graphButton(text, label) {
const button = document.createElement("button")
button.type = "button"
button.textContent = text
button.setAttribute("aria-label", label)
return button
}
document.addEventListener("nav", bootZyphosBiosphereGraph)
document.addEventListener("DOMContentLoaded", bootZyphosBiosphereGraph)
bootZyphosBiosphereGraph()
`
export default (() => {
const ZyphosBiosphereGraphScript: QuartzComponent = () => (
<script dangerouslySetInnerHTML={{ __html: zyphosBiosphereGraphScript }} />
)
return ZyphosBiosphereGraphScript
}) satisfies QuartzComponentConstructor

View file

@ -257,200 +257,14 @@ article th {
border-color: rgba(142, 223, 176, 0.12);
}
.zyphos-biosphere-graph {
margin: 1.25rem 0 1.6rem;
}
.zyphos-graph-shell {
display: grid;
gap: 0.85rem;
}
.zyphos-graph-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.85rem 1rem;
border: 1px solid rgba(142, 223, 176, 0.14);
border-radius: 18px;
background: linear-gradient(135deg, rgba(7, 17, 15, 0.94), rgba(13, 20, 31, 0.9));
}
.zyphos-graph-title {
display: grid;
gap: 0.25rem;
}
.zyphos-graph-title strong {
color: #f2fff8;
font-family: var(--titleFont);
font-size: 1rem;
font-weight: 300;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.zyphos-graph-title span {
color: rgba(189, 212, 202, 0.76);
font-size: 0.88rem;
}
.zyphos-graph-controls {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.zyphos-graph-controls button {
min-height: 2.1rem;
border: 1px solid rgba(142, 223, 176, 0.18);
border-radius: 999px;
padding: 0.25rem 0.75rem;
background: rgba(142, 223, 176, 0.07);
color: #e2f6ee;
cursor: pointer;
}
.zyphos-graph-controls button:hover {
border-color: rgba(255, 174, 88, 0.42);
background: rgba(255, 174, 88, 0.14);
}
.zyphos-graph-body {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.9rem;
min-height: 620px;
}
.zyphos-graph-viewport,
.zyphos-graph-detail {
border: 1px solid rgba(142, 223, 176, 0.12);
border-radius: 20px;
background:
radial-gradient(circle at 70% 14%, rgba(255, 174, 88, 0.14), transparent 22%),
linear-gradient(180deg, rgba(4, 9, 14, 0.96), rgba(7, 17, 15, 0.94));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.zyphos-graph-viewport {
min-height: 620px;
overflow: hidden;
}
.zyphos-graph-viewport svg {
.zyphos-epiphany-graph-root {
display: block;
width: 100%;
height: 100%;
min-height: 620px;
cursor: grab;
touch-action: none;
}
.zyphos-graph-viewport svg:active {
cursor: grabbing;
}
.zyphos-graph-viewport line {
stroke: rgba(142, 223, 176, 0.34);
stroke-width: 2.3;
vector-effect: non-scaling-stroke;
cursor: pointer;
}
.zyphos-graph-viewport line.selected {
stroke: #ffae58;
stroke-width: 4;
}
.zyphos-graph-edge-label {
fill: rgba(239, 252, 248, 0.62);
font-size: 12px;
font-family: var(--codeFont);
paint-order: stroke;
stroke: rgba(4, 9, 14, 0.86);
stroke-width: 5px;
text-anchor: middle;
pointer-events: none;
}
.zyphos-graph-node {
cursor: pointer;
}
.zyphos-graph-node circle:first-child {
opacity: 0.28;
}
.zyphos-graph-node circle:nth-child(2) {
stroke: rgba(239, 252, 248, 0.7);
stroke-width: 1.4;
vector-effect: non-scaling-stroke;
}
.zyphos-graph-node.selected circle:nth-child(2) {
stroke: #fff3e8;
stroke-width: 3;
}
.zyphos-graph-node text {
fill: #effcf8;
font-family: var(--titleFont);
font-size: 13px;
font-weight: 400;
paint-order: stroke;
pointer-events: none;
stroke: rgba(4, 9, 14, 0.92);
stroke-width: 5px;
}
.zyphos-graph-detail {
display: grid;
align-content: start;
gap: 0.75rem;
padding: 1rem;
grid-template-columns: minmax(0, 1fr);
}
.zyphos-graph-detail h2 {
margin: 0;
color: #f2fff8;
font-family: var(--titleFont);
font-size: 1.35rem;
font-weight: 300;
line-height: 1.08;
}
.zyphos-graph-detail p {
margin: 0;
color: rgba(189, 212, 202, 0.84);
line-height: 1.55;
}
.zyphos-graph-kicker {
color: var(--secondary) !important;
font-family: var(--titleFont);
font-size: 0.76rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.zyphos-graph-detail dl {
display: grid;
grid-template-columns: repeat(2, auto 1fr);
gap: 0.35rem 0.7rem;
margin: 0;
}
.zyphos-graph-detail dt {
color: rgba(189, 212, 202, 0.62);
}
.zyphos-graph-detail dd {
margin: 0;
color: #effcf8;
min-height: 820px;
margin: 1.25rem 0 1.6rem;
border: 1px solid rgba(142, 223, 176, 0.14);
border-radius: 20px;
background: #03070d;
}
@media all and not ($desktop) {
@ -468,12 +282,7 @@ article th {
margin-inline: -1rem;
}
.zyphos-graph-viewport,
.zyphos-graph-viewport svg {
min-height: 520px;
}
.zyphos-graph-detail dl {
grid-template-columns: auto 1fr;
.zyphos-epiphany-graph-root {
min-height: 780px;
}
}