Compare commits

...

17 Commits

Author SHA1 Message Date
Matt Walsh
e57b9bcb20 6.2.1 2025-09-24 22:33:59 -05:00
Matt Walsh
e27750e915 fix load order on scroll when compiled 2025-09-24 22:33:47 -05:00
Matt Walsh
f5431a04c7 6.2.0 2025-09-24 22:27:44 -05:00
Matt Walsh
5117a9d475 move bottom scroll to single div #144 2025-09-24 22:27:31 -05:00
Matt Walsh
28baa022a9 6.1.11 2025-09-15 08:54:20 -05:00
Matt Walsh
e8b8890260 fix full screen centering on chrome #139 2025-09-15 08:54:11 -05:00
Matt Walsh
b797a10b9e 6.1.10 2025-09-15 08:04:24 -05:00
Matt Walsh
2a64cda383 Merge branch 'fullscreen-kiosk-sizing' 2025-09-15 08:03:53 -05:00
Matt Walsh
e6e357c51b separate full screen container and scaling #139 2025-09-15 08:01:28 -05:00
Matt Walsh
24deb4dce4 additional full screen scaling calculation adjustments 2025-09-11 15:34:02 -05:00
Matt Walsh
f17f69f60e 6.1.9 2025-09-09 22:07:51 -05:00
Matt Walsh
fa16095355 filter for actual alerts (not test) close #141 2025-09-09 22:07:42 -05:00
Matt Walsh
cc3dbeb043 6.1.8 2025-09-09 21:35:51 -05:00
Matt Walsh
8ee1e954eb better background and wide screen for hazard displays 2025-09-09 21:35:31 -05:00
Matt Walsh
bfc4bddfef Add quick start to readme 2025-09-09 20:54:13 -05:00
Matt Walsh
567325e3c5 update dependencies 2025-09-09 20:47:40 -05:00
Matt Walsh
cc05aafb95 patch for kiosk drawing off screen 2025-09-09 19:22:18 -05:00
27 changed files with 883 additions and 837 deletions

View File

@@ -11,4 +11,4 @@ Please do not report issues with api.weather.gov being down. It's a new service
Please include:
* Web browser and OS
* Forecast Information text block from the very bottom of the web page
* Headend Information text block from the very bottom of the web page

View File

