feat: faster splash with static logo and aperture exit
This commit is contained in:
parent
0175645f3a
commit
dbc78a1760
2 changed files with 66 additions and 68 deletions
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue