{{- /*
  knowledge-galaxy — 3D 知识星系 / 关于「我」的交互式入口

  数据驱动：中心节点是站主，环绕节点是身份/主题，可拖拽旋转、点击跳转。
  - Three.js 本地托管（assets/js/vendor），经 Hugo fingerprint
  - 通过 importmap 解析裸导入 'three' / 'three/addons/...'
  - 渐进增强：无 WebGL / reduced-motion / 极小屏 → 优雅降级为静态文字
  参数（可选）：height（画布高度，默认 480）
*/ -}}

{{- $three := resources.Get "js/vendor/three.module.min.js" | resources.Fingerprint "sha384" -}}
{{- $orbit := resources.Get "js/vendor/addons/controls/OrbitControls.js" -}}
{{- $height := .Get "height" | default "480" -}}
{{- $isZH := eq site.Language.Lang "zh" -}}

{{- /* 节点数据：lang 决定文案与链接前缀 */ -}}
{{- $prefix := cond $isZH "/zh" "/en" -}}

<div class="kg-wrap" data-kg-height="{{ $height }}">
  <div class="kg-stage" id="kg-stage" role="img"
       aria-label="{{ if $isZH }}关于熊鑫伟的交互式 3D 知识星系{{ else }}Interactive 3D knowledge galaxy about Xinwei Xiong{{ end }}">
    <div class="kg-hint" id="kg-hint">
      {{ if $isZH }}拖动旋转 · 点击节点进入{{ else }}Drag to rotate · Click a node to enter{{ end }}
    </div>
  </div>

  {{- /* 降级 / no-JS / no-WebGL 时展示这份静态版本 */ -}}
  <noscript>
    <div class="kg-fallback">
      <p>{{ if $isZH }}我是 <strong>熊鑫伟（cubxxw）</strong> —— AI 创业者、开源贡献者、数字游民。{{ else }}I'm <strong>Xinwei Xiong (cubxxw)</strong> — AI founder, open-source contributor, digital nomad.{{ end }}</p>
    </div>
  </noscript>
</div>

{{- /* 用 dict + jsonify 一次性生成，避免手写 JSON 时重复转义 */ -}}
{{- $payload := dict
  "center" (dict
    "label" (cond $isZH "熊鑫伟" "Xinwei Xiong")
    "sub" "cubxxw"
    "url" (printf "%s/about/" $prefix)
    "color" "#ffffff")
  "nodes" (slice
    (dict "label" (cond $isZH "AI 创业者" "AI Founder")   "url" (printf "%s/ai-technology/" $prefix)                                                              "color" "#667eea")
    (dict "label" (cond $isZH "开源贡献" "Open Source")    "url" "https://github.com/cubxxw"                                                                       "color" "#764ba2" "external" true)
    (dict "label" (cond $isZH "数字游民" "Digital Nomad") "url" (printf "%s/travel/" $prefix)                                                                     "color" "#17a2b8")
    (dict "label" "Kubernetes"                            "url" (printf "%s/ai-technology/posts/kubernetes-an-article-to-get-started-quickly/" $prefix)          "color" "#326ce5")
    (dict "label" "Go"                                    "url" (printf "%s/ai-technology/posts/go-release-tools/" $prefix)                                       "color" "#00add8")
    (dict "label" (cond $isZH "心流状态" "Flow State")    "url" (printf "%s/growth/posts/flow-state/" $prefix)                                                    "color" "#28a745")
    (dict "label" (cond $isZH "年度复盘" "Annual Review") "url" (printf "%s/growth/posts/2025-annual-review/" $prefix)                                            "color" "#f0883e")
    (dict "label" (cond $isZH "9 国徒步" "9 Countries")   "url" (printf "%s/growth/posts/in-2023-i-was-wandering-at-the-edge-of-the-world/" $prefix)              "color" "#e84393")
  )
-}}
<script type="importmap">
{
  "imports": {
    "three": "{{ $three.RelPermalink }}",
    "three/addons/controls/OrbitControls.js": "{{ $orbit.RelPermalink }}"
  }
}
</script>

