diff --git a/server/scripts/index.mjs b/server/scripts/index.mjs index 8878ddc..bd7eea7 100644 --- a/server/scripts/index.mjs +++ b/server/scripts/index.mjs @@ -89,6 +89,7 @@ const init = () => { const query = parsedParameters.latLonQuery ?? localStorage.getItem('latLonQuery'); const latLon = parsedParameters.latLon ?? localStorage.getItem('latLon'); const fromGPS = localStorage.getItem('latLonFromGPS') && !loadFromParsed; + if (query && latLon && !fromGPS) { const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR); txtAddress.value = query; @@ -171,16 +172,29 @@ const btnFullScreenClick = () => { return false; }; -const enterFullScreen = () => { +// This is async because modern browsers return a Promise from requestFullscreen +const enterFullScreen = async () => { const element = document.querySelector('#divTwc'); // Supports most browsers and their versions. - const requestMethod = element.requestFullScreen || element.webkitRequestFullScreen - || element.mozRequestFullScreen || element.msRequestFullscreen; + const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen + || element.mozRequestFullscreen || element.msRequestFullscreen; if (requestMethod) { - // Native full screen. - requestMethod.call(element, { navigationUI: 'hide' }); + try { + // Native full screen with options for optimal display + await requestMethod.call(element, { + navigationUI: 'hide', + allowsInlineMediaPlayback: true, + }); + + // Allow a moment for fullscreen to engage, then optimize + setTimeout(() => { + resize(); + }, 100); + } catch (error) { + console.error('❌ Fullscreen request failed:', error); + } } else { // iOS doesn't support FullScreen API. window.scrollTo(0, 0); @@ -202,8 +216,8 @@ const exitFullscreen = () => { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); + } else if (document.mozCancelFullscreen) { + document.mozCancelFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } diff --git a/server/scripts/modules/navigation.mjs b/server/scripts/modules/navigation.mjs index 40e110e..23bfafd 100644 --- a/server/scripts/modules/navigation.mjs +++ b/server/scripts/modules/navigation.mjs @@ -216,11 +216,15 @@ const setPlaying = (newValue) => { localStorage.setItem('play', playing); if (playing) { - noSleep(true); + noSleep(true).catch(() => { + // Wake lock failed, but continue normally + }); playButton.title = 'Pause'; playButton.src = 'images/nav/ic_pause_white_24dp_2x.png'; } else { - noSleep(false); + noSleep(false).catch(() => { + // Wake lock disable failed, but continue normally + }); playButton.title = 'Play'; playButton.src = 'images/nav/ic_play_arrow_white_24dp_2x.png'; } @@ -272,15 +276,24 @@ const getDisplay = (index) => displays[index]; // resize the container on a page resize const resize = () => { + // Check for display optimization opportunities before applying zoom + const displayInfo = getDisplayInfo(); + const targetWidth = settings.wide.value ? 640 + 107 + 107 : 640; const widthZoomPercent = (document.querySelector('#divTwcBottom').getBoundingClientRect().width) / targetWidth; const heightZoomPercent = (window.innerHeight) / 480; const scale = Math.min(widthZoomPercent, heightZoomPercent); - if (scale < 1.0 || document.fullscreenElement || settings.kiosk) { + const { isKioskLike } = displayInfo; + + if (scale < 1.0 || isKioskLike) { document.querySelector('#container').style.zoom = scale; + // Apply scanline scaling for low-resolution displays and kiosk mode + applyScanlineScaling(scale); } else { document.querySelector('#container').style.zoom = 'unset'; + // Reset scanline scaling + applyScanlineScaling(1.0); } }; @@ -289,6 +302,678 @@ const resetStatuses = () => { displays.forEach((display) => { display.status = STATUS.loading; }); }; +// Enhanced kiosk detection with automatic fullscreen optimization +const getDisplayInfo = () => { + const isKiosk = settings.kiosk?.value || false; + const isFullscreen = !!document.fullscreenElement; + const isKioskLike = isKiosk || isFullscreen || (window.innerHeight >= window.screen.height - 10); + + return { isKiosk, isFullscreen, isKioskLike }; +}; + +// Make function globally available for debugging +window.getDisplayInfo = getDisplayInfo; + +// Apply dynamic scanline scaling based on zoom level +const applyScanlineScaling = (zoomScale) => { + // Only apply if scanlines are enabled + const container = document.querySelector('#container'); + if (!container || !container.classList.contains('scanlines')) { + return; + } + + // Get display and viewport information + const displayWidth = window.screen.width; + const displayHeight = window.screen.height; + const devicePixelRatio = window.devicePixelRatio || 1; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const isFullscreen = !!document.fullscreenElement; + const isKiosk = settings.kiosk?.value || false; + const isKioskLike = isKiosk || isFullscreen || (window.innerHeight >= window.screen.height - 10); + + // Check for sub-pixel rendering issues + const effectiveScanlineHeight = 1 * zoomScale * devicePixelRatio; + const willCauseAliasing = effectiveScanlineHeight < 1.0 || (effectiveScanlineHeight % 1 !== 0); + + // Calculate optimal scanline thickness + let scanlineScale = 1; + let scalingReason = 'default'; + + // Primary strategy: Ensure scanlines render as whole pixels + if (willCauseAliasing) { + if (zoomScale > 1.0) { + // Upscaling scenario (like 1024x768 → 1.6x zoom) + const targetThickness = Math.ceil(1 / zoomScale); + scanlineScale = Math.max(1, targetThickness); + scalingReason = 'upscaling aliasing prevention'; + } else { + // Downscaling scenario + scanlineScale = Math.ceil(1 / zoomScale); + scalingReason = 'downscaling aliasing prevention'; + } + } + + // Specific display-based adjustments + if (displayWidth <= 1024 && displayHeight <= 768 && devicePixelRatio < 2) { + if (zoomScale > 1.4) { + scanlineScale = Math.max(scanlineScale, Math.round(1 / zoomScale * 2)); + scalingReason = '1024x768 high upscaling compensation'; + } else { + scanlineScale = Math.max(scanlineScale, 1); + scalingReason = '1024x768 display optimization'; + } + } + + // Override for kiosk/fullscreen mode with specific viewport dimensions + if (isKioskLike && ( + Math.abs(zoomScale - 1.598) < 0.05 // More flexible zoom detection for 1024x768 scenarios + || (viewportWidth === 1023 && viewportHeight === 767) // Exact Chrome kiosk viewport + || (viewportWidth === 1024 && viewportHeight === 768) // Perfect viewport + )) { + // Kiosk mode optimization for 1024x768 displays + // Use optimal scanlines that render as exactly 2px with no banding + if (viewportWidth === 1023 && viewportHeight === 767) { + // For the exact 1023x767 Chrome kiosk viewport + // Calculate precise thickness for exactly 2px rendering + const targetRendered = 2.0; + scanlineScale = targetRendered / zoomScale; // This gives us exactly 2px + scalingReason = 'Chrome kiosk 1023x767 - optimal 2px scanlines'; + } else { + // For 1024x768 or similar zoomed scenarios + scanlineScale = 1.25; // Standard 2px optimization + scalingReason = 'Kiosk/fullscreen 1024x768 - optimal 2px scanlines'; + } + } + + // Calculate precise thickness to avoid sub-pixel rendering + let preciseThickness = scanlineScale; + let backgroundSize = scanlineScale * 2; + + // For upscaling scenarios, try to make the final rendered size a whole number + // BUT skip this if we already have a specific override for the zoom level + if (zoomScale > 1.0 && willCauseAliasing && !scalingReason.includes('optimal') && !scalingReason.includes('Kiosk')) { + const targetRenderedHeight = Math.round(effectiveScanlineHeight); + preciseThickness = targetRenderedHeight / zoomScale / devicePixelRatio; + backgroundSize = preciseThickness * 2; + } + + // Apply dynamic styles with fractional pixel compensation + let styleElement = document.getElementById('dynamic-scanlines'); + if (!styleElement) { + styleElement = document.createElement('style'); + styleElement.id = 'dynamic-scanlines'; + document.head.appendChild(styleElement); + } + + const cssRules = ` + .scanlines:before { + height: ${preciseThickness}px !important; + image-rendering: pixelated !important; + image-rendering: crisp-edges !important; + } + .scanlines:after { + background-size: 100% ${backgroundSize}px !important; + image-rendering: pixelated !important; + image-rendering: crisp-edges !important; + } + `; + + styleElement.textContent = cssRules; + + // Only log when optimal kiosk mode is applied (minimize debug output) + if (scalingReason.includes('optimal') && !window.scanlineLoggedOnce) { + console.log(`Scanlines: ${preciseThickness}px (${scalingReason})`); + window.scanlineLoggedOnce = true; + } +}; + +// Debug function for scanlines +// All these can be called from browser console. +// Leaving them here for now, but they can potentially be removed later. +// Function to request perfect fullscreen for optimal display +const requestPerfectFullscreen = async () => { + const element = document.querySelector('#divTwc'); + + try { + // Use the Fullscreen API to get perfect viewport control + const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen + || element.mozRequestFullScreen || element.msRequestFullscreen; + + if (requestMethod) { + // Request fullscreen with minimal logging + await requestMethod.call(element, { + navigationUI: 'hide', + // Request specific fullscreen options if supported + allowsInlineMediaPlayback: true, + }); + + // Allow a moment for fullscreen to engage + setTimeout(() => { + // Re-trigger resize to apply optimal scaling + resize(); + + // Apply scanline scaling based on new dimensions + const container = document.querySelector('#container'); + const zoomScale = parseFloat(container.style.zoom) || 1; + applyScanlineScaling(zoomScale); + }, 100); + + return true; + } + console.warn('Fullscreen API not supported'); + return false; + } catch (error) { + console.error('Failed to request fullscreen:', error); + return false; + } +}; + +// Make function globally available for debugging +window.requestPerfectFullscreen = requestPerfectFullscreen; + +const debugScanlines = () => { + console.group('Manual Scanlines Debug'); + + const container = document.querySelector('#container'); + if (!container) { + console.error('Container element not found'); + console.groupEnd(); + return { error: 'Container element not found' }; + } + + const hasScanlinesClass = container.classList.contains('scanlines'); + const containerRect = container.getBoundingClientRect(); + const currentZoom = parseFloat(container.style.zoom) || 1; + + console.log(`Scanlines class present: ${hasScanlinesClass}`); + console.log(`Container dimensions: ${containerRect.width.toFixed(2)}x${containerRect.height.toFixed(2)}`); + console.log(`Current zoom: ${currentZoom}`); + + const debugInfo = { + hasScanlinesClass, + containerDimensions: { + width: containerRect.width, + height: containerRect.height, + left: containerRect.left, + top: containerRect.top, + }, + currentZoom, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + screen: { + width: window.screen.width, + height: window.screen.height, + }, + devicePixelRatio: window.devicePixelRatio || 1, + isFullscreen: !!document.fullscreenElement, + }; + + if (hasScanlinesClass) { + console.log(`Triggering applyScanlineScaling with zoom: ${currentZoom}`); + applyScanlineScaling(currentZoom); + + // Check if dynamic styles exist + const dynamicStyle = document.getElementById('dynamic-scanlines'); + if (dynamicStyle) { + console.log('Current dynamic CSS:', dynamicStyle.textContent); + debugInfo.dynamicCSS = dynamicStyle.textContent; + } else { + console.log('No dynamic scanlines styles found'); + debugInfo.dynamicCSS = null; + } + + // Get computed styles for scanlines + const beforeStyle = window.getComputedStyle(container, ':before'); + const afterStyle = window.getComputedStyle(container, ':after'); + + const computedStyles = { + before: { + height: beforeStyle.height, + background: beforeStyle.background, + opacity: beforeStyle.opacity, + imageRendering: beforeStyle.imageRendering, + }, + after: { + backgroundSize: afterStyle.backgroundSize, + backgroundImage: afterStyle.backgroundImage, + opacity: afterStyle.opacity, + imageRendering: afterStyle.imageRendering, + }, + }; + + console.log('Computed :before styles:'); + console.log(' height:', computedStyles.before.height); + console.log(' background:', computedStyles.before.background); + console.log(' opacity:', computedStyles.before.opacity); + console.log(' image-rendering:', computedStyles.before.imageRendering); + + console.log('Computed :after styles:'); + console.log(' background-size:', computedStyles.after.backgroundSize); + console.log(' background-image:', computedStyles.after.backgroundImage); + console.log(' opacity:', computedStyles.after.opacity); + console.log(' image-rendering:', computedStyles.after.imageRendering); + + debugInfo.computedStyles = computedStyles; + } + + console.groupEnd(); + return debugInfo; +}; + +// Make debug function globally available +window.debugScanlines = debugScanlines; + +// Test function to manually set scanline scale - can be called from browser console +const testScanlineScale = (scale) => { + console.log(`Testing scanline scale: ${scale}x`); + + let styleElement = document.getElementById('dynamic-scanlines'); + if (!styleElement) { + styleElement = document.createElement('style'); + styleElement.id = 'dynamic-scanlines'; + document.head.appendChild(styleElement); + } + + const cssRules = ` + .scanlines:before { + height: ${scale}px !important; + image-rendering: pixelated !important; + image-rendering: crisp-edges !important; + } + .scanlines:after { + background-size: 100% ${scale * 2}px !important; + image-rendering: pixelated !important; + image-rendering: crisp-edges !important; + } + `; + + styleElement.textContent = cssRules; + + // Calculate what this will look like when rendered + const container = document.querySelector('#container'); + const zoom = parseFloat(container?.style.zoom) || 1; + const expectedRendered = scale * zoom; + const isWholePixel = Math.abs(expectedRendered % 1) < 0.01; + + const result = { + appliedScale: scale, + backgroundSize: scale * 2, + currentZoom: zoom, + expectedRendered, + isWholePixel, + cssRules: cssRules.trim(), + }; + + console.log(`Applied ${scale}px scanline height with ${scale * 2}px background-size`); + console.log(`Expected rendered height: ${expectedRendered.toFixed(4)}px`); + console.log(`Will render as whole pixels: ${isWholePixel}`); + + return result; +}; + +// Make test function globally available +window.testScanlineScale = testScanlineScale; + +// Test function for precise fractional values to eliminate banding +const testPreciseScanlines = () => { + const container = document.querySelector('#container'); + const zoom = parseFloat(container?.style.zoom) || 1; + + console.group('Testing Precise Scanline Values'); + console.log(`Current zoom: ${zoom.toFixed(4)}`); + + // Test values that should result in whole pixel rendering + const testValues = [ + 0.625, // Should render as 1px (0.625 * 1.598 ≈ 1.0) + 1.25, // Should render as 2px (1.25 * 1.598 ≈ 2.0) + 1.875, // Should render as 3px (1.875 * 1.598 ≈ 3.0) + 2.5, // Should render as 4px (2.5 * 1.598 ≈ 4.0) + ]; + + const results = testValues.map((value) => { + const rendered = value * zoom; + const isWholePixel = Math.abs(rendered % 1) < 0.01; + const result = { + inputValue: value, + renderedValue: rendered, + isWholePixel, + fractionalPart: rendered % 1, + }; + console.log(`Test ${value}px → ${rendered.toFixed(4)}px rendered (${isWholePixel ? '✅ whole' : '❌ fractional'})`); + return result; + }); + + console.log('Use testScanlineScale(value) to try these values'); + console.groupEnd(); + + return { + currentZoom: zoom, + testResults: results, + recommendation: 'Use testScanlineScale(value) to apply a specific value', + }; +}; + +// Make precise test function globally available +window.testPreciseScanlines = testPreciseScanlines; + +// Function to analyze container dimension issues +const analyzeContainerDimensions = () => { + const container = document.querySelector('#container'); + if (!container) { + return { error: 'Container not found' }; + } + + const containerRect = container.getBoundingClientRect(); + const containerStyle = window.getComputedStyle(container); + const { parentElement } = container; + const parentRect = parentElement ? parentElement.getBoundingClientRect() : null; + const parentStyle = parentElement ? window.getComputedStyle(parentElement) : null; + + const analysis = { + container: { + rect: { + width: containerRect.width, + height: containerRect.height, + left: containerRect.left, + top: containerRect.top, + }, + computedStyle: { + width: containerStyle.width, + height: containerStyle.height, + padding: containerStyle.padding, + margin: containerStyle.margin, + border: containerStyle.border, + boxSizing: containerStyle.boxSizing, + zoom: containerStyle.zoom, + transform: containerStyle.transform, + }, + }, + parent: parentRect ? { + rect: { + width: parentRect.width, + height: parentRect.height, + left: parentRect.left, + top: parentRect.top, + }, + computedStyle: { + width: parentStyle.width, + height: parentStyle.height, + padding: parentStyle.padding, + margin: parentStyle.margin, + border: parentStyle.border, + boxSizing: parentStyle.boxSizing, + }, + } : null, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + screen: { + width: window.screen.width, + height: window.screen.height, + }, + devicePixelRatio: window.devicePixelRatio || 1, + isFullscreen: !!document.fullscreenElement, + }; + + console.group('Container Dimension Analysis'); + console.log('Container Rect:', analysis.container.rect); + console.log('Container Computed Style:', analysis.container.computedStyle); + if (analysis.parent) { + console.log('Parent Rect:', analysis.parent.rect); + console.log('Parent Computed Style:', analysis.parent.computedStyle); + } + console.log('Viewport:', analysis.viewport); + console.log('Screen:', analysis.screen); + + // Check for fractional dimension causes + const expectedTargetWidth = 640; // Base width + const expectedTargetHeight = 480; // Base height + const actualScale = Math.min(analysis.viewport.width / expectedTargetWidth, analysis.viewport.height / expectedTargetHeight); + const fractionalWidth = analysis.container.rect.width % 1; + const fractionalHeight = analysis.container.rect.height % 1; + + console.log(`Expected scale: ${actualScale.toFixed(4)}`); + console.log(`Fractional width: ${fractionalWidth.toFixed(4)}px`); + console.log(`Fractional height: ${fractionalHeight.toFixed(4)}px`); + console.log(`Width is fractional: ${fractionalWidth > 0.01}`); + console.log(`Height is fractional: ${fractionalHeight > 0.01}`); + + analysis.scaling = { + expectedScale: actualScale, + fractionalWidth, + fractionalHeight, + hasFractionalDimensions: fractionalWidth > 0.01 || fractionalHeight > 0.01, + }; + + console.groupEnd(); + return analysis; +}; + +// Make container analysis function globally available +window.analyzeContainerDimensions = analyzeContainerDimensions; + +// Function to calculate optimal scanline thickness that eliminates fractional rendering +const calculateOptimalScanlineThickness = (targetZoom = null) => { + const container = document.querySelector('#container'); + if (!container) { + return { error: 'Container not found' }; + } + + const currentZoom = targetZoom || parseFloat(container.style.zoom) || 1; + const devicePixelRatio = window.devicePixelRatio || 1; + + console.group('Calculating Optimal Scanline Thickness'); + console.log(`Current zoom: ${currentZoom.toFixed(4)}`); + console.log(`Device pixel ratio: ${devicePixelRatio}`); + + // Calculate possible thickness values that result in whole pixel rendering + const candidates = []; + + // Test thickness values from 0.1 to 3.0 in 0.001 increments + for (let thickness = 0.1; thickness <= 3.0; thickness += 0.001) { + const renderedHeight = thickness * currentZoom * devicePixelRatio; + const fractionalPart = renderedHeight % 1; + + // If the rendered height is very close to a whole number + if (fractionalPart < 0.001 || fractionalPart > 0.999) { + const wholePixelHeight = Math.round(renderedHeight); + candidates.push({ + thickness: Math.round(thickness * 1000) / 1000, // Round to 3 decimal places + renderedHeight: wholePixelHeight, + actualRendered: renderedHeight, + error: Math.abs(renderedHeight - wholePixelHeight), + }); + } + } + + // Sort by error (closest to whole pixel) and prefer reasonable thickness values + candidates.sort((a, b) => { + if (Math.abs(a.error - b.error) < 0.0001) { + // If errors are similar, prefer thickness closer to 1 + return Math.abs(a.thickness - 1) - Math.abs(b.thickness - 1); + } + return a.error - b.error; + }); + + // Take the best candidates for different pixel heights + const recommendations = []; + const seenHeights = new Set(); + + candidates.some((candidate) => { + if (!seenHeights.has(candidate.renderedHeight) && recommendations.length < 5) { + seenHeights.add(candidate.renderedHeight); + recommendations.push(candidate); + } + return recommendations.length >= 5; // Stop when we have 5 recommendations + }); + + console.log('Recommendations:'); + recommendations.forEach((rec, index) => { + console.log(`${index + 1}. ${rec.thickness}px → ${rec.renderedHeight}px (error: ${rec.error.toFixed(6)})`); + }); + + const result = { + currentZoom, + devicePixelRatio, + recommendations, + bestRecommendation: recommendations[0] || null, + }; + + if (result.bestRecommendation) { + console.log(`Best recommendation: ${result.bestRecommendation.thickness}px`); + console.log(` Will render as: ${result.bestRecommendation.renderedHeight}px`); + console.log(` Use: testScanlineScale(${result.bestRecommendation.thickness})`); + } + + console.groupEnd(); + return result; +}; + +// Make optimal calculation function globally available +window.calculateOptimalScanlineThickness = calculateOptimalScanlineThickness; + +// Function to analyze viewport and provide fullscreen optimization recommendations +const analyzeViewportOptimization = () => { + const viewport = { + width: window.innerWidth, + height: window.innerHeight, + screen: { + width: window.screen.width, + height: window.screen.height, + }, + devicePixelRatio: window.devicePixelRatio || 1, + isFullscreen: !!document.fullscreenElement, + isKiosk: settings.kiosk?.value || false, + }; + + // Check for fractional viewport dimensions + const hasFractionalViewport = (viewport.width % 1 !== 0) || (viewport.height % 1 !== 0); + + // Check for common kiosk viewport sizes + const isKnownKioskSize = ( + (viewport.width === 1023 && viewport.height === 767) // Common Chrome kiosk issue + || (viewport.width === 1024 && viewport.height === 768) // Perfect kiosk size + ); + + // Minimize debug output for production use + if (window.debugMode) { + console.group('Viewport Optimization Analysis'); + console.log('Current viewport:', `${viewport.width}x${viewport.height}`); + console.log('Screen resolution:', `${viewport.screen.width}x${viewport.screen.height}`); + console.log('Device pixel ratio:', viewport.devicePixelRatio); + console.log('Has fractional viewport:', hasFractionalViewport); + console.log('Is known kiosk size:', isKnownKioskSize); + console.log('Is fullscreen:', viewport.isFullscreen); + console.log('Is kiosk mode:', viewport.isKiosk); + } + + // Kiosk-specific analysis + const recommendations = []; + + if (viewport.isKiosk && isKnownKioskSize) { + if (viewport.width === 1023 && viewport.height === 767) { + recommendations.push('Detected 1023x767 kiosk viewport - using calculated optimal scanlines for perfect 2px rendering'); + } else if (viewport.width === 1024 && viewport.height === 768) { + recommendations.push('Perfect 1024x768 kiosk viewport detected - optimal scanlines will be applied'); + } + } else if (viewport.isKiosk && hasFractionalViewport) { + recommendations.push('Custom kiosk viewport detected - scanlines will be optimized for exact dimensions'); + } + + // Calculate what the zoom scale would be with current dimensions + const targetWidth = settings.wide?.value ? 640 + 107 + 107 : 640; + const targetHeight = 480; + + const currentWidthRatio = viewport.width / targetWidth; + const currentHeightRatio = viewport.height / targetHeight; + const currentScale = Math.min(currentWidthRatio, currentHeightRatio); + + // Calculate scanline rendering for current setup + const currentScanlineHeight = 1 * currentScale * viewport.devicePixelRatio; + const willCauseAliasing = currentScanlineHeight < 1.0 || (currentScanlineHeight % 1 !== 0); + + if (window.debugMode) { + console.log('Scaling Analysis:'); + console.log(` Current scale: ${currentScale.toFixed(6)}`); + console.log(` Base scanline rendering: ${currentScanlineHeight.toFixed(6)}px`); + console.log(` Will cause aliasing: ${willCauseAliasing}`); + + if (viewport.isKiosk && isKnownKioskSize) { + // Calculate what our optimal scanline thickness would be + const targetRendered = 2.0; // We want 2px scanlines + const optimalThickness = targetRendered / (currentScale * viewport.devicePixelRatio); + console.log(`Optimal scanline thickness: ${optimalThickness.toFixed(6)}px`); + console.log(`Expected rendered height: ${(optimalThickness * currentScale * viewport.devicePixelRatio).toFixed(6)}px`); + } + + if (recommendations.length > 0) { + console.log('Kiosk Optimization Status:'); + recommendations.forEach((rec) => console.log(` • ${rec}`)); + } else if (viewport.isKiosk) { + console.log('Custom kiosk configuration - using automatic optimization'); + } else { + console.log('Not in kiosk mode - standard scaling applies'); + } + + console.groupEnd(); + } + + return { + viewport, + hasFractionalViewport, + isKnownKioskSize, + recommendations, + scaling: { + current: currentScale, + scanlineRendering: currentScanlineHeight, + willCauseAliasing, + }, + }; +}; + +// Make function globally available for debugging +window.analyzeViewportOptimization = analyzeViewportOptimization; + +// Function to test fullscreen API capabilities +const testFullscreenCapabilities = () => { + const element = document.querySelector('#divTwc'); + + console.group('Fullscreen API Test'); + + const capabilities = { + requestFullscreen: !!element.requestFullscreen, + webkitRequestFullscreen: !!element.webkitRequestFullscreen, + mozRequestFullScreen: !!element.mozRequestFullScreen, + msRequestFullscreen: !!element.msRequestFullscreen, + fullscreenEnabled: !!document.fullscreenEnabled, + currentlyFullscreen: !!document.fullscreenElement, + }; + + console.log('API Support:', capabilities); + + // Determine the best method + const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen + || element.mozRequestFullScreen || element.msRequestFullscreen; + + if (requestMethod) { + console.log('Fullscreen API available'); + console.log('Can attempt programmatic fullscreen for viewport optimization'); + } else { + console.log('Fullscreen API not supported'); + } + + console.groupEnd(); + + return capabilities; +}; + +// Make function globally available for debugging +window.testFullscreenCapabilities = testFullscreenCapabilities; + // allow displays to register themselves const registerDisplay = (display) => { if (displays[display.navId]) console.warn(`Display nav ID ${display.navId} already in use`); diff --git a/server/scripts/modules/utils/nosleep.mjs b/server/scripts/modules/utils/nosleep.mjs index 53fe5f9..25125bc 100644 --- a/server/scripts/modules/utils/nosleep.mjs +++ b/server/scripts/modules/utils/nosleep.mjs @@ -7,12 +7,19 @@ const noSleep = (enable = false) => { // get a nosleep controller if (!noSleep.controller) noSleep.controller = new NoSleep(); // don't call anything if the states match - if (wakeLock === enable) return false; + if (wakeLock === enable) return Promise.resolve(false); // store the value wakeLock = enable; // call the function - if (enable) return noSleep.controller.enable(); - return noSleep.controller.disable(); + if (enable) { + return noSleep.controller.enable().catch((error) => { + // Handle wake lock request failures gracefully + console.warn('Wake lock request failed:', error.message); + wakeLock = false; + return false; + }); + } + return Promise.resolve(noSleep.controller.disable()); }; export default noSleep; diff --git a/server/styles/scss/shared/_scanlines.scss b/server/styles/scss/shared/_scanlines.scss index 70c664a..afed6ba 100644 --- a/server/styles/scss/shared/_scanlines.scss +++ b/server/styles/scss/shared/_scanlines.scss @@ -1,7 +1,8 @@ /* REGULAR SCANLINES SETTINGS */ -// width of 1 scanline (min.: 1px) +// width of 1 scanline (responsive units to prevent banding) $scan-width: 1px; +$scan-width-scaled: 0.15vh; // viewport-relative unit for better scaling // emulates a damage-your-eyes bad pre-2000 CRT screen ♥ (true, false) $scan-crt: false; @@ -75,7 +76,7 @@ $scan-opacity: .75; @include scan-moving($scan-moving-line); } - // the scanlines, so! + // the scanlines, so! - with responsive scaling for low-res displays &:after { top: 0; right: 0; @@ -87,6 +88,57 @@ $scan-opacity: .75; $scan-color 51%); background-size: 100% $scan-width*2; @include scan-crt($scan-crt); + + // Prevent sub-pixel aliasing on scaled displays + image-rendering: crisp-edges; + image-rendering: pixelated; + } + + // Responsive scanlines for different display scenarios + + // High DPI displays - use original sizing + @media (-webkit-min-device-pixel-ratio: 2), + (min-resolution: 192dpi) { + &:before { + height: $scan-width; + } + + &:after { + background-size: 100% calc($scan-width * 2); + } + } + + // Medium resolution displays (1024x768 and similar) + @media (max-width: 1200px) and (max-height: 900px) and (-webkit-max-device-pixel-ratio: 1.5) { + &:before { + height: calc($scan-width * 1.5); + } + + &:after { + background-size: 100% calc($scan-width * 3); + } + } + + // Low resolution displays - increase thickness to prevent banding + @media (max-width: 1024px) and (max-height: 768px) { + &:before { + height: calc($scan-width * 2); + } + + &:after { + background-size: 100% calc($scan-width * 4); + } + } + + // Very low resolution displays + @media (max-width: 800px) and (max-height: 600px) { + &:before { + height: calc($scan-width * 3); + } + + &:after { + background-size: 100% calc($scan-width * 6); + } } } @@ -103,4 +155,4 @@ $scan-opacity: .75; background-position: 0 50%; // bottom: 0%; // to have a continuous scanline move, use this line (here in 0% step) instead of transform and write, in &:before, { position: absolute; bottom: 100%; } } -} \ No newline at end of file +}