/* Aifit — TopHero (真のトップヒーロー)
コンセプト: 見出しをタイプライター式で打ち出し →
打ち終えたら ご依頼件数NO.1 バナーが灯りとともに登場。
variants: a=中央 / b=左右 / c=縦書き */
(function () {
const Icon = window.Icon;
const { useState, useEffect, useRef } = React;
/* 見出しのセグメント(pop=金色強調 / br=この後で改行) */
const HEAD = [
{ text: "葬儀費用", br: true },
{ text: "総額が分かりにくい", br: true },
{ text: "積立て式の互助会を解約し、", br: true },
{ text: "1回の入会金のみで家族全員を保証する", br: true },
{ text: "アイフィット会員", pop: true },
{ text: "を選んでくれた", br: true },
{ text: "ご家族の力。" },
];
const TOTAL = HEAD.reduce((n, s) => n + s.text.length, 0);
const SPEED = 55; // ms/字(少し早め)
const COPY = { cta: "NO.1の根拠を見る", href: "reason-no1.html" };
/* タイプライター: 表示文字数 count を受け、セグメント単位に色分けして描画 */
function TypedHead({ vertical, count, done }) {
let cursor = 0;
const lines = [[]];
HEAD.forEach((seg, si) => {
const shown = Math.max(0, Math.min(seg.text.length, count - cursor));
if (shown > 0) {
const piece = seg.text.slice(0, shown);
lines[lines.length - 1].push(
seg.pop ? {piece} : {piece}
);
}
cursor += seg.text.length;
if (seg.br && count > cursor - 1) lines.push([]);
});
return (
s.text).join("")}>
{lines.map((parts, i) => (
{parts}
{!done && i === lines.length - 1 && }
))}
);
}
/* 伏す灯りとともに登場する NO.1 バナー */
function Lamp({ show }) {
return (
);
}
function Cta({ show }) {
return (
{COPY.cta}
);
}
function TopHero({ variant }) {
const v = variant || "a";
const [count, setCount] = useState(0);
const [done, setDone] = useState(false);
const ref = useRef(null);
const videoRef = useRef(null);
// 背景動画(iPhone対策でBlob方式)
useEffect(() => {
const vid = videoRef.current; if (!vid) return;
const saveData = navigator.connection && (navigator.connection.saveData ||
/2g/.test(navigator.connection.effectiveType || ""));
if (saveData) return; // 省データ/低速回線はポスターのみ
// 本番(Apache)はHTTPレンジ対応。直接srcでストリーミング再生(最初の
// チャンクで即再生。iOSが自動再生できる方式。サイト共通=genres.jsxと統一)
vid.src = "video/tophero.mp4";
if (vid.load) vid.load();
vid.play().catch(() => {});
}, []);
useEffect(() => {
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) { setCount(TOTAL); setDone(true); return; }
let started = false, timer = null;
const start = () => {
if (started) return; started = true;
let i = 0;
timer = setInterval(() => {
i += 1; setCount(i);
if (i >= TOTAL) { clearInterval(timer); setTimeout(() => setDone(true), 280); }
}, SPEED);
};
// ヒーローが見えてからタイプ開始(初回ロードはほぼ即時)
const io = new IntersectionObserver((es) => {
if (es.some((e) => e.isIntersecting)) { start(); io.disconnect(); }
}, { threshold: 0.35 });
if (ref.current) io.observe(ref.current);
const fallback = setTimeout(start, 700);
return () => { clearInterval(timer); clearTimeout(fallback); io.disconnect(); };
}, []);
return (
{v === "a" && (
)}
{v === "b" && (
)}
{v === "c" && (
)}
創業の物語へ
);
}
Object.assign(window, { TopHero });
})();