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

View file

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