diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index d4673748..52bd7695 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -16,6 +16,7 @@ declare global { interface Window { __claudeTeamsSplashEnhancedStartedAt?: number; __claudeTeamsSplashScene?: SplashSceneHandle; + __claudeTeamsSplashEnhancedDisabled?: boolean; __claudeTeamsSplashStartedAt?: number; } } @@ -38,7 +39,11 @@ export const App = (): React.JSX.Element => { const splash = document.getElementById('splash'); if (splash) { const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - const scene = window.__claudeTeamsSplashScene ?? startSplashScene(splash, { reducedMotion }); + const scene: SplashSceneHandle = + window.__claudeTeamsSplashScene ?? + (window.__claudeTeamsSplashEnhancedDisabled + ? { stop: () => undefined, ready: Promise.resolve() } + : startSplashScene(splash, { reducedMotion })); const startedAt = window.__claudeTeamsSplashStartedAt ?? performance.now(); const enhancedStartedAt = window.__claudeTeamsSplashEnhancedStartedAt ?? performance.now(); const elapsed = performance.now() - startedAt; @@ -62,6 +67,7 @@ export const App = (): React.JSX.Element => { scene.stop(); window.__claudeTeamsSplashScene = undefined; window.__claudeTeamsSplashEnhancedStartedAt = undefined; + window.__claudeTeamsSplashEnhancedDisabled = undefined; splash.remove(); }, fadeDuration); }; diff --git a/src/renderer/components/splash/splashScene.ts b/src/renderer/components/splash/splashScene.ts index 3ecdd0af..8e5a2092 100644 --- a/src/renderer/components/splash/splashScene.ts +++ b/src/renderer/components/splash/splashScene.ts @@ -71,6 +71,7 @@ const TEAM_MEMBER_COUNTS = [4, 3, 5] as const; const TEAM_MEMBER_OFFSETS = [0, 4, 7] as const; const TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'] as const; const MAX_DPR = 2; +const SPLASH_SCENE_FRAME_INTERVAL_MS = 1000 / 30; const avatarCache = new Map(); const avatarLoading = new Map>(); @@ -112,6 +113,7 @@ export function startSplashScene( particles: [] as DepthParticle[], running: true, frameId: 0, + lastRenderedAt: 0, startedAt: performance.now(), }; @@ -139,9 +141,16 @@ export function startSplashScene( const render = (now: number): void => { if (!state.running) return; - resize(); - const time = (now - state.startedAt) / 1000; - drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion); + if ( + reducedMotion || + state.lastRenderedAt === 0 || + now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS + ) { + resize(); + const time = (now - state.startedAt) / 1000; + drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion); + state.lastRenderedAt = now; + } if (!reducedMotion) { state.frameId = window.requestAnimationFrame(render); diff --git a/src/renderer/index.html b/src/renderer/index.html index 80edd058..11f064a9 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -561,6 +561,7 @@ var TEAM_MEMBER_OFFSETS = [0, 4, 7]; var TEAM_LABELS = ['Marketing', 'Researchers', 'Coding']; var MAX_DPR = 2; + var SPLASH_SCENE_FRAME_INTERVAL_MS = 1000 / 30; var FALLBACK_AVATAR_URLS = [ './assets/participant-avatars/01.png', './assets/participant-avatars/02.png', @@ -637,6 +638,7 @@ particles: [], running: true, frameId: 0, + lastRenderedAt: 0, startedAt: performance.now(), }; @@ -661,14 +663,21 @@ function render(now) { if (!state.running) return; - resize(); - drawScene( - ctx, - state.width, - state.height, - reducedMotion ? 1.2 : (now - state.startedAt) / 1000, - state.particles - ); + if ( + reducedMotion || + state.lastRenderedAt === 0 || + now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS + ) { + resize(); + drawScene( + ctx, + state.width, + state.height, + reducedMotion ? 1.2 : (now - state.startedAt) / 1000, + state.particles + ); + state.lastRenderedAt = now; + } if (!reducedMotion) state.frameId = window.requestAnimationFrame(render); } diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index b6137a82..91643c58 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -9,12 +9,15 @@ import { App } from './App'; import { initSentryRenderer } from './sentry'; import { initializeNotificationListeners } from './store'; +import type { SplashSceneHandle } from './components/splash/splashScene'; import type { AppStartupStatus, AppStartupStep } from '@shared/types/api'; declare global { interface Window { __claudeTeamsUiDidInit?: boolean; __claudeTeamsSplashStaticTimer?: number; + __claudeTeamsSplashScene?: SplashSceneHandle; + __claudeTeamsSplashEnhancedDisabled?: boolean; } } @@ -170,6 +173,11 @@ function stopStartupTicker(): void { startupTicker = undefined; } +function stopEnhancedSplashScene(): void { + window.__claudeTeamsSplashEnhancedDisabled = true; + window.__claudeTeamsSplashScene?.stop(); +} + function mountApp(): void { if (root) return; @@ -204,6 +212,11 @@ async function bootstrapRenderer(): Promise { return; } updateStartupSplash(nextStatus); + const currentStep = getCurrentStartupStep(nextStatus); + const stepElapsedMs = getStepElapsedMs(currentStep, nextStatus); + if (!nextStatus.ready && (nextStatus.error || stepElapsedMs >= VERY_SLOW_STEP_MS)) { + stopEnhancedSplashScene(); + } if (nextStatus.ready) { finished = true; cleanup();