@@ -32,6 +32,18 @@ From a learning standpoint, this codebase make use of a lot of different methods
* Hand written CSS made easier to mange with SASS
* A linting library to keep code style consistent
## Quck Start
Ensure you have Node installed.
```bash
git clone https://github.com/netbymatt/ws4kp.git
cd ws4kp
npm install
npm start
```
Open your browser and navigate to https://localhost:8080
## Does WeatherStar 4000+ work outside of the USA?
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclusive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
@@ -57,14 +69,7 @@ WeatherStar 4000+ supports two deployment modes:
* Browser-based caching
* Used by: static file hosting and default `Dockerfile`
## Run Your WeatherStar
Ensure you have Node installed. Clone the repository:
```bash
git clone https://github.com/netbymatt/ws4kp.git
cd ws4kp
npm install
```
## Other methods to run Ws4kp
### Development Mode (individual JS files, easier debugging)
```bash

1169
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "6.1.7",
"version": "6.2.1",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs",
"type": "module",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,5 +1,4 @@
import { locationCleanup } from './utils/string.mjs';
import { elemForEach } from './utils/elem.mjs';
import getCurrentWeather from './currentweather.mjs';
import { currentDisplay } from './navigation.mjs';
import getHazards from './hazards.mjs';
@@ -12,6 +11,16 @@ const TICK_INTERVAL_MS = 500; // milliseconds per tick
const secondsToTicks = (seconds) => Math.ceil((seconds * 1000) / TICK_INTERVAL_MS);
const DEFAULT_UPDATE = secondsToTicks(4.0); // 4 second default for each current conditions
// items on page
let mainScroll;
let fixedScroll;
let header;
document.addEventListener('DOMContentLoaded', () => {
mainScroll = document.querySelector('#container>.scroll');
fixedScroll = document.querySelector('#container>.scroll .fixed');
header = document.querySelector('#container>.scroll .scroll-header');
});
// local variables
let interval;
let screenIndex = 0;
@@ -23,6 +32,8 @@ let defaultScreensLoaded = true;
// start drawing conditions
// reset starts from the first item in the text scroll list
const start = () => {
// show the block
show();
// if already started, draw the screen on a reset flag and return
if (interval) {
if (resetFlag) drawScreen();
@@ -62,6 +73,7 @@ const incrementInterval = (force) => {
const display = currentDisplay();
if (!display?.okToDrawCurrentConditions) {
stop(display?.elemId === 'progress');
hide();
return;
}
screenIndex = (screenIndex + 1) % (workingScreens.length);
@@ -91,12 +103,8 @@ const drawScreen = async () => {
const thisScreen = workingScreens[screenIndex](scrollData);
// update classes on the scroll area
elemForEach('.weather-display .scroll', (elem) => {
elem.classList.forEach((cls) => { if (cls !== 'scroll') elem.classList.remove(cls); });
// no scroll on progress
if (elem.parentElement.id === 'progress-html') return;
thisScreen?.classes?.forEach((cls) => elem.classList.add(cls));
});
mainScroll.classList.forEach((cls) => { if (cls !== 'scroll') mainScroll.classList.remove(cls); });
thisScreen?.classes?.forEach((cls) => mainScroll.classList.add(cls));
if (typeof thisScreen === 'string') {
// only a string
@@ -125,9 +133,7 @@ const hazards = (data) => {
// test for data
if (!data.hazards || data.hazards.length === 0) return false;
// since the hazard scroll element has no left/right margins, pad the beginning and end with non-breaking spaces
const padding = ' '.repeat(4);
const hazard = `${padding}${data.hazards[0].properties.event} ${data.hazards[0].properties.description}${padding}`;
const hazard = `${data.hazards[0].properties.event} ${data.hazards[0].properties.description}`;
return {
text: hazard,
@@ -190,17 +196,12 @@ let workingScreens = [...baseScreens, ...additionalScreens];
// internal draw function with preset parameters
const drawCondition = (text) => {
// update all html scroll elements
elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = text;
});
fixedScroll.innerHTML = text;
setHeader('');
};
const setHeader = (text) => {
elemForEach('.weather-display .scroll .scroll-header', (elem) => {
elem.innerHTML = text ?? '';
});
header.innerHTML = text ?? '';
};
// reset the screens back to the original set
@@ -223,14 +224,14 @@ const drawScrollCondition = (screen) => {
scrollElement.classList.add('scroll-area');
scrollElement.innerHTML = screen.text;
// add it to the page to get the width
document.querySelector('.weather-display .scroll .fixed').innerHTML = scrollElement.outerHTML;
fixedScroll.innerHTML = scrollElement.outerHTML;
// grab the width
const { scrollWidth, clientWidth } = document.querySelector('.weather-display .scroll .fixed .scroll-area');
const { scrollWidth, clientWidth } = document.querySelector('#container>.scroll .fixed .scroll-area');
// calculate the scroll distance and set a minimum scroll
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
// calculate the scroll time (scaled by global speed setting)
const scrollTime = scrollDistance / SCROLL_SPEED * settings.speed.value;
// calculate the scroll time (scaled by global speed setting), minimum 2s (4s when added to start and end delays)
const scrollTime = Math.max(scrollDistance / SCROLL_SPEED * settings.speed.value, 2);
// add 1 second pause at the end of the scroll animation
const endPauseTime = 1.0;
const totalAnimationTime = scrollTime + endPauseTime;
@@ -246,17 +247,13 @@ const drawScrollCondition = (screen) => {
scrollElement.style.backfaceVisibility = 'hidden'; // Force hardware acceleration
scrollElement.style.perspective = '1000px'; // Enable 3D rendering context
elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = '';
elem.append(scrollElement.cloneNode(true));
});
fixedScroll.innerHTML = '';
fixedScroll.append(scrollElement.cloneNode(true));
// start the scroll after the specified delay
setTimeout(() => {
// change the transform to trigger the scroll
elemForEach('.weather-display .scroll .fixed .scroll-area', (elem) => {
elem.style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
});
document.querySelector('#container>.scroll .fixed .scroll-area').style.transform = `translateX(-${scrollDistance.toFixed(0)}px)`;
}, startDelayTime * 1000);
};
@@ -264,9 +261,19 @@ const parseMessage = (event) => {
if (event?.data?.type === 'current-weather-scroll') {
if (event.data?.method === 'start') start();
if (event.data?.method === 'reload') stop(true);
if (event.data?.method === 'show') show();
if (event.data?.method === 'hide') hide();
}
};
const show = () => {
mainScroll.style.display = 'block';
};
const hide = () => {
mainScroll.style.display = 'none';
};
const screenCount = () => workingScreens.length;
const atDefault = () => defaultScreensLoaded;
@@ -277,6 +284,8 @@ window.CurrentWeatherScroll = {
addScreen,
reset,
start,
show,
hide,
screenCount,
atDefault,
};
@@ -285,6 +294,8 @@ export {
addScreen,
reset,
start,
show,
hide,
screenCount,
atDefault,
};

View File

@@ -71,6 +71,7 @@ class Hazards extends WeatherDisplay {
// get the forecast using centralized safe handling
const url = new URL('https://api.weather.gov/alerts/active');
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
url.searchParams.append('status', 'actual');
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
if (!alerts) {

View File

@@ -330,6 +330,7 @@ const handleNavButton = (button) => {
break;
case 'menu':
setPlaying(false);
postMessage({ type: 'current-weather-scroll', method: 'hide' });
if (progress) {
progress.showCanvas();
} else if (settings?.kiosk?.value) {
@@ -357,6 +358,17 @@ const isIOS = () => {
let lastAppliedScale = null;
let lastAppliedKioskMode = null;
// Helper function to clear CSS properties from elements
const clearElementStyles = (element, properties) => {
properties.forEach((prop) => element.style.removeProperty(prop));
};
// Define property groups for different scaling modes
const SCALING_PROPERTIES = {
wrapper: ['width', 'height', 'transform', 'transform-origin'],
positioning: ['transform', 'transform-origin', 'width', 'height', 'position', 'left', 'top', 'margin-left', 'margin-top'],
};
// resize the container on a page resize
const resize = (force = false) => {
// Ignore resize events caused by pinch-to-zoom on mobile
@@ -376,9 +388,8 @@ const resize = (force = false) => {
// Standard scaling: fit within both dimensions
const scale = Math.min(widthZoomPercent, heightZoomPercent);
// For Mobile Safari in kiosk mode, always use centering behavior regardless of scale
// For other platforms, only use fullscreen/centering behavior for actual fullscreen or kiosk mode where content fits naturally
const isKioskLike = isFullscreen || (isKioskMode && scale >= 1.0) || isMobileSafariKiosk;
// Use centering behavior for fullscreen, kiosk mode, or Mobile Safari kiosk mode
const isKioskLike = isFullscreen || isKioskMode || isMobileSafariKiosk;
if (debugFlag('resize') || debugFlag('fullscreen')) {
console.log(`🖥️ Resize: force=${force} isKioskLike=${isKioskLike} window=${window.innerWidth}x${window.innerHeight} targetWidth=${targetWidth} widthZoom=${widthZoomPercent.toFixed(3)} heightZoom=${heightZoomPercent.toFixed(3)} finalScale=${scale.toFixed(3)} fullscreenElement=${!!document.fullscreenElement} isIOS=${isIOS()} standalone=${window.navigator.standalone} isMobileSafariKiosk=${isMobileSafariKiosk} kioskMode=${settings.kiosk?.value} wideMode=${settings.wide.value}`);
@@ -412,40 +423,35 @@ const resize = (force = false) => {
console.log('🖥️ Resetting fullscreen/kiosk styles to normal');
}
// Reset wrapper styles (only properties that are actually set in fullscreen/scaling modes)
wrapper.style.removeProperty('width');
wrapper.style.removeProperty('height');
wrapper.style.removeProperty('overflow');
wrapper.style.removeProperty('transform');
wrapper.style.removeProperty('transform-origin');
// Reset container styles that might have been applied during fullscreen
mainContainer.style.removeProperty('transform');
mainContainer.style.removeProperty('transform-origin');
mainContainer.style.removeProperty('width');
mainContainer.style.removeProperty('height');
mainContainer.style.removeProperty('position');
mainContainer.style.removeProperty('left');
mainContainer.style.removeProperty('top');
mainContainer.style.removeProperty('margin-left');
mainContainer.style.removeProperty('margin-top');
// Reset all scaling-related styles
const container = document.querySelector('#container');
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
clearElementStyles(container, SCALING_PROPERTIES.positioning);
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
applyScanlineScaling(1.0);
return;
}
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not Mobile Safari kiosk mode)
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk) {
// MOBILE SCALING: Use wrapper scaling for mobile devices (but not when in fullscreen/kiosk mode)
if ((scale < 1.0 || (isKioskMode && !isKioskLike)) && !isMobileSafariKiosk && !isKioskLike) {
/*
* MOBILE SCALING (Wrapper Scaling)
*
* This path is used for regular mobile browsing (NOT fullscreen/kiosk modes).
* Why scale the wrapper instead of mainContainer?
* - For mobile devices where content is larger than viewport, we need to scale the entire layout
* - The wrapper (#divTwc) contains both the main content AND the bottom navigation bar
* - Scaling the wrapper ensures both elements are scaled together as a unit
* - No centering is applied - content aligns to top-left for typical mobile behavior
* - Content aligns to top-left for typical mobile web browsing behavior (no centering)
* - Uses explicit dimensions to prevent layout issues and eliminate gaps after scaling
*/
// Reset any container/mainContainer styles that might have been set during fullscreen/kiosk mode
const container = document.querySelector('#container');
clearElementStyles(container, SCALING_PROPERTIES.positioning);
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
wrapper.style.setProperty('transform', `scale(${scale})`);
wrapper.style.setProperty('transform-origin', 'top left'); // Scale from top-left corner
@@ -458,7 +464,7 @@ const resize = (force = false) => {
const scaledHeight = totalHeight * scale; // Height after scaling
wrapper.style.setProperty('width', `${wrapperWidth}px`);
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap
wrapper.style.setProperty('height', `${scaledHeight}px`); // Use scaled height to eliminate gap under #divTwc on index page
applyScanlineScaling(scale);
return;
}
@@ -468,10 +474,7 @@ const resize = (force = false) => {
const wrapperHeight = 480;
// Reset wrapper styles to avoid double scaling (wrapper remains unstyled)
wrapper.style.removeProperty('width');
wrapper.style.removeProperty('height');
wrapper.style.removeProperty('transform');
wrapper.style.removeProperty('transform-origin');
clearElementStyles(wrapper, SCALING_PROPERTIES.wrapper);
// Platform-specific positioning logic
let transformOrigin;
@@ -529,7 +532,7 @@ const resize = (force = false) => {
const offsetY = (window.innerHeight - scaledHeight) / 2;
if (debugFlag('fullscreen')) {
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} transform: scale(${scale}) translate(${offsetX / scale}px, ${offsetY / scale}px)`);
console.log(`🖥️ Applying fullscreen/kiosk scaling: wrapper=${wrapperWidth}x${wrapperHeight} scale=${scale.toFixed(3)} offset=${offsetX.toFixed(1)},${offsetY.toFixed(1)} target=${isFullscreen ? '#container' : '#divTwcMain'}`);
}
// Set positioning values for CSS-based centering
@@ -540,25 +543,41 @@ const resize = (force = false) => {
marginTop = `-${wrapperHeight / 2}px`; // Pull back by half height
}
// Apply shared mainContainer properties (same for both kiosk modes)
mainContainer.style.setProperty('transform', `scale(${scale})`, 'important');
mainContainer.style.setProperty('transform-origin', transformOrigin, 'important');
mainContainer.style.setProperty('width', `${wrapperWidth}px`, 'important');
mainContainer.style.setProperty('height', `${wrapperHeight}px`, 'important');
mainContainer.style.setProperty('position', 'absolute', 'important');
mainContainer.style.setProperty('left', leftPosition, 'important');
mainContainer.style.setProperty('top', topPosition, 'important');
// Chrome fullscreen compatibility: apply transform to #container instead of #divTwcMain
// This works around Chrome's restriction on styling fullscreen elements directly
const container = document.querySelector('#container');
const targetElement = isFullscreen ? container : mainContainer;
// Reset the other element's styles to avoid conflicts
if (isFullscreen) {
// Reset mainContainer styles when using container for fullscreen
clearElementStyles(mainContainer, SCALING_PROPERTIES.positioning);
} else {
// Reset container styles when using mainContainer for kiosk mode
clearElementStyles(container, SCALING_PROPERTIES.positioning);
}
// Apply shared properties to the target element
targetElement.style.setProperty('transform', `scale(${scale})`, 'important');
targetElement.style.setProperty('transform-origin', transformOrigin, 'important');
// the width of the target element does not change it is the fixed width of the 4:3 display which is then scaled
// the wrapper adds margins and padding to achieve widescreen
// targetElement.style.setProperty('width', `${wrapperWidth}px`, 'important');
targetElement.style.setProperty('height', `${wrapperHeight}px`, 'important');
targetElement.style.setProperty('position', 'absolute', 'important');
targetElement.style.setProperty('left', leftPosition, 'important');
targetElement.style.setProperty('top', topPosition, 'important');
// Apply or clear margin properties based on positioning method
if (marginLeft !== null) {
mainContainer.style.setProperty('margin-left', marginLeft, 'important');
targetElement.style.setProperty('margin-left', marginLeft, 'important');
} else {
mainContainer.style.removeProperty('margin-left');
targetElement.style.removeProperty('margin-left');
}
if (marginTop !== null) {
mainContainer.style.setProperty('margin-top', marginTop, 'important');
targetElement.style.setProperty('margin-top', marginTop, 'important');
} else {
mainContainer.style.removeProperty('margin-top');
targetElement.style.removeProperty('margin-top');
}
applyScanlineScaling(scale);

