From 25ac866bdc94c78bc4c7f55e6abb07c2ddb27c5b Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Tue, 10 Mar 2026 18:02:51 -0400 Subject: [PATCH 1/4] First version of an analemma layer. --- src/SeasonsStory.vue | 52 +++++++++++++++++++++++++++++++++++++++++++- src/wwt-hacks.ts | 5 +++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/SeasonsStory.vue b/src/SeasonsStory.vue index 63e2d60..f136a80 100644 --- a/src/SeasonsStory.vue +++ b/src/SeasonsStory.vue @@ -810,7 +810,8 @@ import { v4 } from "uuid"; import { AstroTime, Seasons } from "astronomy-engine"; -import { Color, Grids, Planets, Settings, WWTControl } from "@wwtelescope/engine"; +import { Color, Grids, Planets, Settings, SpreadSheetLayer, WWTControl } from "@wwtelescope/engine"; +import { MarkerScales } from "@wwtelescope/engine-types"; import { GotoRADecZoomParams, engineStore } from "@wwtelescope/engine-pinia"; import { BackgroundImageset, @@ -1453,6 +1454,8 @@ const selectedLocaledTimeDateString = computed(() => { const MAX_ZOOM = 500; +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; + function aspectRatioSetup() { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error WWTControl does have a canvas element (that's not TS-exposed) @@ -1520,6 +1523,10 @@ onMounted(() => { layersLoaded.value = true; questionDisplaySetup(); + + const layer = await createAnalemmaLayer({ year: 2026, dayFraction: 0.35, daysBetween: 5 }); + console.log(layer); + }); createUserEntry(); @@ -1677,6 +1684,49 @@ function resetView(zoomDeg?: number, withAzOffset=true) { }); } +interface AnalemmaLayerOptions { + year: number; + dayFraction: number; + daysBetween: number; +} + +function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise { + const start = new Date(options.year, 0); + const delta = options.daysBetween * 24 * 60 * 60 * 1000; + const end = (new Date(options.year + 1, 0)).getTime(); + let time = start.getTime() + options.dayFraction * MILLISECONDS_PER_DAY; + + const points: string[] = []; + while (time < end) { + const date = new Date(time); + const sunPosition = getSunPositionAtTime(date); + const raDec = horizontalToEquatorial( + sunPosition.altRad, + sunPosition.azRad, + selectedLocation.value.latitudeDeg * D2R, + selectedLocation.value.longitudeDeg * D2R, + store.currentTime, + ); + points.push(`${raDec.raRad * R2D}\t${raDec.decRad * R2D}`); + time += delta; + } + + const dataCsv = `RA\tDec\r\n${points.join('\r\n')}`; + return store.createTableLayer({ + name: "Analemma", + referenceFrame: "Sky", + dataCsv, + }).then(layer => { + layer.set_color(Color.fromArgb(255, 255, 255, 0)); + layer.set_scaleFactor(50); + layer.set_lngColumn(0); + layer.set_latColumn(1); + layer.set_markerScale(MarkerScales.screen); + return layer; + }); + +} + function updateWWTLocation(location: LocationDeg) { wwtSettings.set_locationLat(location.latitudeDeg); wwtSettings.set_locationLng(location.longitudeDeg); diff --git a/src/wwt-hacks.ts b/src/wwt-hacks.ts index 6a8ff6f..76af9f2 100644 --- a/src/wwt-hacks.ts +++ b/src/wwt-hacks.ts @@ -13,6 +13,7 @@ import { DT, GlyphCache, Grids, + LayerManager, Matrix3d, Planets, RenderContext, @@ -225,6 +226,7 @@ export function renderOneFrame(showHorizon=true, } } } + if (this.uiController != null) { this.uiController.render(this.renderContext); } @@ -267,6 +269,9 @@ export function renderOneFrame(showHorizon=true, drawHorizon(this.renderContext, { opacity: 0.9, color: "#01362C" }); } + const referenceFrame = this.getCurrentReferenceFrame(); + LayerManager._draw(this.renderContext, 1, this.get_space(), referenceFrame, true, this.get_space()); + const worldSave = this.renderContext.get_world(); const viewSave = this.renderContext.get_view(); const projSave = this.renderContext.get_projection(); From 74097c452ae1e0e79f6ae5472abc6691c488c78f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 11 Mar 2026 12:34:33 -0400 Subject: [PATCH 2/4] More work on analemma layer setup. --- src/SeasonsStory.vue | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/SeasonsStory.vue b/src/SeasonsStory.vue index f136a80..de3e62c 100644 --- a/src/SeasonsStory.vue +++ b/src/SeasonsStory.vue @@ -1481,6 +1481,8 @@ function aspectRatioSetup() { updateAzOffsets(); } +let analemmaLayer: SpreadSheetLayer | null = null; + onMounted(() => { if (!isWebGL2Enabled()) { @@ -1524,9 +1526,6 @@ onMounted(() => { questionDisplaySetup(); - const layer = await createAnalemmaLayer({ year: 2026, dayFraction: 0.35, daysBetween: 5 }); - console.log(layer); - }); createUserEntry(); @@ -1692,9 +1691,12 @@ interface AnalemmaLayerOptions { function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise { const start = new Date(options.year, 0); - const delta = options.daysBetween * 24 * 60 * 60 * 1000; + const delta = options.daysBetween * MILLISECONDS_PER_DAY; const end = (new Date(options.year + 1, 0)).getTime(); let time = start.getTime() + options.dayFraction * MILLISECONDS_PER_DAY; + console.log("Start time"); + console.log(new Date(time)); + console.log(selectedTimezoneOffset.value); const points: string[] = []; while (time < end) { @@ -1789,6 +1791,23 @@ watch(currentTime, (_time: Date) => { sunDistance.value = sunPlace.get_distance(); }); +watch([selectedLocation, selectedCustomDate], async (values: [LocationDeg, Date | null]) => { + if (analemmaLayer) { + store.deleteLayer(analemmaLayer); + } + + const date = values[1]; + if (date != null) { + analemmaLayer = await createAnalemmaLayer({ + year: date.getFullYear(), + dayFraction: 0.5, + daysBetween: 5, + }); + } else { + analemmaLayer = null; + } +}); + watch(forceCamera, (value: boolean) => { if (value) { resetView(); From c7682c77137efbea1b4c57f2d6f16df0184404f5 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Wed, 11 Mar 2026 15:29:34 -0400 Subject: [PATCH 3/4] Get analemma layer working, sort of. --- src/SeasonsStory.vue | 87 ++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/src/SeasonsStory.vue b/src/SeasonsStory.vue index de3e62c..83d2ba2 100644 --- a/src/SeasonsStory.vue +++ b/src/SeasonsStory.vue @@ -811,7 +811,7 @@ import { v4 } from "uuid"; import { AstroTime, Seasons } from "astronomy-engine"; import { Color, Grids, Planets, Settings, SpreadSheetLayer, WWTControl } from "@wwtelescope/engine"; -import { MarkerScales } from "@wwtelescope/engine-types"; +import { RAUnits, MarkerScales } from "@wwtelescope/engine-types"; import { GotoRADecZoomParams, engineStore } from "@wwtelescope/engine-pinia"; import { BackgroundImageset, @@ -1689,31 +1689,42 @@ interface AnalemmaLayerOptions { daysBetween: number; } -function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise { +let analemmaAltAz: [number, number][] = []; + +function createAnalemmaAltAz(options: AnalemmaLayerOptions): [number, number][] { const start = new Date(options.year, 0); const delta = options.daysBetween * MILLISECONDS_PER_DAY; const end = (new Date(options.year + 1, 0)).getTime(); let time = start.getTime() + options.dayFraction * MILLISECONDS_PER_DAY; - console.log("Start time"); - console.log(new Date(time)); - console.log(selectedTimezoneOffset.value); - - const points: string[] = []; + const points: [number, number][] = []; while (time < end) { const date = new Date(time); const sunPosition = getSunPositionAtTime(date); + points.push([sunPosition.altRad, sunPosition.azRad]); + time += delta; + } + return points; +} + +function createAnalemmaRADec(options: AnalemmaLayerOptions) { + if (analemmaAltAz.length === 0) { + analemmaAltAz = createAnalemmaAltAz(options); + } + return analemmaAltAz.map(([altRad, azRad]) => { const raDec = horizontalToEquatorial( - sunPosition.altRad, - sunPosition.azRad, + altRad, + azRad, selectedLocation.value.latitudeDeg * D2R, selectedLocation.value.longitudeDeg * D2R, - store.currentTime, + currentTime.value, ); - points.push(`${raDec.raRad * R2D}\t${raDec.decRad * R2D}`); - time += delta; - } + return [raDec.raRad * R2D, raDec.decRad * R2D]; + }); +} - const dataCsv = `RA\tDec\r\n${points.join('\r\n')}`; +function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise { + const points = createAnalemmaRADec(options); + const dataCsv = `RA\tDec\r\n${points.map(pt => pt.join('\t')).join('\r\n')}`; return store.createTableLayer({ name: "Analemma", referenceFrame: "Sky", @@ -1721,6 +1732,7 @@ function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise { layer.set_color(Color.fromArgb(255, 255, 255, 0)); layer.set_scaleFactor(50); + layer.set_raUnits(RAUnits.degrees); layer.set_lngColumn(0); layer.set_latColumn(1); layer.set_markerScale(MarkerScales.screen); @@ -1729,6 +1741,17 @@ function createAnalemmaLayer(options: AnalemmaLayerOptions): Promise pt.map(t => String(t))); + } +} + function updateWWTLocation(location: LocationDeg) { wwtSettings.set_locationLat(location.latitudeDeg); wwtSettings.set_locationLng(location.longitudeDeg); @@ -1777,6 +1800,15 @@ watch(selectedLocation, (location: LocationDeg, oldLocation: LocationDeg) => { updateWWTLocation(location); updateSliderBounds(location, oldLocation); resetView(); + + const options: AnalemmaLayerOptions = { + year: selectedCustomDate.value?.getFullYear() ?? 2026, + dayFraction: 0.5, + daysBetween: 5, + }; + + analemmaAltAz = createAnalemmaAltAz(options); + updateAnalemmaLayer(options); }); watch(currentTime, (_time: Date) => { @@ -1789,23 +1821,11 @@ watch(currentTime, (_time: Date) => { resetView(store.zoomDeg); } sunDistance.value = sunPlace.get_distance(); -}); - -watch([selectedLocation, selectedCustomDate], async (values: [LocationDeg, Date | null]) => { - if (analemmaLayer) { - store.deleteLayer(analemmaLayer); - } - - const date = values[1]; - if (date != null) { - analemmaLayer = await createAnalemmaLayer({ - year: date.getFullYear(), - dayFraction: 0.5, - daysBetween: 5, - }); - } else { - analemmaLayer = null; - } + updateAnalemmaLayer({ + year: selectedCustomDate.value?.getYear() ?? 2026, + dayFraction: 0.5, + daysBetween: 5, + }); }); watch(forceCamera, (value: boolean) => { @@ -1833,6 +1853,11 @@ watch(selectedCustomDate, (date: Date | null) => { userSelectedDates.push(representation); events.push(representation); } + updateAnalemmaLayer({ + year: selectedCustomDate.value?.getYear() ?? 2026, + dayFraction: 0.5, + daysBetween: 5, + }); }); watch(inNorthernHemisphere, (_inNorth: boolean) => resetNSEWText()); From 616572dd2b81d651f0ec8a64dc4b01582dc4e9a8 Mon Sep 17 00:00:00 2001 From: John Arban Lewis Date: Wed, 25 Mar 2026 13:20:00 -0400 Subject: [PATCH 4/4] use UTC and timezone offset to set analemma position correctly for user selected locations --- src/SeasonsStory.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SeasonsStory.vue b/src/SeasonsStory.vue index 83d2ba2..51c9fdb 100644 --- a/src/SeasonsStory.vue +++ b/src/SeasonsStory.vue @@ -1692,10 +1692,10 @@ interface AnalemmaLayerOptions { let analemmaAltAz: [number, number][] = []; function createAnalemmaAltAz(options: AnalemmaLayerOptions): [number, number][] { - const start = new Date(options.year, 0); + const start = new Date(Date.UTC(options.year, 0)); const delta = options.daysBetween * MILLISECONDS_PER_DAY; const end = (new Date(options.year + 1, 0)).getTime(); - let time = start.getTime() + options.dayFraction * MILLISECONDS_PER_DAY; + let time = start.getTime() + options.dayFraction * MILLISECONDS_PER_DAY - getTimezoneOffset(selectedTimezone.value, start); const points: [number, number][] = []; while (time < end) { const date = new Date(time);