Let the reader set the clock to a date, and run it backwards

The clock from #33 could only run forwards from now. The Display panel's
clock now has a Date (UTC) field, a native datetime-local in a form, so Enter
submits it and the browser holds it to its min and max. It jumps the clock to
that date, and the clock carries on from there at the rate it was running at.
A Backwards toggle (aria-pressed) runs the same four rates the other way. The
radios still pick the rate's size and keep the direction when it changes.

The window is AD 1 to AD 3000. The end is where Standish's Table 2 stops
being fitted (3000 BC to AD 3000; every planet within 0.29 degrees of
Horizons at each date measured out to 3000). The start is the date input's
own floor. TimeStore.setDate refuses anything outside it, and NaN, and leaves
the clock where it was. The field is read as UTC. Its dates are proleptic
Gregorian, as a Date is, so before 1582 they run up to ten days ahead of the
Julian-calendar dates history gives. The window is written beside
CLOCK_WINDOW, with the moons' shorter reach (Phobos 11 degrees out by 2100).

The system note now names the date it is drawn for, to the minute:
"... to 2020-12-21 18:00 UTC.", or "to now, <date> UTC." at the present.

Measured in the app (port 4311, keyboard only: fill, Enter):
- Set to 2020-12-21 18:00 UTC, Jupiter and Saturn seen from Earth's drawn
  position are 0.113 degrees apart. Horizons gives 0.102 geocentric
  (geometric 0.1017, astrometric 0.1018). Distances: 5.9267 and 10.8296 AU
  against Horizons' 5.9258 and 10.8270.
- 3001-06-01 is refused by the form (validity false) and the clock does not
  move.
- Backwards at 1 d/s: -2.011 days in about 2 s.
- The field's accessible name is "Date (UTC)" and its description is the
  window. Its colour-scheme is dark, so the picker icon shows on the HUD.
- Back to now puts the field back on the present too.

Unit suite 799 -> 805: two tests for the store, three for the dock, one for
the scene note. Eleven guarded mutants; each changed its file and made its
named test fail. Among them: the window check dropped, the wall clock not
re-anchored on a jump, the radio dropping the direction, the field read as
local time, and the note not naming the date.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 00:05:50 +02:00
co-authored by Claude Opus 5.5
parent dc20accfdf
commit ac6a3bb1ea
6 changed files with 229 additions and 10 deletions
+28
View File
@@ -71,4 +71,32 @@ describe('TimeStore', () => {
// Now, not the moment the store was built: five seconds of wall clock have passed.
expect(time.date().toISOString()).toBe(new Date(START.getTime() + 5000).toISOString());
});
it('jumps to a date and carries on from it at the rate it was running at', () => {
vi.advanceTimersByTime(5000); // five seconds before the jump, which it must not add on
expect(time.setDate(new Date('2020-12-21T18:00Z'))).toBe(true);
// Away from now although still at real time: the rate never changed, the date did.
expect(time.atNow()).toBe(false);
vi.advanceTimersByTime(1000);
expect(time.date().toISOString()).toBe('2020-12-21T18:00:01.000Z');
time.setRate(3600);
vi.advanceTimersByTime(1000);
expect(time.date().toISOString()).toBe('2020-12-21T19:00:01.000Z');
});
it('refuses a date the planets’ elements were never fitted for, and stays where it was', () => {
const before = time.date().toISOString();
expect(time.setDate(new Date('3000-01-01T00:01Z'))).toBe(false);
expect(time.setDate(new Date(Date.UTC(-100, 0, 1)))).toBe(false); // 101 BC
expect(time.setDate(new Date('not a date'))).toBe(false);
expect(time.date().toISOString()).toBe(before);
expect(time.atNow()).toBe(true);
// Both ends are in: the first day a date input can hold, and the end of Standish's fit.
expect(time.setDate(new Date('0001-01-01T00:00Z'))).toBe(true);
expect(time.date().toISOString()).toBe('0001-01-01T00:00:00.000Z');
expect(time.setDate(new Date('3000-01-01T00:00Z'))).toBe(true);
});
});
+36 -1
View File
@@ -25,6 +25,22 @@ export const TIME_RATES = [
const MS_PER_DAY = 86_400_000;
const JULIAN_DATE_AT_EPOCH = 2440587.5;
/**
* The dates the clock can be set to, as `datetime-local` values read as UTC.
*
* The end is where Standish's Table 2, the mean elements that carry the planets, stops being
* fitted: it covers 3000 BC to AD 3000, and every planet was within 0.29 degrees of Horizons at
* each date measured out to 3000. The start is not the fit's but the date input's, which cannot
* go before 0001-01-01. Both are proleptic Gregorian, as a `Date` is, so before 1582 they run
* ahead of the Julian-calendar dates history gives: two days at AD 1, ten by 1582. The moons and
* dwarf planets hold for far less of it: Phobos is 11 degrees out by 2100, Ceres 11.6 by 2200.
*/
export const CLOCK_WINDOW = { min: '0001-01-01T00:00', max: '3000-01-01T00:00' } as const;
const WINDOW_MS = {
min: Date.parse(`${CLOCK_WINDOW.min}Z`),
max: Date.parse(`${CLOCK_WINDOW.max}Z`),
};
/**
* The date the map is drawn for.
*
@@ -33,7 +49,9 @@ const JULIAN_DATE_AT_EPOCH = 2440587.5;
* watching. The rate *is* a signal, since a reader sets it and the controls read it back.
*
* Changing the rate re-anchors instead of rewinding: the date carries on from where it had got
* to, so speeding up and slowing down never jumps the sky.
* to, so speeding up and slowing down never jumps the sky. A negative rate runs the same clock
* backwards: every orbit and every rotation is a function of the date, so going back is the same
* sum with the sign turned.
*/
@Injectable({ providedIn: 'root' })
export class TimeStore {
@@ -72,6 +90,23 @@ export class TimeStore {
}
}
/**
* Jumps the clock to a date, from which it carries on at whatever rate it was running at.
* Refuses one outside {@link CLOCK_WINDOW}, rather than draw planets where elements that were
* never fitted there put them.
*/
setDate(date: Date): boolean {
const ms = date.getTime();
// Written so that NaN, an unparsable field, fails it too.
if (!(ms >= WINDOW_MS.min && ms <= WINDOW_MS.max)) {
return false;
}
this.anchorJd = dateToJulianDate(date);
this.anchorWallMs = Date.now();
this.atNow.set(false);
return true;
}
/** Back to now, at real time — the state the map opens in. */
reset(): void {
this.anchorJd = dateToJulianDate();