View File

@@ -22,7 +22,9 @@ class Progress extends WeatherDisplay {
}
async drawCanvas(displays, loadedCount) {
// skip drawing if not displayed, or not yet available
if (!this.elem) return;
if (this.elem.classList.contains('show') === false) return;
super.drawCanvas();
// get the progress bar cover (makes percentage)

View File

@@ -172,6 +172,7 @@ class WeatherDisplay {
if (this.screenIndex < 0) this.screenIndex = 0;
if (this.okToDrawCurrentDateTime) this.drawCurrentDateTime();
if (this.okToDrawCurrentConditions) postMessage({ type: 'current-weather-scroll', method: 'start' });
if (this.okToDrawCurrentConditions === false) postMessage({ type: 'current-weather-scroll', method: 'hide' });
}
finishDraw() {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,9 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#hazards-html.weather-display {
background-image: url('../images/backgrounds/7.png');
}
.weather-display .main.hazards {
&.main {
@@ -7,6 +11,7 @@
height: 480px;
background-color: rgb(112, 35, 35);
.hazard-lines {
min-height: 400px;
padding-top: 10px;
@@ -26,3 +31,7 @@
}
}
}
.wide.hazards #container {
background: url(../images/backgrounds/7-wide.png);
}

View File

@@ -1,5 +1,5 @@
@use 'shared/_utils' as u;
@use 'shared/_colors' as c;
@use 'shared/_utils'as u;
@use 'shared/_colors'as c;
@font-face {
font-family: "Star4000";
@@ -161,6 +161,7 @@ body {
#divTwcMain {
width: 640px;
height: 480px;
position: relative;
.wide & {
width: 854px;
@@ -340,13 +341,14 @@ body {
// overflow: hidden;
background-image: url(../images/backgrounds/1.png);
transform-origin: 0 0;
background-repeat: no-repeat;
}
.wide #container {
padding-left: 107px;
padding-right: 107px;
background: url(../images/backgrounds/1-wide.png);
background-repeat: no-repeat;
background: url(../images/backgrounds/1-wide.png)
}
#divTwc:fullscreen #container,
@@ -813,4 +815,4 @@ body.kiosk #loading .instructions {
>*:not(#divTwc) {
display: none !important;
}
}
}

