pransitions smoothing
This commit is contained in:
parent
4c41205225
commit
0728923dd1
@ -290,6 +290,94 @@ const resolveAssetPlaybackUrl = (value?: string) => {
|
||||
return `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(normalizedPrivateUrl)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for all images on a page to be decoded before switching.
|
||||
* This eliminates white flash by ensuring images are ready to paint.
|
||||
*/
|
||||
const waitForPageImages = async (
|
||||
page: TourPage | null,
|
||||
timeoutMs = 2000,
|
||||
): Promise<void> => {
|
||||
if (!page) return;
|
||||
|
||||
const imageUrls: string[] = [];
|
||||
|
||||
// Background image
|
||||
if (page.background_image_url) {
|
||||
const url = resolveAssetPlaybackUrl(page.background_image_url);
|
||||
if (url) imageUrls.push(url);
|
||||
}
|
||||
|
||||
// Parse ui_schema_json for element images
|
||||
try {
|
||||
const uiSchema =
|
||||
typeof page.ui_schema_json === 'string'
|
||||
? JSON.parse(page.ui_schema_json)
|
||||
: page.ui_schema_json;
|
||||
|
||||
const pageElements = Array.isArray(uiSchema?.elements)
|
||||
? uiSchema.elements
|
||||
: [];
|
||||
|
||||
pageElements.forEach((el: Record<string, unknown>) => {
|
||||
const imageFields = ['iconUrl', 'imageUrl', 'mediaUrl', 'src'];
|
||||
imageFields.forEach((field) => {
|
||||
const value = el[field];
|
||||
if (typeof value === 'string' && value) {
|
||||
const url = resolveAssetPlaybackUrl(value);
|
||||
if (url && !imageUrls.includes(url)) imageUrls.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Carousel slides
|
||||
const slides = el.carouselSlides;
|
||||
if (Array.isArray(slides)) {
|
||||
slides.forEach((slide: Record<string, unknown>) => {
|
||||
if (typeof slide.imageUrl === 'string' && slide.imageUrl) {
|
||||
const url = resolveAssetPlaybackUrl(slide.imageUrl);
|
||||
if (url && !imageUrls.includes(url)) imageUrls.push(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Cards
|
||||
const cards = el.cards;
|
||||
if (Array.isArray(cards)) {
|
||||
cards.forEach((card: Record<string, unknown>) => {
|
||||
if (typeof card.imageUrl === 'string' && card.imageUrl) {
|
||||
const url = resolveAssetPlaybackUrl(card.imageUrl);
|
||||
if (url && !imageUrls.includes(url)) imageUrls.push(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
if (imageUrls.length === 0) return;
|
||||
|
||||
// Decode all images in parallel with timeout
|
||||
const decodePromises = imageUrls.map(
|
||||
(url) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
if (typeof img.decode === 'function') {
|
||||
img.decode().then(() => resolve()).catch(() => resolve());
|
||||
} else {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.race([
|
||||
Promise.all(decodePromises),
|
||||
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
||||
]);
|
||||
};
|
||||
|
||||
const readMediaDuration = (
|
||||
playbackUrl: string,
|
||||
mediaType: 'video' | 'audio',
|
||||
@ -2137,10 +2225,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
if (!hasPlayableTransition) {
|
||||
setPendingNavigationPageId('');
|
||||
setTransitionPreview(null);
|
||||
setActivePageId(targetPageId);
|
||||
setSelectedElementId('');
|
||||
setSelectedMenuItem('none');
|
||||
setErrorMessage('');
|
||||
// Wait for target page images to decode before switching (eliminates white flash)
|
||||
const targetPage = pages.find((p) => p.id === targetPageId) || null;
|
||||
waitForPageImages(targetPage).then(() => {
|
||||
setActivePageId(targetPageId);
|
||||
setSelectedElementId('');
|
||||
setSelectedMenuItem('none');
|
||||
setErrorMessage('');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2638,25 +2730,42 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
||||
clearTimers();
|
||||
cleanupReverseFrame();
|
||||
setIsBufferingForReverse(false);
|
||||
// Pause video but keep last frame visible
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
// Don't cleanup blob URL here - might be reused for reverse
|
||||
setTransitionPreview(null);
|
||||
setPendingNavigationPageId((pendingPageId) => {
|
||||
const nextPageId = String(pendingPageId || '').trim();
|
||||
if (nextPageId) {
|
||||
setActivePageId(nextPageId);
|
||||
setSelectedElementId('');
|
||||
setSelectedMenuItem('none');
|
||||
setErrorMessage('');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
logger.info('Transition preview finished', {
|
||||
reason,
|
||||
src: video.currentSrc || sourceUrl || '',
|
||||
});
|
||||
|
||||
setPendingNavigationPageId((pendingPageId) => {
|
||||
const nextPageId = String(pendingPageId || '').trim();
|
||||
if (nextPageId) {
|
||||
// Wait for target page images to decode before hiding transition
|
||||
const targetPage = pages.find((p) => p.id === nextPageId) || null;
|
||||
waitForPageImages(targetPage).then(() => {
|
||||
// Switch page first (while transition still covers it)
|
||||
setActivePageId(nextPageId);
|
||||
setSelectedElementId('');
|
||||
setSelectedMenuItem('none');
|
||||
setErrorMessage('');
|
||||
// Wait for React to render, then hide transition
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
setTransitionPreview(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// No pending navigation - just cleanup
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
setTransitionPreview(null);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
};
|
||||
|
||||
const configuredDurationSec = Number(transitionPreview.durationSec);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user