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 MAX_DPR = 2;
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 avatarLoading = new Map<string, Promise<HTMLImageElement | null>>();
@ -84,7 +86,7 @@ export function startSplashScene(
return existingScene;
}
const ready = preloadAvatarImages();
const avatarsReady = preloadAvatarImages();
const previousCanvas = splash.querySelector<HTMLCanvasElement>('#splash-enhanced-canvas');
previousCanvas?.remove();
@ -99,7 +101,7 @@ export function startSplashScene(
stop: () => {
canvas.remove();
},
ready,
ready: avatarsReady,
};
return emptyHandle;
}
@ -138,6 +140,13 @@ export function startSplashScene(
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 => {
if (!state.running) return;
@ -146,10 +155,7 @@ export function startSplashScene(
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;
renderFrame(now);
}
if (!reducedMotion) {
@ -162,6 +168,13 @@ export function startSplashScene(
resize();
render(performance.now());
const ready = avatarsReady.then(() => {
if (state.running) {
renderFrame(performance.now());
canvas.classList.add('splash-canvas-ready');
}
});
const handle: SplashSceneHandle = {
stop: () => {
state.running = false;
@ -301,7 +314,7 @@ function buildTeams(
receivePulse: 0,
avatarUrl:
PARTICIPANT_AVATAR_URLS[(TEAM_MEMBER_OFFSETS[teamIndex] ?? 0) + robotIndex] ??
PARTICIPANT_AVATAR_URLS[0],
SPLASH_FALLBACK_AVATAR_URL,
x: centerWithDrift.x + Math.cos(orbit) * orbitRadius,
y: centerWithDrift.y + Math.sin(orbit) * orbitRadius,
};
@ -769,8 +782,22 @@ function getAvatarImage(url: string): HTMLImageElement | 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> {
return Promise.allSettled(PARTICIPANT_AVATAR_URLS.map((url) => loadAvatarImage(url))).then(
return Promise.allSettled(SPLASH_AVATAR_URLS.map((url) => loadAvatarImage(url))).then(
() => undefined
);
}

View file

@ -8,6 +8,18 @@
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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>
<style>
/* Splash: animated gradient background */
@ -69,6 +81,8 @@
pointer-events: none;
z-index: 1;
opacity: 0;
}
#splash-enhanced-canvas.splash-canvas-ready {
animation: splash-canvas-in 0.62s ease-out 0.08s forwards;
}
@keyframes splash-canvas-in {
@ -393,7 +407,7 @@
.splash-node {
animation: none !important;
}
#splash-enhanced-canvas {
#splash-enhanced-canvas.splash-canvas-ready {
opacity: 1;
}
#splash-tagline > span {
@ -578,6 +592,7 @@
'./assets/participant-avatars/13.png',
];
var AVATAR_URLS = resolveAvatarUrls();
var SPLASH_AVATAR_URLS = getSplashAvatarUrls();
var avatarCache = new Map();
var avatarLoading = new Map();
@ -597,22 +612,38 @@
urlsByIndex[Number(match[1]) - 1] = href;
}
var resolved = [];
var resolved = FALLBACK_AVATAR_URLS.slice();
for (var avatarIndex = 0; avatarIndex < FALLBACK_AVATAR_URLS.length; avatarIndex++) {
var url = urlsByIndex[avatarIndex];
if (!url) return FALLBACK_AVATAR_URLS;
resolved.push(url);
if (url) resolved[avatarIndex] = url;
}
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) {
if (window.__claudeTeamsSplashScene && splash.querySelector('#splash-enhanced-canvas')) {
return window.__claudeTeamsSplashScene;
}
var ready = preloadAvatarImages();
var avatarsReady = preloadAvatarImages();
var previousCanvas = splash.querySelector('#splash-enhanced-canvas');
if (previousCanvas) previousCanvas.remove();
@ -627,7 +658,7 @@
stop: function () {
canvas.remove();
},
ready: ready,
ready: avatarsReady,
};
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
@ -661,6 +692,18 @@
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) {
if (!state.running) return;
if (
@ -668,15 +711,7 @@
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;
renderFrame(now);
}
if (!reducedMotion) state.frameId = window.requestAnimationFrame(render);
}
@ -689,6 +724,13 @@
resize();
render(performance.now());
var ready = avatarsReady.then(function () {
if (state.running) {
renderFrame(performance.now());
canvas.classList.add('splash-canvas-ready');
}
});
var handle = {
stop: function () {
state.running = false;
@ -1181,7 +1223,7 @@
function preloadAvatarImages() {
return Promise.allSettled(
AVATAR_URLS.map(function (url) {
SPLASH_AVATAR_URLS.map(function (url) {
return loadAvatarImage(url);
})
).then(function () {});