<script type="module">
(async () => {
  // 数据由 Hugo 注入：html/template 在 JS 上下文会把 jsonify 结果转义为
  // 合法的 JSON 字符串字面量，因此这里用 JSON.parse 还原为对象。
  const DATA = JSON.parse({{ $payload | jsonify }});

  const stage = document.getElementById('kg-stage');
  if (!stage) return;

  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // WebGL 能力探测，失败则降级
  function hasWebGL() {
    try {
      const c = document.createElement('canvas');
      return !!(window.WebGLRenderingContext &&
        (c.getContext('webgl') || c.getContext('experimental-webgl')));
    } catch (e) { return false; }
  }

  function renderFallback() {
    stage.classList.add('kg-degraded');
    const links = DATA.nodes.map(n =>
      `<a class="kg-chip" href="${n.url}"${n.external ? ' target="_blank" rel="noopener"' : ''} style="--c:${n.color}">${n.label}</a>`
    ).join('');
    stage.innerHTML =
      `<div class="kg-fallback">
        <a class="kg-chip kg-chip--center" href="${DATA.center.url}">${DATA.center.label}</a>
        <div class="kg-chips">${links}</div>
      </div>`;
  }

  if (!hasWebGL()) { renderFallback(); return; }

  let THREE, OrbitControls;
  try {
    THREE = await import('three');
    ({ OrbitControls } = await import('three/addons/controls/OrbitControls.js'));
  } catch (e) {
    console.warn('[knowledge-galaxy] three.js load failed, falling back', e);
    renderFallback();
    return;
  }

  // ---- 场景搭建 ----
  const hint = document.getElementById('kg-hint');
  let width = stage.clientWidth;
  let height = parseInt(stage.parentElement.dataset.kgHeight || '480', 10);

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000);
  camera.position.set(0, 0, 30);

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(width, height);
  stage.appendChild(renderer.domElement);
  if (hint) stage.appendChild(hint); // 保证 hint 在 canvas 之上

  // 灯光
  scene.add(new THREE.AmbientLight(0xffffff, 0.9));
  const point = new THREE.PointLight(0xffffff, 1.2, 200);
  point.position.set(20, 20, 30);
  scene.add(point);

  // 文字 Sprite（支持中文）
  function makeLabel(text, color, scale) {
    const pad = 24, font = 'bold 56px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif';
    const measure = document.createElement('canvas').getContext('2d');
    measure.font = font;
    const tw = Math.ceil(measure.measureText(text).width);
    const cw = tw + pad * 2, ch = 56 + pad * 2;
    const cvs = document.createElement('canvas');
    cvs.width = cw; cvs.height = ch;
    const ctx = cvs.getContext('2d');
    ctx.font = font;
    ctx.fillStyle = color;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.shadowColor = 'rgba(0,0,0,0.45)';
    ctx.shadowBlur = 8;
    ctx.fillText(text, cw / 2, ch / 2);
    const tex = new THREE.CanvasTexture(cvs);
    tex.anisotropy = 4;
    const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
    const sprite = new THREE.Sprite(mat);
    const k = (scale || 1) * 0.018;
    sprite.scale.set(cw * k, ch * k, 1);
    return sprite;
  }

  const group = new THREE.Group();
  scene.add(group);

  const clickable = []; // { mesh, url, external }

  // 中心节点
  const centerGeo = new THREE.SphereGeometry(2.4, 48, 48);
  const centerMat = new THREE.MeshStandardMaterial({
    color: 0xffffff, emissive: 0x667eea, emissiveIntensity: 0.5, roughness: 0.3, metalness: 0.1
  });
  const center = new THREE.Mesh(centerGeo, centerMat);
  center.userData = { url: DATA.center.url, external: false };
  group.add(center);
  clickable.push(center);
  const centerLabel = makeLabel(DATA.center.label, '#ffffff', 1.3);
  centerLabel.position.set(0, -3.6, 0);
  group.add(centerLabel);

  // 环绕节点：黄金角分布在球面上
  const N = DATA.nodes.length;
  const R = 11;
  const golden = Math.PI * (3 - Math.sqrt(5));
  DATA.nodes.forEach((n, i) => {
    const y = 1 - (i / (N - 1)) * 2;       // 1 → -1
    const r = Math.sqrt(1 - y * y);
    const theta = golden * i;
    const pos = new THREE.Vector3(Math.cos(theta) * r, y, Math.sin(theta) * r).multiplyScalar(R);

    const col = new THREE.Color(n.color);
    const geo = new THREE.SphereGeometry(1.3, 32, 32);
    const mat = new THREE.MeshStandardMaterial({
      color: col, emissive: col, emissiveIntensity: 0.45, roughness: 0.35, metalness: 0.15
    });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.copy(pos);
    mesh.userData = { url: n.url, external: !!n.external };
    group.add(mesh);
    clickable.push(mesh);

    // 连线
    const lineGeo = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0, 0, 0), pos]);
    const lineMat = new THREE.LineBasicMaterial({ color: col, transparent: true, opacity: 0.25 });
    group.add(new THREE.Line(lineGeo, lineMat));

    // 标签
    const label = makeLabel(n.label, '#e8e8f0', 0.85);
    label.position.copy(pos.clone().multiplyScalar(1.22));
    group.add(label);
  });

  // 星尘背景
  const starCount = 350;
  const starPos = new Float32Array(starCount * 3);
  for (let i = 0; i < starCount; i++) {
    const rr = 40 + Math.random() * 40;
    const th = Math.random() * Math.PI * 2;
    const ph = Math.acos(2 * Math.random() - 1);
    starPos[i * 3] = rr * Math.sin(ph) * Math.cos(th);
    starPos[i * 3 + 1] = rr * Math.sin(ph) * Math.sin(th);
    starPos[i * 3 + 2] = rr * Math.cos(ph);
  }
  const starGeo = new THREE.BufferGeometry();
  starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
  const stars = new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0x8888aa, size: 0.35, transparent: true, opacity: 0.7 }));
  scene.add(stars);

  // 控制器：仅旋转
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.08;
  controls.enableZoom = false;
  controls.enablePan = false;
  controls.autoRotate = !reduceMotion;
  controls.autoRotateSpeed = 0.6;
  controls.rotateSpeed = 0.6;

  // 交互：hover 放大 + 指针，点击跳转
  const raycaster = new THREE.Raycaster();
  const pointer = new THREE.Vector2();
  let hovered = null;

  function updatePointer(e) {
    const rect = renderer.domElement.getBoundingClientRect();
    const cx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
    const cy = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
    pointer.x = (cx / rect.width) * 2 - 1;
    pointer.y = -(cy / rect.height) * 2 + 1;
  }

  function pick() {
    raycaster.setFromCamera(pointer, camera);
    const hits = raycaster.intersectObjects(clickable, false);
    return hits.length ? hits[0].object : null;
  }

  renderer.domElement.addEventListener('pointermove', (e) => {
    updatePointer(e);
    const hit = pick();
    if (hit !== hovered) {
      hovered = hit;
      renderer.domElement.style.cursor = hit ? 'pointer' : 'grab';
    }
  });

  let downXY = null;
  renderer.domElement.addEventListener('pointerdown', (e) => { downXY = [e.clientX, e.clientY]; });
  renderer.domElement.addEventListener('pointerup', (e) => {
    if (!downXY) return;
    const moved = Math.hypot(e.clientX - downXY[0], e.clientY - downXY[1]);
    downXY = null;
    if (moved > 6) return; // 视为拖拽，不触发跳转
    updatePointer(e);
    const hit = pick();
    if (hit && hit.userData.url) {
      if (hit.userData.external) window.open(hit.userData.url, '_blank', 'noopener');
      else window.location.href = hit.userData.url;
    }
  });

  // 首次交互后隐藏提示
  let interacted = false;
  function fadeHint() {
    if (interacted || !hint) return;
    interacted = true;
    hint.classList.add('kg-hint--hidden');
  }
  renderer.domElement.addEventListener('pointerdown', fadeHint, { once: true });

  // 动画循环
  const t0 = performance.now();
  function animate() {
    requestAnimationFrame(animate);
    controls.update();

    // hover 节点微微脉冲放大
    const t = (performance.now() - t0) / 1000;
    clickable.forEach(m => {
      const target = (m === hovered) ? 1.35 : 1.0;
      m.scale.lerp(new THREE.Vector3(target, target, target), 0.15);
    });
    center.material.emissiveIntensity = 0.5 + Math.sin(t * 1.5) * 0.15;

    renderer.render(scene, camera);
  }
  animate();

  // 响应式
  function onResize() {
    width = stage.clientWidth;
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.setSize(width, height);
  }
  window.addEventListener('resize', onResize);
})();
</script>

