v1.41 - Add 2027 Dates and Fix Cosmic Events
This commit is contained in:
@@ -62,6 +62,30 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Slideshow dots */
|
||||||
|
.event-banner-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 7px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.event-banner-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.event-banner-dot.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
.event-banner-dot:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
/* Cell tag — tiny event icon in the hour column */
|
/* Cell tag — tiny event icon in the hour column */
|
||||||
.event-cell-tag {
|
.event-cell-tag {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
+83
-31
@@ -31,7 +31,7 @@ import {
|
|||||||
cloudCategory, confidenceBand, moonGlyph,
|
cloudCategory, confidenceBand, moonGlyph,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
|
import { SkyScope, WindVane, CloudIcon, ScopeReticle, PrecipIcon, CustomSelect, VentPill } from './components.js';
|
||||||
import { getActiveEvent, shouldShowCellTag, getUpcomingEvents } from './events.js';
|
import { getActiveEvents, getLensEvent, getCellTagEvents, getUpcomingEvents } from './events.js';
|
||||||
|
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
|
|
||||||
@@ -719,12 +719,30 @@ export function UTCIForecast() {
|
|||||||
? utciCategory(currentRow.utciAdj)
|
? utciCategory(currentRow.utciAdj)
|
||||||
: { bg: '#4a4228', fg: '#ede4cc', label: 'No data' };
|
: { bg: '#4a4228', fg: '#ede4cc', label: 'No data' };
|
||||||
|
|
||||||
// ─── COSMIC/WEATHER EVENT ─────────────────────────────────────────────
|
// ─── COSMIC/WEATHER EVENTS ────────────────────────────────────────────
|
||||||
// Compute once per render. todayRows = the rows for today (day 0).
|
// activeEvents — today's events → drives banner & lens overlay.
|
||||||
const [dismissedEventId, setDismissedEventId] = useState(null);
|
// selectedDayEvents — viewed day's events → drives cell tags.
|
||||||
|
const [dismissedEventIds, setDismissedEventIds] = useState([]);
|
||||||
|
const [bannerIndex, setBannerIndex] = useState(0);
|
||||||
const todayRows = days[0]?.rows || [];
|
const todayRows = days[0]?.rows || [];
|
||||||
const activeEvent = getActiveEvent(todayRows, location);
|
const activeEvents = getActiveEvents(todayRows, location)
|
||||||
const showBanner = activeEvent && activeEvent.id !== dismissedEventId;
|
.filter(ev => !dismissedEventIds.includes(ev.id));
|
||||||
|
const lensEvent = getLensEvent(activeEvents);
|
||||||
|
const selectedDayEvents = getActiveEvents(visible, location);
|
||||||
|
|
||||||
|
// Auto-advance banner slideshow every 10 seconds when multiple events
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeEvents.length <= 1) { setBannerIndex(0); return; }
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setBannerIndex(i => (i + 1) % activeEvents.length);
|
||||||
|
}, 10000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [activeEvents.length, activeEvents.map(e => e.id).join(',')]);
|
||||||
|
|
||||||
|
// Keep index in bounds if events change
|
||||||
|
useEffect(() => {
|
||||||
|
if (bannerIndex >= activeEvents.length) setBannerIndex(0);
|
||||||
|
}, [activeEvents.length]);
|
||||||
|
|
||||||
// ─── 4. JSX RETURN ───────────────────────────────────────────────────
|
// ─── 4. JSX RETURN ───────────────────────────────────────────────────
|
||||||
// Everything below is the actual page markup, written as one big HTM
|
// Everything below is the actual page markup, written as one big HTM
|
||||||
@@ -766,27 +784,61 @@ export function UTCIForecast() {
|
|||||||
</nav>
|
</nav>
|
||||||
<main class="utci-shell">
|
<main class="utci-shell">
|
||||||
|
|
||||||
<!-- ── EVENT BANNER ── slides down when an event is active ── -->
|
<!-- ── EVENT BANNER ── slideshow when multiple events active ── -->
|
||||||
<div class=${`event-banner-wrap${showBanner ? ' visible' : ''}`}>
|
<div class=${`event-banner-wrap${activeEvents.length > 0 ? ' visible' : ''}`}>
|
||||||
${showBanner && html`
|
${activeEvents.length > 0 && (() => {
|
||||||
<div class="event-banner" style=${{
|
const ev = activeEvents[bannerIndex] || activeEvents[0];
|
||||||
background: activeEvent.color,
|
const fmtDate = (iso) => iso
|
||||||
color: activeEvent.textColor,
|
? new Date(iso + 'T00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' })
|
||||||
}}>
|
: null;
|
||||||
<span class="event-banner-emoji">${activeEvent.emoji}</span>
|
const startFmt = fmtDate(ev.start);
|
||||||
<div class="event-banner-body">
|
const peakFmt = fmtDate(ev.peak);
|
||||||
<div class="event-banner-title">${activeEvent.title}</div>
|
const endFmt = fmtDate(ev.end);
|
||||||
<div class="event-banner-msg">${activeEvent.message}</div>
|
// Only show date line if the event has start/end (cosmic events do; weather events don't)
|
||||||
</div>
|
const showDates = startFmt && endFmt;
|
||||||
<button
|
const dateLine = showDates
|
||||||
class="event-banner-dismiss"
|
? (startFmt === endFmt
|
||||||
style=${{ color: activeEvent.textColor }}
|
? peakFmt ? `Peak: ${peakFmt}` : `Date: ${startFmt}`
|
||||||
onClick=${() => setDismissedEventId(activeEvent.id)}
|
: peakFmt && peakFmt !== startFmt && peakFmt !== endFmt
|
||||||
aria-label="Dismiss"
|
? `Active ${startFmt} – ${endFmt} · Peak: ${peakFmt}`
|
||||||
title="Dismiss"
|
: `Active ${startFmt} – ${endFmt}`)
|
||||||
>✕</button>
|
: null;
|
||||||
</div>
|
return html`
|
||||||
`}
|
<div class="event-banner"
|
||||||
|
style=${{ background: ev.color, color: ev.textColor }}
|
||||||
|
onMouseEnter=${() => {}}
|
||||||
|
>
|
||||||
|
<span class="event-banner-emoji">${ev.emoji}</span>
|
||||||
|
<div class="event-banner-body">
|
||||||
|
<div class="event-banner-title">${ev.title}</div>
|
||||||
|
<div class="event-banner-msg">${ev.message}</div>
|
||||||
|
${dateLine && html`
|
||||||
|
<div class="event-banner-dates" style=${{ opacity: 0.75, fontSize: '11px', fontFamily: 'JetBrains Mono, monospace', letterSpacing: '0.08em', textTransform: 'uppercase', marginTop: '5px' }}>
|
||||||
|
${dateLine}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
${activeEvents.length > 1 && html`
|
||||||
|
<div class="event-banner-dots">
|
||||||
|
${activeEvents.map((_, i) => html`
|
||||||
|
<span
|
||||||
|
key=${i}
|
||||||
|
class=${`event-banner-dot${i === bannerIndex ? ' active' : ''}`}
|
||||||
|
onClick=${() => setBannerIndex(i)}
|
||||||
|
style=${{ background: ev.textColor }}
|
||||||
|
/>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="event-banner-dismiss"
|
||||||
|
style=${{ color: ev.textColor }}
|
||||||
|
onClick=${() => setDismissedEventIds(ids => [...ids, ev.id])}
|
||||||
|
aria-label="Dismiss"
|
||||||
|
title="Dismiss this event"
|
||||||
|
>✕</button>
|
||||||
|
</div>`;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="utci-header">
|
<div class="utci-header">
|
||||||
@@ -811,7 +863,7 @@ export function UTCIForecast() {
|
|||||||
elev=${currentRow?.elev ?? 0}
|
elev=${currentRow?.elev ?? 0}
|
||||||
dt=${currentRow?.dt ?? new Date()}
|
dt=${currentRow?.dt ?? new Date()}
|
||||||
glob=${currentRow?.glob ?? 0}
|
glob=${currentRow?.glob ?? 0}
|
||||||
activeEvent=${activeEvent}
|
activeEvent=${lensEvent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1457,9 +1509,9 @@ ${isPro && (activeProfile === 'custom' || FILTER_PROFILES[activeProfile].cols['a
|
|||||||
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
<span style=${{ display: 'inline-flex', alignItems: 'center', gap: '7px', verticalAlign: 'middle' }}>
|
||||||
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${30} />
|
<${SkyScope} elev=${r.elev} dt=${r.dt} glob=${r.glob} size=${30} />
|
||||||
<span>${localHHMM}</span>
|
<span>${localHHMM}</span>
|
||||||
${shouldShowCellTag(activeEvent, r) && html`
|
${getCellTagEvents(selectedDayEvents, r).map(ev => html`
|
||||||
<span class="event-cell-tag" title=${activeEvent.title}>${activeEvent.emoji}</span>
|
<span key=${ev.id} class="event-cell-tag" title=${ev.title}>${ev.emoji}</span>
|
||||||
`}
|
`)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
${visibleCols.air && html`<td style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`}
|
${visibleCols.air && html`<td style=${{ background: tempBg(r.Ta) }}>${r.Ta.toFixed(1)}</td>`}
|
||||||
|
|||||||
+343
-113
@@ -190,6 +190,121 @@ const COSMIC_CALENDAR = [
|
|||||||
start: '2026-06-30', end: '2026-07-02',
|
start: '2026-06-30', end: '2026-07-02',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── 2027 METEOR SHOWERS ─────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'quadrantids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Quadrantid Meteor Shower',
|
||||||
|
message: 'The Quadrantid meteor shower is active — up to 120 meteors/hour at peak. Best in the hours before dawn on 4 Jan from a dark site.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-01-01', end: '2027-01-05', peak: '2027-01-04',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lyrids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Lyrid Meteor Shower',
|
||||||
|
message: 'The Lyrid meteor shower peaks — up to 20 meteors/hour after midnight. Note: bright waning gibbous moon may reduce visibility this year.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-04-16', end: '2027-04-25', peak: '2027-04-23',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eta-aquariids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Eta Aquariid Meteor Shower',
|
||||||
|
message: 'The Eta Aquariids peak — fragments of Halley\'s Comet, up to 50/hour before dawn. New moon on 6 May means excellent dark skies this year.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-04-19', end: '2027-05-28', peak: '2027-05-05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perseids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Perseid Meteor Shower',
|
||||||
|
message: 'The Perseids are active — one of the best showers of the year, up to 100 meteors/hour at peak around 13 Aug.',
|
||||||
|
color: '#3a1a00', textColor: '#ffe8c0',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-07-17', end: '2027-08-24', peak: '2027-08-13',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orionids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Orionid Meteor Shower',
|
||||||
|
message: 'The Orionid shower peaks — fast, bright meteors from Halley\'s Comet debris. Up to 25/hour.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-10-02', end: '2027-11-07', peak: '2027-10-21',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'leonids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Leonid Meteor Shower',
|
||||||
|
message: 'The Leonid shower peaks — fast meteors from comet Tempel-Tuttle, up to 15/hour.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-11-06', end: '2027-11-30', peak: '2027-11-17',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'geminids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Geminid Meteor Shower',
|
||||||
|
message: 'The Geminids peak — the finest shower of the year, up to 150 meteors/hour, visible even before midnight.',
|
||||||
|
color: '#3a1a00', textColor: '#ffe8c0',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-12-04', end: '2027-12-20', peak: '2027-12-14',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ursids-2027',
|
||||||
|
emoji: String.fromCodePoint(0x2604, 0xFE0F),
|
||||||
|
title: 'Ursid Meteor Shower',
|
||||||
|
message: 'The Ursid shower peaks near the winter solstice — circumpolar, best from northern latitudes.',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-12-17', end: '2027-12-26', peak: '2027-12-22',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 2027 ECLIPSES ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'annular-solar-eclipse-2027-feb',
|
||||||
|
emoji: String.fromCodePoint(0x1F311),
|
||||||
|
title: 'Annular Solar Eclipse',
|
||||||
|
message: 'An annular solar eclipse creates a "ring of fire" effect — visible across parts of South America, Africa and the Indian Ocean.',
|
||||||
|
color: '#1a0a2a', textColor: '#d8c8f8',
|
||||||
|
type: 'cosmic', nightOnly: false,
|
||||||
|
start: '2027-02-06', end: '2027-02-06', peak: '2027-02-06',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total-solar-eclipse-2027-aug',
|
||||||
|
emoji: String.fromCodePoint(0x1F311),
|
||||||
|
title: 'Total Solar Eclipse',
|
||||||
|
message: 'A spectacular total solar eclipse — the path of totality crosses Morocco, Spain, Algeria, Libya, Egypt and Saudi Arabia. One of the longest totalities of the century.',
|
||||||
|
color: '#1a0a2a', textColor: '#d8c8f8',
|
||||||
|
type: 'cosmic', nightOnly: false,
|
||||||
|
start: '2027-08-02', end: '2027-08-02', peak: '2027-08-02',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 2027 PLANETARY EVENTS ───────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'mars-opposition-2027',
|
||||||
|
emoji: String.fromCodePoint(0x1F534),
|
||||||
|
title: 'Mars at Opposition',
|
||||||
|
message: 'Mars is at its closest and brightest — a vivid red beacon visible all night long. Good binoculars will reveal its surface colour.',
|
||||||
|
color: '#2a1520', textColor: '#ffc8d8',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-02-19', end: '2027-02-19', peak: '2027-02-19',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'venus-jupiter-conjunction-2027',
|
||||||
|
emoji: String.fromCodePoint(0x1FA90),
|
||||||
|
title: 'Venus & Jupiter Conjunction',
|
||||||
|
message: 'Venus and Jupiter appear strikingly close together in the evening sky — a dazzling naked-eye pairing of the two brightest planets.',
|
||||||
|
color: '#1a2a3a', textColor: '#c8e8ff',
|
||||||
|
type: 'cosmic', nightOnly: true,
|
||||||
|
start: '2027-08-23', end: '2027-08-27', peak: '2027-08-25',
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ─── WEATHER-DERIVED EVENTS ───────────────────────────────────────────────
|
// ─── WEATHER-DERIVED EVENTS ───────────────────────────────────────────────
|
||||||
@@ -421,6 +536,128 @@ const ALMANAC_CALENDAR = [
|
|||||||
latHint: 'north',
|
latHint: 'north',
|
||||||
type: 'meteor',
|
type: 'meteor',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── 2027 ─────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'quadrantids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Quadrantid Meteor Shower',
|
||||||
|
desc: 'Up to 120 meteors/hour at peak — one of the strongest showers but with a very sharp peak. Best in the hours before dawn on 4 Jan.',
|
||||||
|
start: '2027-01-01', peak: '2027-01-04',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: 'north',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'annular-solar-eclipse-2027-feb',
|
||||||
|
emoji: '🌑',
|
||||||
|
title: 'Annular Solar Eclipse',
|
||||||
|
desc: 'A "ring of fire" eclipse visible from parts of South America, Africa and the Indian Ocean. Partial eclipse across much of the southern hemisphere.',
|
||||||
|
start: '2027-02-06', peak: '2027-02-06',
|
||||||
|
color: '#1a0a2a', textColor: '#d8c8f8',
|
||||||
|
latHint: 'path:South America, southern Africa, Indian Ocean',
|
||||||
|
type: 'eclipse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mars-opposition-2027',
|
||||||
|
emoji: '🔴',
|
||||||
|
title: 'Mars at Opposition',
|
||||||
|
desc: 'Mars is at its closest and brightest — a vivid red beacon visible all night long. Good binoculars reveal its distinct colour.',
|
||||||
|
start: '2027-02-19', peak: '2027-02-19',
|
||||||
|
color: '#2a1520', textColor: '#ffc8d8',
|
||||||
|
latHint: null,
|
||||||
|
type: 'planetary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lyrids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Lyrid Meteor Shower',
|
||||||
|
desc: 'Up to 20 meteors/hour at peak. Note: bright waning gibbous moon may reduce visibility this year. Active Apr 16–25.',
|
||||||
|
start: '2027-04-16', peak: '2027-04-23',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: 'north',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eta-aquariids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Eta Aquariid Meteor Shower',
|
||||||
|
desc: 'Debris from Halley\'s Comet — up to 50/hour before dawn. New moon on 6 May means excellent dark skies this year.',
|
||||||
|
start: '2027-04-19', peak: '2027-05-05',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: 'south',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perseids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Perseid Meteor Shower',
|
||||||
|
desc: 'One of the best showers of the year — up to 100/hour at peak, no telescope needed. Active Jul 17 – Aug 24.',
|
||||||
|
start: '2027-07-17', peak: '2027-08-13',
|
||||||
|
color: '#3a1a00', textColor: '#ffe8c0',
|
||||||
|
latHint: 'north',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total-solar-eclipse-2027-aug',
|
||||||
|
emoji: '🌑',
|
||||||
|
title: 'Total Solar Eclipse',
|
||||||
|
desc: 'One of the longest total solar eclipses of the century. Path of totality crosses Morocco, Spain, Algeria, Libya, Egypt and Saudi Arabia.',
|
||||||
|
start: '2027-08-02', peak: '2027-08-02',
|
||||||
|
color: '#1a0a2a', textColor: '#d8c8f8',
|
||||||
|
latHint: 'path:Morocco, Spain, Algeria, Libya, Egypt, Saudi Arabia',
|
||||||
|
type: 'eclipse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'venus-jupiter-conjunction-2027',
|
||||||
|
emoji: '🪐',
|
||||||
|
title: 'Venus & Jupiter Conjunction',
|
||||||
|
desc: 'Venus and Jupiter appear strikingly close together in the evening sky — a dazzling naked-eye pairing of the two brightest planets.',
|
||||||
|
start: '2027-08-23', peak: '2027-08-25',
|
||||||
|
color: '#1a2a3a', textColor: '#c8e8ff',
|
||||||
|
latHint: null,
|
||||||
|
type: 'planetary',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'orionids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Orionid Meteor Shower',
|
||||||
|
desc: 'Fast, bright meteors from Halley\'s Comet debris — up to 25/hour. Active Oct 2 – Nov 7.',
|
||||||
|
start: '2027-10-02', peak: '2027-10-21',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: null,
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'leonids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Leonid Meteor Shower',
|
||||||
|
desc: 'Fast meteors from comet Tempel-Tuttle. Up to 15/hour at peak. Active Nov 6–30.',
|
||||||
|
start: '2027-11-06', peak: '2027-11-17',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: null,
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'geminids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Geminid Meteor Shower',
|
||||||
|
desc: 'The finest shower of the year — up to 150 meteors/hour, visible even before midnight. Active Dec 4–20.',
|
||||||
|
start: '2027-12-04', peak: '2027-12-14',
|
||||||
|
color: '#3a1a00', textColor: '#ffe8c0',
|
||||||
|
latHint: 'north',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ursids-2027',
|
||||||
|
emoji: '☄️',
|
||||||
|
title: 'Ursid Meteor Shower',
|
||||||
|
desc: 'A quieter shower near the winter solstice — circumpolar, best from northern latitudes. Active Dec 17–26.',
|
||||||
|
start: '2027-12-17', peak: '2027-12-22',
|
||||||
|
color: '#2a1a5a', textColor: '#e8d8ff',
|
||||||
|
latHint: 'north',
|
||||||
|
type: 'meteor',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Returns a location-aware visibility note for an almanac entry.
|
// Returns a location-aware visibility note for an almanac entry.
|
||||||
@@ -465,47 +702,68 @@ export function getUpcomingEvents(location, days = 90) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── CELL TAG LOGIC ──────────────────────────────────────────────────────
|
// ─── CELL TAG LOGIC ──────────────────────────────────────────────────────
|
||||||
// For timetable cell icons: given an active event and a row,
|
// Returns the subset of active events that should show an icon for this row.
|
||||||
// should this hour show the event icon?
|
export function getCellTagEvents(events, row) {
|
||||||
export function shouldShowCellTag(event, row) {
|
if (!events || events.length === 0) return [];
|
||||||
if (!event) return false;
|
return events.filter(ev => {
|
||||||
if (event.type === 'promo') return false;
|
if (ev.type === 'promo') return false;
|
||||||
// Night-only events only badge night rows
|
if (ev.nightOnly && row.elev >= -5) return false;
|
||||||
if (event.nightOnly && row.elev >= -5) return false;
|
if (ev.id === 'heat-spike' && row.elev < 0) return false;
|
||||||
// Weather events: storm/frost badge all hours; heat badges daytime only
|
return true;
|
||||||
if (event.id === 'heat-spike' && row.elev < 0) return false;
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── MAIN EXPORT ─────────────────────────────────────────────────────────
|
// ─── MAIN EXPORT ─────────────────────────────────────────────────────────
|
||||||
// Call this once per render with today's rows and location.
|
// Returns ALL active events for the given rows/date as an array.
|
||||||
// Returns the single highest-priority active event, or null.
|
// Empty array = nothing active.
|
||||||
//
|
//
|
||||||
// Priority order:
|
// Order within array:
|
||||||
// 1. PROMO_OVERRIDE (manual)
|
// 1. PROMO_OVERRIDE (if set, returned alone)
|
||||||
// 2. Cosmic calendar events (sorted by priority: eclipse > meteor > planetary)
|
// 2. All matching cosmic calendar events for this date
|
||||||
// 3. Weather events (heat > storm > frost > sunset > stargazing)
|
// 3. All matching weather-derived events
|
||||||
|
|
||||||
export function getActiveEvent(todayRows, location) {
|
export function getActiveEvents(rows, location) {
|
||||||
// 1. Manual promo override
|
// 1. Manual promo override — shown alone, no mixing with other events
|
||||||
if (PROMO_OVERRIDE) return PROMO_OVERRIDE;
|
if (PROMO_OVERRIDE) return [PROMO_OVERRIDE];
|
||||||
|
|
||||||
// 2. Cosmic calendar
|
// 2. Cosmic calendar — use the date of the rows being viewed, not today.
|
||||||
const today = new Date();
|
const dateStr = (rows && rows.length > 0)
|
||||||
const todayStr = today.toISOString().slice(0, 10);
|
? rows[0].iso.slice(0, 10)
|
||||||
const cosmicHit = COSMIC_CALENDAR.find(ev => todayStr >= ev.start && todayStr <= ev.end);
|
: new Date().toISOString().slice(0, 10);
|
||||||
if (cosmicHit) return cosmicHit;
|
const cosmicHits = COSMIC_CALENDAR.filter(ev => dateStr >= ev.start && dateStr <= ev.end);
|
||||||
|
|
||||||
// 3. Weather-derived (needs forecast data)
|
// 3. Weather-derived events (all that match, not just first)
|
||||||
if (!todayRows || todayRows.length === 0) return null;
|
const weatherEvents = [];
|
||||||
|
if (rows && rows.length > 0) {
|
||||||
|
const checks = [
|
||||||
|
checkHeatSpike(rows),
|
||||||
|
checkStorm(rows),
|
||||||
|
checkFrost(rows),
|
||||||
|
checkSunset(rows, location),
|
||||||
|
checkStargazing(rows),
|
||||||
|
];
|
||||||
|
checks.forEach(ev => { if (ev) weatherEvents.push(ev); });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return [...cosmicHits, ...weatherEvents];
|
||||||
checkHeatSpike(todayRows) ||
|
}
|
||||||
checkStorm(todayRows) ||
|
|
||||||
checkFrost(todayRows) ||
|
// ─── LENS EVENT PICKER ───────────────────────────────────────────────────
|
||||||
checkSunset(todayRows, location) ||
|
// From an array of active events, returns the single one closest to its peak
|
||||||
checkStargazing(todayRows)
|
// (most "intense"). Weather events without a peak use today's date.
|
||||||
);
|
// Returns null if events array is empty.
|
||||||
|
|
||||||
|
export function getLensEvent(events) {
|
||||||
|
if (!events || events.length === 0) return null;
|
||||||
|
if (events.length === 1) return events[0];
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
return events.reduce((best, ev) => {
|
||||||
|
const peakB = best.peak || today;
|
||||||
|
const peakE = ev.peak || today;
|
||||||
|
const distB = Math.abs(new Date(today) - new Date(peakB));
|
||||||
|
const distE = Math.abs(new Date(today) - new Date(peakE));
|
||||||
|
return distE < distB ? ev : best;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── LENS OVERLAY RENDERER ───────────────────────────────────────────────
|
// ─── LENS OVERLAY RENDERER ───────────────────────────────────────────────
|
||||||
@@ -518,114 +776,86 @@ export function getLensOverlaySVG(event, cx, cy, lensR) {
|
|||||||
if (!event) return null;
|
if (!event) return null;
|
||||||
const id = event.id;
|
const id = event.id;
|
||||||
|
|
||||||
if (event.emoji === '☄️') {
|
if (event.emoji === String.fromCodePoint(0x2604, 0xFE0F)) {
|
||||||
return [
|
return [
|
||||||
'<g opacity="0.55" clip-path="url(#scope-lens-clip)">',
|
'<g opacity="0.55" clip-path="url(#scope-lens-clip)">',
|
||||||
`<line x1="${cx-60}" y1="${cy-50}" x2="${cx-20}" y2="${cy-20}" stroke="#ffe8a0" stroke-width="1.5" stroke-linecap="round" opacity="0.9"/>`,
|
'<line x1="' + (cx-60) + '" y1="' + (cy-50) + '" x2="' + (cx-20) + '" y2="' + (cy-20) + '" stroke="#ffe8a0" stroke-width="1.5" stroke-linecap="round" opacity="0.9"/>',
|
||||||
`<line x1="${cx-40}" y1="${cy-70}" x2="${cx+5}" y2="${cy-30}" stroke="#fff0c0" stroke-width="1" stroke-linecap="round" opacity="0.7"/>`,
|
'<line x1="' + (cx-40) + '" y1="' + (cy-70) + '" x2="' + (cx+5) + '" y2="' + (cy-30) + '" stroke="#fff0c0" stroke-width="1" stroke-linecap="round" opacity="0.7"/>',
|
||||||
`<line x1="${cx+30}" y1="${cy-60}" x2="${cx+65}" y2="${cy-25}" stroke="#ffe8a0" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>`,
|
'<line x1="' + (cx+30) + '" y1="' + (cy-60) + '" x2="' + (cx+65) + '" y2="' + (cy-25) + '" stroke="#ffe8a0" stroke-width="1.8" stroke-linecap="round" opacity="0.8"/>',
|
||||||
`<line x1="${cx-10}" y1="${cy-40}" x2="${cx+30}" y2="${cy-5}" stroke="#fff0c0" stroke-width="0.9" stroke-linecap="round" opacity="0.65"/>`,
|
'<line x1="' + (cx-10) + '" y1="' + (cy-40) + '" x2="' + (cx+30) + '" y2="' + (cy-5) + '" stroke="#fff0c0" stroke-width="0.9" stroke-linecap="round" opacity="0.65"/>',
|
||||||
`<line x1="${cx-70}" y1="${cy-20}" x2="${cx-35}" y2="${cy+10}" stroke="#ffe8a0" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>`,
|
'<line x1="' + (cx-70) + '" y1="' + (cy-20) + '" x2="' + (cx-35) + '" y2="' + (cy+10) + '" stroke="#ffe8a0" stroke-width="1.2" stroke-linecap="round" opacity="0.6"/>',
|
||||||
'</g>',
|
'</g>',
|
||||||
].join('\n');
|
].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.includes('solar-eclipse')) {
|
if (id.includes('solar-eclipse')) {
|
||||||
return [
|
const cx_ = cx, cy_ = cy - 20;
|
||||||
'<g clip-path="url(#scope-lens-clip)">',
|
return '<g clip-path="url(#scope-lens-clip)">'
|
||||||
`<radialGradient id="corona-glow" cx="${cx}" cy="${cy-20}" r="38" gradientUnits="userSpaceOnUse">`,
|
+ '<radialGradient id="corona-glow" cx="' + cx_ + '" cy="' + cy_ + '" r="38" gradientUnits="userSpaceOnUse">'
|
||||||
'<stop offset="0%" stop-color="rgba(255,220,100,0)" />',
|
+ '<stop offset="0%" stop-color="rgba(255,220,100,0)" />'
|
||||||
'<stop offset="55%" stop-color="rgba(255,220,100,0.55)" />',
|
+ '<stop offset="55%" stop-color="rgba(255,220,100,0.55)" />'
|
||||||
'<stop offset="100%" stop-color="rgba(255,180,40,0)" />',
|
+ '<stop offset="100%" stop-color="rgba(255,180,40,0)" /></radialGradient>'
|
||||||
'</radialGradient>',
|
+ '<circle cx="' + cx_ + '" cy="' + cy_ + '" r="38" fill="url(#corona-glow)" opacity="0.8" />'
|
||||||
`<circle cx="${cx}" cy="${cy-20}" r="38" fill="url(#corona-glow)" opacity="0.8" />`,
|
+ '<circle cx="' + cx_ + '" cy="' + cy_ + '" r="18" fill="rgba(5,2,15,0.88)" /></g>';
|
||||||
`<circle cx="${cx}" cy="${cy-20}" r="18" fill="rgba(5,2,15,0.88)" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id.includes('lunar-eclipse')) {
|
if (id.includes('lunar-eclipse')) {
|
||||||
return [
|
const cx_ = cx, cy_ = cy - 30;
|
||||||
'<g clip-path="url(#scope-lens-clip)">',
|
return '<g clip-path="url(#scope-lens-clip)">'
|
||||||
`<radialGradient id="blood-moon-glow" cx="${cx}" cy="${cy-30}" r="30" gradientUnits="userSpaceOnUse">`,
|
+ '<radialGradient id="blood-moon-glow" cx="' + cx_ + '" cy="' + cy_ + '" r="30" gradientUnits="userSpaceOnUse">'
|
||||||
'<stop offset="0%" stop-color="rgba(180,30,10,0.7)" />',
|
+ '<stop offset="0%" stop-color="rgba(180,30,10,0.7)" />'
|
||||||
'<stop offset="100%" stop-color="rgba(120,10,5,0)" />',
|
+ '<stop offset="100%" stop-color="rgba(120,10,5,0)" /></radialGradient>'
|
||||||
'</radialGradient>',
|
+ '<circle cx="' + cx_ + '" cy="' + cy_ + '" r="30" fill="url(#blood-moon-glow)" />'
|
||||||
`<circle cx="${cx}" cy="${cy-30}" r="30" fill="url(#blood-moon-glow)" />`,
|
+ '<circle cx="' + cx_ + '" cy="' + cy_ + '" r="16" fill="rgba(160,25,10,0.75)" />'
|
||||||
`<circle cx="${cx}" cy="${cy-30}" r="16" fill="rgba(160,25,10,0.75)" />`,
|
+ '<circle cx="' + cx_ + '" cy="' + cy_ + '" r="16" fill="none" stroke="rgba(220,60,20,0.5)" stroke-width="1.5" /></g>';
|
||||||
`<circle cx="${cx}" cy="${cy-30}" r="16" fill="none" stroke="rgba(220,60,20,0.5)" stroke-width="1.5" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.emoji === '🪐') {
|
if (event.emoji === String.fromCodePoint(0x1FA90)) {
|
||||||
return [
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.65">'
|
||||||
'<g clip-path="url(#scope-lens-clip)" opacity="0.65">',
|
+ '<circle cx="' + (cx+20) + '" cy="' + (cy-35) + '" r="10" fill="rgba(210,185,130,0.7)" />'
|
||||||
`<circle cx="${cx+20}" cy="${cy-35}" r="10" fill="rgba(210,185,130,0.7)" />`,
|
+ '<ellipse cx="' + (cx+20) + '" cy="' + (cy-35) + '" rx="18" ry="4.5" fill="none" stroke="rgba(200,170,100,0.65)" stroke-width="2.5" /></g>';
|
||||||
`<ellipse cx="${cx+20}" cy="${cy-35}" rx="18" ry="4.5" fill="none" stroke="rgba(200,170,100,0.65)" stroke-width="2.5" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.emoji === '🔴') {
|
if (event.emoji === String.fromCodePoint(0x1F534)) {
|
||||||
return [
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.6">'
|
||||||
'<g clip-path="url(#scope-lens-clip)" opacity="0.6">',
|
+ '<circle cx="' + (cx+30) + '" cy="' + (cy-30) + '" r="7" fill="rgba(200,80,40,0.75)" />'
|
||||||
`<circle cx="${cx+30}" cy="${cy-30}" r="7" fill="rgba(200,80,40,0.75)" />`,
|
+ '<circle cx="' + (cx-20) + '" cy="' + (cy-45) + '" r="5" fill="rgba(220,160,80,0.7)" /></g>';
|
||||||
`<circle cx="${cx-20}" cy="${cy-45}" r="5" fill="rgba(220,160,80,0.7)" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'stargazing') {
|
if (id === 'stargazing') {
|
||||||
const stars = [
|
const pts = [[cx-50,cy-55,1.8],[cx+40,cy-65,1.4],[cx-20,cy-70,1.0],[cx+65,cy-40,1.6],[cx-60,cy-30,1.2],[cx+50,cy-55,1.0],[cx-35,cy-45,1.4],[cx+20,cy-50,1.8],[cx-75,cy-50,1.0]];
|
||||||
[cx-50,cy-55,1.8],[cx+40,cy-65,1.4],[cx-20,cy-70,1.0],
|
const dots = pts.map(function(s){return '<circle cx="'+s[0]+'" cy="'+s[1]+'" r="'+s[2]+'" fill="rgba(255,255,255,0.9)" />';}).join('');
|
||||||
[cx+65,cy-40,1.6],[cx-60,cy-30,1.2],[cx+50,cy-55,1.0],
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.7">' + dots + '</g>';
|
||||||
[cx-35,cy-45,1.4],[cx+20,cy-50,1.8],[cx-75,cy-50,1.0],
|
|
||||||
];
|
|
||||||
const dots = stars.map(([x,y,r]) => `<circle cx="${x}" cy="${y}" r="${r}" fill="rgba(255,255,255,0.9)" />`).join('');
|
|
||||||
return `<g clip-path="url(#scope-lens-clip)" opacity="0.7">${dots}</g>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'perfect-sunset') {
|
if (id === 'perfect-sunset') {
|
||||||
return [
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.5">'
|
||||||
'<g clip-path="url(#scope-lens-clip)" opacity="0.5">',
|
+ '<radialGradient id="sunset-lens-glow" cx="' + cx + '" cy="' + (cy+20) + '" r="' + lensR + '" gradientUnits="userSpaceOnUse">'
|
||||||
`<radialGradient id="sunset-lens-glow" cx="${cx}" cy="${cy+20}" r="${lensR}" gradientUnits="userSpaceOnUse">`,
|
+ '<stop offset="0%" stop-color="rgba(255,120,20,0.7)" />'
|
||||||
'<stop offset="0%" stop-color="rgba(255,120,20,0.7)" />',
|
+ '<stop offset="50%" stop-color="rgba(255,60,10,0.25)" />'
|
||||||
'<stop offset="50%" stop-color="rgba(255,60,10,0.25)" />',
|
+ '<stop offset="100%" stop-color="rgba(200,20,0,0)" /></radialGradient>'
|
||||||
'<stop offset="100%" stop-color="rgba(200,20,0,0)" />',
|
+ '<circle cx="' + cx + '" cy="' + cy + '" r="' + lensR + '" fill="url(#sunset-lens-glow)" /></g>';
|
||||||
'</radialGradient>',
|
|
||||||
`<circle cx="${cx}" cy="${cy}" r="${lensR}" fill="url(#sunset-lens-glow)" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'heat-spike') {
|
if (id === 'heat-spike') {
|
||||||
return [
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.35">'
|
||||||
'<g clip-path="url(#scope-lens-clip)" opacity="0.35">',
|
+ '<path d="M ' + (cx-lensR+10) + ' ' + (cy+30) + ' Q ' + (cx-20) + ' ' + (cy+15) + ' ' + (cx+20) + ' ' + (cy+30) + ' Q ' + (cx+60) + ' ' + (cy+45) + ' ' + (cx+lensR-10) + ' ' + (cy+30) + '" fill="none" stroke="rgba(255,120,20,0.6)" stroke-width="2" />'
|
||||||
`<path d="M ${cx-lensR+10} ${cy+30} Q ${cx-20} ${cy+15} ${cx+20} ${cy+30} Q ${cx+60} ${cy+45} ${cx+lensR-10} ${cy+30}" fill="none" stroke="rgba(255,120,20,0.6)" stroke-width="2" />`,
|
+ '<path d="M ' + (cx-lensR+10) + ' ' + (cy+45) + ' Q ' + (cx-10) + ' ' + (cy+30) + ' ' + (cx+30) + ' ' + (cy+45) + ' Q ' + (cx+65) + ' ' + (cy+60) + ' ' + (cx+lensR-10) + ' ' + (cy+45) + '" fill="none" stroke="rgba(255,100,10,0.5)" stroke-width="2" /></g>';
|
||||||
`<path d="M ${cx-lensR+10} ${cy+45} Q ${cx-10} ${cy+30} ${cx+30} ${cy+45} Q ${cx+65} ${cy+60} ${cx+lensR-10} ${cy+45}" fill="none" stroke="rgba(255,100,10,0.5)" stroke-width="2" />`,
|
|
||||||
`<path d="M ${cx-lensR+20} ${cy+60} Q ${cx+10} ${cy+45} ${cx+40} ${cy+60} Q ${cx+70} ${cy+75} ${cx+lensR-20} ${cy+60}" fill="none" stroke="rgba(255,80,0,0.4)" stroke-width="1.5" />`,
|
|
||||||
'</g>',
|
|
||||||
].join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'storm') {
|
if (id === 'storm') {
|
||||||
const lines = [-55,-30,-5,20,45,65].map(x =>
|
const sl = [-55,-30,-5,20,45,65].map(function(x){
|
||||||
`<line x1="${cx+x}" y1="${cy-lensR+10}" x2="${cx+x-15}" y2="${cy+lensR-10}" stroke="rgba(140,180,220,0.6)" stroke-width="1" stroke-linecap="round" />`
|
return '<line x1="' + (cx+x) + '" y1="' + (cy-lensR+10) + '" x2="' + (cx+x-15) + '" y2="' + (cy+lensR-10) + '" stroke="rgba(140,180,220,0.6)" stroke-width="1" stroke-linecap="round" />';
|
||||||
).join('');
|
}).join('');
|
||||||
return `<g clip-path="url(#scope-lens-clip)" opacity="0.45">${lines}</g>`;
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.45">' + sl + '</g>';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === 'frost') {
|
if (id === 'frost') {
|
||||||
const crystals = [[-50,40],[0,55],[50,40],[-30,65],[30,65]].map(([dx,dy]) =>
|
const fc = [[-50,40],[0,55],[50,40],[-30,65],[30,65]].map(function(p){
|
||||||
`<g transform="translate(${cx+dx},${cy+dy})">` +
|
var dx=p[0], dy=p[1];
|
||||||
'<line x1="-6" y1="0" x2="6" y2="0" stroke="rgba(180,220,255,0.8)" stroke-width="1" />' +
|
return '<g transform="translate(' + (cx+dx) + ',' + (cy+dy) + ')"><line x1="-6" y1="0" x2="6" y2="0" stroke="rgba(180,220,255,0.8)" stroke-width="1" /><line x1="0" y1="-6" x2="0" y2="6" stroke="rgba(180,220,255,0.8)" stroke-width="1" /><line x1="-4" y1="-4" x2="4" y2="4" stroke="rgba(180,220,255,0.7)" stroke-width="0.8" /><line x1="4" y1="-4" x2="-4" y2="4" stroke="rgba(180,220,255,0.7)" stroke-width="0.8" /></g>';
|
||||||
'<line x1="0" y1="-6" x2="0" y2="6" stroke="rgba(180,220,255,0.8)" stroke-width="1" />' +
|
}).join('');
|
||||||
'<line x1="-4" y1="-4" x2="4" y2="4" stroke="rgba(180,220,255,0.7)" stroke-width="0.8" />' +
|
return '<g clip-path="url(#scope-lens-clip)" opacity="0.5">' + fc + '</g>';
|
||||||
'<line x1="4" y1="-4" x2="-4" y2="4" stroke="rgba(180,220,255,0.7)" stroke-width="0.8" />' +
|
|
||||||
'</g>'
|
|
||||||
).join('');
|
|
||||||
return `<g clip-path="url(#scope-lens-clip)" opacity="0.5">${crystals}</g>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user