/* 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 (
東広島ご依頼件数ランキング1位 10年連続獲得(一般社団法人日本儀礼文化調査協会調べ)
); } 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 }); })();