<style>
.kg-wrap { margin: 0 0 28px; }
.kg-stage {
  position: relative;
  width: 100%;
  height: 480px;
  border-radius: 16px;
  overflow: hidden;
  background:
    radial-gradient(120% 120% at 50% 0%, #1b1d3a 0%, #0d0e22 55%, #07081a 100%);
  box-shadow: 0 12px 40px rgba(20, 22, 60, 0.35);
  cursor: grab;
}
.kg-stage:active { cursor: grabbing; }
.kg-stage canvas { display: block; }

.kg-hint {
  position: absolute;
  left: 50%;
  bottom: 16px;
  transform: translateX(-50%);
  padding: 6px 14px;
  font-size: 0.82rem;
  color: #c8cae8;
  background: rgba(255, 255, 255, 0.08);
  backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 999px;
  pointer-events: none;
  transition: opacity 0.6s ease;
  z-index: 2;
}
.kg-hint--hidden { opacity: 0; }

/* 降级版本：发光胶囊布局 */
.kg-stage.kg-degraded {
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
  padding: 32px 20px;
}
.kg-fallback { text-align: center; color: #eef; }
.kg-chips {
  margin-top: 18px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.kg-chip {
  --c: #667eea;
  display: inline-block;
  padding: 8px 16px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 0.9rem;
  color: #fff;
  text-decoration: none;
  background: color-mix(in srgb, var(--c) 28%, transparent);
  border: 1px solid var(--c);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.kg-chip:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--c) 50%, transparent);
  color: #fff;
  text-decoration: none;
}
.kg-chip--center {
  font-size: 1.15rem;
  padding: 12px 28px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  border: none;
}

@media (max-width: 768px) {
  .kg-stage { height: 380px; }
}
@media (max-width: 480px) {
  .kg-stage { height: 340px; }
}
</style>