View File

@@ -1,5 +1,5 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
.weather-display {
width: 640px;
@@ -112,29 +112,32 @@
}
}
.scroll {
@include u.text-shadow(3px, 1.5px);
#container>.scroll {
display: none;
@include u.text-shadow(3px, 1.5px);
width: 640px;
height: 77px;
overflow: hidden;
margin-top: 3px;
position: absolute;
bottom: 0px;
z-index: 1;
&.hazard {
background-color: rgb(112, 35, 35);
}
.scroll-container {
width: 640px;
height: 70px;
overflow: hidden;
margin-top: 3px;
&.hazard {
background-color: rgb(112, 35, 35);
}
.fixed,
.scroll-header {
margin-left: 55px;
margin-right: 55px;
overflow: hidden;
}
// Remove margins for hazard scrolls to maximize text space
&.hazard .fixed {
margin-left: 0;
margin-right: 0;
white-space: nowrap;
}
.scroll-header {
@@ -156,6 +159,17 @@
// left: calc((elem width) - 640px);
}
}
}
}
.wide #container>.scroll {
width: 854px;
margin-left: -107px;
.scroll-container {
margin-left: 107px;
}
}

View File

@@ -133,6 +133,7 @@
<div id="hazards-html" class="weather-display">
<%- include('partials/hazards.ejs') %>
</div>
<%- include('partials/scroll.ejs') %>
</div>
</div>
<div id="divTwcBottom">
@@ -185,7 +186,7 @@
</div>
</div>
<div class='heading'>Forecast Information</div>
<div class='heading'>Headend Information</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />

