perf(splash): preload startup avatar assets
This commit is contained in:
parent
67a2c9ebac
commit
a17cdd19e7
2 changed files with 93 additions and 24 deletions
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 () {});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue