mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
- Prevent infinitie recursion by ensuring the same display is not
selected in loadDisplay()
1063 lines
35 KiB
JavaScript
1063 lines
35 KiB
JavaScript
// navigation handles progress, next/previous and initial load messages from the parent frame
|
|
import noSleep from './utils/nosleep.mjs';
|
|
import STATUS from './status.mjs';
|
|
import { wrap } from './utils/calc.mjs';
|
|
import { safeJson } from './utils/fetch.mjs';
|
|
import { getPoint } from './utils/weather.mjs';
|
|
import settings from './settings.mjs';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
init();
|
|
});
|
|
|
|
const displays = [];
|
|
let playing = false;
|
|
let progress;
|
|
const weatherParameters = {};
|
|
|
|
const init = async () => {
|
|
// set up resize handler
|
|
window.addEventListener('resize', resize);
|
|
resize();
|
|
|
|
generateCheckboxes();
|
|
};
|
|
|
|
const message = (data) => {
|
|
// dispatch event
|
|
if (!data.type) return false;
|
|
if (data.type === 'navButton') return handleNavButton(data.message);
|
|
return console.error(`Unknown event ${data.type}`);
|
|
};
|
|
|
|
const getWeather = async (latLon, haveDataCallback) => {
|
|
// get initial weather data
|
|
const point = await getPoint(latLon.lat, latLon.lon);
|
|
|
|
// check if point data was successfully retrieved
|
|
if (!point) {
|
|
return;
|
|
}
|
|
|
|
if (typeof haveDataCallback === 'function') haveDataCallback(point);
|
|
|
|
try {
|
|
// get stations using centralized safe handling
|
|
const stations = await safeJson(point.properties.observationStations);
|
|
|
|
if (!stations) {
|
|
console.warn('Failed to get Observation Stations');
|
|
return;
|
|
}
|
|
|
|
// check if stations data is valid
|
|
if (!stations || !stations.features || stations.features.length === 0) {
|
|
console.warn('No Observation Stations found for this location');
|
|
return;
|
|
}
|
|
|
|
const StationId = stations.features[0].properties.stationIdentifier;
|
|
|
|
let { city } = point.properties.relativeLocation.properties;
|
|
const { state } = point.properties.relativeLocation.properties;
|
|
|
|
if (StationId in StationInfo) {
|
|
city = StationInfo[StationId].city;
|
|
[city] = city.split('/');
|
|
city = city.replace(/\s+$/, '');
|
|
}
|
|
|
|
// populate the weather parameters
|
|
weatherParameters.latitude = latLon.lat;
|
|
weatherParameters.longitude = latLon.lon;
|
|
weatherParameters.zoneId = point.properties.forecastZone.substr(-6);
|
|
weatherParameters.radarId = point.properties.radarStation.substr(-3);
|
|
weatherParameters.stationId = StationId;
|
|
weatherParameters.weatherOffice = point.properties.cwa;
|
|
weatherParameters.city = city;
|
|
weatherParameters.state = state;
|
|
weatherParameters.timeZone = point.properties.timeZone;
|
|
weatherParameters.forecast = point.properties.forecast;
|
|
weatherParameters.forecastGridData = point.properties.forecastGridData;
|
|
weatherParameters.stations = stations.features;
|
|
|
|
// update the main process for display purposes
|
|
populateWeatherParameters(weatherParameters);
|
|
|
|
// reset the scroll
|
|
postMessage({ type: 'current-weather-scroll', method: 'reload' });
|
|
|
|
// draw the progress canvas and hide others
|
|
hideAllCanvases();
|
|
if (!settings?.kiosk?.value) {
|
|
// In normal mode, hide loading screen and show progress
|
|
// (In kiosk mode, keep the loading screen visible until autoplay starts)
|
|
document.querySelector('#loading').style.display = 'none';
|
|
if (progress) {
|
|
await progress.drawCanvas();
|
|
progress.showCanvas();
|
|
}
|
|
}
|
|
|
|
// call for new data on each display
|
|
displays.forEach((display) => display.getData(weatherParameters));
|
|
} catch (error) {
|
|
console.error(`Failed to get weather data: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
// receive a status update from a module {id, value}
|
|
const updateStatus = (value) => {
|
|
if (value.id < 0) return;
|
|
if (!progress && !settings?.kiosk?.value) return;
|
|
|
|
if (progress) progress.drawCanvas(displays, countLoadedDisplays());
|
|
|
|
// first display is hazards and it must load before evaluating the first display
|
|
if (displays[0].status === STATUS.loading) return;
|
|
|
|
// calculate first enabled display
|
|
const firstDisplayIndex = displays.findIndex((display) => display?.enabled && display?.timing?.totalScreens > 0);
|
|
|
|
// value.id = 0 is hazards, if they fail to load hot-wire a new value.id to the current display to see if it needs to be loaded
|
|
// typically this plays out as current conditions loads, then hazards fails.
|
|
if (value.id === 0 && (value.status === STATUS.failed || value.status === STATUS.retrying)) {
|
|
value.id = firstDisplayIndex;
|
|
value.status = displays[firstDisplayIndex].status;
|
|
}
|
|
|
|
// if hazards data arrives after the firstDisplayIndex loads, then we need to hot wire this to the first display
|
|
if (value.id === 0 && value.status === STATUS.loaded && displays[0].timing.totalScreens === 0) {
|
|
value.id = firstDisplayIndex;
|
|
value.status = displays[firstDisplayIndex].status;
|
|
}
|
|
|
|
// if this is the first display and we're playing, load it up so it starts playing
|
|
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
|
|
navTo(msg.command.firstFrame);
|
|
}
|
|
};
|
|
|
|
// note: a display that is "still waiting"/"retrying" is considered loaded intentionally
|
|
// the weather.gov api has long load times for some products when you are the first
|
|
// requester for the product after the cache expires
|
|
const countLoadedDisplays = () => displays.reduce((acc, display) => {
|
|
if (display.status !== STATUS.loading) return acc + 1;
|
|
return acc;
|
|
}, 0);
|
|
|
|
const hideAllCanvases = () => {
|
|
displays.forEach((display) => display.hideCanvas());
|
|
};
|
|
|
|
// is playing interface
|
|
const isPlaying = () => playing;
|
|
|
|
// navigation message constants
|
|
const msg = {
|
|
response: { // display to navigation
|
|
previous: Symbol('previous'), // already at first frame, calling function should switch to previous canvas
|
|
inProgress: Symbol('inProgress'), // have data to display, calling function should do nothing
|
|
next: Symbol('next'), // end of frames reached, calling function should switch to next canvas
|
|
},
|
|
command: { // navigation to display
|
|
firstFrame: Symbol('firstFrame'),
|
|
previousFrame: Symbol('previousFrame'),
|
|
nextFrame: Symbol('nextFrame'),
|
|
lastFrame: Symbol('lastFrame'), // used when navigating backwards from the begining of the next canvas
|
|
},
|
|
};
|
|
|
|
// receive navigation messages from displays
|
|
const displayNavMessage = (myMessage) => {
|
|
if (myMessage.type === msg.response.previous) loadDisplay(-1);
|
|
if (myMessage.type === msg.response.next) loadDisplay(1);
|
|
};
|
|
|
|
// navigate to next or previous
|
|
const navTo = (direction) => {
|
|
// test for a current display
|
|
const current = currentDisplay();
|
|
if (progress) progress.hideCanvas();
|
|
if (!current) {
|
|
// special case for no active displays (typically on progress screen)
|
|
// find the first ready display
|
|
let firstDisplay;
|
|
let displayCount = 0;
|
|
do {
|
|
if (displays[displayCount].status === STATUS.loaded && displays[displayCount].timing.totalScreens > 0) firstDisplay = displays[displayCount];
|
|
displayCount += 1;
|
|
} while (!firstDisplay && displayCount < displays.length);
|
|
|
|
if (!firstDisplay) return;
|
|
|
|
// In kiosk mode, hide the loading screen when we start showing the first display
|
|
if (settings?.kiosk?.value) {
|
|
document.querySelector('#loading').style.display = 'none';
|
|
}
|
|
|
|
firstDisplay.navNext(msg.command.firstFrame);
|
|
firstDisplay.showCanvas();
|
|
return;
|
|
}
|
|
if (direction === msg.command.nextFrame) currentDisplay().navNext();
|
|
if (direction === msg.command.previousFrame) currentDisplay().navPrev();
|
|
};
|
|
|
|
// find the next or previous available display
|
|
const loadDisplay = (direction) => {
|
|
const totalDisplays = displays.length;
|
|
const curIdx = currentDisplayIndex();
|
|
let idx;
|
|
let foundSuitableDisplay = false;
|
|
|
|
for (let i = 0; i < totalDisplays; i += 1) {
|
|
// convert form simple 0-10 to start at current display index +/-1 and wrap
|
|
idx = wrap(curIdx + (i + 1) * direction, totalDisplays);
|
|
if (displays[idx].status === STATUS.loaded && displays[idx].timing.totalScreens > 0) {
|
|
// Prevent infinite recursion by ensuring we don't select the same display
|
|
if (idx !== curIdx) {
|
|
foundSuitableDisplay = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if no suitable display was found, do NOT proceed to avoid infinite recursion
|
|
if (!foundSuitableDisplay) {
|
|
console.warn('No suitable display found for navigation');
|
|
return;
|
|
}
|
|
|
|
const newDisplay = displays[idx];
|
|
// hide all displays
|
|
hideAllCanvases();
|
|
// show the new display and navigate to an appropriate display
|
|
if (direction < 0) newDisplay.showCanvas(msg.command.lastFrame);
|
|
if (direction > 0) newDisplay.showCanvas(msg.command.firstFrame);
|
|
};
|
|
|
|
// get the current display index or value
|
|
const currentDisplayIndex = () => displays.findIndex((display) => display.active);
|
|
const currentDisplay = () => displays[currentDisplayIndex()];
|
|
|
|
const setPlaying = (newValue) => {
|
|
playing = newValue;
|
|
const playButton = document.querySelector('#NavigatePlay');
|
|
localStorage.setItem('play', playing);
|
|
|
|
if (playing) {
|
|
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).catch(() => {
|
|
// Wake lock disable failed, but continue normally
|
|
});
|
|
playButton.title = 'Play';
|
|
playButton.src = 'images/nav/ic_play_arrow_white_24dp_2x.png';
|
|
}
|
|
// if we're playing and on the progress screen (or in kiosk mode), jump to the next screen
|
|
if (playing && !currentDisplay()) {
|
|
if (progress || settings?.kiosk?.value) {
|
|
navTo(msg.command.firstFrame);
|
|
}
|
|
}
|
|
};
|
|
|
|
// handle all navigation buttons
|
|
const handleNavButton = (button) => {
|
|
switch (button) {
|
|
case 'play':
|
|
setPlaying(true);
|
|
break;
|
|
case 'playToggle':
|
|
setPlaying(!playing);
|
|
break;
|
|
case 'stop':
|
|
setPlaying(false);
|
|
break;
|
|
case 'next':
|
|
setPlaying(false);
|
|
navTo(msg.command.nextFrame);
|
|
break;
|
|
case 'previous':
|
|
setPlaying(false);
|
|
navTo(msg.command.previousFrame);
|
|
break;
|
|
case 'menu':
|
|
setPlaying(false);
|
|
if (progress) {
|
|
progress.showCanvas();
|
|
} else if (settings?.kiosk?.value) {
|
|
// In kiosk mode without progress, show the loading screen
|
|
document.querySelector('#loading').style.display = 'flex';
|
|
}
|
|
hideAllCanvases();
|
|
break;
|
|
default:
|
|
console.error(`Unknown navButton ${button}`);
|
|
}
|
|
};
|
|
|
|
// return the specificed display
|
|
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);
|
|
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);
|
|
}
|
|
};
|
|
|
|
// reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh
|
|
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`);
|
|
displays[display.navId] = display;
|
|
|
|
// generate checkboxes
|
|
generateCheckboxes();
|
|
};
|
|
|
|
const generateCheckboxes = () => {
|
|
const availableDisplays = document.querySelector('#enabledDisplays');
|
|
|
|
if (!availableDisplays) return;
|
|
// generate checkboxes
|
|
const checkboxes = displays.map((d) => d.generateCheckbox(d.defaultEnabled)).filter((d) => d);
|
|
|
|
// write to page
|
|
availableDisplays.innerHTML = '';
|
|
availableDisplays.append(...checkboxes);
|
|
};
|
|
|
|
// special registration method for progress display
|
|
const registerProgress = (_progress) => {
|
|
progress = _progress;
|
|
};
|
|
|
|
const populateWeatherParameters = (params) => {
|
|
document.querySelector('#spanCity').innerHTML = `${params.city}, `;
|
|
document.querySelector('#spanState').innerHTML = params.state;
|
|
document.querySelector('#spanStationId').innerHTML = params.stationId;
|
|
document.querySelector('#spanRadarId').innerHTML = params.radarId;
|
|
document.querySelector('#spanZoneId').innerHTML = params.zoneId;
|
|
};
|
|
|
|
const latLonReceived = (data, haveDataCallback) => {
|
|
getWeather(data, haveDataCallback);
|
|
};
|
|
|
|
const timeZone = () => weatherParameters.timeZone;
|
|
|
|
export {
|
|
updateStatus,
|
|
displayNavMessage,
|
|
resetStatuses,
|
|
isPlaying,
|
|
resize,
|
|
registerDisplay,
|
|
registerProgress,
|
|
currentDisplay,
|
|
getDisplay,
|
|
msg,
|
|
message,
|
|
latLonReceived,
|
|
timeZone,
|
|
};
|