/** * No.6092 site for Charles Stankievech * - load db.json * - map the nodes to a graph * - click the nodes to open them * - some of the nodes are images, others are 3D */ import ForceGraph3D from "3d-force-graph"; import SpriteText from "three-spritetext"; import { union } from "./utils/set_utils.js"; import stars from "./utils/stars.js"; const colors = [ "rgba(111,53,158,1.0)", "rgba(220,188,253,1.0)", "rgba(30,177,237,1.0)", "rgba(148,206,88,1.0)", "rgba(252,42,28,1.0)", "rgba(255,253,56,1.0)", "rgba(43,253,183,1.0)", "rgba(252,76,252,1.0)", "rgba(205,254,170,1.0)", "rgba(254,205,195,1.0)", "rgba(199,227,254,1.0)", "rgba(253,191,45,1.0)", "rgba(253,191,45,1.0)", ]; async function main() { const db = await loadDB(); const groups = {}; const linkable = {}; const data = { nodes: [], links: [] }; console.log(db); /** * load nodes and links */ db.page.forEach((item, index) => { const node = { title: item.title, id: index, groups: [], }; data.nodes.push(node); for (let tagIndex = 0; tagIndex < 8; tagIndex += 1) { const group = item["tag_" + tagIndex]; if (!group) continue; group -= 1; node.groups.push(group); if (group in groups) { groups[group].push(index); } else { groups[group] = [index]; } if (group in linkable) { data.links.push({ // source: choice(linkable[group]), source: linkable[group][0], target: index, }); // option: don't link to the root node more than once // if (window.location.hash === "#dense" && linkable[group][0]) { if (linkable[group][0]) { linkable[group].push(index); } else { linkable[group] = [index]; } } else { linkable[group] = [index]; } } }); /** * find common links */ data.links.forEach((link) => { const a = data.nodes[link.source]; const b = data.nodes[link.target]; !a.links && (a.links = []); !b.links && (b.links = []); a.links.push(link); b.links.push(link); }); const highlightNodes = new Set(); const highlightLinks = new Set(); let selectedNode = null; /** build group */ let graph = ForceGraph3D(); graph(document.querySelector("#graph")) .graphData(data) .nodeLabel((node) => node.title) .nodeThreeObject((node) => { const sprite = new SpriteText( node.title.split(/[ :]+/).slice(0, 3).join(" ") ); sprite.material.depthWrite = false; // make sprite background transparent sprite.color = colors[node.groups[0]]; // node.groups.length - 1]]; sprite.textHeight = 4; return sprite; }) // .nodeColor((node) => // highlightNodes.has(node.id) // ? node === selectedNode // ? colors[commonGroups(selectedNode, node)[0]] // : colors[commonGroups(selectedNode, node)[0]].replace("1.0", "0.8") // : colors[commonGroups(selectedNode, node)[0]].replace("1.0", "0.6") // ) .linkWidth((link) => (highlightLinks.has(link) ? 4 : 1)) .onNodeClick((node) => { // no state change if (!node && !highlightNodes.size) return; selectedNode = selectedNode === node ? null : node; highlightNodes.clear(); highlightLinks.clear(); if (node) { node.groups.forEach((group) => groups[group].forEach((neighbor) => highlightNodes.add(neighbor)) ); node.links.forEach((link) => highlightLinks.add(link)); } updateHighlight(); }); function updateHighlight() { graph.nodeColor(graph.nodeColor()).linkWidth(graph.linkWidth()); } graph.d3Force("charge").strength(-150); // camera orbit const distance = 250; let angle = 0; graph.cameraPosition({ x: distance * Math.sin(angle), z: distance * Math.cos(angle), }); stars(); } const randint = (limit) => Math.floor(Math.random() * limit); const choice = (list) => list[randint(list.length)]; const commonGroups = (a, b) => union(a.groups, b.groups); async function loadDB() { const request = await fetch("/assets/db.json"); return await request.json(); } main();