foxclock/foxclock.lua
2025-01-24 18:22:33 +00:00

624 lines
19 KiB
Lua

#!/usr/bin/lua
--[[ foxclock version ᚢ.1 please read until the next 🐇
foxclock is a fuzzy clock for fuzzy aminals. it uses a heretical take on the
ancient method of unequal, or seasonal, hours which was displaced by the
unceasing tide of standardization, where the daylight and the nightdark are each
divided into 7 sections (not the traditional 12), so that the length of a day
hour and a night hour are only the same on equinoxes. summerday hours are long
and lazy, winterday hours are fleeting and glittering.
the non-suncalc parts of this software were written by someone and their friend,
© a year of bnuuys. they are distributed under the Little Aminal Public License.
you are free to use, redistribute, modify, destroy, love, hate, pet, kiss, or
fuck the software ("the software") under the following conditia:
- you agree ur cute soft little aminal in a big wide world
- if ur plant/mycelium/rock/ocean/ghost/star/house/superorganism or something
Else, that's cool too actually, don't worry about it.
- u'll affirm your animality(/beingness) in some way today, such as by
- sniffing at a berry
- stretching your wings
- feeling sunlight on your skin
- feeling wind in your fur
- feeling a tree looking back at you
- you will help a worm or snail in their time of need
- you will remember how sugar smells when it is poured into a bowl
- you will know how it feels for a robin to startle you awake
- you will think about what it would have been like to grow up by the sea
- you will never be normal again
- you will change
- you will change
- you will change
- YOU WILL CHANGE
- you will learn to tell the hours by looking at the light of the world
- you will know this is a map and not the territory
- you will redistribute this license along with the code
--]]
-- it is currently hardcoded to be in seattle. (its bring your own coordinates
-- go look 'em up urself) also if you use this above the arctic circle it will
-- probably break. if you have ideas for timekeeping in the arctic, reach out to
-- me by searching tirelessly for someone who keeps changing their name, their
-- face, their body, their language, and you lose hope you will ever know who i
-- am, until one day, you see me on a train and you somehow just Know that it is
-- Me, and you work up the courage to approach me but just as you clear your
-- throat, I get off the train, and you stand asweved in the twilight. or find
-- me on horny trans discord servers.
local lat = 47.6
local lng = -122.3
-- set this to false in the summer
local winter_mode=true
-- set this to true if you want a table of hours!!
local print_timetable_mode=false
-- change these to whatever you want. add as many seasons as you want. add
-- impossible seasons. add more hours, subtract more hours. add local seasons.
-- add dreaming-seasons. rearrange the hour names. you are alive in a world
-- where men no longer believe in unicorns and time is running out and all we
-- can do is love each other and make magic.
-- foxclock will adjust fine if you make it have more or fewer hours.
local day_hour_names
local night_hour_names
if winter_mode then
-- winter
day_hour_names = {
"halo",
"steam",
"pang",
"mill",
"glean",
"gleam",
"glance"
}
night_hour_names = {
"gloam",
"glow",
"glim",
"nill",
"dream",
"seem",
"frost"
}
else
-- summer
day_hour_names = {
"dew",
"stir",
"snack",
"mill",
"sprawl",
"brill",
"honey"
}
night_hour_names = {
"owl's light",
"still",
"snuck",
"nill",
"dream",
"seem",
"robin's light"
}
end
-- change the prefix for the part of the hour
-- make them unfur. make them blossom. make them shake.
local Early_Name = "early"
local Mid_Name = "mid"
local Late_Name = "late"
-- 🐇 🐇 🐇 🐇 🐇 🐇 🐇 🐇
-- you now no longer have to read the code unless you're using this as
-- a library then also have a look at the last few lines.
-- now begins Most Of Suncalc.Lua which we just copypasted into here --
-- Thanks suncalc.lua! --
--[[
Copyright (c) 2014, Vladimir 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.
--]]
-- for simple translation of javascript ternary comparison operator ?:
local function ternary(a, b, c) if a then return b end return c end
-- shortcuts for easier to read formulas
local PI = math.pi
local sin = math.sin
local cos = math.cos
local tan = math.tan
local asin = math.asin
local atan = math.atan
local acos = math.acos
local rad = PI / 180
-- sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas
-- date/time constants and conversions
local dayS = 60 * 60 * 24
local J1970 = 2440588
local J2000 = 2451545
local function toJulian(date) return date / dayS - 0.5 + J1970 end
local function fromJulian(j) return (j + 0.5 - J1970) * dayS end
local function toDays(date) return toJulian(date) - J2000 end
-- general calculations for position
local e = rad * 23.4397 -- obliquity of the Earth
local function rightAscension(l, b) return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)) end
local function declination(l, b) return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)) end
local function azimuth(H, phi, dec) return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)) end
local function altitude(H, phi, dec) return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)) end
local function siderealTime(d, lw) return rad * (280.16 + 360.9856235 * d) - lw end
local function astroRefraction(h)
if (h < 0) then -- the following formula works for positive altitudes only.
h = 0 end -- if h = -0.08901179 a div/0 would occur.
-- formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
-- 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
return 0.0002967 / math.tan(h + 0.00312536 / (h + 0.08901179))
end
-- general sun calculations
local function solarMeanAnomaly(d) return rad * (357.5291 + 0.98560028 * d) end
local function eclipticLongitude(M)
local C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)) -- equation of center
local P = rad * 102.9372 -- perihelion of the Earth
return M + C + P + PI
end
local function sunCoords(d)
local M = solarMeanAnomaly(d)
local L = eclipticLongitude(M)
return {
dec = declination(L, 0),
ra = rightAscension(L, 0)
}
end
local SunCalc = {}
-- calculates sun position for a given date and latitude/longitude
SunCalc.getPosition = function (date, lat, lng)
local lw = rad * -lng
local phi = rad * lat
local d = toDays(date)
local c = sunCoords(d)
local H = siderealTime(d, lw) - c.ra
return {
azimuth = azimuth(H, phi, c.dec),
altitude = altitude(H, phi, c.dec)
}
end
-- sun times configuration (angle, morning name, evening name)
SunCalc.times = {
{-0.833, 'sunrise', 'sunset' },
{ -0.3, 'sunriseEnd', 'sunsetStart' },
{ -6, 'dawn', 'dusk' },
{ -12, 'nauticalDawn', 'nauticalDusk'},
{ -18, 'nightEnd', 'night' },
{ 6, 'goldenHourEnd', 'goldenHour' }
}
-- adds a custom time to the times config
SunCalc.addTime = function (angle, riseName, setName)
table.insert(SunCalc.times, {angle, riseName, setName})
end
-- calculations for sun times
local J0 = 0.0009
local function julianCycle(d, lw) return math.floor(0.5 + (d - J0 - lw / (2 * PI))) end
local function approxTransit(Ht, lw, n) return J0 + (Ht + lw) / (2 * PI) + n end
local function solarTransitJ(ds, M, L) return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L) end
local function hourAngle(h, phi, d) return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))) end
local function observerAngle(height) return -2.076 * math.sqrt(height) / 60 end
-- returns set time for the given sun altitude
local function getSetJ(h, lw, phi, dec, n, M, L)
local w = hourAngle(h, phi, dec)
local a = approxTransit(w, lw, n)
return solarTransitJ(a, M, L)
end
-- calculates sun times for a given date, latitude/longitude, and, optionally,
-- the observer height (in meters) relative to the horizon
SunCalc.getTimes = function (date, lat, lng, height, times)
times = times or SunCalc.times
height = height or 0
local lw = rad * -lng
local phi = rad * lat
local dh = observerAngle(height)
local d = toDays(date)
local n = julianCycle(d, lw)
local ds = approxTransit(0, lw, n)
local M = solarMeanAnomaly(ds)
local L = eclipticLongitude(M)
local dec = declination(L, 0)
local Jnoon = solarTransitJ(ds, M, L)
local h0, Jset, Jrise
local result = {
solarNoon = fromJulian(Jnoon),
nadir = fromJulian(Jnoon - 0.5)
}
for _, time in ipairs(times) do
h0 = (time[01] + dh) * rad
Jset = getSetJ(h0, lw, phi, dec, n, M, L)
Jrise = Jnoon - (Jset - Jnoon)
result[time[2]] = fromJulian(Jrise)
result[time[3]] = fromJulian(Jset)
end
return result
end
-- 🦊 🦊 🦊 🦊 🦊
-- okay suncalc over now we are in the foxclock zone --
-- this is a utility function for printing out tables
local function tprint (tbl, indent)
if not indent then indent = 0 end
local toprint = string.rep(" ", indent) .. "{\r\n"
indent = indent + 2
for k, v in pairs(tbl) do
toprint = toprint .. string.rep(" ", indent)
if (type(k) == "number") then
toprint = toprint .. "[" .. k .. "] = "
elseif (type(k) == "string") then
toprint = toprint .. k .. "= "
end
if (type(v) == "number") then
toprint = toprint .. v .. ",\r\n"
elseif (type(v) == "string") then
toprint = toprint .. "\"" .. v .. "\",\r\n"
elseif (type(v) == "table") then
toprint = toprint .. tprint(v, indent + 2) .. ",\r\n"
else
toprint = toprint .. "\"" .. tostring(v) .. "\",\r\n"
end
end
toprint = toprint .. string.rep(" ", indent-2) .. "}"
print(toprint)
end
local function hoursLater(date, h)
-- dayS is a constant
return date + h * dayS / 24
end
--[[
So let's say that time_list is a **list** of times, in time-order. Like this:
{
{ -10, "sunset yesterday" },
{ -5, "sunrise today" },
{ 5, "sunset today" },
{ 10, "sunrise tomorrow" }
}
And then say `now` is 0. We want to know the two times we are inbetween. So in
this case we would return "sunrise today", "sunset today" to show we are in the
day-part of today. That's what this function does.
You need to make sure that there are enough times here that we are in between
two of them. If not... well that'd be weird.
]]
local function when_even_are_we(now, time_list)
for i in 1, #time_list - 1 do
-- event time and name
local etime_prev, ename_prev = table.unpack(time_list[i])
local etime_next, ename_next = table.unpack(time_list[i + 1])
if now >= etime_prev and now < etime_next then
-- We are inbetween these two times
return ename_prev, ename_next
end
end
end
--[[
This little friend will convert all our fun friendly hour names and say when
they happen in the silly little world of "24 hour" that the "humans" like to
use.
]]
local function print_hour_timetable(fuzziest_time, suntimes)
local day_length = suntimes.sunset - suntimes.sunrise
local night_length = (60 * 60 * 24) + suntimes.sunrise - suntimes.sunset
-- We want to print the night first if its night, and the day first if its
-- day. then we print the other.
local first, second
-- We start off with these in day, night order. But then if it's night we
-- swap them. The start time just needs to be the start of *a* segment, it
-- doesn't really need to be exactly today's segment because itll be close
-- enough.
first = {
start = suntimes.sunrise,
length = day_length,
names = day_hour_names
}
second = {
start = suntimes.sunset,
length = night_length,
names = night_hour_names
}
if fuzziest_time == 'night' then
-- swap for night
local tmp = first
first = second
second = tmp
end
-- Do the first, then the second.
for _, seg in ipairs({first, second}) do
-- How long is a hour?
local hourlen = seg.length / #seg.names
-- Now go through the segment, one hourlength at a time. Print out what
-- 24-hour time it would be, and our hour name.
for i = 1, #seg.names do
local t = math.floor(seg.start + (i - 1) * hourlen)
-- 24hour date, without the day
-- For some reason lua5.4 on debian doesn't support %l in strftime,
-- so we use %I which formats it with a leading 0, and then replace
-- that with a space. lol
local time_of_day_normalpeople = os.date('%I:%M %p', t):gsub('^0', ' ')
local hour_name = seg.names[i]
print(time_of_day_normalpeople .. ' - ' .. hour_name)
end
end
end
local function print_hour_timetable_for_date(date)
-- HEY BUDDY YOU CANT BE COPY PASTING LAT/LNG LIKE THAT BUDDY
-- probably somethings in the code should rearrange...
local time = os.time(date)
--[[
local lat = 47.6
local lng = -122.3 --]]
local suntimes = SunCalc.getTimes(time, lat, lng)
print_hour_timetable("day", suntimes)
end
--[[
For anything that wants to use us as a library, here's how they can do it! Any
function we put in "module" is a thing they can use
]]
local foxclock = {}
function foxclock.what_time_is_it(now)
-- tprint(SunCalc.getPosition(now, lat, lng))
-- tprint(SunCalc.getTimes(now, lat, lng))
--[[
This gives us the times of a bunch of interesting solar events, with their
absolute timestamp in seconds since the epoch. Some of them will be before
Now, and some of them will be after Now. Specifically, we have:
- nadir
- nightEnd
- nauticalDawn
- dawn
- sunrise
- sunriseEnd
- goldenHour
- goldenHourEnd
- solarNoon
- sunsetStart
- sunset
- dusk
- nauticalDusk
- night
]]
local suntimes = SunCalc.getTimes(now, lat, lng)
--[[
We want to know if it is day time or not time. If we are between sunrise and
sunset for the day, then that means it's day time! Otherwise it is night. It
is up to you, really, whether to use the start or end of the sunrise and
sunset. but for now we will use `sunrise` and `sunset`
]]
local fuzziest_time
local seg_start
local seg_end
if now > suntimes.sunrise and now < suntimes.sunset then
-- DAY
fuzziest_time = 'day'
seg_start = suntimes.sunrise
seg_end = suntimes.sunset
else
-- NIGHT
fuzziest_time = 'night'
--[[print(now)
print(suntimes.sunrise)
print(suntimes.sunset)
--]]
--[[
If we are on the left of sunrise, then the segment ends at sunrise and
starts at the *previous days* sunset. If we are on the right of sunset,
then sunset is when the segment started and it will end on tomorrows
sunrise.
--]]
if now < suntimes.sunrise then
--[[
For now, do the inaccurate but maybe close enough thing of taking
24 hours off of today's number. This will be least accurate around
the equinox and most accurate around the solstice.
--]]
seg_start = suntimes.sunset - 60 * 60 * 24
seg_end = suntimes.sunrise
else
seg_start = suntimes.sunset
seg_end = suntimes.sunrise + 60 * 60 * 24
--print(seg_start)
--print(seg_end)
end
end
--[[
Now we want to know how far through the current time-section we are. We want
this as like a fraction out of 1.0, because we're going to re-scale it to
our own fucked up time system :3 :3 :3
]]
local seg_progression = (now - seg_start) / (seg_end - seg_start)
--[[
now we re-scale it to ""hours"", which are some number of divisions of the
segment. We will use the length of the names list to decide this, that way
we don't need to remember to update both places when we make changes. Plus
then you can have the day and night have different number of hours, and isnt
that kinda neat?
]]
local num_hours
if fuzziest_time == 'day' then
num_hours = #day_hour_names
else
num_hours = #night_hour_names
end
local current_hour = math.floor(seg_progression * num_hours)
--[[print("current hour")
print(current_hour)--]]
--[[
If seg_progression is **exactly** 1.0, then current_hour can be num_hours.
Which is weird. For a very small instant you might have that, if the maths
work out that way. But that adds a secret num_hours + 1 hour which doesnt
fit. So if current_hour == num_hours we should make it be num_hours - 1 for
that instant.
]]
if current_hour == num_hours then
current_hour = num_hours - 1
end
--[[
But what is this hour named?? Let's find out! Don't forget to add 1, because
current_hour from the maths we did will be from 0 to (num_hours - 1)
]]
local hour_name
if fuzziest_time == 'day' then
hour_name = day_hour_names[current_hour + 1]
else
hour_name = night_hour_names[current_hour + 1]
end
--[[ let's do math to find out how far we are within the hour ]]
local hour_length = (seg_end - seg_start) / num_hours
--[[]]
local current_hour_start = seg_start + ((current_hour)*hour_length)
local current_hour_progress = (now - current_hour_start) / hour_length
--print(string.format("now %s current_hour %s seg_start %s seg_end %s hour_length %s current_hour_start %s current_hour_progress", now, current_hour, seg_start, seg_end, hour_length, current_hour_start, current_hour_progress))
local minute_name
if current_hour_progress < 0 then
minute_name = "errneg"
elseif current_hour_progress <= 0.33 then
minute_name = Early_Name
elseif current_hour_progress <= 0.66 then
minute_name = Mid_Name
elseif current_hour_progress <= 1 then
minute_name = Late_Name
else minute_name = "errpos"
end
--[[
And now we know what time it is!
]]
if print_timetable_mode then
print_hour_timetable("day", suntimes)
end
return string.format("%s %s", minute_name, hour_name)
end
local function mane()
local now = os.time()
print(foxclock.what_time_is_it(now))
end
-- swap these comments out to use this as a library (for instance, to put this on koreader)
-- return foxclock
mane()