diff options
| -rw-r--r-- | Cargo.toml | 16 | ||||
| -rw-r--r-- | frontend/index.html | 3 | ||||
| -rw-r--r-- | frontend/src/components/LandingPage.tsx | 237 | ||||
| -rw-r--r-- | frontend/src/styles/pc98.css | 20 | ||||
| -rw-r--r-- | src/main.rs | 172 |
5 files changed, 126 insertions, 322 deletions
diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index fdbfbd1..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "soryu-backend" -version = "0.1.0" -edition = "2021" - -[dependencies] -axum = { version = "0.7", features = ["macros", "json"] } -tokio = { version = "1.37", features = ["rt-multi-thread", "macros"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tower-http = { version = "0.5", features = ["cors", "trace"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -tokio-stream = "0.1" -futures = "0.3" -chrono = { version = "0.4", default-features = false, features = ["clock"] } diff --git a/frontend/index.html b/frontend/index.html index 4bb8d23..095310b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,8 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> - <title>PC-98 VN Skeleton</title> + <title>soryu.co</title> + <link rel="icon" type="image/png" href="/logo/crane-logo-transparent.png" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=DotGothic16&display=swap" rel="stylesheet"> </head> diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx index f5dc55c..0489794 100644 --- a/frontend/src/components/LandingPage.tsx +++ b/frontend/src/components/LandingPage.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react' import { LoadingScreen } from './LoadingScreen' import { HeartLogo } from './HeartLogo' -// Using direct PNG logo on landing header interface LandingPageProps { onLogin: () => void @@ -11,80 +10,95 @@ 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 [asciiArt, setAsciiArt] = useState('') - const [isGlitching, setIsGlitching] = useState(false) - // Note: Removed VN preview and ASCII features to focus on an Art Deco/Nouveau landing. + const [velocity, setVelocity] = useState(0) + const [energy, setEnergy] = useState(0) + const [ramped, setRamped] = useState(false) + const [pendingAction, setPendingAction] = useState<null | 'makima' | 'mission'>(null) - // Auto-fade in landing page content after component mounts + // Fade-in landing page content after mount useEffect(() => { - const timer = setTimeout(() => { - setShowLanding(true) - }, 500) // Delay before fading in content - + const timer = setTimeout(() => setShowLanding(true), 500) return () => clearTimeout(timer) }, []) - // Removed VN dialogue preview / typing effect. - - // Load ASCII art for overlay in hero (right/bottom) - useEffect(() => { - fetch('/ascii/ascii1.txt') - .then((res) => res.text()) - .then((txt) => setAsciiArt(txt.replace(/\n+$/, ''))) - .catch(() => setAsciiArt('')) - }, []) - - // Occasionally trigger a brief glitch on the word "futurist" + // Ramp up stats, then keep them fluctuating near max useEffect(() => { - let armTimer: number | undefined - let activeTimer: number | undefined - let cancelled = false - - const arm = () => { - // Random delay between glitches: 0.8s–2s (slightly punchier) - const delay = 800 + Math.random() * 1200 - armTimer = window.setTimeout(() => { - setIsGlitching(true) - // Glitch duration: 150ms–350ms (snappier) - const dur = 150 + Math.random() * 200 - activeTimer = window.setTimeout(() => { - setIsGlitching(false) - if (!cancelled) arm() - }, dur) - }, delay) - } - - arm() + 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 () => { - cancelled = true - if (armTimer) clearTimeout(armTimer) - if (activeTimer) clearTimeout(activeTimer) + if (rampInterval) window.clearInterval(rampInterval) + if (fluctuateInterval) window.clearInterval(fluctuateInterval) } }, []) - - // Handle loading screen completion const handleLoadingComplete = () => { - // Call the original onLogin immediately to transition to VN page - // Don't reset loading states as we're leaving the landing page + if (pendingAction === 'makima') { + window.location.assign('https://makima.jp') + return + } onLogin() } - // Handle login button click - const handleLogin = () => { + const handleMakima = () => { + setPendingAction('makima') setLoading(true) } - // Removed ASCII art effects and rendering. + const handleMission = () => { + // Placeholder action for now + setPendingAction('mission') + } return ( <div> - {loading && ( - <LoadingScreen onComplete={handleLoadingComplete} /> - )} - - {/* Floating Header Bar - Hidden during loading */} + {loading && <LoadingScreen onComplete={handleLoadingComplete} />} + {!loading && ( <div className={`floating-header ${showLanding ? 'fade-in' : 'hidden'}`}> <div className="header-content"> @@ -94,7 +108,11 @@ export function LandingPage({ onLogin }: LandingPageProps) { alt="Soryu" height={40} className="brand-mark" - onError={(e) => { const img = (e.currentTarget as HTMLImageElement); img.onerror = null; img.src = '/logo/crane-logo.png'; }} + onError={(e) => { + const img = e.currentTarget as HTMLImageElement + img.onerror = null + img.src = '/logo/crane-logo.png' + }} /> </div> <div className="header-center"> @@ -121,20 +139,17 @@ export function LandingPage({ onLogin }: LandingPageProps) { </div> </div> )} - + <div className={`modern-landing-page manga-style ${showLanding && !loading ? 'fade-in' : 'hidden'}`}> - {/* Retro-futuristic page background */} - <div className="rf-page-bg" aria-hidden="true"> - <div className="rf-page-speedlines layer-a"></div> - <div className="rf-page-speedlines layer-b"></div> + {/* Background GIF fills the main body */} + <div className="background-only" aria-hidden="true"> + <img src="/background-animation.gif" alt="" className="background-gif" /> </div> - {/* Taisho Magazine Cover Backdrop */} - <div className="taisho-cover"> - <div className="cover-backdrop" aria-hidden="true"></div> - {/* Cover Content Grid */} + {/* Minimal overlay: masthead, issue badge, and CTA */} + <div className="taisho-cover"> <div className="cover-content"> - {/* Vertical Masthead (magazine-style) */} + {/* Masthead + Issue badge (kept) */} <div className="masthead"> <div className="masthead-vertical"> <span className="jp">そりゅう</span> @@ -143,73 +158,39 @@ export function LandingPage({ onLogin }: LandingPageProps) { <div className="issue-badge">かはいい Vol.01</div> </div> - {/* Central Hero - full-frame retro-futuristic */} - <div className="hero"> - <div className="hero-frame taisho-modern-frame"> - <div className="hero-inner hero-fill"> - {/* Retro-futuristic racing hero content */} - <div className="rf-hero" aria-hidden="true"> - <div className="rf-speedlines layer-a"></div> - <div className="rf-speedlines layer-b"></div> - <div className="rf-hero-content"> - <div className="rf-badge">Engineering • Systems • para bellum</div> - <h2 className="rf-headline"> - Mission driven{' '} - <span - className={`glitch-word ${isGlitching ? 'is-glitching' : ''}`} - data-text="futurist" - > - futurist - </span>{' '} - technology - </h2> - <p className="rf-tagline">Avant-garde. Aesthetic Engineering. A Race of Steel</p> - <div className="rf-stats"> - <div className="rf-stat"><span className="label">Velocity</span><span className="value">0–603 km/h</span></div> - <div className="rf-stat"><span className="label">Energy</span><span className="value">32 MJ</span></div> - <div className="rf-stat"><span className="label">Flow</span><span className="value">MAX</span></div> - </div> - </div> - <div className="rf-accent-diagonal"></div> - {/* ASCII overlay (transparent, gradient text) */} - {asciiArt && ( - <div className="rf-ascii-overlay" aria-hidden="true"> - {asciiArt.split('\n').map((line, i) => ( - <div key={i} className="ascii-line"> - {Array.from(line).map((ch, j) => ( - <span - key={`${i}-${j}`} - className="ascii-char" - style={{ - '--delay': `${(i * 0.08 + j * 0.01)}s`, - } as React.CSSProperties} - > - {ch} - </span> - ))} - </div> - ))} - </div> - )} - </div> - </div> + {/* Empty hero area to preserve grid; background sits behind */} + <div className="hero" /> + + {/* CTA row spanning full width: left Mission/Contact, right Login */} + <div className="cta-area"> + <div className="cta-left"> + <button className="taisho-cta" onClick={handleMission}> + <span className="cta-text">Mission</span> + </button> + <button className="taisho-cta" onClick={() => {/* placeholder contact */}}> + <span className="cta-text">Contact</span> + </button> + </div> + <div className="cta-right"> + <button className="taisho-cta" onClick={handleMakima}> + <span className="cta-icon">▶</span> + <span className="cta-text">Login</span> + </button> </div> </div> + </div> + </div> - {/* CTA */} - <div className="cta-area"> - <button className="taisho-cta" onClick={handleLogin}> - <span className="cta-icon">▶</span> - <span className="cta-text">Enter</span> - </button> + {/* Bottom stats: Velocity + Energy only */} + <div className="bottom-stats"> + <div className="rf-stats"> + <div className="rf-stat"> + <span className="label">Velocity</span> + <span className="value">{Math.round(velocity)} km/h</span> </div> - {/* Visitor Counter */} - <div className="visit-counter" aria-label="visitor counter"> - <img - src="https://count.getloli.com/get/@soryu-landing?theme=booru-jaypee&darkmode=0" - alt="visit counter" - referrerPolicy="no-referrer-when-downgrade" - /> + <div className="rf-stat"> + <span className="label">Energy</span> + <span className="value">{energy.toFixed(1)} MJ</span> </div> </div> </div> diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css index 63e1996..39b8392 100644 --- a/frontend/src/styles/pc98.css +++ b/frontend/src/styles/pc98.css @@ -641,6 +641,14 @@ button:focus-visible { pointer-events: none; } +/* Minimal landing background */ +.background-only { position: absolute; inset: 0; z-index: 0; } +.background-only .background-gif { width: 100%; height: 100%; object-fit: cover; display: block; } + +/* Bottom overlay for Velocity/Energy */ +.bottom-stats { position: absolute; left: 0; right: 0; bottom: 16px; display: flex; justify-content: center; z-index: 2; pointer-events: none; } +.bottom-stats .rf-stats { background: rgba(10, 12, 20, 0.55); border: 1px solid rgba(102,204,255,0.6); padding: 8px 12px; border-radius: 6px; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); margin: 0; pointer-events: auto; } + /* Floating Header Bar */ .floating-header { position: fixed; @@ -1670,11 +1678,11 @@ button:focus-visible { z-index: 1; height: 100%; display: grid; - grid-template-columns: 1fr 220px; /* hero left, masthead right */ - grid-template-rows: 1fr auto; /* CTA sits below hero */ + grid-template-columns: 220px 1fr; /* masthead left, hero right */ + grid-template-rows: 1fr auto; /* CTA spans bottom */ grid-template-areas: - "hero masthead" - "hero cta"; + "masthead hero" + "cta cta"; gap: 28px; padding: 40px 48px; } @@ -2128,7 +2136,9 @@ button:focus-visible { /* .ascii-section / .ascii-frame / .ascii-art-container not used */ /* CTA */ -.cta-area { grid-area: cta; align-self: end; display: flex; justify-content: flex-end; } +.cta-area { grid-area: cta; align-self: end; display: flex; justify-content: space-between; align-items: center; gap: 16px; } +.cta-left { display: flex; flex-direction: column; gap: 12px; } +.cta-right { display: flex; align-items: center; } .taisho-cta { position: relative; background: var(--ink); diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a2bf354..0000000 --- a/src/main.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::{convert::Infallible, env, net::SocketAddr, time::Duration}; - -use axum::{ - extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State}, - http::Method, - response::IntoResponse, - routing::{get, post}, - Json, Router, -}; -use axum::response::sse::{Event, KeepAlive, Sse}; -use serde::{Deserialize, Serialize}; -use tower_http::{cors::{Any, CorsLayer}, trace::TraceLayer}; -use tracing::info; -use tracing_subscriber::EnvFilter; -use tokio_stream::{wrappers::IntervalStream, StreamExt as _}; -use futures::StreamExt as _; // for websocket split/next -use futures::stream::Stream; // for SSE return type - -#[derive(Clone, Default)] -struct AppState {} - -#[derive(Serialize)] -struct HealthResponse { - status: &'static str, -} - -#[derive(Serialize)] -struct HelloResponse { - message: &'static str, -} - -#[derive(Deserialize, Serialize)] -struct EchoPayload { - #[serde(flatten)] - rest: serde_json::Value, -} - -#[tokio::main] -async fn main() { - // Logging setup - let filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,tower_http=info,axum::rejection=trace")); - tracing_subscriber::fmt() - .with_env_filter(filter) - .with_target(false) - .compact() - .init(); - - // Shared app state (extend as needed) - let state = AppState::default(); - - // CORS to allow local frontend dev at 5173 and others - let cors = CorsLayer::new() - .allow_origin(Any) - .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) - .allow_headers(Any); - - // Router - let app = Router::new() - .route("/", get(root)) - .route("/health", get(health)) - .route("/api/hello", get(hello)) - .route("/api/echo", post(echo)) - .route("/api/stream/transcript", get(stream_transcript_sse)) - .route("/ws", get(ws_handler)) - .with_state(state) - .layer(cors) - .layer(TraceLayer::new_for_http()); - - // Bind address - let port = env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080); - let addr = SocketAddr::from(([0, 0, 0, 0], port)); - info!(%addr, "starting soryu backend"); - let listener = tokio::net::TcpListener::bind(addr).await.expect("bind port"); - axum::serve(listener, app).await.expect("server error"); -} - -async fn root() -> impl IntoResponse { - Json(serde_json::json!({ - "service": "soryu-backend", - "endpoints": [ - "/health", - "/api/hello", - "/api/echo", - "/api/stream/transcript", - "/ws" - ], - })) -} - -async fn health() -> impl IntoResponse { - Json(HealthResponse { status: "ok" }) -} - -async fn hello(State(_state): State<AppState>) -> impl IntoResponse { - Json(HelloResponse { message: "Hello from Soryu backend" }) -} - -async fn echo(Json(body): Json<serde_json::Value>) -> impl IntoResponse { - Json(EchoPayload { rest: body }) -} - -// --- -// Streaming transcript (SSE) skeleton -// Provides a low-latency server-sent events stream that emits example -// transcript chunks every ~250ms. -async fn stream_transcript_sse(State(_state): State<AppState>) -> Sse<impl Stream<Item = Result<Event, Infallible>>> { - const SAMPLE_LINES: &[&str] = &[ - "speaker_a: hey there, can you hear me?", - "speaker_b: loud and clear — let's begin.", - "speaker_a: streaming transcript looks smooth so far.", - "speaker_b: agreed, latency feels low.", - "speaker_a: wrapping up the demo now.", - ]; - - let mut idx = 0usize; - let interval = tokio::time::interval(Duration::from_millis(250)); - let stream = IntervalStream::new(interval).map(move |_| { - let line = SAMPLE_LINES[idx % SAMPLE_LINES.len()]; - idx += 1; - let data = serde_json::json!({ - "type": "transcript_chunk", - "text": line, - "ts_ms": chrono::Utc::now().timestamp_millis(), - }); - Ok(Event::default().json_data(&data).unwrap()) - }); - - Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(10))) -} - -// --- -// WebSocket transcript stream skeleton -// Sends example transcript messages on connect; ignores inbound messages. -async fn ws_handler(ws: WebSocketUpgrade, State(_state): State<AppState>) -> impl IntoResponse { - ws.on_upgrade(handle_socket) -} - -async fn handle_socket(mut socket: WebSocket) { - const SAMPLE_LINES: &[&str] = &[ - "speaker_a: hey there, can you hear me?", - "speaker_b: loud and clear — let's begin.", - "speaker_a: streaming transcript looks smooth so far.", - "speaker_b: agreed, latency feels low.", - "speaker_a: wrapping up the demo now.", - ]; - - // Spawn a task to drain inbound messages (optional for skeleton) - let mut recv_socket = socket.split().1; - tokio::spawn(async move { - while let Some(Ok(_msg)) = recv_socket.next().await { - // Ignore inbound for skeleton; handle pings/acks here if needed - } - }); - - // Send a small transcript stream - for line in SAMPLE_LINES { - let payload = serde_json::json!({ - "type": "transcript_chunk", - "text": line, - "ts_ms": chrono::Utc::now().timestamp_millis(), - }) - .to_string(); - if socket.send(Message::Text(payload)).await.is_err() { - return; - } - tokio::time::sleep(Duration::from_millis(250)).await; - } - - let done = serde_json::json!({"type":"done"}).to_string(); - let _ = socket.send(Message::Text(done)).await; -} |
