Compare commits

...

24 Commits

Author SHA1 Message Date
Matt Walsh
6f6efe801c 5.20.3 2025-05-23 15:47:27 -05:00
Matt Walsh
bc77a1891c remove limit for alert endpoint due to recent api change 2025-05-23 15:43:58 -05:00
Matt Walsh
4666878250 5.20.2 2025-05-21 13:55:39 -05:00
Matt Walsh
5813dd9a92 title overflow cleanup 2025-05-21 13:55:27 -05:00
Matt Walsh
8cb8873760 add hooks for geoip lookup 2025-05-21 13:49:49 -05:00
Matt Walsh
323c175936 css cleanup 2025-05-20 22:19:46 -05:00
Matt Walsh
85e2553cb2 5.20.1 2025-05-20 22:10:26 -05:00
Matt Walsh
101d0ac9ea page delivery tweaks 2025-05-20 22:10:13 -05:00
Matt Walsh
834d68f9e3 expose invalidate gulp taks 2025-05-20 18:26:50 -05:00
Matt Walsh
0ee7fdc9f8 Update README.md with ws3kp link 2025-05-20 18:02:25 -05:00
Matt Walsh
d75121e894 5.20.0 2025-05-20 16:29:16 -05:00
Matt Walsh
4cdced3659 prep for additional bottom line displays 2025-05-20 16:28:56 -05:00
Matt Walsh
1a5548d135 5.19.3 2025-05-16 14:42:19 -05:00
Matt Walsh
11c826a2af fix local forecast paging 2025-05-16 14:42:11 -05:00
Matt Walsh
7a129c1cd3 autocomplete cleanup 2025-05-16 11:17:35 -05:00
Matt Walsh
867657a965 5.19.2 2025-05-16 09:39:54 -05:00
Matt Walsh
e89dc52541 fix spc changing locations close #80 2025-05-16 09:39:48 -05:00
Matt Walsh
317883fc04 Add version number to bottom of page 2025-05-16 09:21:04 -05:00
Matt Walsh
a4a601a387 5.19.1 2025-05-15 22:48:44 -05:00
Matt Walsh
375812c024 better spc labeling 2025-05-15 22:48:37 -05:00
Matt Walsh
6af8b58f14 5.19.0 2025-05-15 22:29:10 -05:00
Matt Walsh
6287db7483 Merge branch 'spc-outlook' 2025-05-15 22:28:22 -05:00
Matt Walsh
7a196ac64a skip spc outlook if not in the next 3 days 2025-05-15 22:07:18 -05:00
Matt Walsh
5946ee495a initial data and graph 2025-05-15 16:04:57 -05:00
46 changed files with 456 additions and 76 deletions

View File