View File

@@ -21,5 +21,4 @@
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>

View File

@@ -1,43 +1,42 @@
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src="" /></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll has-box current-weather">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>
<div class="condition center"></div>
<div class="icon center"><img src="" /></div>
<div class="wind-container">
<div class="wind-label">Wind:</div>
<div class="wind"></div>
</div>
<div class="wind-gusts"></div>
</div>
<div class="right col">
<div class="location"></div>
<div class="row">
<div class="label">Humidity:</div>
<div class="humidity value"></div>
</div>
<div class="row">
<div class="label">Dewpoint:</div>
<div class="dewpoint value"></div>
</div>
<div class="row">
<div class="label">Ceiling:</div>
<div class="ceiling value"></div>
</div>
<div class="row">
<div class="label">Visibility:</div>
<div class="visibility value"></div>
</div>
<div class="row">
<div class="label">Pressure:</div>
<div class="pressure value"></div>
</div>
<div class="row">
<div class="heat-index-label label"></div>
<div class="heat-index value"></div>
</div>
</div>
</div>
</div>

View File

@@ -19,5 +19,4 @@
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>

View File

@@ -1,24 +1,23 @@
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
<div class="main has-scroll hourly-graph">
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
<div class="y-axis">
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
<div class="y-axis">
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
</div>
<div class="chart">
<img id="chart-area"></img>
</div>
<div class="x-axis">
<div class="label l-1">12a</div>
<div class="label l-2">6a</div>
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
</div>
</div>

