2026-09-29 16:03:23 +08:00
|
|
|
import { useLayoutEffect, useRef, useState } from "react";
|
2026-10-09 13:56:41 +08:00
|
|
|
import { FESTIVAL_GREETINGS, getNewYearDisplayYear, type FestivalId } from "@/lib/festival";
|
2026-09-29 16:03:23 +08:00
|
|
|
import "./festival.css";
|
2026-10-09 13:56:41 +08:00
|
|
|
import "./newyear.css";
|
2026-09-29 16:03:23 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* VU 区的静态插画横幅:两端角色保持比例,中央背景平铺,文字独立缩放。
|
|
|
|
|
* 按容器宽度切换横排/双行,保留顶部设备信息;不参与布局、不拦截点击。
|
|
|
|
|
*/
|
|
|
|
|
export default function FestivalGreeting({
|
|
|
|
|
festival,
|
|
|
|
|
}: {
|
|
|
|
|
festival: FestivalId;
|
|
|
|
|
}) {
|
|
|
|
|
const boxRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const textBoxRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const innerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [scale, setScale] = useState(1);
|
2026-10-09 13:56:41 +08:00
|
|
|
const [newYear] = useState(() => getNewYearDisplayYear());
|
|
|
|
|
const isNewYear = festival === "newyear";
|
2026-09-29 16:03:23 +08:00
|
|
|
const artworkRoot = `${import.meta.env.BASE_URL}images/festival/${festival}/`;
|
|
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
const box = boxRef.current;
|
|
|
|
|
const textBox = textBoxRef.current;
|
|
|
|
|
const inner = innerRef.current;
|
|
|
|
|
if (!box || !textBox || !inner) return;
|
|
|
|
|
const status = box.parentElement?.querySelector<HTMLElement>(
|
|
|
|
|
".festival-vu-status"
|
|
|
|
|
);
|
|
|
|
|
let disposed = false;
|
|
|
|
|
const fit = () => {
|
|
|
|
|
if (disposed) return;
|
|
|
|
|
// 先应用排版再测量;使用容器宽度,兼容横屏、分屏及 legacy。
|
|
|
|
|
box.dataset.layout = box.clientWidth >= 420 ? "wide" : "compact";
|
|
|
|
|
// 极窄屏的格式和音量分成两行,标题跟随实际状态栏高度下移。
|
|
|
|
|
textBox.style.top = `${Math.max(17, status?.offsetHeight ?? 0) + 2}px`;
|
|
|
|
|
const bw = textBox.clientWidth - 8;
|
|
|
|
|
const bh = textBox.clientHeight - 4;
|
|
|
|
|
const tw = inner.offsetWidth;
|
|
|
|
|
const th = inner.offsetHeight;
|
|
|
|
|
if (bw <= 0 || bh <= 0 || tw <= 0 || th <= 0) return;
|
|
|
|
|
setScale(Math.min(bw / tw, bh / th));
|
|
|
|
|
};
|
|
|
|
|
fit();
|
|
|
|
|
const ro =
|
|
|
|
|
typeof ResizeObserver === "undefined" ? null : new ResizeObserver(fit);
|
|
|
|
|
ro?.observe(box);
|
|
|
|
|
ro?.observe(inner);
|
|
|
|
|
if (status) ro?.observe(status);
|
|
|
|
|
window.addEventListener("resize", fit);
|
|
|
|
|
// 字体就绪后重新测量,避免初次回退字体导致文字越界。
|
|
|
|
|
void document.fonts?.ready.then(fit);
|
|
|
|
|
return () => {
|
|
|
|
|
disposed = true;
|
|
|
|
|
ro?.disconnect();
|
|
|
|
|
window.removeEventListener("resize", fit);
|
|
|
|
|
};
|
|
|
|
|
}, [festival]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={boxRef}
|
|
|
|
|
className={`festival-greeting festival-greeting--${festival}`}
|
|
|
|
|
role="img"
|
2026-10-09 13:56:41 +08:00
|
|
|
aria-label={isNewYear ? `${FESTIVAL_GREETINGS[festival]} ${newYear}` : FESTIVAL_GREETINGS[festival]}
|
2026-09-29 16:03:23 +08:00
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="festival-greeting-backdrop"
|
|
|
|
|
style={{ backgroundImage: `url("${artworkRoot}banner-center.jpg")` }}
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
|
|
|
|
{(["left", "right"] as const).map(side => (
|
|
|
|
|
<img
|
|
|
|
|
key={side}
|
|
|
|
|
src={`${artworkRoot}banner-${side}.jpg`}
|
|
|
|
|
className={`festival-greeting-art festival-greeting-art--${side}`}
|
|
|
|
|
alt=""
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
draggable={false}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
<div className="festival-greeting-shade" aria-hidden="true" />
|
2026-10-09 13:56:41 +08:00
|
|
|
{isNewYear && (
|
|
|
|
|
<img
|
|
|
|
|
src={`${artworkRoot}calendar.svg`}
|
|
|
|
|
className="newyear-greeting-calendar"
|
|
|
|
|
alt=""
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
draggable={false}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-09-29 16:03:23 +08:00
|
|
|
<div
|
|
|
|
|
ref={textBoxRef}
|
|
|
|
|
className="festival-greeting-title"
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
ref={innerRef}
|
|
|
|
|
className={`${festival}-greeting-text festival-greeting-inner`}
|
|
|
|
|
style={{ transform: `scale(${scale})` }}
|
|
|
|
|
>
|
2026-10-09 13:56:41 +08:00
|
|
|
{isNewYear ? (
|
|
|
|
|
<>
|
|
|
|
|
<div className="newyear-greeting-heading">
|
|
|
|
|
<div className="newyear-greeting-words">
|
|
|
|
|
<span>Happy</span>
|
|
|
|
|
<span>New Year</span>
|
|
|
|
|
</div>
|
|
|
|
|
<strong className="newyear-greeting-year">{newYear}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="newyear-greeting-tagline">
|
|
|
|
|
NEW YEAR · NEW CHAPTER · MORE POSSIBILITIES
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
) : FESTIVAL_GREETINGS[festival].split(" ").map(word => (
|
2026-09-29 16:03:23 +08:00
|
|
|
<span key={word} className="festival-greeting-line">
|
|
|
|
|
{word}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|