diff --git a/atlas-fediverse/package-lock.json b/atlas-fediverse/package-lock.json index 6e59e7e..3a76386 100644 --- a/atlas-fediverse/package-lock.json +++ b/atlas-fediverse/package-lock.json @@ -9,15 +9,14 @@ "version": "4.0.0", "license": "ISC", "dependencies": { - "d3-force": "^3.0.0", - "three": "^0.185.1" + "@fontsource/archivo": "^5.3.0" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", "@sveltejs/kit": "^2.70.1", "@sveltejs/vite-plugin-svelte": "^7.2.0", "@types/d3-force": "^3.0.10", - "@types/three": "^0.185.1", + "d3-force": "^3.0.0", "svelte": "^5.56.6", "svelte-check": "^4.7.3", "typescript": "^6.0.3", @@ -25,13 +24,6 @@ "vitest": "^4.1.10" } }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -66,6 +58,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@fontsource/archivo": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/archivo/-/archivo-5.3.0.tgz", + "integrity": "sha512-5DIMgPVJRi62OqdOVoogCFxP73EkNM/E0YVTSDIQlDEJfDbxqZduwM/YoNZwC9Sx1CDgpKinqf8ckRY1QNIecw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -515,13 +516,6 @@ "vite": "^8.0.0-beta.7 || ^8.0.0" } }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "dev": true, - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -572,28 +566,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.185.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz", - "integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "fflate": "~0.8.2", - "meshoptimizer": "~1.1.1" - } - }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -601,13 +573,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", @@ -821,6 +786,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -830,6 +796,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -844,6 +811,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -853,6 +821,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -955,13 +924,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "dev": true, - "license": "MIT" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1275,13 +1237,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/meshoptimizer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", - "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", - "dev": true, - "license": "MIT" - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -1558,12 +1513,6 @@ "typescript": ">=5.0.0" } }, - "node_modules/three": { - "version": "0.185.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz", - "integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==", - "license": "MIT" - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/atlas-fediverse/package.json b/atlas-fediverse/package.json index 51c8b7e..dccde64 100644 --- a/atlas-fediverse/package.json +++ b/atlas-fediverse/package.json @@ -1,34 +1,32 @@ { - "name": "atlas-fediverse", - "version": "4.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "vitest run", - "dev": "vite dev", - "build": "vite build && node scripts/postbuild-csp.mjs", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" - }, - "keywords": [], - "author": "", - "license": "ISC", - "type": "module", - "private": "true", - "dependencies": { - "d3-force": "^3.0.0", - "three": "^0.185.1" - }, - "devDependencies": { - "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.70.1", - "@sveltejs/vite-plugin-svelte": "^7.2.0", - "@types/d3-force": "^3.0.10", - "@types/three": "^0.185.1", - "svelte": "^5.56.6", - "svelte-check": "^4.7.3", - "typescript": "^6.0.3", - "vite": "^8.1.5", - "vitest": "^4.1.10" - } + "name": "atlas-fediverse", + "version": "4.0.0", + "description": "", + "scripts": { + "test": "vitest run", + "dev": "vite dev", + "build": "vite build && node scripts/postbuild-csp.mjs", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "keywords": [], + "author": "", + "license": "CC-BY-SA-4.0", + "type": "module", + "private": "true", + "dependencies": { + "@fontsource/archivo": "^5.3.0" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.70.1", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@types/d3-force": "^3.0.10", + "d3-force": "^3.0.0", + "svelte": "^5.56.6", + "svelte-check": "^4.7.3", + "typescript": "^6.0.3", + "vite": "^8.1.5", + "vitest": "^4.1.10" + } } diff --git a/atlas-fediverse/scripts/build-layout.mjs b/atlas-fediverse/scripts/build-layout.mjs new file mode 100644 index 0000000..93bcbcc --- /dev/null +++ b/atlas-fediverse/scripts/build-layout.mjs @@ -0,0 +1,176 @@ +#!/usr/bin/env node +/* build-layout.mjs — pré-calcule les positions 2D de la carte de l'atlas. + * + * ⚠️ REJOUER CE SCRIPT À CHAQUE MODIFICATION de `src/lib/data/network.json` : + * node scripts/build-layout.mjs + * puis committer `src/lib/data/network-layout.json` avec les données. + * + * La simulation d3-force est entièrement déterministe : + * - positions initiales dérivées d'un hash stable de l'id (FNV-1a), pas de + * Math.random() — même entrée ⇒ même sortie, bit pour bit ; + * - ticks synchrones à nombre fixe (simulation.stop() + tick()), indépendants + * de l'horloge et de l'environnement. + * + * Sortie : src/lib/data/network-layout.json + * { viewBox: { width, height }, nodes: { [id]: { x, y } } } + * coordonnées normalisées dans le viewBox, arrondies à 0.1 près. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + forceSimulation, + forceLink, + forceManyBody, + forceCollide, + forceX, + forceY +} from 'd3-force'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const networkPath = join(root, 'src/lib/data/network.json'); +const outPath = join(root, 'src/lib/data/network-layout.json'); + +const network = JSON.parse(readFileSync(networkPath, 'utf8')); + +/** Hash FNV-1a 32 bits — graine stable par id, indépendante de la plateforme. */ +function hashId(str) { + let h = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h = Math.imul(h, 0x01000193); + } + return h >>> 0; +} + +/* ————— Positions initiales déterministes ————— + Chaque catégorie occupe un secteur d'un grand cercle ; les nœuds sont + dispersés autour du centre de leur catégorie selon leur hash. La simulation + part ainsi d'un état proche de la topologie finale (convergence rapide, + clusters lisibles). */ +const categories = [...new Set(network.nodes.map((n) => n.category))].sort(); +const catIndex = new Map(categories.map((c, i) => [c, i])); +// Ellipse large (ratio proche du viewBox 1200×800) : les clusters de +// catégories s'y répartissent et la carte remplit le cadre. +const CLUSTER_RX = 340; +const CLUSTER_RY = 210; +const JITTER_RADIUS = 60; + +/** Cible de placement d'une catégorie sur l'ellipse. */ +function categoryTarget(category) { + const angle = (2 * Math.PI * catIndex.get(category)) / categories.length; + return { x: CLUSTER_RX * Math.cos(angle), y: CLUSTER_RY * Math.sin(angle) }; +} + +const nodes = network.nodes.map((n, i) => { + const h = hashId(n.id); + const target = categoryTarget(n.category); + const jAngle = ((h % 1000) / 1000) * 2 * Math.PI; + const jRadius = (((h >> 10) % 1000) / 1000) * JITTER_RADIUS; + return { + ...n, + index: i, + x: target.x + jRadius * Math.cos(jAngle), + y: target.y + jRadius * Math.sin(jAngle) + }; +}); + +const links = network.links.map((l) => ({ + source: l.source, + target: l.target, + type: l.type +})); + +/* ————— Paramètres de forces ————— + - liens « strong » courts (30) pour souder les communautés, « weak » + longs (70) pour espacer les ponts inter-catégories ; + - répulsion modérée (-45) : le graphe est dense (873 liens / 105 nœuds), + une charge trop forte éclaterait les clusters ; + - collision = rayon visuel du nœud + marge, pour éviter les + superpositions une fois rendu en SVG ; + - forceX/forceY par catégorie vers la cible elliptique (0.08) : ancre + chaque cluster sans écraser la structure interne. */ +const simulation = forceSimulation(nodes) + .force( + 'link', + forceLink(links) + .id((d) => d.id) + .distance((d) => (d.type === 'strong' ? 30 : 70)) + .strength((d) => (d.type === 'strong' ? 0.7 : 0.2)) + ) + .force('charge', forceManyBody().strength(-45)) + .force('collide', forceCollide().radius((d) => 4 + (d.val ?? 5) * 0.6 + 6).iterations(2)) + .force('x', forceX((d) => categoryTarget(d.category).x).strength(0.08)) + .force('y', forceY((d) => categoryTarget(d.category).y).strength(0.08)) + .alpha(1) + .alphaDecay(0.02) + .stop(); + +// Convergence synchrone à nombre de ticks fixe (≈ 3× le temps de stabilisation +// théorique log(0.001)/log(1-0.02) ≈ 342) : reproductible à l'identique. +const TICKS = 600; +for (let i = 0; i < TICKS; i++) simulation.tick(); + +/* ————— Normalisation dans le viewBox 1200×800 (marge 60) ————— */ +const VIEW_W = 1200; +const VIEW_H = 800; +const MARGIN = 60; + +const xs = nodes.map((n) => n.x); +const ys = nodes.map((n) => n.y); +const minX = Math.min(...xs); +const maxX = Math.max(...xs); +const minY = Math.min(...ys); +const maxY = Math.max(...ys); +const scale = Math.min( + (VIEW_W - 2 * MARGIN) / Math.max(maxX - minX, 1e-9), + (VIEW_H - 2 * MARGIN) / Math.max(maxY - minY, 1e-9) +); +// Centrage du nuage dans le viewBox. +const offX = MARGIN + (VIEW_W - 2 * MARGIN - (maxX - minX) * scale) / 2; +const offY = MARGIN + (VIEW_H - 2 * MARGIN - (maxY - minY) * scale) / 2; + +const positions = {}; +for (const n of nodes) { + const x = Math.round((offX + (n.x - minX) * scale) * 10) / 10; + const y = Math.round((offY + (n.y - minY) * scale) * 10) / 10; + if (!Number.isFinite(x) || !Number.isFinite(y)) { + throw new Error(`Position non finie pour « ${n.id} » — ajuster les forces.`); + } + positions[n.id] = { x, y }; +} + +/* ————— Contrôles qualité ————— */ +const ids = Object.keys(positions); +if (ids.length !== network.nodes.length) { + throw new Error(`${ids.length} positions pour ${network.nodes.length} nœuds.`); +} +let minDist = Infinity; +let closest = null; +for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const a = positions[ids[i]]; + const b = positions[ids[j]]; + const d = Math.hypot(a.x - b.x, a.y - b.y); + if (d < minDist) { + minDist = d; + closest = [ids[i], ids[j]]; + } + } +} +if (minDist < 8) { + console.warn( + `⚠️ distance minimale ${minDist.toFixed(1)}px entre « ${closest[0]} » et « ${closest[1]} » — envisager forceCollide plus large.` + ); +} + +const layout = { + viewBox: { width: VIEW_W, height: VIEW_H }, + nodes: positions +}; +writeFileSync(outPath, JSON.stringify(layout, null, 2) + '\n'); + +console.log(`✔ ${ids.length} positions écrites dans src/lib/data/network-layout.json`); +console.log(` distance minimale entre nœuds : ${minDist.toFixed(1)}px (${closest[0]} ↔ ${closest[1]})`); +console.log(` alpha résiduel après ${TICKS} ticks : ${simulation.alpha().toFixed(5)}`); diff --git a/atlas-fediverse/src/app.css b/atlas-fediverse/src/app.css index c00b98a..04f2715 100644 --- a/atlas-fediverse/src/app.css +++ b/atlas-fediverse/src/app.css @@ -1,8 +1,13 @@ -/* app.css — point d'entrée des styles, ordre de cascade explicite */ +/* app.css — point d'entrée des styles, ordre de cascade explicite. + oki-tokens.css et oki-bridge.css sont hors layer : ils priment sur + tokens.css / themes/default.css (charte OKI = seule identité). */ @layer reset, tokens, base, layouts, components, utilities; @import './styles/reset.css' layer(reset); @import './styles/tokens.css' layer(tokens); @import './styles/themes/default.css' layer(tokens); +@import './styles/oki-tokens.css'; +@import './styles/oki-bridge.css'; @import './styles/base.css' layer(base); +@import './styles/oki-base.css' layer(base); @import './styles/layouts.css' layer(layouts); diff --git a/atlas-fediverse/src/app.html b/atlas-fediverse/src/app.html index 319ee43..bbd3f21 100644 --- a/atlas-fediverse/src/app.html +++ b/atlas-fediverse/src/app.html @@ -8,14 +8,58 @@ name="description" content="Cartographie interactive du Fédiverse : 105 logiciels, 12 protocoles, 18 ans d'histoire du réseau social décentralisé." /> - - + + + + + + + + + + + + + + + + + + + + + + + - + + %sveltekit.head%
@@ -53,7 +80,7 @@file://), servez le dossier
@@ -63,10 +90,10 @@