Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 54 additions & 18 deletions assets/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -336,32 +336,45 @@ a:focus {
}

.nav-tree__item {
--ia-weight-factor: var(--ia-weight, 0.68);
display: flex;
flex-direction: column;
gap: 0.6rem;
padding: 0.85rem 1rem;
border-radius: var(--radius-md);
border: 1px solid rgba(0, 245, 255, 0.18);
background: rgba(6, 12, 24, 0.75);
box-shadow: 0 16px 38px rgba(0, 12, 40, 0.35);
border: 1px solid rgb(0 245 255 / calc(0.18 + var(--ia-weight-factor) * 0.24));
background: linear-gradient(
145deg,
rgb(6 12 24 / calc(0.55 + var(--ia-weight-factor) * 0.22)),
rgb(3 10 22 / calc(0.5 + var(--ia-weight-factor) * 0.32))
);
box-shadow: 0 16px 38px rgb(0 12 40 / calc(0.26 + var(--ia-weight-factor) * 0.26));
transition: transform var(--transition), border-color var(--transition), box-shadow var(--transition);
}

.nav-tree__item:hover,
.nav-tree__item:focus-within {
transform: translateY(-2px);
border-color: rgba(0, 245, 255, 0.42);
box-shadow: 0 24px 52px rgba(0, 12, 40, 0.45);
border-color: rgb(0 245 255 / calc(0.3 + var(--ia-weight-factor) * 0.32));
box-shadow: 0 24px 52px rgb(0 12 40 / calc(0.32 + var(--ia-weight-factor) * 0.34));
}

.nav-tree__item[data-depth='1'] {
background: rgba(5, 10, 22, 0.72);
border-color: rgba(0, 245, 255, 0.22);
background: linear-gradient(
150deg,
rgb(5 10 22 / calc(0.62 + var(--ia-weight-factor) * 0.2)),
rgb(3 12 28 / calc(0.46 + var(--ia-weight-factor) * 0.28))
);
border-color: rgb(0 245 255 / calc(0.22 + var(--ia-weight-factor) * 0.28));
}

.nav-tree__item[data-depth='2'] {
background: rgba(8, 14, 26, 0.75);
border-color: rgba(0, 245, 255, 0.28);
background: linear-gradient(
150deg,
rgb(8 14 26 / calc(0.6 + var(--ia-weight-factor) * 0.24)),
rgb(4 10 20 / calc(0.46 + var(--ia-weight-factor) * 0.32))
);
border-color: rgb(0 245 255 / calc(0.24 + var(--ia-weight-factor) * 0.28));
}

