fix: restore looping heaven animation footers
This commit is contained in:
+112
-24
@@ -47,6 +47,16 @@
|
||||
"复", "颐", "屯", "益", "震", "噬嗑", "随", "无妄", "明夷", "贲", "既济", "家人", "丰", "革", "同人", "临",
|
||||
"损", "节", "中孚", "归妹", "睽", "兑", "履", "泰", "大畜", "需", "小畜", "大壮", "大有", "夬", "乾",
|
||||
];
|
||||
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));
|
||||
@@ -107,10 +117,11 @@
|
||||
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" ? 11000 : 10800, performance.now());
|
||||
this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now());
|
||||
} else {
|
||||
this.frameId = requestAnimationFrame((now) => this.frame(now));
|
||||
}
|
||||
@@ -137,6 +148,7 @@
|
||||
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;
|
||||
@@ -154,7 +166,7 @@
|
||||
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" ? 11000 : 10800, performance.now());
|
||||
this.draw(this.scene === "fortune" ? 10950 : 10600, performance.now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +181,11 @@
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const holdAt = this.scene === "fortune" ? 11000 : 10800;
|
||||
this.draw(Math.min(now - this.startedAt, holdAt), now);
|
||||
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));
|
||||
}
|
||||
@@ -295,6 +310,71 @@
|
||||
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();
|
||||
@@ -364,8 +444,8 @@
|
||||
if (time >= 4700 && time < 6800) {
|
||||
this.drawTrigramRing(cx, cy, trigramRadius, trigramWidth, trigramLine, this.stageAlpha(time, 4700, 6800), now, true, time);
|
||||
}
|
||||
if (time >= 6800) {
|
||||
const alpha = this.stageAlpha(time, 6800, 10800, 350, true);
|
||||
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);
|
||||
@@ -395,6 +475,17 @@
|
||||
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) {
|
||||
@@ -406,7 +497,18 @@
|
||||
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) this.drawAnnualQi(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) {
|
||||
@@ -512,7 +614,7 @@
|
||||
}
|
||||
|
||||
drawAnnualQi(time, now, cx, cy, scale) {
|
||||
const alpha = this.stageAlpha(time, 7900, 11000, 350, true);
|
||||
const alpha = this.stageAlpha(time, 7900, 11000, 350);
|
||||
const sixQi = this.data.sixQi || {};
|
||||
const pillar = this.data.yearPillar || "岁运";
|
||||
const movement = this.data.movement || "中运合参";
|
||||
@@ -554,25 +656,11 @@
|
||||
|
||||
drawCompletion(progress, now) {
|
||||
this.drawBackground(now);
|
||||
const { width, height } = this;
|
||||
const cx = width / 2;
|
||||
const cy = height * 0.44;
|
||||
const scale = Math.min(width, height);
|
||||
const eased = easeOut(progress);
|
||||
if (this.scene === "fortune") {
|
||||
const radius = scale * 0.30 * (1 - eased);
|
||||
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 - eased) * 0.8, 8);
|
||||
});
|
||||
this.drawFortune(11000 + progress * (FORTUNE_TOTAL - 11000), now);
|
||||
} else {
|
||||
const radius = scale * 0.365 * (1 - eased);
|
||||
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 - eased) * 0.7);
|
||||
}
|
||||
this.drawHexagram(10800 + progress * (HEX_TOTAL - 10800), now);
|
||||
}
|
||||
this.node(cx, cy, 3 + eased * 6, GOLD_BRIGHT, 0.3 + eased * 0.7, 12 + eased * 40);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -354,6 +354,7 @@ test("heaven reading loading uses the matching canvas scene and stops cleanly",
|
||||
await expect(canvas).toBeVisible();
|
||||
await expect(canvas).toHaveAttribute("data-scene", scene);
|
||||
await expect(canvas).toHaveAttribute("data-running", "true");
|
||||
await expect(canvas).toHaveAttribute("data-looping", "true");
|
||||
await page.waitForTimeout(180);
|
||||
const pixels = await canvas.evaluate((element) => {
|
||||
const context = element.getContext("2d");
|
||||
@@ -368,6 +369,9 @@ test("heaven reading loading uses the matching canvas scene and stops cleanly",
|
||||
expect(pixels.width).toBeGreaterThan(300);
|
||||
expect(pixels.height).toBeGreaterThan(300);
|
||||
expect(pixels.colors).toBeGreaterThan(3);
|
||||
await page.evaluate(() => { heavenReadingAnimation.startedAt = performance.now() - 13_100; });
|
||||
await expect(canvas).toHaveAttribute("data-cycle", "1");
|
||||
await expect(canvas).toHaveAttribute("data-running", "true");
|
||||
if (mode === "trend") {
|
||||
const completionMs = await page.evaluate(async () => {
|
||||
const started = performance.now();
|
||||
@@ -380,6 +384,7 @@ test("heaven reading loading uses the matching canvas scene and stops cleanly",
|
||||
}
|
||||
await page.locator("#closeHeavenReadingDialog").click();
|
||||
await expect(canvas).toHaveAttribute("data-running", "false");
|
||||
await expect(canvas).toHaveAttribute("data-looping", "false");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user