View File

@@ -1,18 +1,17 @@
<%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img /></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll hourly">
<div class="column-headers">
<div class="temp">TEMP</div>
<div class="like">LIKE</div>
<div class="wind">WIND</div>
</div>
<div class="hourly-lines">
<div class="hourly-row template">
<div class="hour"></div>
<div class="icon"><img /></div>
<div class="temp"></div>
<div class="like"></div>
<div class="wind"></div>
</div>
</div>
</div>

View File

@@ -1,20 +1,19 @@
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll latest-observations has-box">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
<div class="observation-lines">
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>
</div>
</div>
</div>

View File

@@ -1,14 +1,13 @@
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">
<img src="" />
</div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll regional-forecast">
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">
<img src="" />
</div>
<div class="city"></div>
<div class="temp"></div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
<div class="scroll">
<div class="scrolling template"></div>
<div class="scroll-header"></div>
<div class="fixed"></div>
<div class="scroll-container">
<div class="scroll-header"></div>
<div class="fixed"></div>
</div>
</div>

View File

@@ -1,20 +1,19 @@
<%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: true}) %>
<div class="main has-scroll spc-outlook">
<div class="container">
<div class="risk-levels">
<div class="risk-level">High</div>
<div class="risk-level">Moderate</div>
<div class="risk-level">Enhanced</div>
<div class="risk-level">Slight</div>
<div class="risk-level">Marginal</div>
<div class="risk-level">T'Storm</div>
</div>
<div class="days">
<div class="day template">
<div class="day-name">Monday</div>
<div class="risk-bar"></div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll spc-outlook">
<div class="container">
<div class="risk-levels">
<div class="risk-level">High</div>
<div class="risk-level">Moderate</div>
<div class="risk-level">Enhanced</div>
<div class="risk-level">Slight</div>
<div class="risk-level">Marginal</div>
<div class="risk-level">T'Storm</div>
</div>
<div class="days">
<div class="day template">
<div class="day-name">Monday</div>
<div class="risk-bar"></div>
</div>
</div>
</div>
</div>

View File

@@ -12,5 +12,4 @@
<div class="temp high"></div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
</div>