.nav-tree__label {
Expand All @@ -388,9 +401,13 @@ a:focus {
}

.nav-tree__label.is-active {
border-color: rgba(0, 245, 255, 0.65);
background: linear-gradient(135deg, rgba(0, 245, 255, 0.22), rgba(76, 111, 255, 0.22));
box-shadow: 0 18px 40px rgba(0, 245, 255, 0.32);
border-color: rgb(0 245 255 / calc(0.45 + var(--ia-weight-factor) * 0.36));
background: linear-gradient(
135deg,
rgb(0 245 255 / calc(0.18 + var(--ia-weight-factor) * 0.34)),
rgb(76 111 255 / calc(0.18 + var(--ia-weight-factor) * 0.34))
);
box-shadow: 0 18px 40px rgb(0 245 255 / calc(0.26 + var(--ia-weight-factor) * 0.3));
}

.nav-tree__index {
Expand All @@ -415,6 +432,24 @@ a:focus {
letter-spacing: 0.04em;
}

.nav-tree__weight {
margin-left: auto;
padding: 0.18rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.72rem;
font-variant-numeric: tabular-nums;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgb(226 239 255 / 0.92);
background: linear-gradient(
135deg,
rgb(0 245 255 / calc(0.2 + var(--ia-weight-factor) * 0.22)),
rgb(76 111 255 / calc(0.2 + var(--ia-weight-factor) * 0.24))
);
border: 1px solid rgb(0 245 255 / calc(0.28 + var(--ia-weight-factor) * 0.32));
box-shadow: inset 0 0 12px rgb(0 245 255 / calc(0.08 + var(--ia-weight-factor) * 0.12));
}

.nav-tree__children {
display: grid;
gap: 0.55rem;
Expand Down Expand Up @@ -1121,6 +1156,7 @@ main {

[data-ia-depth]:not(#hero) {
position: relative;
--ia-weight-factor: var(--ia-weight, 0.6);
}

[data-ia-depth]:not(#hero)::before {
Expand All @@ -1129,7 +1165,7 @@ main {
inset: 0;
z-index: 0;
background: linear-gradient(140deg, rgba(0, 245, 255, 0.12), rgba(76, 111, 255, 0.18));
opacity: 0.85;
opacity: calc(0.45 + var(--ia-weight-factor) * 0.4);
pointer-events: none;
}

Expand All @@ -1138,7 +1174,7 @@ main {
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
border: 1px solid rgba(0, 245, 255, 0.16);
border: 1px solid rgb(0 245 255 / calc(0.14 + var(--ia-weight-factor) * 0.26));
pointer-events: none;
z-index: 0;
}
Expand All @@ -1149,7 +1185,7 @@ main {
}

[data-ia-depth='2']:not(#hero) {
background: rgba(10, 18, 34, 0.82);
background: rgb(10 18 34 / calc(0.62 + var(--ia-weight-factor) * 0.26));
}

[data-ia-depth='2']:not(#hero)::before {
Expand All @@ -1158,11 +1194,11 @@ main {
}

[data-ia-depth='2']:not(#hero)::after {
border-color: rgba(0, 245, 255, 0.22);
border-color: rgb(0 245 255 / calc(0.22 + var(--ia-weight-factor) * 0.28));
}

[data-ia-depth='3']:not(#hero) {
background: rgba(14, 20, 36, 0.86);
background: rgb(14 20 36 / calc(0.64 + var(--ia-weight-factor) * 0.26));
}

[data-ia-depth='3']:not(#hero)::before {
Expand All @@ -1171,7 +1207,7 @@ main {
}

[data-ia-depth='3']:not(#hero)::after {
border-color: rgba(255, 142, 102, 0.4);
border-color: rgb(255 142 102 / calc(0.28 + var(--ia-weight-factor) * 0.42));
}

[data-ia-index] .section__eyebrow::before {
Expand Down
65 changes: 56 additions & 9 deletions assets/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
populateLanguageSelect,
resolveTranslation
} from './i18n/registry.js';
import { computeInformationGradient, mapGradientToPercentages } from './recursive-gradient.js';

const counts = {
demos: 25,
Expand Down Expand Up @@ -1538,6 +1539,8 @@ const earthSceneControls = {
};

let navObserver = null;
let informationGradientWeights = new Map();
let informationGradientPercents = new Map();

function traverseHierarchy(nodes, callback, depth = 0) {
if (!Array.isArray(nodes)) return;
Expand All @@ -1551,31 +1554,55 @@ function traverseHierarchy(nodes, callback, depth = 0) {
}

function assignInformationDepth(hierarchy) {
const gradientMap = computeInformationGradient(hierarchy, {
rootWeight: 1,
learningRate: 0.52,
depthDecay: 0.7,
iterations: 24,
minimumWeight: 0.02,
tolerance: 0.00025
});
informationGradientWeights = gradientMap;
informationGradientPercents = mapGradientToPercentages(gradientMap);

const visited = new Set();

traverseHierarchy(hierarchy, (node, depth) => {
const element = document.getElementById(node.id);
if (!element) return;

element.setAttribute('data-ia-depth', String(depth));
if (node.index) {
element.setAttribute('data-ia-index', node.index);
} else {
element.removeAttribute('data-ia-index');
}
visited.add(element);
});

document.querySelectorAll('[data-ia-depth]').forEach((element) => {
if (!visited.has(element)) {
element.removeAttribute('data-ia-depth');
const weight = gradientMap.get(node.id);
if (typeof weight === 'number') {
const normalised = Math.max(0, Math.min(1, weight));
const fixed = normalised.toFixed(3);
element.setAttribute('data-ia-weight', fixed);
element.style.setProperty('--ia-weight', fixed);
} else {
element.removeAttribute('data-ia-weight');
element.style.removeProperty('--ia-weight');
}

visited.add(element);
});

document.querySelectorAll('[data-ia-index]').forEach((element) => {
if (!visited.has(element)) {
document
.querySelectorAll('[data-ia-depth], [data-ia-index], [data-ia-weight]')
.forEach((element) => {
if (visited.has(element)) return;
element.removeAttribute('data-ia-depth');
element.removeAttribute('data-ia-index');
}
});
element.removeAttribute('data-ia-weight');
if (element && element.style && typeof element.style.removeProperty === 'function') {
element.style.removeProperty('--ia-weight');
}
});
}

function escapeSelector(value) {
Expand Down Expand Up @@ -1619,6 +1646,26 @@ function createNavList(
label.textContent = item.label || fallback;
anchor.appendChild(label);

const weight = informationGradientWeights.get(item.id);
if (typeof weight === 'number') {
const normalised = Math.max(0, Math.min(1, weight));
const fixed = normalised.toFixed(3);
const badge = document.createElement('span');
badge.className = 'nav-tree__weight';
const percent = informationGradientPercents.get(item.id);
const percentValue = typeof percent === 'number' ? percent : Math.round(normalised * 100);
const densityLabel = lang === 'zh' ? '信息密度' : 'Signal density';
badge.textContent = `${percentValue}%`;
badge.setAttribute('aria-label', `${densityLabel} ${percentValue}%`);
badge.title = `${densityLabel}: ${percentValue}%`;
anchor.appendChild(badge);
li.style.setProperty('--ia-weight', fixed);
li.dataset.gradientWeight = fixed;
} else {
li.style.removeProperty('--ia-weight');
delete li.dataset.gradientWeight;
}

li.appendChild(anchor);

if (Array.isArray(item.children) && item.children.length) {
Expand Down
93 changes: 93 additions & 0 deletions assets/js/recursive-gradient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const DEFAULT_OPTIONS = {
rootWeight: 1,
learningRate: 0.45,
depthDecay: 0.68,
iterations: 18,
tolerance: 0.0005,
minimumWeight: 0.015
};

function toNumber(value, fallback = 0) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
}

function deriveBaseWeight(node, depth = 0) {
const label = typeof node.label === 'string' ? node.label : '';
const labelComplexity = Math.min(label.length / 6, 12);
const childCount = Array.isArray(node.children) ? node.children.length : 0;
const depthPenalty = 1 / Math.pow(1.15 + depth * 0.08, 1 + depth * 0.15);
const base = 1 + childCount * 0.65 + labelComplexity * 0.08;
return Math.max(0.35, base * depthPenalty);
}

function normaliseWeights(weights, targetTotal) {
if (!Array.isArray(weights) || weights.length === 0) {
return [];
}
const sum = weights.reduce((total, value) => total + value, 0);
if (sum <= 0) {
const fallback = targetTotal / weights.length;
return weights.map(() => fallback);
}
return weights.map((value) => (value / sum) * targetTotal);
}

export function computeInformationGradient(nodes, options = {}) {
const config = { ...DEFAULT_OPTIONS, ...options };
const result = new Map();

function descend(currentNodes, targetWeight, depth) {
if (!Array.isArray(currentNodes) || !currentNodes.length) return;
const target = toNumber(targetWeight, config.rootWeight);
if (target <= 0) return;

const baseWeights = currentNodes.map((node) => deriveBaseWeight(node, depth));
const desiredTotals = normaliseWeights(baseWeights, target);
let working = [...desiredTotals];

for (let iteration = 0; iteration < config.iterations; iteration += 1) {
const normalised = normaliseWeights(working, target);
let largestDelta = 0;

working = working.map((value, index) => {
const desired = desiredTotals[index];
const gradient = normalised[index] - desired;
largestDelta = Math.max(largestDelta, Math.abs(gradient));
const adjustment = gradient * config.learningRate * Math.pow(config.depthDecay, depth);
const nextValue = value - adjustment;
return Math.max(nextValue, config.minimumWeight);
});

if (largestDelta < config.tolerance) {
break;
}
}

const resolved = normaliseWeights(working, target);

currentNodes.forEach((node, index) => {
const resolvedWeight = resolved[index];
if (node && typeof node.id === 'string' && node.id.length > 0) {
result.set(node.id, resolvedWeight);
}
const children = node && Array.isArray(node.children) ? node.children : null;
if (children && children.length) {
const childTarget = resolvedWeight * config.depthDecay;
descend(children, childTarget, depth + 1);
}
});
}

descend(nodes, config.rootWeight, 0);
return result;
}

export function mapGradientToPercentages(gradientMap) {
const mapped = new Map();
gradientMap.forEach((value, key) => {
const clamped = Math.max(0, Math.min(1, value));
mapped.set(key, Math.round(clamped * 100));
});
return mapped;
}