Fix frame jiggle and improve screenshot quality

- Add wait for frame 0 tiles to load before calculating bounding box
- Preload all frame images on client to prevent layout shifts
- Use absolute positioning for radar images to prevent reflow
- Add 2x device scale factor for higher resolution screenshots
- Explicit PNG format and disable animations for better quality
- Wait for fonts to load before capturing screenshots
This commit is contained in:
2025-12-07 16:55:09 +10:00
parent 151ea1c6ac
commit 47741faaae
4 changed files with 57 additions and 28 deletions
+1
View File
@@ -95,6 +95,7 @@ public class BrowserService : IBrowserService
return await browser.NewContextAsync(new BrowserNewContextOptions
{
ViewportSize = new ViewportSize { Width = 1920, Height = 1080 },
DeviceScaleFactor = 2.0f, // 2x scale for higher resolution screenshots (better text quality)
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
Locale = "en-AU",
TimezoneId = _timezone,
+27 -23
View File
@@ -414,12 +414,18 @@ public class ScrapingService : IScrapingService
await _debugService.SaveStepDebugAsync(debugFolder, 10, "scrubber_at_position_0", page, consoleMessages, networkRequests, cancellationToken);
// Step 11: Extract metadata and frame information
// Step 11: Wait for frame 0 tiles to fully load before calculating bounding box
// This ensures the map viewport is stable and prevents jiggle between frames
_logger.LogInformation("Waiting for frame 0 tiles to fully render");
await page.WaitForTimeoutAsync(_tileRenderWaitMs);
// Step 12: Extract metadata and frame information
_logger.LogInformation("Extracting metadata and frame information");
var lastUpdatedInfo = await _timeParsingService.ExtractLastUpdatedInfoAsync(page);
var frameInfo = await ExtractFrameInfoAsync(page);
// Step 12: Get map container and calculate bounding box once
// Step 13: Get map container and calculate bounding box once
// Calculate after frame 0 tiles are loaded to ensure consistent viewport
_logger.LogInformation("Preparing map container for screenshot");
var mapContainer = page.Locator(".esri-view-surface").First;
await mapContainer.WaitForAsync(new LocatorWaitForOptions { Timeout = 10000 });
@@ -445,11 +451,11 @@ public class ScrapingService : IScrapingService
Height = boundingBox.Height
};
// Step 13: Use provided cache folder (already created by BomRadarService)
// Step 14: Use provided cache folder (already created by BomRadarService)
Directory.CreateDirectory(cacheFolderPath);
_logger.LogInformation("Using cache folder: {Path}", cacheFolderPath);
// Step 14-20: Capture all 7 frames
// Step 15-21: Capture all 7 frames
var frames = new List<RadarFrame>();
var stepForwardButton = page.Locator("button[data-testid='bom-scrub-utils__right__step-forward']").First;
@@ -485,7 +491,7 @@ public class ScrapingService : IScrapingService
frameIndex, framePath, minutesAgo.Value);
// Save debug screenshot BEFORE clicking step forward
await _debugService.SaveStepDebugAsync(debugFolder, 14 + frameIndex, $"frame_{frameIndex}_captured", page, consoleMessages, networkRequests, cancellationToken);
await _debugService.SaveStepDebugAsync(debugFolder, 15 + frameIndex, $"frame_{frameIndex}_captured", page, consoleMessages, networkRequests, cancellationToken);
// If not the last frame, click step forward to prepare for next frame
if (frameIndex < 6)
@@ -521,26 +527,11 @@ public class ScrapingService : IScrapingService
_logger.LogInformation("All 7 frames captured successfully");
// Step 21: Save metadata and frame information
// Step 22: Save metadata and frame information
await _cacheService.SaveMetadataAsync(cacheFolderPath, lastUpdatedInfo, cancellationToken);
await _cacheService.SaveFramesMetadataAsync(cacheFolderPath, frames, cancellationToken);
// Remove lock file to indicate cache folder is complete
var lockFilePath = FilePathHelper.GetCacheLockFilePath(cacheFolderPath);
try
{
if (File.Exists(lockFilePath))
{
File.Delete(lockFilePath);
_logger.LogDebug("Removed cache lock file: {Path}", lockFilePath);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to remove cache lock file: {Path}", lockFilePath);
}
// Step 22: Return response with all frames
// Step 23: Return response with all frames
// Note: ScrapingService doesn't have access to cache management check interval
// Default to 5 minutes (standard check interval)
return ResponseBuilder.CreateRadarResponse(cacheFolderPath, frames, lastUpdatedInfo, suburb, state, cacheIsValid: true, cacheExpiresAt: null, isUpdating: false, cacheManagementCheckIntervalMinutes: 5);
@@ -728,10 +719,23 @@ public class ScrapingService : IScrapingService
cropArea = containerClip;
}
// Wait for fonts to be loaded to prevent text rendering artifacts
try
{
await page.WaitForLoadStateAsync(LoadState.NetworkIdle, new PageWaitForLoadStateOptions { Timeout = 5000 });
}
catch
{
// Continue if network idle timeout - fonts may already be loaded
}
// Take high-quality screenshot with explicit PNG format and disabled animations
await page.ScreenshotAsync(new PageScreenshotOptions
{
Path = outputPath,
Clip = cropArea
Clip = cropArea,
Type = ScreenshotType.Png, // Explicit PNG for lossless quality
Animations = ScreenshotAnimations.Disabled // Disable animations to prevent artifacts
});
_logger.LogDebug("Screenshot saved: {Path} (crop: {X},{Y} {Width}x{Height})",
+28 -4
View File
@@ -80,13 +80,18 @@
overflow: hidden;
margin-bottom: 20px;
aspect-ratio: 16/9;
min-height: 400px; /* Prevent layout shift */
}
.radar-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
display: block;
image-rendering: crisp-edges; /* Prevent blurry scaling */
}
.loading {
@@ -732,6 +737,9 @@
</button>
`).join('');
// Preload all frame images to prevent jiggle when switching
preloadImages(frames);
// Show first frame
if (frames.length > 0) {
showFrame(0);
@@ -745,6 +753,14 @@
updateRelativeTimes();
}
// Preload all frame images to prevent jiggle
function preloadImages(frames) {
frames.forEach(frame => {
const img = new Image();
img.src = frame.imageUrl;
});
}
// Show specific frame
function showFrame(index) {
if (index < 0 || index >= frames.length) return;
@@ -756,13 +772,21 @@
const imgEl = document.getElementById('radar-image');
const loadingEl = document.getElementById('loading');
imgEl.onload = () => {
// Check if image is already loaded (from preload)
const preloadedImg = new Image();
preloadedImg.onload = () => {
// Image is ready, switch immediately to prevent jiggle
imgEl.src = frame.imageUrl;
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
loadingEl.style.display = 'none';
imgEl.style.display = 'block';
};
imgEl.src = frame.imageUrl;
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
preloadedImg.onerror = () => {
// Fallback if preload failed
imgEl.src = frame.imageUrl;
imgEl.alt = `Radar frame ${frame.frameIndex} (${frame.minutesAgo} minutes ago)`;
};
preloadedImg.src = frame.imageUrl;
// Update frame info
document.getElementById('frame-info').textContent =
+1 -1
View File
@@ -29,7 +29,7 @@ services:
- CACHECLEANUP__INTERVALHOURS=${CACHECLEANUP__INTERVALHOURS:-1}
- SCREENSHOT__DYNAMICCONTENTWAITMS=${SCREENSHOT__DYNAMICCONTENTWAITMS:-2000}
- SCREENSHOT__TILERENDERWAITMS=${SCREENSHOT__TILERENDERWAITMS:-5000}
- DEBUG__ENABLED=${DEBUG__ENABLED:-false}
- DEBUG__ENABLED=${DEBUG__ENABLED:-true}
- DEBUG__WAITMS=${DEBUG__WAITMS:-2000}
- TIMEZONE=${TIMEZONE:-Australia/Brisbane}
shm_size: '1gb'