feat: faster splash with static logo and aperture exit

This commit is contained in:
SkateSpots 2026-10-30 09:30:50 +07:00
parent 0175645f3a
commit dbc78a1760
2 changed files with 66 additions and 68 deletions

View file

@ -23,23 +23,23 @@ import { useTranslation } from '@/contexts/language';
*/
const SESSION_KEY = 'splash-shown-session';
const MIN_MS = 900; // choreography needs room to breathe
const WARM_GRACE_MS = 550; // wait after connection for content to stream in
const MAX_MS = 2600; // hard cap — never block on a slow/offline network
const FADE_MS = 480;
const READY_HOLD_MS = 340; // savour the 100% "ready" beat before revealing
const MIN_MS = 380; // keep the brand beat, lose the artificial wait
const WARM_GRACE_MS = 180; // a short breath after relays connect
const MAX_MS = 900; // hard cap — reveal the shell even on a dead network
const FADE_MS = 280; // iris-close exit
const READY_HOLD_MS = 120; // show the ready check, then move
const SPARKS = [
{ sx: '-8px', sy: '18px', ex: '-54px', ey: '44px', delay: '0ms' },
{ sx: '4px', sy: '22px', ex: '38px', ey: '58px', delay: '90ms' },
{ sx: '-16px', sy: '10px', ex: '-72px', ey: '18px', delay: '180ms' },
{ sx: '12px', sy: '14px', ex: '66px', ey: '30px', delay: '260ms' },
{ sx: '-4px', sy: '24px', ex: '-28px', ey: '76px', delay: '340ms' },
{ sx: '8px', sy: '24px', ex: '22px', ey: '78px', delay: '430ms' },
{ sx: '-12px', sy: '2px', ex: '-48px', ey: '-16px', delay: '520ms' },
{ sx: '14px', sy: '4px', ex: '52px', ey: '-12px', delay: '610ms' },
{ sx: '-2px', sy: '28px', ex: '-10px', ey: '88px', delay: '700ms' },
{ sx: '2px', sy: '28px', ex: '12px', ey: '90px', delay: '790ms' },
{ sx: '4px', sy: '22px', ex: '38px', ey: '58px', delay: '20ms' },
{ sx: '-16px', sy: '10px', ex: '-72px', ey: '18px', delay: '40ms' },
{ sx: '12px', sy: '14px', ex: '66px', ey: '30px', delay: '60ms' },
{ sx: '-4px', sy: '24px', ex: '-28px', ey: '76px', delay: '80ms' },
{ sx: '8px', sy: '24px', ex: '22px', ey: '78px', delay: '100ms' },
{ sx: '-12px', sy: '2px', ex: '-48px', ey: '-16px', delay: '120ms' },
{ sx: '14px', sy: '4px', ex: '52px', ey: '-12px', delay: '140ms' },
{ sx: '-2px', sy: '28px', ex: '-10px', ey: '88px', delay: '160ms' },
{ sx: '2px', sy: '28px', ex: '12px', ey: '90px', delay: '180ms' },
];
const STATUS_KEYS = [
@ -75,7 +75,7 @@ export function SplashScreen() {
if (!visible || ready) return;
const id = setInterval(() => {
setStatusIndex((i) => (i + 1) % STATUS_KEYS.length);
}, 640);
}, 380);
return () => clearInterval(id);
}, [visible, ready]);
@ -84,8 +84,8 @@ export function SplashScreen() {
useEffect(() => {
if (!visible || ready) return;
const id = setInterval(() => {
setProgress((p) => (p >= 85 ? p : p + Math.max(0.6, (85 - p) * 0.045)));
}, 90);
setProgress((p) => (p >= 85 ? p : p + Math.max(0.9, (85 - p) * 0.065)));
}, 60);
return () => clearInterval(id);
}, [visible, ready]);
@ -155,8 +155,24 @@ export function SplashScreen() {
<div
className="fixed inset-0 z-[9999] flex flex-col items-center justify-center bg-[#050505] overflow-hidden"
style={{
opacity: leaving ? 0 : 1,
transition: `opacity ${FADE_MS}ms cubic-bezier(0.4,0,0.2,1)`,
// The exit is an aperture-style iris closing into the logo. It reveals
// the already-painted app from the edges without moving the map. Users
// who prefer reduced motion get a simple, quick fade instead.
opacity: leaving && prefersReducedMotion ? 0 : 1,
clipPath: prefersReducedMotion
? undefined
: leaving
? 'circle(0px at 50% 42%)'
: 'circle(150vmax at 50% 42%)',
WebkitClipPath: prefersReducedMotion
? undefined
: leaving
? 'circle(0px at 50% 42%)'
: 'circle(150vmax at 50% 42%)',
transition: prefersReducedMotion
? `opacity ${FADE_MS}ms ease`
: `clip-path ${FADE_MS}ms cubic-bezier(0.7,0,0.84,0)`,
willChange: 'clip-path',
paddingTop: 'env(safe-area-inset-top, 0px)',
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
paddingLeft: 'env(safe-area-inset-left, 0px)',
@ -191,17 +207,19 @@ export function SplashScreen() {
<div
className="relative z-10 flex flex-col items-center px-8"
style={{
opacity: leaving ? 0 : 1,
transform: leaving && !prefersReducedMotion ? 'translateY(-10px) scale(0.985)' : 'translateY(0) scale(1)',
transition: 'opacity 380ms ease, transform 520ms cubic-bezier(0.22,1,0.36,1)',
// Keep the mark visible all the way through the iris-close exit.
// The shrinking aperture clips it away, so there is never a dead
// black frame between the splash and the app underneath.
opacity: 1,
transform: leaving && !prefersReducedMotion ? 'translateY(-4px)' : 'translateY(0)',
transition: 'transform 280ms cubic-bezier(0.22,1,0.36,1)',
}}
>
{/* The orange wheel mark rolls in, keeps a slow roll, and throws sparks.
The handoff wrapper starts near the native splash's centred logo and
settles into the column, so the native → web handoff has no jump. */}
<div style={anim('splashLogoHandoff 760ms cubic-bezier(0.22,1,0.36,1) both')}>
{/* The mark arrives from the native splash's centre and settles into the
column. No rotation: a quick, grounded drop with one landing burst. */}
<div style={anim('splashLogoHandoff 440ms cubic-bezier(0.22,1,0.36,1) both')}>
<div className="relative">
{/* Rotating conic ring — orange to match the wheel, with a lime kick */}
{/* Static conic halo — colour and breathing glow, no spin */}
<div
className="absolute -inset-3 rounded-full"
style={{
@ -209,15 +227,14 @@ export function SplashScreen() {
'conic-gradient(from 0deg, transparent 0deg, #ff6a00 90deg, #ccff00 190deg, transparent 310deg)',
opacity: ready ? 0.9 : 0.58,
filter: 'blur(7px)',
transition: 'opacity 400ms ease',
...anim('splashSpin 3.2s linear infinite'),
transition: 'opacity 240ms ease',
}}
aria-hidden="true"
/>
{/* Mask ring interior so only a thin halo shows */}
<div className="absolute -inset-1 rounded-full bg-[#050505]" aria-hidden="true" />
{/* Sparks are decorative and disabled for reduced-motion users. */}
{/* One-shot landing sparks, disabled for reduced-motion users. */}
{!prefersReducedMotion && (
<div className="pointer-events-none absolute inset-0" aria-hidden="true">
{SPARKS.map((spark, index) => (
@ -236,25 +253,16 @@ export function SplashScreen() {
</div>
)}
{/* Bounce wrapper + rolling wheel. Nested transforms keep the spin
from fighting the entry animation. */}
<div
style={anim('splashLogoRollIn 720ms cubic-bezier(0.22,1,0.36,1) both')}
style={anim('splashLogoDrop 380ms cubic-bezier(0.22,1,0.36,1) both')}
className="relative"
>
<div
style={anim('splashHover 1.9s ease-in-out infinite')}
className="relative"
>
<div style={anim('splashWheelRoll 5.4s linear infinite')} className="relative">
<img
src="/images/icon-1024.png"
alt="SkateSpots"
draggable={false}
className="relative h-28 w-28 select-none drop-shadow-[0_0_34px_rgba(255,106,0,0.65)]"
/>
</div>
</div>
<img
src="/images/icon-1024.png"
alt="SkateSpots"
draggable={false}
className="relative h-28 w-28 select-none drop-shadow-[0_0_34px_rgba(255,106,0,0.65)]"
/>
{/* Contact shadow / dust line under the wheel */}
<div
className="absolute -bottom-2 left-1/2 h-2 w-20 -translate-x-1/2 rounded-full bg-[#ff6a00]/30 blur-md"
@ -270,8 +278,8 @@ export function SplashScreen() {
className="font-display neon-red mt-7 text-4xl tracking-[0.06em]"
style={{
textShadow: '0 0 14px #ff0040, 0 0 30px rgba(255,0,64,0.6)',
...anim('splashRise 620ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '120ms',
...anim('splashRise 320ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '0ms',
}}
>
SKATESPOTS
@ -281,8 +289,8 @@ export function SplashScreen() {
<div
className="relative mt-8 h-1.5 w-52 overflow-hidden rounded-full bg-white/[0.06] ring-1 ring-white/[0.05]"
style={{
...anim('splashRise 620ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '220ms',
...anim('splashRise 320ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '30ms',
}}
role="progressbar"
aria-valuemin={0}
@ -293,7 +301,7 @@ export function SplashScreen() {
className="relative h-full rounded-full bg-gradient-to-r from-[#ff0040] via-[#ff0040] to-[#ccff00]"
style={{
width: `${progress}%`,
transition: 'width 420ms cubic-bezier(0.4,0,0.2,1)',
transition: 'width 240ms cubic-bezier(0.4,0,0.2,1)',
boxShadow: '0 0 12px rgba(255,0,64,0.6)',
}}
>
@ -315,8 +323,8 @@ export function SplashScreen() {
<div
className="mt-5 flex h-4 items-center gap-2"
style={{
...anim('splashRise 620ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '320ms',
...anim('splashRise 320ms cubic-bezier(0.22,1,0.36,1) both'),
animationDelay: prefersReducedMotion ? undefined : '60ms',
}}
>
{ready ? (

View file

@ -337,24 +337,14 @@
0%, 100% { opacity: 0.7; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
}
@keyframes splashSpin {
to { transform: rotate(360deg); }
}
@keyframes splashLogoHandoff {
0% { opacity: 1; transform: translateY(56px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes splashLogoRollIn {
0% { opacity: 0; transform: translateX(-58px) rotate(-210deg) scale(0.82); }
62% { opacity: 1; transform: translateX(5px) rotate(14deg) scale(1.03); }
100% { opacity: 1; transform: translateX(0) rotate(0deg) scale(1); }
}
@keyframes splashWheelRoll {
to { transform: rotate(360deg); }
}
@keyframes splashHover {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
@keyframes splashLogoDrop {
0% { opacity: 0; transform: translateY(-14px) scale(0.92); }
62% { opacity: 1; transform: translateY(2px) scale(1.02); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes splashDust {
0%, 100% { opacity: 0.28; transform: translateX(-50%) scaleX(1); }
@ -374,7 +364,7 @@
border-radius: 9999px;
background: #ffb347;
box-shadow: 0 0 10px 3px rgba(255, 106, 0, 0.85);
animation: splashSpark 1.15s ease-out infinite;
animation: splashSpark 560ms ease-out 1 both;
}
.splash-spark:nth-child(3n) {
width: 4px;