diff --git a/assets/css/styles.css b/assets/css/styles.css index 7650a26..e5fc1ee 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -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 { @@ -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 { @@ -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; @@ -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 { @@ -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; } @@ -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; } @@ -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 { @@ -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 { @@ -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 { diff --git a/assets/js/main.js b/assets/js/main.js index 920f856..8c1c8e4 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -11,6 +11,7 @@ import { populateLanguageSelect, resolveTranslation } from './i18n/registry.js'; +import { computeInformationGradient, mapGradientToPercentages } from './recursive-gradient.js'; const counts = { demos: 25, @@ -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; @@ -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) { @@ -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) { diff --git a/assets/js/recursive-gradient.js b/assets/js/recursive-gradient.js new file mode 100644 index 0000000..0fd391b --- /dev/null +++ b/assets/js/recursive-gradient.js @@ -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; +}