(function exposeHeavenLoading(global) {
  "use strict";

  const PAPER = "#fdfcf8";
  const PAPER_CENTER = "#f1e8d9";
  const NODE_TEXT = "#fffaf0";
  const INK = "#68493d";
  const INK_BRIGHT = "#963f37";
  const GOLD = "#80533e";
  const GOLD_BRIGHT = "#b64e43";
  const CINNABAR = "#b94038";
  const DIM = "rgba(68,57,49,0.62)";
  const PARTICLE_COLORS = ["#a94b42", "#456b62", "#506b85"];
  const SERIF = '"Noto Serif SC","Songti SC","STSong","SimSun",serif';
  const ELEMENT_COLORS = {
    木: "#4f7a4a",
    火: "#b3483d",
    土: "#96702c",
    金: "#70685b",
    水: "#496d92",
  };
  const QI6 = [
    { name: "厥阴风木", element: "木" },
    { name: "少阴君火", element: "火" },
    { name: "少阳相火", element: "火" },
    { name: "太阴湿土", element: "土" },
    { name: "阳明燥金", element: "金" },
    { name: "太阳寒水", element: "水" },
  ];
  const STEP_RANGES = ["大寒 — 春分", "春分 — 小满", "小满 — 大暑", "大暑 — 秋分", "秋分 — 小雪", "小雪 — 大寒"];
  const TRIGRAMS = [
    { name: "乾", bits: [1, 1, 1], angle: -90 },
    { name: "兑", bits: [1, 1, 0], angle: -135 },
    { name: "离", bits: [1, 0, 1], angle: 180 },
    { name: "震", bits: [1, 0, 0], angle: 135 },
    { name: "巽", bits: [0, 1, 1], angle: -45 },
    { name: "坎", bits: [0, 1, 0], angle: 0 },
    { name: "艮", bits: [0, 0, 1], angle: 45 },
    { name: "坤", bits: [0, 0, 0], angle: 90 },
  ];
  const SIXIANG = [
    { name: "太阳", bits: [1, 1], dx: 0, dy: -1 },
    { name: "少阴", bits: [1, 0], dx: 1, dy: 0 },
    { name: "太阴", bits: [0, 0], dx: 0, dy: 1 },
    { name: "少阳", bits: [0, 1], dx: -1, dy: 0 },
  ];
  const HEXAGRAM_NAMES = [
    "坤", "剥", "比", "观", "豫", "晋", "萃", "否", "谦", "艮", "蹇", "渐", "小过", "旅", "咸", "遁",
    "师", "蒙", "坎", "涣", "解", "未济", "困", "讼", "升", "蛊", "井", "巽", "恒", "鼎", "大过", "姤",
    "复", "颐", "屯", "益", "震", "噬嗑", "随", "无妄", "明夷", "贲", "既济", "家人", "丰", "革", "同人", "临",
    "损", "节", "中孚", "归妹", "睽", "兑", "履", "泰", "大畜", "需", "小畜", "大壮", "大有", "夬", "乾",
  ];
  const HEX_TOTAL = 12500;
  const FORTUNE_TOTAL = 12800;
  const HEX_STAGES = [
    [0, 1800, "太 极", "无极而太极，动而生阳"],
    [1800, 3300, "两 仪", "一阴一阳之谓道"],
    [3300, 4700, "四 象", "阴阳消长，太少相生"],
    [4700, 6800, "八 卦", "天地定位，山泽通气"],
    [6800, 10800, "六 十 四 卦", "卦者挂也，悬物象以示人"],
    [10800, HEX_TOTAL, "归 一", "万物负阴而抱阳，冲气以为和"],
  ];
  const clamp01 = (value) => Math.max(0, Math.min(1, value));
  const smooth = (start, end, value) => {
    const progress = clamp01((value - start) / Math.max(1, end - start));
    return progress * progress * (3 - 2 * progress);
  };
  const easeOut = (value) => 1 - Math.pow(1 - clamp01(value), 3);
  const hexBits = (index) => Array.from({ length: 6 }, (_, bit) => (index >> (5 - bit)) & 1);
  const point = (cx, cy, radius, degrees) => {
    const radians = degrees * Math.PI / 180;
    return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
  };

  class HeavenLoadingCanvas {
    constructor(canvas) {
      this.canvas = canvas;
      this.context = canvas.getContext("2d");
      this.width = 0;
      this.height = 0;
      this.dpr = 1;
      this.scene = "hexagram";
      this.data = {};
      this.startedAt = 0;
      this.frameId = 0;
      this.running = false;
      this.completingAt = 0;
      this.completionResolve = null;
      this.completionTimer = 0;
      this.resizeObserver = new ResizeObserver(() => this.resize());
      this.reducedMotion = global.matchMedia("(prefers-reduced-motion: reduce)").matches;
      this.stars = this.createStars(this.reducedMotion ? 48 : 150);
    }

    createStars(count) {
      let seed = 24681357;
      const random = () => {
        seed = (seed * 1664525 + 1013904223) >>> 0;
        return seed / 4294967296;
      };
      return Array.from({ length: count }, () => ({
        x: random(),
        y: random(),
        radius: 0.3 + random() * 1.3,
        phase: random() * Math.PI * 2,
        speed: 0.00015 + random() * 0.0004,
        colorIndex: Math.floor(random() * PARTICLE_COLORS.length),
      }));
    }

    start(scene, data = {}) {
      const nextScene = scene === "fortune" ? "fortune" : "hexagram";
      if (this.running && this.scene === nextScene) {
        this.data = data;
        return;
      }
      this.stop();
      this.scene = nextScene;
      this.data = data;
      this.startedAt = performance.now();
      this.running = true;
      this.canvas.dataset.scene = this.scene;
      this.canvas.dataset.running = "true";
      this.canvas.dataset.looping = "true";
      this.resizeObserver.observe(this.canvas);
      this.resize();
      if (this.reducedMotion) {
        this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now());
      } else {
        this.frameId = requestAnimationFrame((now) => this.frame(now));
      }
    }

    complete() {
      if (!this.running || this.reducedMotion) {
        this.stop();
        return Promise.resolve();
      }
      if (this.completionResolve) return this.completionPromise;
      this.completingAt = performance.now();
      this.completionPromise = new Promise((resolve) => { this.completionResolve = resolve; });
      this.completionTimer = global.setTimeout(() => this.stop(), 2200);
      return this.completionPromise;
    }

    stop() {
      if (this.frameId) cancelAnimationFrame(this.frameId);
      this.frameId = 0;
      this.running = false;
      this.completingAt = 0;
      if (this.completionTimer) global.clearTimeout(this.completionTimer);
      this.completionTimer = 0;
      this.resizeObserver.disconnect();
      this.canvas.dataset.running = "false";
      this.canvas.dataset.looping = "false";
      if (this.completionResolve) this.completionResolve();
      this.completionResolve = null;
      this.completionPromise = null;
    }

    resize() {
      const rect = this.canvas.getBoundingClientRect();
      const width = Math.max(1, Math.round(rect.width));
      const height = Math.max(1, Math.round(rect.height));
      if (width === this.width && height === this.height) return;
      this.width = width;
      this.height = height;
      this.dpr = Math.min(global.devicePixelRatio || 1, 2);
      this.canvas.width = Math.round(width * this.dpr);
      this.canvas.height = Math.round(height * this.dpr);
      this.context.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
      if (this.running && this.reducedMotion) {
        this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now());
      }
    }

    frame(now) {
      if (!this.running) return;
      if (this.completingAt) {
        const duration = this.scene === "fortune" ? 1800 : 1700;
        const progress = clamp01((now - this.completingAt) / duration);
        this.drawCompletion(progress, now);
        if (progress >= 1) {
          this.stop();
          return;
        }
      } else {
        const total = this.scene === "fortune" ? FORTUNE_TOTAL : HEX_TOTAL;
        const elapsed = Math.max(0, now - this.startedAt);
        const timeline = elapsed % total;
        this.canvas.dataset.cycle = String(Math.floor(elapsed / total));
        this.draw(timeline, now);
      }
      this.frameId = requestAnimationFrame((time) => this.frame(time));
    }

    draw(time, now) {
      if (this.width <= 1 || this.height <= 1) return;
      this.drawBackground(now);
      if (this.scene === "fortune") this.drawFortune(time, now);
      else this.drawHexagram(time, now);
    }

    drawBackground(now) {
      const { context: ctx, width, height } = this;
      const cx = width / 2;
      const cy = height * 0.44;
      const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(width, height) * 0.75);
      gradient.addColorStop(0, PAPER_CENTER);
      gradient.addColorStop(0.52, "#faf7ef");
      gradient.addColorStop(1, PAPER);
      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, width, height);
      for (const star of this.stars) {
        const twinkle = 0.35 + 0.65 * (0.5 + 0.5 * Math.sin(star.phase + now * 0.0012));
        const alpha = twinkle * 0.5;
        ctx.globalAlpha = alpha;
        ctx.fillStyle = PARTICLE_COLORS[star.colorIndex];
        const y = ((star.y + now * star.speed) % 1) * height;
        ctx.fillRect(star.x * width, y, star.radius, star.radius);
      }
      ctx.globalAlpha = 1;
    }

    label(text, x, y, size, color = INK, alpha = 1, weight = "", maxWidth) {
      if (!text || alpha <= 0) return;
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.fillStyle = color;
      ctx.font = `${weight ? `${weight} ` : ""}${size}px ${SERIF}`;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      if (maxWidth) ctx.fillText(text, x, y, maxWidth);
      else ctx.fillText(text, x, y);
      ctx.restore();
    }

    node(x, y, radius, color, alpha = 1, glow = 0) {
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.fillStyle = color;
      ctx.shadowColor = color;
      ctx.shadowBlur = glow;
      ctx.beginPath();
      ctx.arc(x, y, radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.restore();
    }

    line(x1, y1, x2, y2, color, alpha = 1, width = 1) {
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.strokeStyle = color;
      ctx.lineWidth = width;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.lineTo(x2, y2);
      ctx.stroke();
      ctx.restore();
    }

    curvedArrow(x1, y1, x2, y2, mx, my, color, alpha) {
      if (alpha <= 0) return;
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.strokeStyle = color;
      ctx.lineWidth = 1.2;
      ctx.beginPath();
      ctx.moveTo(x1, y1);
      ctx.quadraticCurveTo(mx, my, x2, y2);
      ctx.stroke();
      const angle = Math.atan2(y2 - my, x2 - mx);
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.moveTo(x2, y2);
      ctx.lineTo(x2 - 7 * Math.cos(angle - 0.42), y2 - 7 * Math.sin(angle - 0.42));
      ctx.lineTo(x2 - 7 * Math.cos(angle + 0.42), y2 - 7 * Math.sin(angle + 0.42));
      ctx.closePath();
      ctx.fill();
      ctx.restore();
    }

    drawYao(cx, cy, width, lineWidth, yang, alpha, glow = 0) {
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.fillStyle = INK;
      ctx.shadowColor = GOLD;
      ctx.shadowBlur = glow;
      if (yang) {
        ctx.fillRect(cx - width / 2, cy - lineWidth / 2, width, lineWidth);
      } else {
        const gap = width * 0.18;
        ctx.fillRect(cx - width / 2, cy - lineWidth / 2, (width - gap) / 2, lineWidth);
        ctx.fillRect(cx + gap / 2, cy - lineWidth / 2, (width - gap) / 2, lineWidth);
      }
      ctx.restore();
    }

    drawGua(cx, cy, width, lineWidth, bits, alpha, glow = 0) {
      const gap = lineWidth * 1.7;
      const top = cy - (bits.length - 1) * gap / 2;
      bits.forEach((bit, index) => {
        this.drawYao(cx, top + (bits.length - 1 - index) * gap, width, lineWidth, bit === 1, alpha, glow);
      });
    }

    stageAlpha(time, start, end, fade = 300, hold = false) {
      const enter = smooth(start, start + fade, time);
      return hold ? enter : enter * (1 - smooth(end - fade, end, time));
    }

    fortuneStages() {
      const sixQi = this.data.sixQi || {};
      const pillar = this.data.yearPillar || "岁运";
      const movement = this.data.movement || "中运合参";
      const sitian = sixQi.sitian || "司天气候";
      return [
        [0, 2100, "五 运", "木火土金水，五运相袭，周而复始"],
        [2100, 3900, "十 干 化 运", "甲己土 · 乙庚金 · 丙辛水 · 丁壬木 · 戊癸火"],
        [3900, 5800, "十 二 支 化 气", "子午少阴 · 丑未太阴 · 寅申少阳 · 卯酉阳明 · 辰戌太阳 · 巳亥厥阴"],
        [5800, 7900, "六 气 环 布", "风寒暑湿燥火，分主六步，以应岁时"],
        [7900, 11000, "岁 运 合 参", `${pillar}年 · 中运${movement} · ${sitian}司天`],
        [11000, FORTUNE_TOTAL, "归 一", "谨守病机，无失气宜"],
      ];
    }

    drawFooter(time, now, total, stages, scene) {
      const { context: ctx, width, height } = this;
      const stage = [...stages].reverse().find((item) => time >= item[0]) || stages[0];
      const labelAlpha = smooth(stage[0], stage[0] + 300, time)
        * (1 - smooth(stage[1] - 250, stage[1], time));
      this.label(stage[2], width / 2, height - 108, 19, GOLD, 0.55 + 0.45 * labelAlpha, "600");
      this.label(stage[3], width / 2, height - 84, 12.5, DIM, (0.4 + 0.4 * labelAlpha) * (scene === "fortune" ? 0.85 : 0.8), "", width - 32);

      const baseSlotWidth = 34;
      const baseSlotHeight = 5;
      const baseSlotGap = 12;
      const baseTotalWidth = baseSlotWidth * 6 + baseSlotGap * 5;
      const fit = Math.min(1, (width - 28) / baseTotalWidth);
      const slotWidth = baseSlotWidth * fit;
      const slotHeight = baseSlotHeight * fit;
      const slotGap = baseSlotGap * fit;
      const totalWidth = slotWidth * 6 + slotGap * 5;
      const filled = Math.min(6, Math.floor(time / (total / 6)));
      for (let index = 0; index < 6; index += 1) {
        const x = width / 2 - totalWidth / 2 + index * (slotWidth + slotGap);
        const y = height - 56;
        const color = scene === "fortune" ? ELEMENT_COLORS[QI6[index].element] : GOLD;
        ctx.save();
        ctx.globalAlpha = 0.16;
        ctx.strokeStyle = GOLD;
        ctx.lineWidth = 1;
        ctx.strokeRect(x, y, slotWidth, slotHeight);
        ctx.restore();
        if (index < filled) {
          ctx.save();
          ctx.globalAlpha = 0.9;
          ctx.fillStyle = color;
          ctx.shadowColor = color;
          ctx.shadowBlur = 8;
          ctx.fillRect(x, y, slotWidth, slotHeight);
          ctx.restore();
        } else if (index === filled) {
          ctx.save();
          ctx.globalAlpha = 0.35 + 0.3 * Math.sin(now / 200);
          ctx.fillStyle = color;
          const progress = (time % (total / 6)) / (total / 6);
          ctx.fillRect(x, y, slotWidth * progress, slotHeight);
          ctx.restore();
        }
      }
      const dots = ".".repeat(1 + Math.floor(now / 450) % 3);
      const loadingText = scene === "fortune" ? "推 演 运 气 · 加 载 中" : "推 演 天 机 · 加 载 中";
      this.label(`${loadingText}${dots}`, width / 2, height - 32, 13, GOLD, 0.75);
    }

    drawTrigramRing(cx, cy, radius, width, lineWidth, alpha, now, entering, time) {
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha * 0.13;
      ctx.strokeStyle = GOLD;
      ctx.beginPath();
      ctx.arc(cx, cy, radius, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();
      const breath = 1 + 0.006 * Math.sin(now / 620);
      TRIGRAMS.forEach((trigram, index) => {
        const progress = entering ? easeOut((time - 4700 - index * 130) / 700) : 1;
        if (progress <= 0) return;
        const [x, y] = point(cx, cy, radius * breath * progress, trigram.angle);
        this.drawGua(x, y, width, lineWidth, trigram.bits, alpha * progress, alpha * progress * 8);
        const nameAlpha = entering ? alpha * clamp01((time - 4700 - index * 130 - 480) / 500) : alpha;
        this.label(trigram.name, x, y + lineWidth * 5.2, 13, GOLD, nameAlpha * (0.55 + 0.2 * Math.sin(now / 700 + index)));
      });
    }

    drawHexagram(time, now) {
      const { width, height } = this;
      const cx = width / 2;
      const cy = height * 0.44;
      const scale = Math.min(width, height);
      if (time < 1800) {
        const alpha = this.stageAlpha(time, 0, 1800);
        this.node(cx, cy, 5.5 * (1 + 0.12 * Math.sin(now / 260)), GOLD_BRIGHT, alpha, 34);
        for (let ring = 0; ring < 3; ring += 1) {
          const progress = ((now / 1500) + ring / 3) % 1;
          const ctx = this.context;
          ctx.save();
          ctx.globalAlpha = (1 - progress) * 0.22 * alpha;
          ctx.strokeStyle = GOLD;
          ctx.beginPath();
          ctx.arc(cx, cy, 8 + progress * scale * 0.13, 0, Math.PI * 2);
          ctx.stroke();
          ctx.restore();
        }
      }
      if (time >= 1800 && time < 3300) {
        const alpha = this.stageAlpha(time, 1800, 3300);
        const progress = easeOut((time - 1850) / 850);
        const yaoWidth = scale * 0.19 * progress;
        const yaoLine = Math.max(scale * 0.013, 5);
        this.drawYao(cx, cy - yaoLine * 2.6, yaoWidth, yaoLine, true, alpha, 14);
        this.drawYao(cx, cy + yaoLine * 2.6, yaoWidth, yaoLine, false, alpha, 14);
        this.node(cx, cy, 4, GOLD_BRIGHT, alpha * (1 - progress) * 0.9);
      }
      if (time >= 3300 && time < 4700) {
        const alpha = this.stageAlpha(time, 3300, 4700);
        const distance = scale * 0.085;
        const yaoWidth = Math.max(scale * 0.055, 28);
        const yaoLine = Math.max(scale * 0.009, 3.5);
        SIXIANG.forEach((symbol, index) => {
          const progress = easeOut((time - 3330 - index * 160) / 520);
          if (progress <= 0) return;
          const x = cx + symbol.dx * distance;
          const y = cy + symbol.dy * distance;
          this.drawGua(x, y, yaoWidth * progress, yaoLine, symbol.bits, alpha * progress, 10);
          this.label(symbol.name, x, y + yaoLine * 5.4, 12, GOLD, alpha * progress * 0.55);
        });
      }
      const trigramRadius = scale * 0.215;
      const trigramWidth = Math.max(scale * 0.052, 26);
      const trigramLine = Math.max(scale * 0.0075, 3);
      if (time >= 4700 && time < 6800) {
        this.drawTrigramRing(cx, cy, trigramRadius, trigramWidth, trigramLine, this.stageAlpha(time, 4700, 6800), now, true, time);
      }
      if (time >= 6800 && time < 10800) {
        const alpha = this.stageAlpha(time, 6800, 10800, 350);
        this.drawTrigramRing(cx, cy, trigramRadius, trigramWidth * 0.85, trigramLine * 0.85, alpha * 0.42, now, false, time);
        const ringRadius = scale * 0.365;
        const hexWidth = Math.max(scale * 0.026, 13);
        const hexLine = Math.max(scale * 0.0042, 1.6);
        const count = Math.floor(clamp01((time - 7000) / 3600) * 64);
        for (let index = 0; index < 64; index += 1) {
          const [x, y] = point(cx, cy, ringRadius, -90 + index * 360 / 64);
          this.node(x, y, 1.4, GOLD, alpha * 0.14);
          if (index < count) {
            const freshness = Math.max(0, 1 - (count - 1 - index) / 5);
            if (freshness > 0) {
              const ctx = this.context;
              const gradient = ctx.createLinearGradient(cx, cy, x, y);
              gradient.addColorStop(0, "rgba(128,83,62,0)");
              gradient.addColorStop(1, GOLD);
              this.line(cx, cy, x, y, gradient, alpha * freshness * 0.35);
            }
            this.drawGua(x, y, hexWidth, hexLine, hexBits(index), alpha * (0.55 + 0.45 * freshness), freshness * 9);
          }
        }
        if (count > 0) {
          const current = count - 1;
          const popTime = clamp01((time - (7000 + current * 3600 / 64)) / 130);
          const pop = 1 + 0.22 * (1 - popTime);
          this.drawGua(cx, cy - scale * 0.028, scale * 0.085 * pop, Math.max(scale * 0.011, 4.5), hexBits(current), alpha, 16);
          this.label(HEXAGRAM_NAMES[current], cx, cy + scale * 0.062, Math.max(20, scale * 0.042), GOLD_BRIGHT, alpha, "600");
          this.label(`第 ${current + 1} 卦`, cx, cy + scale * 0.105, 13, GOLD, alpha * 0.55);
        }
      }
      if (time >= 10800) {
        const alpha = this.stageAlpha(time, 10800, HEX_TOTAL, 420);
        const progress = easeOut((time - 10850) / 1150);
        const radius = scale * 0.365 * (1 - progress);
        for (let index = 0; index < 64 && radius >= 8; index += 1) {
          const [x, y] = point(cx, cy, radius, -90 + index * 360 / 64);
          this.drawGua(x, y, Math.max(scale * 0.026, 13), Math.max(scale * 0.0042, 1.6), hexBits(index), (1 - progress) * 0.7 * alpha);
        }
        this.node(cx, cy, 3 + progress * 6, GOLD_BRIGHT, alpha * (0.3 + 0.7 * progress), 12 + progress * 40);
      }
      this.drawFooter(time, now, HEX_TOTAL, HEX_STAGES, "hexagram");
    }

    drawFortune(time, now) {
      const { width, height } = this;
      const cx = width / 2;
      const cy = height * 0.44;
      const scale = Math.min(width, height);
      if (time < 2100) this.drawFiveMovements(time, now, cx, cy, scale);
      if (time >= 2100 && time < 3900) this.drawStems(time, cx, cy, scale);
      if (time >= 3900 && time < 5800) this.drawBranches(time, cx, cy, scale);
      if (time >= 5800 && time < 7900) this.drawSixQi(time, now, cx, cy, scale);
      if (time >= 7900 && time < 11000) this.drawAnnualQi(time, now, cx, cy, scale);
      if (time >= 11000) {
        const alpha = this.stageAlpha(time, 11000, FORTUNE_TOTAL, 420);
        const progress = easeOut((time - 11050) / 1200);
        const radius = scale * 0.30 * (1 - progress);
        QI6.forEach((qi, index) => {
          const [x, y] = point(cx, cy, radius, -90 + index * 60);
          if (radius > 8) this.node(x, y, Math.max(scale * 0.011, 6), ELEMENT_COLORS[qi.element], (1 - progress) * 0.8 * alpha, 8);
        });
        this.node(cx, cy, 3 + progress * 6, GOLD_BRIGHT, alpha * (0.3 + 0.7 * progress), 12 + progress * 40);
      }
      this.drawFooter(time, now, FORTUNE_TOTAL, this.fortuneStages(), "fortune");
    }

    drawFiveMovements(time, now, cx, cy, scale) {
      const alpha = this.stageAlpha(time, 0, 2100);
      const radius = scale * 0.17;
      const nodeRadius = Math.max(scale * 0.018, 9);
      const elements = [
        ["木", 180], ["火", -90], ["金", 0], ["水", 90], ["土", null],
      ];
      const positions = {};
      this.node(cx, cy, 5 + 1.5 * Math.sin(now / 260), GOLD_BRIGHT, alpha * (1 - easeOut((time - 200) / 800)), 30);
      elements.forEach(([element, degrees], index) => {
        const progress = easeOut((time - 500 - index * 170) / 500);
        if (progress <= 0) return;
        const x = degrees === null ? cx : cx + Math.cos(degrees * Math.PI / 180) * radius * progress;
        const y = degrees === null ? cy : cy + Math.sin(degrees * Math.PI / 180) * radius * progress;
        positions[element] = [x, y];
        this.node(x, y, nodeRadius * progress, ELEMENT_COLORS[element], alpha * progress, 16);
        this.label(element, x, y + 0.5, Math.round(nodeRadius * 1.15), NODE_TEXT, alpha * progress, "600");
        const direction = element === "土" ? "中央土" : { 木: "东方木", 火: "南方火", 金: "西方金", 水: "北方水" }[element];
        this.label(direction, x, y + nodeRadius + 14, 12, ELEMENT_COLORS[element], alpha * progress * 0.75);
      });
      const order = ["木", "火", "土", "金", "水"];
      order.forEach((element, index) => {
        const from = positions[element];
        const to = positions[order[(index + 1) % order.length]];
        if (!from || !to) return;
        const progress = smooth(1450 + index * 130, 1700 + index * 130, time);
        const mx = (from[0] + to[0]) / 2 + (cx - (from[0] + to[0]) / 2) * 0.25;
        const my = (from[1] + to[1]) / 2 + (cy - (from[1] + to[1]) / 2) * 0.25;
        this.curvedArrow(from[0], from[1], to[0], to[1], mx, my, GOLD, alpha * progress * 0.4);
      });
    }

    drawStems(time, cx, cy, scale) {
      const alpha = this.stageAlpha(time, 2100, 3900);
      const stems = "甲乙丙丁戊己庚辛壬癸";
      const movements = ["土", "金", "水", "木", "火"];
      const radius = scale * 0.30;
      for (let index = 0; index < 10; index += 1) {
        const progress = smooth(2150 + index * 90, 2450 + index * 90, time);
        if (progress <= 0) continue;
        const [x, y] = point(cx, cy, radius, -90 + index * 36);
        const element = movements[index % 5];
        this.node(x, y, 3, ELEMENT_COLORS[element], alpha * progress, 8);
        this.label(stems[index], x, y - 14, 15, ELEMENT_COLORS[element], alpha * progress, "600");
      }
      for (let index = 0; index < 5; index += 1) {
        const progress = smooth(3150 + index * 110, 3450 + index * 110, time);
        const angle = -90 + index * 36;
        const [x1, y1] = point(cx, cy, radius, angle);
        const [x2, y2] = point(cx, cy, radius, -90 + (index + 5) * 36);
        this.line(x1, y1, x2, y2, ELEMENT_COLORS[movements[index]], alpha * progress * 0.45);
        const [labelX, labelY] = point(cx, cy, scale * 0.055, angle + 90);
        this.label(movements[index], labelX, labelY, 16, ELEMENT_COLORS[movements[index]], alpha * progress, "600");
      }
    }

    drawBranches(time, cx, cy, scale) {
      const alpha = this.stageAlpha(time, 3900, 5800);
      const branches = "子丑寅卯辰巳午未申酉戌亥";
      const qiNames = ["少阴君火", "太阴湿土", "少阳相火", "阳明燥金", "太阳寒水", "厥阴风木"];
      const radius = scale * 0.31;
      const branchAngle = (index) => -90 + ((index - 6 + 12) % 12) * 30;
      for (let index = 0; index < 12; index += 1) {
        const progress = smooth(3950 + index * 70, 4220 + index * 70, time);
        const [x, y] = point(cx, cy, radius, branchAngle(index));
        this.node(x, y, 2.5, GOLD, alpha * progress, 6);
        this.label(branches[index], x, y - 13, 14, GOLD, alpha * progress * 0.9);
      }
      qiNames.forEach((name, index) => {
        const progress = smooth(4900 + index * 130, 5200 + index * 130, time);
        const [x1, y1] = point(cx, cy, radius, branchAngle(index));
        const [x2, y2] = point(cx, cy, radius, branchAngle(index + 6));
        const element = QI6.find((item) => item.name === name)?.element || "土";
        this.line(x1, y1, x2, y2, ELEMENT_COLORS[element], alpha * progress * 0.4);
        const [labelX, labelY] = point(cx, cy, radius + scale * 0.055, branchAngle(index));
        this.label(name, labelX, labelY, 12, ELEMENT_COLORS[element], alpha * progress, "600");
      });
    }

    drawSixQi(time, now, cx, cy, scale) {
      const alpha = this.stageAlpha(time, 5800, 7900);
      const radius = scale * 0.27;
      const drift = now * 0.004;
      const ctx = this.context;
      ctx.save();
      ctx.globalAlpha = alpha * 0.13;
      ctx.strokeStyle = GOLD;
      ctx.beginPath();
      ctx.arc(cx, cy, radius, 0, Math.PI * 2);
      ctx.stroke();
      ctx.restore();
      QI6.forEach((qi, index) => {
        const progress = easeOut((time - 5850 - index * 180) / 550);
        const [x, y] = point(cx, cy, radius * progress, -90 + index * 60 + drift);
        const nodeRadius = Math.max(scale * 0.015, 8) * progress;
        this.node(x, y, nodeRadius, ELEMENT_COLORS[qi.element], alpha * progress, 14);
        this.label(qi.name, x, y - nodeRadius - 12, 13, ELEMENT_COLORS[qi.element], alpha * progress, "600");
        this.label(["初之气", "二之气", "三之气", "四之气", "五之气", "终之气"][index], x, y + nodeRadius + 12, 10.5, DIM, alpha * progress * 0.9);
      });
      this.node(cx, cy, 4 + Math.sin(now / 300), GOLD_BRIGHT, alpha * 0.9, 24);
    }

    drawAnnualQi(time, now, cx, cy, scale) {
      const alpha = this.stageAlpha(time, 7900, 11000, 350);
      const sixQi = this.data.sixQi || {};
      const pillar = this.data.yearPillar || "岁运";
      const movement = this.data.movement || "中运合参";
      const sitian = sixQi.sitian || "司天气候";
      const zaiquan = sixQi.zaiquan || "在泉气化";
      const currentStep = Math.max(1, Math.min(6, Number(sixQi.step) || 1));
      const qiElement = (name) => QI6.find((item) => item.name === name)?.element || "土";
      const movementElement = ["木", "火", "土", "金", "水"].find((element) => movement.includes(element)) || "土";
      this.label("司 天", cx, cy - scale * 0.212, 11, DIM, alpha * smooth(7950, 8450, time));
      this.label(sitian, cx, cy - scale * 0.178, 17, ELEMENT_COLORS[qiElement(sitian)], alpha * smooth(7950, 8450, time), "600");
      this.label(zaiquan, cx, cy + scale * 0.178, 17, ELEMENT_COLORS[qiElement(zaiquan)], alpha * smooth(8200, 8700, time), "600");
      this.label("在 泉", cx, cy + scale * 0.212, 11, DIM, alpha * smooth(8200, 8700, time));
      this.label(pillar, cx, cy - scale * 0.012, Math.max(22, scale * 0.052), GOLD_BRIGHT, alpha * smooth(8500, 9100, time), "600");
      this.label(`${pillar}年 · 中运${movement}`, cx, cy + scale * 0.052, 14, ELEMENT_COLORS[movementElement], alpha * smooth(8500, 9100, time), "600", scale * 0.62);
      const radius = scale * 0.30;
      QI6.forEach((qi, index) => {
        const progress = smooth(9200 + index * 260, 9480 + index * 260, time);
        const [x, y] = point(cx, cy, radius, -90 + index * 60);
        const current = index + 1 === currentStep;
        const pulse = current ? 0.5 + 0.5 * Math.sin(now / 230) : 0;
        this.node(x, y, Math.max(scale * 0.011, 6) + (current ? 2.5 : 0), ELEMENT_COLORS[qi.element], alpha * progress, 12 + pulse * 14);
        if (current) {
          const ctx = this.context;
          ctx.save();
          ctx.globalAlpha = alpha * (0.35 + pulse * 0.35);
          ctx.strokeStyle = CINNABAR;
          ctx.lineWidth = 1.2;
          ctx.beginPath();
          ctx.arc(x, y, Math.max(scale * 0.02, 11) + pulse * 3, 0, Math.PI * 2);
          ctx.stroke();
          ctx.restore();
          this.label("当今", x, y - Math.max(scale * 0.038, 21), 10.5, CINNABAR, alpha * progress, "600");
        }
        const stepName = `${index + 1 === 6 ? "终" : ["初", "二", "三", "四", "五"][index]}之气`;
        this.label(`${stepName} · ${qi.name}`, x, y + Math.max(scale * 0.03, 17), 11.5, current ? GOLD_BRIGHT : ELEMENT_COLORS[qi.element], alpha * progress * (current ? 1 : 0.85), current ? "600" : "");
        if (current) this.label(STEP_RANGES[index], x, y + Math.max(scale * 0.052, 33), 10, DIM, alpha * progress);
      });
    }

    drawCompletion(progress, now) {
      this.drawBackground(now);
      if (this.scene === "fortune") {
        this.drawFortune(11000 + progress * (FORTUNE_TOTAL - 11000), now);
      } else {
        this.drawHexagram(10800 + progress * (HEX_TOTAL - 10800), now);
      }
    }
  }

  global.HeavenLoadingCanvas = HeavenLoadingCanvas;
})(window);