@@ -109,7 +109,7 @@ The resulting files will be in the /dist folder in the root of the project. Thes
## Music
The WeatherStar had wonderful background music from the smooth jazz and new age genres by artists of the time. Lists of the music that played are available by searching online, but it's all copyrighted music and would be difficult to provide as part of this repository.
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks will be posted in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
I've used AI tools to create WeatherStar-inspired music tracks that are unencumbered by copyright and are included in this repo. Too keep the size down, I've only included 4 tracks. Additional tracks are in a companion repository [ws4kp-music](https://github.com/netbymatt/ws4kp-music).
### Customizing the music
Placing .mp3 files in the `/server/music` folder will override the default music included in the repo. Subdirectories will not be scanned. When weatherstar loads in the browser it will load a list if available files and randomize the order when it starts playing. On each loop through the available tracks the order will again be shuffled. If you're using the static files method to host your WeatherStar music is located in `/music`.
@@ -141,6 +141,10 @@ Please do not report issues with api.weather.gov being down. It's a new service
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
## Related Projects
Not retro enough? Try the [Weatherstar 3000+](https://github.com/netbymatt/ws3kp)
## Disclaimer
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.

View File

@@ -76,6 +76,7 @@ const mjsSources = [
'server/scripts/modules/hazards.mjs',
'server/scripts/modules/currentweather.mjs',
'server/scripts/modules/almanac.mjs',
'server/scripts/modules/spc-outlook.mjs',
'server/scripts/modules/icons.mjs',
'server/scripts/modules/extendedforecast.mjs',
'server/scripts/modules/hourly.mjs',
@@ -158,6 +159,9 @@ const uploadImages = () => src(imageSources, { base: './server', encoding: false
s3({
Bucket: process.env.BUCKET,
StorageClass: 'STANDARD',
maps: {
CacheControl: () => 'max-age=31536000',
},
}),
);
@@ -188,4 +192,5 @@ export default publishFrontend;
export {
buildDist,
invalidate,
};

View File

@@ -1,8 +1,9 @@
import updateVendor from './gulp/update-vendor.mjs';
import publishFrontend, { buildDist } from './gulp/publish-frontend.mjs'
import publishFrontend, { buildDist, invalidate } from './gulp/publish-frontend.mjs';
export {
updateVendor,
publishFrontend,
buildDist,
invalidate,
};

View File

@@ -60,16 +60,34 @@ const index = (req, res) => {
});
};
const geoip = (req, res) => {
res.set({
'x-geoip-city': 'Orlando',
'x-geoip-country': 'US',
'x-geoip-country-name': 'United States',
'x-geoip-country-region': 'FL',
'x-geoip-country-region-name': 'Florida',
'x-geoip-latitude': '28.52135',
'x-geoip-longitude': '-81.41079',
'x-geoip-postal-code': '32789',
'x-geoip-time-zone': 'America/New_York',
'content-type': 'application/json',
});
res.json({});
};
// debugging
if (process.env?.DIST === '1') {
// distribution
app.use('/images', express.static('./server/images'));
app.use('/fonts', express.static('./server/fonts'));
app.use('/scripts', express.static('./server/scripts'));
app.use('/geoip', geoip);
app.use('/', express.static('./dist'));
} else {
// debugging
app.get('/index.html', index);
app.use('/geoip', geoip);
app.get('/', index);
app.get('*name', express.static('./server'));
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ws4kp",
"version": "5.18.1",
"version": "5.20.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ws4kp",
"version": "5.18.1",
"version": "5.20.3",
"license": "MIT",
"dependencies": {
"dotenv": "^16.5.0",

View File

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

View File

@@ -1,30 +0,0 @@
--Star 3000--
Star3000.ttf - Standard text style for most screens (and Travel Cities title header)
Star3000 Small.ttf - Time/Date and some page headers
Star3000 Large.ttf - Travel Cities Forecast (Forecast portion only)
Star3000 Extra Large.ttf - Only used on some advertiser text
Star3000 Extended.ttf - Only used on some advertiser text
"Heavy" style is an emboldened version of the standard font (used on some STARs)
Star3000 Outline.ttf - A contrast border (stroke) that surrounds the Star3000.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
Star3000 Small Outline.ttf - A contrast border (stroke) that surrounds the Star3000 Small.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
Star3000 Large Outline.ttf - A contrast border (stroke) that surrounds the Star3000 Large.ttf base font. When used, must be as a text layer undeneath the base font (and is usually black in color).
***Outlines for other font styles are not currently available.
--Star 4000--
Star4000.ttf - Standard text style for zone forecast, observation tables, regional map cities, almanac, extended forecast day/weather/temperature headers, Current Conditions right half data and most page header titles (also Travel Cities title header before Nov. 1992)
Star4000 Small.ttf - Time/Date, NWS Local Update page header, temperature header for Travel Cities Forecast (after Nov. 1992)
Star4000 Large.ttf - City names and temperature data on Travel Cities Forecast (after Nov. 1992), Extended forecast temperature values (after Feb. 1991), Current Conditions temperature value (after Mar. 1991)
Star4000 Large Compressed - Travel Cities Forecast (before Nov. 1992), regional map temperatures
Star4000 Large Compressed Numbers - Temperature values on regional forecast/observation maps
Star4000 Extended - A proportional width font used for the Current Conditions present weather description and wind data
Star 4 Radar.ttf - Radar airport I.D.
--Star Jr.--
StarJr.ttf - Standard text style for most screens (and Travel Cities title header)
StarJr Small.ttf - Time/Date and some page headers
StarJr Compressed.ttf - Travel Cities Forecast

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -78,6 +78,7 @@ const init = () => {
onSelect(suggestion) { autocompleteOnSelect(suggestion); },
width: 490,
});
window.autoComplete = autoComplete;
// attempt to parse the url parameters
const parsedParameters = parseQueryString();
@@ -109,9 +110,6 @@ const init = () => {
document.querySelector('#spanRadarId').innerHTML = '';
document.querySelector('#spanZoneId').innerHTML = '';
document.querySelector('#chkAutoRefresh').checked = true;
localStorage.removeItem('autoRefresh');
localStorage.removeItem('play');
postMessage('navButton', 'play');
@@ -374,6 +372,10 @@ const btnGetGpsClick = async () => {
const position = await getPosition();
const { latitude, longitude } = position.coords;
getForecastFromLatLon(latitude, longitude, true);
};
const getForecastFromLatLon = (latitude, longitude, fromGps = false) => {
const txtAddress = document.querySelector(TXT_ADDRESS_SELECTOR);
txtAddress.value = `${round2(latitude, 4)}, ${round2(longitude, 4)}`;
@@ -383,7 +385,7 @@ const btnGetGpsClick = async () => {
const query = `${location.city}, ${location.state}`;
localStorage.setItem('latLon', JSON.stringify({ lat: latitude, lon: longitude }));
localStorage.setItem('latLonQuery', query);
localStorage.setItem('latLonFromGPS', true);
localStorage.setItem('latLonFromGPS', fromGps);
txtAddress.value = `${location.city}, ${location.state}`;
});
};
@@ -414,3 +416,6 @@ const getCustomCode = async () => {
document.body.append(customElem);
}
};
// expose functions for external use
window.getForecastFromLatLon = getForecastFromLatLon;

View File

@@ -172,6 +172,11 @@ class AutoComplete {
}
}
setValue(newValue) {
this.currentValue = newValue;
this.elem.value = newValue;
}
onValueChange() {
clearTimeout(this.onValueChange);

View File

@@ -5,10 +5,14 @@ import { currentDisplay } from './navigation.mjs';
// constants
const degree = String.fromCharCode(176);
const SCROLL_SPEED = 75; // pixels/second
const DEFAULT_UPDATE = 8; // 0.5s ticks
// local variables
let interval;
let screenIndex = 0;
let sinceLastUpdate = 0;
let nextUpdate = DEFAULT_UPDATE;
// start drawing conditions
// reset starts from the first item in the text scroll list
@@ -17,7 +21,7 @@ const start = () => {
// set up the interval if needed
if (!interval) {
interval = setInterval(incrementInterval, 4000);
interval = setInterval(incrementInterval, 500);
}
// draw the data
@@ -29,14 +33,24 @@ const stop = (reset) => {
};
// increment interval, roll over
const incrementInterval = () => {
// forcing is used when drawScreen receives an invalid screen and needs to request the next one in line
const incrementInterval = (force) => {
if (!force) {
// test for elapsed time (0.5s ticks);
sinceLastUpdate += 1;
if (sinceLastUpdate < nextUpdate) return;
}
// reset flags
sinceLastUpdate = 0;
nextUpdate = DEFAULT_UPDATE;
// test current screen
const display = currentDisplay();
if (!display?.okToDrawCurrentConditions) {
stop(display?.elemId === 'progress');
return;
}
screenIndex = (screenIndex + 1) % (screens.length);
screenIndex = (screenIndex + 1) % (lastScreen);
// draw new text
drawScreen();
};
@@ -48,7 +62,22 @@ const drawScreen = async () => {
// nothing to do if there's no data yet
if (!data) return;
drawCondition(screens[screenIndex](data));
const thisScreen = screens[screenIndex](data);
if (typeof thisScreen === 'string') {
// only a string
drawCondition(thisScreen);
} else if (typeof thisScreen === 'object') {
// an object was provided with additional parameters
switch (thisScreen.type) {
case 'scroll':
drawScrollCondition(thisScreen);
break;
default: drawCondition(thisScreen);
}
} else {
// can't identify screen, get another one
incrementInterval(true);
}
};
// the "screens" are stored in an array for easy addition and removal
@@ -71,7 +100,7 @@ const screens = [
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
// barometric pressure
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
// wind
(data) => {
@@ -102,3 +131,56 @@ const drawCondition = (text) => {
document.addEventListener('DOMContentLoaded', () => {
start();
});
// store the original number of screens
const originalScreens = screens.length;
let lastScreen = originalScreens;
// reset the number of screens
const reset = () => {
lastScreen = originalScreens;
};
// add screen
const addScreen = (screen) => {
screens.push(screen);
lastScreen += 1;
};
const drawScrollCondition = (screen) => {
// create the scroll element
const scrollElement = document.createElement('div');
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;
// grab the width
const { scrollWidth, clientWidth } = document.querySelector('.weather-display .scroll .fixed .scroll-area');
// calculate the scroll distance and set a minimum scroll
const scrollDistance = Math.max(scrollWidth - clientWidth, 0);
// calculate the scroll time
const scrollTime = scrollDistance / SCROLL_SPEED;
// calculate a new minimum on-screen time +1.0s at start and end
nextUpdate = Math.round(Math.ceil(scrollTime / 0.5) + 4);
// update the element transition and set initial left position
scrollElement.style.left = '0px';
scrollElement.style.transition = `left linear ${scrollTime.toFixed(1)}s`;
elemForEach('.weather-display .scroll .fixed', (elem) => {
elem.innerHTML = '';
elem.append(scrollElement.cloneNode(true));
});
// start the scroll after a short delay
setTimeout(() => {
// change the left position to trigger the scroll
elemForEach('.weather-display .scroll .fixed .scroll-area', (elem) => {
elem.style.left = `-${scrollDistance.toFixed(0)}px`;
});
}, 1000);
};
window.CurrentWeatherScroll = {
addScreen,
reset,
};

View File

@@ -39,7 +39,6 @@ class Hazards extends WeatherDisplay {
// get the forecast
const url = new URL('https://api.weather.gov/alerts/active');
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
url.searchParams.append('limit', 5);
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
const unsortedAlerts = alerts.features ?? [];
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);

View File

@@ -113,9 +113,11 @@ const smallIcon = (link, _isNightTime) => {
return addPath('Thunderstorm.gif');
case 'wind':
case 'wind_':
case 'wind_few':
case 'wind_sct':
case 'wind-n':
case 'wind_-n':
case 'wind_few-n':
return addPath('Wind.gif');

View File

@@ -47,7 +47,7 @@ class LocalForecast extends WeatherDisplay {
forecastsElem.append(...templates);
// increase each forecast height to a multiple of container height
this.pageHeight = forecastsElem.parentNode.scrollHeight;
this.pageHeight = forecastsElem.parentNode.offsetHeight;
templates.forEach((forecast) => {
const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight;
forecast.style.height = `${newHeight}px`;

View File

@@ -52,7 +52,7 @@ class Radar extends WeatherDisplay {
}
// get the base map
const src = 'images/maps/radar.jpg';
const src = 'images/maps/radar.webp';
this.baseMap = await loadImg(src);
const baseUrl = 'https://mesonet.agron.iastate.edu/archive/data/';
@@ -224,4 +224,4 @@ class Radar extends WeatherDisplay {
}
// register display
registerDisplay(new Radar(10, 'radar'));
registerDisplay(new Radar(11, 'radar'));

View File

@@ -28,7 +28,7 @@ class RegionalForecast extends WeatherDisplay {
// there are enough other cities available to populate the map sufficiently even if some do not load
// pre-load the base map
let baseMap = 'images/maps/basemap.png';
let baseMap = 'images/maps/basemap.webp';
if (weatherParameters.state === 'HI') {
baseMap = 'images/maps/radar-hawaii.png';
} else if (weatherParameters.state === 'AK') {

View File

@@ -0,0 +1,127 @@
// display spc outlook in a bar graph
import STATUS from './status.mjs';
import { json } from './utils/fetch.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import testPolygon from './utils/polygon.mjs';
// list of interesting files ordered [0] = today, [1] = tomorrow...
const urlPattern = (day) => `https://www.spc.noaa.gov/products/outlook/day${day}otlk_cat.nolyr.geojson`;
const testAllPoints = (point, data) => {
// returns all points where the data matches as an array of days and then matches of the properties of the data
const result = [];
// start with a loop of days
data.forEach((day, index) => {
// initialize the result
result[index] = false;
// loop through each category
day.features.forEach((feature) => {
if (!feature.geometry.coordinates) return;
const inPolygon = testPolygon(point, feature.geometry);
if (inPolygon) result[index] = feature.properties;
});
});
return result;
};
const barSizes = {
TSTM: 60,
MRGL: 150,
SLGT: 210,
ENH: 270,
MDT: 330,
HIGH: 390,
};
class SpcOutlook extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'SPC Outlook', true);
// don't display on progress/navigation screen
this.showOnProgress = false;
// calculate file names
this.files = [null, null, null].map((v, i) => urlPattern(i + 1));
// set timings
this.timing.totalScreens = 1;
}
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
// initial data does not need to be reloaded on a location change, only during silent refresh
if (!this.initialData || refresh) {
try {
// get the three categorical files to get started
const filePromises = await Promise.allSettled(this.files.map((file) => json(file)));
// store the data, promise will always be fulfilled
this.initialData = filePromises.map((outlookDay) => outlookDay.value);
} catch (error) {
console.error('Unable to get spc outlook');
console.error(error.status, error.responseJSON);
// if there's no previous data, fail
if (!this.initialData) {
this.setStatus(STATUS.failed);
return;
}
}
}
// do the initial parsing of the data
this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.initialData);
// if all the data returns false the there's nothing to do, skip this screen
if (this.data.reduce((prev, cur) => prev || !!cur, false)) {
this.timing.totalScreens = 1;
} else {
this.timing.totalScreens = 0;
}
this.calcNavTiming();
// we only get here if there was no error above
this.screenIndex = 0;
this.setStatus(STATUS.loaded);
}
async drawCanvas() {
super.drawCanvas();
// analyze each day
const days = this.data.map((day, index) => {
// get the day name
const dayName = DateTime.now().plus({ days: index }).toLocaleString({ weekday: 'long' });
// fill the name
const fill = {};
fill['day-name'] = dayName;
// create the element
const elem = this.fillTemplate('day', fill);
// update the bar length
const bar = elem.querySelector('.risk-bar');
if (day.LABEL) {
bar.style.width = `${barSizes[day.LABEL]}px`;
} else {
bar.style.display = 'none';
}
return elem;
});
// add the days to the display
const dayContainer = this.elem.querySelector('.days');
dayContainer.innerHTML = '';
dayContainer.append(...days);
// finish drawing
this.finishDraw();
}
}
// register display
registerDisplay(new SpcOutlook(10, 'spc-outlook'));

View File

@@ -0,0 +1,51 @@
// handle multi-polygon and holes
const testPolygon = (point, _polygons) => {
// turn everything into a multi polygon for ease of processing
let polygons = [[..._polygons.coordinates]];
if (_polygons.type === 'MultiPolygon') polygons = [..._polygons.coordinates];
let inArea = false;
polygons.forEach((_polygon) => {
// copy the polygon
const polygon = [..._polygon];
// if a match has been found don't do anything more
if (inArea) return;
// polygons are defined as [[area], [optional hole 1], [optional hole 2], ...]
const area = polygon.shift();
// test if inside the initial area
inArea = pointInPolygon(point, area);
// if not in the area return false
if (!inArea) return;
// test the holes, if in any hole return false
polygon.forEach((hole) => {
if (pointInPolygon(point, hole)) {
inArea = false;
}
});
});
return inArea;
};
const pointInPolygon = (point, polygon) => {
// ray casting method from https://github.com/substack/point-in-polygon
const x = point[0];
const y = point[1];
let inside = false;
// eslint-disable-next-line no-plusplus
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0];
const yi = polygon[i][1];
const xj = polygon[j][0];
const yj = polygon[j][1];
const intersect = ((yi > y) !== (yj > y))
&& (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
export default testPolygon;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -90,7 +90,9 @@
.location {
color: c.$title-color;
max-height: 32px;
margin-bottom: 10px;
overflow: hidden;
}
}
}

View File

@@ -4,6 +4,7 @@
@font-face {
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
font-display: swap;
}
body {
@@ -251,39 +252,22 @@ body {
width: 475px;
}
@font-face {
font-family: "Star4000";
src: url('../fonts/Star4000.woff') format('woff');
}
@font-face {
font-family: "Star 4 Radar";
src: url('../fonts/Star 4 Radar.woff') format('woff');
}
@font-face {
font-family: 'Star4000 Extended';
src: url('../fonts/Star4000 Extended.woff') format('woff');
}
@font-face {
font-family: 'Star4000LCN';
src: url('../fonts/Star4000LCN.woff') format('woff');
}
@font-face {
font-family: 'Star4000 Large Compressed';
src: url('../fonts/Star4000 Large Compressed.woff') format('woff');
font-display: swap;
}
@font-face {
font-family: 'Star4000 Large';
src: url('../fonts/Star4000 Large.ttf') format('truetype');
src: url('../fonts/Star4000 Large.woff') format('woff');
font-display: swap;
}
@font-face {
font-family: 'Star4000 Small';
src: url('../fonts/Star4000 Small.woff') format('woff');
font-display: swap;
}
#display {

View File

@@ -0,0 +1,86 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#spc-outlook-html.weather-display {
background-image: url('../images/backgrounds/6.png');
}
.weather-display .spc-outlook {
.container {
position: relative;
top: 0px;
margin: 0px 10px;
box-sizing: border-box;
height: 300px;
overflow: hidden;
}
.risk-levels {
position: absolute;
left: 206px;
font-family: 'Star4000 Small';
font-size: 32px;
@include u.text-shadow();
.risk-level {
position: relative;
top: -14px;
height: 20px;
&:nth-child(1) {
left: calc(20px * 5);
}
&:nth-child(2) {
left: calc(20px * 4);
}
&:nth-child(3) {
left: calc(20px * 3);
}
&:nth-child(4) {
left: calc(20px * 2);
}
&:nth-child(5) {
left: calc(20px * 1);
}
&:nth-child(6) {
left: calc(20px * 0);
}
}
}
.days {
position: absolute;
top: 120px;
.day {
height: 60px;
.day-name {
position: absolute;
font-family: 'Star4000';
font-size: 24pt;
width: 200px;
text-align: right;
@include u.text-shadow();
padding-top: 20px;
}
.risk-bar {
position: absolute;
width: 150px;
height: 40px;
left: 210px;
margin-top: 20px;
border: 3px outset hsl(0, 0%, 70%);
background: linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%);
}
}
}
}

View File

@@ -113,7 +113,7 @@
.scroll {
@include u.text-shadow(3px, 1.5px);
width: 640px;
width: calc(640px - 2 * 30px);
height: 70px;
overflow: hidden;
margin-top: 10px;
@@ -122,6 +122,15 @@
font-family: 'Star4000';
font-size: 24pt;
margin-left: 55px;
overflow: hidden;
.scroll-area {
text-wrap: nowrap;
position: relative;
// the following added by js code as it is dependent on the content of the element
// transition: left (x)s;
// left: calc((elem width) - 640px);
}
}
}
}

View File

@@ -12,4 +12,5 @@
@use 'regional-forecast';
@use 'almanac';
@use 'hazards';
@use 'media';
@use 'media';
@use 'spc-outlook';

View File

@@ -18,6 +18,10 @@
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="627">
<link rel="preload" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" type="font/woff" crossorigin>
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
<% if (production) { %>
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
@@ -33,6 +37,7 @@
<script type="module" src="scripts/modules/currentweatherscroll.mjs"></script>
<script type="module" src="scripts/modules/currentweather.mjs"></script>
<script type="module" src="scripts/modules/almanac.mjs"></script>
<script type="module" src="scripts/modules/spc-outlook.mjs"></script>
<script type="module" src="scripts/modules/icons.mjs"></script>
<script type="module" src="scripts/modules/extendedforecast.mjs"></script>
<script type="module" src="scripts/modules/hourly-graph.mjs"></script>
@@ -109,6 +114,9 @@
<div id="almanac-html" class="weather-display">
<%- include('partials/almanac.ejs') %>
</div>
<div id="spc-outlook-html" class="weather-display">
<%- include('partials/spc-outlook.ejs') %>
</div>
<div id="extended-forecast-html" class="weather-display">
<%- include('partials/extended-forecast.ejs') %>
</div>
@@ -171,6 +179,7 @@
Station Id: <span id="spanStationId"></span><br />
Radar Id: <span id="spanRadarId"></span><br />
Zone Id: <span id="spanZoneId"></span><br />
Ws4kp Version: <span><%- version %></span>
</div>
</body>

View File

@@ -35,7 +35,7 @@
<div class="scroll-area">
<div class="frame template">
<div class="map">
<img src="images/maps/radar.jpg" />
<img src="images/maps/radar.webp" />
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<%- 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.png" /></div>
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">
<div class="icon">

View File

@@ -0,0 +1,20 @@
<%- 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') %>