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)}`;
|
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 = (
|
const readMediaDuration = (
|
||||||
playbackUrl: string,
|
playbackUrl: string,
|
||||||
mediaType: 'video' | 'audio',
|
mediaType: 'video' | 'audio',
|
||||||
@ -2137,10 +2225,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
if (!hasPlayableTransition) {
|
if (!hasPlayableTransition) {
|
||||||
setPendingNavigationPageId('');
|
setPendingNavigationPageId('');
|
||||||
setTransitionPreview(null);
|
setTransitionPreview(null);
|
||||||
setActivePageId(targetPageId);
|
// Wait for target page images to decode before switching (eliminates white flash)
|
||||||
setSelectedElementId('');
|
const targetPage = pages.find((p) => p.id === targetPageId) || null;
|
||||||
setSelectedMenuItem('none');
|
waitForPageImages(targetPage).then(() => {
|
||||||
setErrorMessage('');
|
setActivePageId(targetPageId);
|
||||||
|
setSelectedElementId('');
|
||||||
|
setSelectedMenuItem('none');
|
||||||
|
setErrorMessage('');
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2638,25 +2730,42 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
clearTimers();
|
clearTimers();
|
||||||
cleanupReverseFrame();
|
cleanupReverseFrame();
|
||||||
setIsBufferingForReverse(false);
|
setIsBufferingForReverse(false);
|
||||||
|
// Pause video but keep last frame visible
|
||||||
video.pause();
|
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', {
|
logger.info('Transition preview finished', {
|
||||||
reason,
|
reason,
|
||||||
src: video.currentSrc || sourceUrl || '',
|
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);
|
const configuredDurationSec = Number(transitionPreview.durationSec);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user