perf(splash): preload startup avatar assets

This commit is contained in:
777genius 2026-05-31 20:19:22 +03:00
parent 67a2c9ebac
commit a17cdd19e7
2 changed files with 93 additions and 24 deletions

View file

@ -72,6 +72,8 @@ const TEAM_MEMBER_OFFSETS = [0, 4, 7] as const;
const TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'] as const; const TEAM_LABELS = ['Marketing', 'Researchers', 'Coding'] as const;
const MAX_DPR = 2; const MAX_DPR = 2;
const SPLASH_SCENE_FRAME_INTERVAL_MS = 1000 / 30; const SPLASH_SCENE_FRAME_INTERVAL_MS = 1000 / 30;
const SPLASH_FALLBACK_AVATAR_URL = PARTICIPANT_AVATAR_URLS[0];
const SPLASH_AVATAR_URLS = getSplashAvatarUrls();
const avatarCache = new Map<string, HTMLImageElement>(); const avatarCache = new Map<string, HTMLImageElement>();
const avatarLoading = new Map<string, Promise<HTMLImageElement | null>>(); const avatarLoading = new Map<string, Promise<HTMLImageElement | null>>();
@ -84,7 +86,7 @@ export function startSplashScene(
return existingScene; return existingScene;
} }
const ready = preloadAvatarImages(); const avatarsReady = preloadAvatarImages();
const previousCanvas = splash.querySelector<HTMLCanvasElement>('#splash-enhanced-canvas'); const previousCanvas = splash.querySelector<HTMLCanvasElement>('#splash-enhanced-canvas');
previousCanvas?.remove(); previousCanvas?.remove();
@ -99,7 +101,7 @@ export function startSplashScene(
stop: () => { stop: () => {
canvas.remove(); canvas.remove();
}, },
ready, ready: avatarsReady,
}; };
return emptyHandle; return emptyHandle;
} }
@ -138,6 +140,13 @@ export function startSplashScene(
state.particles = createDepthParticles(width, height); state.particles = createDepthParticles(width, height);
}; };
const renderFrame = (now: number): void => {
resize();
const time = (now - state.startedAt) / 1000;
drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion);
state.lastRenderedAt = now;
};
const render = (now: number): void => { const render = (now: number): void => {
if (!state.running) return; if (!state.running) return;
@ -146,10 +155,7 @@ export function startSplashScene(
state.lastRenderedAt === 0 || state.lastRenderedAt === 0 ||
now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS
) { ) {
resize(); renderFrame(now);
const time = (now - state.startedAt) / 1000;
drawScene(ctx, state.width, state.height, time, state.particles, reducedMotion);
state.lastRenderedAt = now;
} }
if (!reducedMotion) { if (!reducedMotion) {
@ -162,6 +168,13 @@ export function startSplashScene(
resize(); resize();
render(performance.now()); render(performance.now());
const ready = avatarsReady.then(() => {
if (state.running) {
renderFrame(performance.now());
canvas.classList.add('splash-canvas-ready');
}
});
const handle: SplashSceneHandle = { const handle: SplashSceneHandle = {
stop: () => { stop: () => {
state.running = false; state.running = false;
@ -301,7 +314,7 @@ function buildTeams(
receivePulse: 0, receivePulse: 0,
avatarUrl: avatarUrl:
PARTICIPANT_AVATAR_URLS[(TEAM_MEMBER_OFFSETS[teamIndex] ?? 0) + robotIndex] ?? PARTICIPANT_AVATAR_URLS[(TEAM_MEMBER_OFFSETS[teamIndex] ?? 0) + robotIndex] ??
PARTICIPANT_AVATAR_URLS[0], SPLASH_FALLBACK_AVATAR_URL,
x: centerWithDrift.x + Math.cos(orbit) * orbitRadius, x: centerWithDrift.x + Math.cos(orbit) * orbitRadius,
y: centerWithDrift.y + Math.sin(orbit) * orbitRadius, y: centerWithDrift.y + Math.sin(orbit) * orbitRadius,
}; };
@ -769,8 +782,22 @@ function getAvatarImage(url: string): HTMLImageElement | null {
return null; return null;
} }
function getSplashAvatarUrls(): readonly string[] {
const urls = new Set<string>();
for (let teamIndex = 0; teamIndex < TEAM_MEMBER_COUNTS.length; teamIndex++) {
const memberCount = TEAM_MEMBER_COUNTS[teamIndex] ?? 0;
const offset = TEAM_MEMBER_OFFSETS[teamIndex] ?? 0;
for (let robotIndex = 0; robotIndex < memberCount; robotIndex++) {
urls.add(PARTICIPANT_AVATAR_URLS[offset + robotIndex] ?? SPLASH_FALLBACK_AVATAR_URL);
}
}
return Array.from(urls);
}
function preloadAvatarImages(): Promise<void> { function preloadAvatarImages(): Promise<void> {
return Promise.allSettled(PARTICIPANT_AVATAR_URLS.map((url) => loadAvatarImage(url))).then( return Promise.allSettled(SPLASH_AVATAR_URLS.map((url) => loadAvatarImage(url))).then(
() => undefined () => undefined
); );
} }

View file

@ -8,6 +8,18 @@
/> />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="./favicon.png" /> <link rel="icon" type="image/png" href="./favicon.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/01.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/02.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/03.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/04.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/05.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/06.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/07.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/08.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/09.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/10.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/11.png" />
<link rel="preload" as="image" type="image/png" fetchpriority="low" href="./assets/participant-avatars/12.png" />
<title>Agent Teams AI</title> <title>Agent Teams AI</title>
<style> <style>
/* Splash: animated gradient background */ /* Splash: animated gradient background */
@ -69,6 +81,8 @@
pointer-events: none; pointer-events: none;
z-index: 1; z-index: 1;
opacity: 0; opacity: 0;
}
#splash-enhanced-canvas.splash-canvas-ready {
animation: splash-canvas-in 0.62s ease-out 0.08s forwards; animation: splash-canvas-in 0.62s ease-out 0.08s forwards;
} }
@keyframes splash-canvas-in { @keyframes splash-canvas-in {
@ -393,7 +407,7 @@
.splash-node { .splash-node {
animation: none !important; animation: none !important;
} }
#splash-enhanced-canvas { #splash-enhanced-canvas.splash-canvas-ready {
opacity: 1; opacity: 1;
} }
#splash-tagline > span { #splash-tagline > span {
@ -578,6 +592,7 @@
'./assets/participant-avatars/13.png', './assets/participant-avatars/13.png',
]; ];
var AVATAR_URLS = resolveAvatarUrls(); var AVATAR_URLS = resolveAvatarUrls();
var SPLASH_AVATAR_URLS = getSplashAvatarUrls();
var avatarCache = new Map(); var avatarCache = new Map();
var avatarLoading = new Map(); var avatarLoading = new Map();
@ -597,22 +612,38 @@
urlsByIndex[Number(match[1]) - 1] = href; urlsByIndex[Number(match[1]) - 1] = href;
} }
var resolved = []; var resolved = FALLBACK_AVATAR_URLS.slice();
for (var avatarIndex = 0; avatarIndex < FALLBACK_AVATAR_URLS.length; avatarIndex++) { for (var avatarIndex = 0; avatarIndex < FALLBACK_AVATAR_URLS.length; avatarIndex++) {
var url = urlsByIndex[avatarIndex]; var url = urlsByIndex[avatarIndex];
if (!url) return FALLBACK_AVATAR_URLS; if (url) resolved[avatarIndex] = url;
resolved.push(url);
} }
return resolved; return resolved;
} }
function getSplashAvatarUrls() {
var urls = [];
var seen = new Set();
for (var teamIndex = 0; teamIndex < TEAM_MEMBER_COUNTS.length; teamIndex++) {
var memberCount = TEAM_MEMBER_COUNTS[teamIndex] || 0;
var offset = TEAM_MEMBER_OFFSETS[teamIndex] || 0;
for (var robotIndex = 0; robotIndex < memberCount; robotIndex++) {
var url = AVATAR_URLS[offset + robotIndex] || AVATAR_URLS[0];
if (!seen.has(url)) {
seen.add(url);
urls.push(url);
}
}
}
return urls;
}
function startFileSplashScene(splash) { function startFileSplashScene(splash) {
if (window.__claudeTeamsSplashScene && splash.querySelector('#splash-enhanced-canvas')) { if (window.__claudeTeamsSplashScene && splash.querySelector('#splash-enhanced-canvas')) {
return window.__claudeTeamsSplashScene; return window.__claudeTeamsSplashScene;
} }
var ready = preloadAvatarImages(); var avatarsReady = preloadAvatarImages();
var previousCanvas = splash.querySelector('#splash-enhanced-canvas'); var previousCanvas = splash.querySelector('#splash-enhanced-canvas');
if (previousCanvas) previousCanvas.remove(); if (previousCanvas) previousCanvas.remove();
@ -627,7 +658,7 @@
stop: function () { stop: function () {
canvas.remove(); canvas.remove();
}, },
ready: ready, ready: avatarsReady,
}; };
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
@ -661,6 +692,18 @@
state.particles = createParticles(width, height); state.particles = createParticles(width, height);
} }
function renderFrame(now) {
resize();
drawScene(
ctx,
state.width,
state.height,
reducedMotion ? 1.2 : (now - state.startedAt) / 1000,
state.particles
);
state.lastRenderedAt = now;
}
function render(now) { function render(now) {
if (!state.running) return; if (!state.running) return;
if ( if (
@ -668,15 +711,7 @@
state.lastRenderedAt === 0 || state.lastRenderedAt === 0 ||
now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS now - state.lastRenderedAt >= SPLASH_SCENE_FRAME_INTERVAL_MS
) { ) {
resize(); renderFrame(now);
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); if (!reducedMotion) state.frameId = window.requestAnimationFrame(render);
} }
@ -689,6 +724,13 @@
resize(); resize();
render(performance.now()); render(performance.now());
var ready = avatarsReady.then(function () {
if (state.running) {
renderFrame(performance.now());
canvas.classList.add('splash-canvas-ready');
}
});
var handle = { var handle = {
stop: function () { stop: function () {
state.running = false; state.running = false;
@ -1181,7 +1223,7 @@
function preloadAvatarImages() { function preloadAvatarImages() {
return Promise.allSettled( return Promise.allSettled(
AVATAR_URLS.map(function (url) { SPLASH_AVATAR_URLS.map(function (url) {
return loadAvatarImage(url); return loadAvatarImage(url);
}) })
).then(function () {}); ).then(function () {});