mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 07:39:29 -07:00
Add comprehensive responsive scanline scaling system with anti-aliasing
- Attempt pixel-perfect scanline rendering for scaled displays and zoom scenarios - Implement dynamic scanline thickness calculation to prevent sub-pixel rendering issues - Add enhanced kiosk detection via isKioskLike for better fullscreen optimization - Optimize scanlines for specific kiosk resolutions (1024x768, 1023x767) - Add responsive SCSS media queries for different display densities - Include extensive debugging utilities for scanline troubleshooting - Improve noSleep error handling with proper promise rejection handling - Update to modern fullscreen API method names - Add async/await error handling for fullscreen requests - Trigger resize after fullscreen engagement to apply optimal scaling
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user