From ea34ec4464f786a4e17cffe26fc1f7f3888ba670 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Thu, 16 Jul 2026 16:33:45 +0100 Subject: [PATCH] feat(easter-eggs): weave in blog archive motifs --- .../ostara-eggs/BlogArchiveEggs.svelte | 256 ++++++++++++++++++ src/lib/components/ostara-eggs/index.ts | 1 + src/lib/utils/moonPhase.ts | 111 ++++++++ src/routes/+layout.svelte | 2 + static/assets/happy-mac.svg | 37 +++ static/licenses/happy-mac.txt | 9 + static/licenses/suncalc.txt | 28 ++ 7 files changed, 444 insertions(+) create mode 100644 src/lib/components/ostara-eggs/BlogArchiveEggs.svelte create mode 100644 src/lib/utils/moonPhase.ts create mode 100644 static/assets/happy-mac.svg create mode 100644 static/licenses/happy-mac.txt create mode 100644 static/licenses/suncalc.txt diff --git a/src/lib/components/ostara-eggs/BlogArchiveEggs.svelte b/src/lib/components/ostara-eggs/BlogArchiveEggs.svelte new file mode 100644 index 0000000..a7e0cab --- /dev/null +++ b/src/lib/components/ostara-eggs/BlogArchiveEggs.svelte @@ -0,0 +1,256 @@ + + + + +{#if isFullMoon} + +{/if} + +{#if cubeVisible} +
+ + M4 MINI // CUBE OF COMPUTING // 67 DAYS LATER +
+{/if} + +{#if entropyVisible} +
+ AGAINST ENTROPY + STILL HERE. +
+{/if} + + diff --git a/src/lib/components/ostara-eggs/index.ts b/src/lib/components/ostara-eggs/index.ts index 96395d9..3f24b90 100644 --- a/src/lib/components/ostara-eggs/index.ts +++ b/src/lib/components/ostara-eggs/index.ts @@ -3,4 +3,5 @@ export { default as ThreeToast } from "./ThreeToast.svelte"; export { default as MondayEgg } from "./MondayEgg.svelte"; export { default as IdleEgg } from "./IdleEgg.svelte"; export { default as NotFoundEgg } from "./NotFoundEgg.svelte"; +export { default as BlogArchiveEggs } from "./BlogArchiveEggs.svelte"; export { threeVisible, triggerThree } from "./eggStores"; diff --git a/src/lib/utils/moonPhase.ts b/src/lib/utils/moonPhase.ts new file mode 100644 index 0000000..2f74179 --- /dev/null +++ b/src/lib/utils/moonPhase.ts @@ -0,0 +1,111 @@ +/** + * Lunar illumination derived from SunCalc's astronomical implementation. + * + * The calculation uses Julian days plus geocentric solar and lunar coordinates + * to determine the phase angle. Unlike a fixed 29.53-day clock, it accounts for + * the Moon's orbital anomaly and changing distance. + * + * Formula sources: + * - Jean Meeus, Astronomical Algorithms, 2nd ed., chapter 48 + * - https://github.com/mourner/suncalc + * + * SunCalc is distributed under the BSD 2-Clause License: + * Copyright (c) 2026, Volodymyr Agafonkin. All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the copyright notice and this + * permission notice are retained. THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT + * WARRANTY OF ANY KIND. The complete deployed notice is at /licenses/suncalc.txt. + */ + +const PI = Math.PI; +const RAD = PI / 180; +const DAY_MS = 86_400_000; +const JULIAN_1970 = 2_440_588; +const JULIAN_2000 = 2_451_545; +const EARTH_OBLIQUITY = RAD * 23.4397; +const SUN_DISTANCE_KM = 149_598_000; + +function toDays(date: Date) { + const julianDay = date.valueOf() / DAY_MS - 0.5 + JULIAN_1970; + return julianDay - JULIAN_2000; +} + +function rightAscension(longitude: number, latitude: number) { + return Math.atan2( + Math.sin(longitude) * Math.cos(EARTH_OBLIQUITY) - + Math.tan(latitude) * Math.sin(EARTH_OBLIQUITY), + Math.cos(longitude), + ); +} + +function declination(longitude: number, latitude: number) { + return Math.asin( + Math.sin(latitude) * Math.cos(EARTH_OBLIQUITY) + + Math.cos(latitude) * Math.sin(EARTH_OBLIQUITY) * Math.sin(longitude), + ); +} + +function sunCoordinates(days: number) { + const meanAnomaly = RAD * (357.5291 + 0.98560028 * days); + const equationOfCentre = + RAD * + (1.9148 * Math.sin(meanAnomaly) + + 0.02 * Math.sin(2 * meanAnomaly) + + 0.0003 * Math.sin(3 * meanAnomaly)); + const eclipticLongitude = + meanAnomaly + equationOfCentre + RAD * 102.9372 + PI; + + return { + rightAscension: rightAscension(eclipticLongitude, 0), + declination: declination(eclipticLongitude, 0), + }; +} + +function moonCoordinates(days: number) { + const meanLongitude = RAD * (218.316 + 13.176396 * days); + const meanAnomaly = RAD * (134.963 + 13.064993 * days); + const meanDistance = RAD * (93.272 + 13.22935 * days); + const longitude = meanLongitude + RAD * 6.289 * Math.sin(meanAnomaly); + const latitude = RAD * 5.128 * Math.sin(meanDistance); + + return { + rightAscension: rightAscension(longitude, latitude), + declination: declination(longitude, latitude), + distance: 385_001 - 20_905 * Math.cos(meanAnomaly), + }; +} + +export function getMoonIllumination(date = new Date()) { + const days = toDays(date); + const sun = sunCoordinates(days); + const moon = moonCoordinates(days); + const elongation = Math.acos( + Math.sin(sun.declination) * Math.sin(moon.declination) + + Math.cos(sun.declination) * + Math.cos(moon.declination) * + Math.cos(sun.rightAscension - moon.rightAscension), + ); + const incidence = Math.atan2( + SUN_DISTANCE_KM * Math.sin(elongation), + moon.distance - SUN_DISTANCE_KM * Math.cos(elongation), + ); + const limbAngle = Math.atan2( + Math.cos(sun.declination) * + Math.sin(sun.rightAscension - moon.rightAscension), + Math.sin(sun.declination) * Math.cos(moon.declination) - + Math.cos(sun.declination) * + Math.sin(moon.declination) * + Math.cos(sun.rightAscension - moon.rightAscension), + ); + + return { + fraction: (1 + Math.cos(incidence)) / 2, + phase: 0.5 + (0.5 * incidence * (limbAngle < 0 ? -1 : 1)) / PI, + angle: limbAngle, + }; +} + +/** Treat the roughly one-day period above 99% illumination as visibly full. */ +export function isVisiblyFullMoon(date = new Date()) { + return getMoonIllumination(date).fraction >= 0.99; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4f47b6f..2fa12c6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -9,6 +9,7 @@ import MondayEgg from '$lib/components/ostara-eggs/MondayEgg.svelte'; import IdleEgg from '$lib/components/ostara-eggs/IdleEgg.svelte'; import ThreeToast from '$lib/components/ostara-eggs/ThreeToast.svelte'; + import BlogArchiveEggs from '$lib/components/ostara-eggs/BlogArchiveEggs.svelte'; import './layout.css'; let { children } = $props(); @@ -40,6 +41,7 @@ + Skip to content
{#if dev}DEV{/if} diff --git a/static/assets/happy-mac.svg b/static/assets/happy-mac.svg new file mode 100644 index 0000000..97e017f --- /dev/null +++ b/static/assets/happy-mac.svg @@ -0,0 +1,37 @@ + + + + Happy Mac + The classic smiling Macintosh startup icon. + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/licenses/happy-mac.txt b/static/licenses/happy-mac.txt new file mode 100644 index 0000000..28f3600 --- /dev/null +++ b/static/licenses/happy-mac.txt @@ -0,0 +1,9 @@ +Happy Mac SVG + +Author: NiloGlock at Italian Wikipedia +Source: https://commons.wikimedia.org/wiki/File:Happy_Mac.svg +License: Creative Commons Attribution-ShareAlike 4.0 International +License text: https://creativecommons.org/licenses/by-sa/4.0/ + +The copy used by this site has only had editor metadata, redundant attributes, +and path formatting removed. Its artwork and colour design are unchanged. diff --git a/static/licenses/suncalc.txt b/static/licenses/suncalc.txt new file mode 100644 index 0000000..beb7ad4 --- /dev/null +++ b/static/licenses/suncalc.txt @@ -0,0 +1,28 @@ +SunCalc + +Copyright (c) 2026, Volodymyr Agafonkin +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Source: https://github.com/mourner/suncalc -- 2.51.2