import React, { useState, useEffect, useRef } from 'react' import { LoadingScreen } from './LoadingScreen' import { HeartLogo } from './HeartLogo' import { TypewriterRotator } from './TypewriterRotator' const HERO_PHRASES = [ '低遅延ストリーミング · LOW-LATENCY OBSERVABILITY', 'リアルタイム監視 · REAL-TIME SURVEILLANCE', 'ミッションクリティカル · MISSION-CRITICAL INFRASTRUCTURE', 'エンドツーエンド可視化 · END-TO-END VISIBILITY', '安全な意思決定 · SECURE DECISIONS AT THE EDGE', ] interface LandingPageProps { onLogin: () => void } export function LandingPage({ onLogin }: LandingPageProps) { const [loading, setLoading] = useState(false) const [showLanding, setShowLanding] = useState(false) const [isStandby, setIsStandby] = useState(true) // false = LIVE, true = STDBY const [velocity, setVelocity] = useState(0) const [energy, setEnergy] = useState(0) const [ramped, setRamped] = useState(false) const [pendingAction, setPendingAction] = useState(null) const [activePanel, setActivePanel] = useState(null) // Fade-in landing page content after mount useEffect(() => { const timer = setTimeout(() => setShowLanding(true), 500) return () => clearTimeout(timer) }, []) // Ramp up stats, then keep them fluctuating near max useEffect(() => { const VELOCITY_MAX = 603 const ENERGY_MAX = 32 let rampInterval: number | undefined let fluctuateInterval: number | undefined // Ramp-up for ~2 seconds const rampDurationMs = 2000 const tickMs = 30 const vStep = VELOCITY_MAX / (rampDurationMs / tickMs) const eStep = ENERGY_MAX / (rampDurationMs / tickMs) rampInterval = window.setInterval(() => { setVelocity((v) => { const next = v + vStep return next >= VELOCITY_MAX ? VELOCITY_MAX : next }) setEnergy((e) => { const next = e + eStep return next >= ENERGY_MAX ? ENERGY_MAX : next }) }, tickMs) const stopRamp = window.setTimeout(() => { if (rampInterval) window.clearInterval(rampInterval) setVelocity(VELOCITY_MAX) setEnergy(ENERGY_MAX) setRamped(true) // Fluctuate near the top fluctuateInterval = window.setInterval(() => { setVelocity((v) => { const min = VELOCITY_MAX - 18 const max = VELOCITY_MAX const delta = (Math.random() - 0.5) * 6 // ±3 const next = Math.max(min, Math.min(max, v + delta)) return next }) setEnergy((e) => { const min = ENERGY_MAX - 2 const max = ENERGY_MAX const delta = (Math.random() - 0.5) * 0.25 // ±0.125 const next = Math.max(min, Math.min(max, e + delta)) return next }) }, 220) }, rampDurationMs + 60) return () => { if (rampInterval) window.clearInterval(rampInterval) if (fluctuateInterval) window.clearInterval(fluctuateInterval) } }, []) const handleLoadingComplete = () => { if (pendingAction === 'makimaRedirect') { window.location.assign('https://makima.jp') return } onLogin() } const handleLogin = () => { setPendingAction('makimaRedirect') setLoading(true) } const handleMission = () => { setActivePanel((mode) => (mode === 'mission' ? null : 'mission')) } const handleMakimaPanel = () => { setActivePanel((mode) => (mode === 'makima' ? null : 'makima')) } return (
{loading && } {!loading && (
Soryu { const img = e.currentTarget as HTMLImageElement img.onerror = null img.src = '/logo/crane-logo.png' }} />
System: setIsStandby(!isStandby)} title="Click to toggle between LIVE and STANDBY" > {isStandby ? 'STDBY' : 'LIVE'}
Version: v1.0.0
)}
{/* Background GIF fills the main body */} {/* Minimal overlay: masthead, issue badge, and CTA */}
{/* Bottom stats: Velocity + Energy only (hidden in mission mode) */} {!activePanel && (
Velocity {Math.round(velocity)} km/h
Energy {energy.toFixed(1)} MJ
)}
{/* MissionDrawer removed in favor of mission screen transformation */}
) }