// What to paint: a fixed theme and pattern, or ones that follow the sun
// through night, sunrise, day and sunset, each phase with its own.
//
// Following the sun blends between the four phase themes by the sun's
// elevation, so the change is continuous rather than a switch at fixed
// times. Blending happens in OKLCH, the shorter way round the hue
// circle: a straight mix of two near-opposite hues (clays and sky, say)
// cancels to grey halfway, where turning the hue keeps full colour. For
// these palettes every twilight transition turns through magenta and
// purple, which is what the sky does anyway.
.pragma library
.import "themes.js" as Themes
var ROLES = ["base", "deep", "ink", "tone"]
// ------------------------------------------------------------------ sun
// Solar elevation in degrees, after the approximation in the
// Astronomical Almanac: good to a fraction of a degree, which is far
// finer than a wallpaper can show.
function sunElevation(date, latitude, longitude) {
var rad = Math.PI / 180
var d = date.getTime() / 86400000 - 10957.5 // days since J2000.0
var g = (357.529 + 0.98560028 * d) * rad
var q = 280.459 + 0.98564736 * d
var L = (q + 1.915 * Math.sin(g) + 0.020 * Math.sin(2 * g)) * rad
var e = (23.439 - 0.00000036 * d) * rad
var ra = Math.atan2(Math.cos(e) * Math.sin(L), Math.cos(L))
var dec = Math.asin(Math.sin(e) * Math.sin(L))
var gmst = 18.697374558 + 24.06570982441908 * d // hours
var hourAngle = ((gmst * 15 + longitude) * rad) - ra
var lat = latitude * rad
return Math.asin(Math.sin(lat) * Math.sin(dec)
+ Math.cos(lat) * Math.cos(dec) * Math.cos(hourAngle)) / rad
}
function minutesOf(hhmm) {
var parts = String(hhmm).split(":")
var m = Number(parts[0]) * 60 + Number(parts[1] || 0)
return isNaN(m) ? 0 : m
}
// Without a location, sunrise and sunset are clock times, and this
// stands in for the elevation: a sine arc peaking at 50 degrees
// between them and dipping to -30 at the middle of the night, which
// passes through the same thresholds as the real sun would.
function clockElevation(date, sunrise, sunset) {
var now = date.getHours() * 60 + date.getMinutes() + date.getSeconds() / 60
var rise = minutesOf(sunrise)
var set = minutesOf(sunset)
var dayLength = (set - rise + 1440) % 1440 || 720
var sinceRise = (now - rise + 1440) % 1440
if (sinceRise < dayLength)
return 50 * Math.sin(Math.PI * sinceRise / dayLength)
return -30 * Math.sin(Math.PI * (sinceRise - dayLength) / (1440 - dayLength))
}
// ---------------------------------------------------------------- colour
function oklab(lch) {
var h = lch[2] * Math.PI / 180
return [lch[0], lch[1] * Math.cos(h), lch[1] * Math.sin(h)]
}
function oklabToHex(lab) {
var l = Math.pow(lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2], 3)
var m = Math.pow(lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2], 3)
var s = Math.pow(lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2], 3)
var linear = [
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
]
var hex = "#"
for (var i = 0; i < 3; i++) {
var x = Math.min(Math.max(linear[i], 0), 1)
x = x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055
hex += ("0" + Math.round(x * 255).toString(16)).slice(-2)
}
return hex
}
function theme(name) {
return Themes.themes[name] || Themes.themes.her
}
// Two OKLCH colours, t of the way from a to b. A colour with almost no
// chroma has no meaningful hue, so it takes the other one's: black to
// a colour then darkens the colour rather than sweeping round the wheel.
function mixLch(a, b, t) {
var ha = a[1] < 0.01 ? b[2] : a[2]
var hb = b[1] < 0.01 ? a[2] : b[2]
var dh = ((hb - ha) % 360 + 540) % 360 - 180
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, ha + dh * t]
}
// weights: [[themeName, weight]] or [[from, 1 - t], [to, t]].
function blend(weights) {
var out = {}
for (var r = 0; r < ROLES.length; r++) {
var lch = theme(weights[0][0])[ROLES[r]]
if (weights.length > 1)
lch = mixLch(lch, theme(weights[1][0])[ROLES[r]], weights[1][1])
out[ROLES[r]] = oklabToHex(oklab(lch))
}
return out
}
// ---------------------------------------------------------------- phase
function smooth(t) {
t = Math.min(Math.max(t, 0), 1)
return t * t * (3 - 2 * t)
}
// Night below -12 degrees (nautical dusk), twilight across -4 to 2,
// day above 10, crossfading in between. Whether twilight is sunrise or
// sunset depends on which way the sun is going. Returns the phases in
// play, as [["night", weight], ...].
function weights(elevation, rising) {
var twilight = rising ? "sunrise" : "sunset"
if (elevation < -12)
return { label: "night", weights: [["night", 1]] }
if (elevation < -4) {
var t = smooth((elevation + 12) / 8)
return { label: t < 0.5 ? "night" : twilight,
weights: [["night", 1 - t], [twilight, t]] }
}
if (elevation < 2)
return { label: twilight, weights: [[twilight, 1]] }
if (elevation < 10) {
var u = smooth((elevation - 2) / 8)
return { label: u < 0.5 ? twilight : "day",
weights: [[twilight, 1 - u], ["day", u]] }
}
return { label: "day", weights: [["day", 1]] }
}
// The config key for a phase's setting: ("sunrise", "Theme") is
// SunriseTheme.
function key(phase, what) {
return phase.charAt(0).toUpperCase() + phase.slice(1) + what
}
// cfg is the wallpaper configuration (or anything with the same keys).
// Returns { base, deep, ink, tone, patterns, label, elevation }, where
// patterns is [[pattern, weight], ...] for Field's "mix" pattern.
function palette(cfg, date) {
if (cfg.ThemeMode !== "sun") {
var fixed = blend([[cfg.Theme, 1]])
fixed.patterns = [[cfg.Pattern, 1]]
fixed.label = cfg.Theme
return fixed
}
var elevationAt = cfg.UseLocation
? function (d) { return sunElevation(d, Number(cfg.Latitude), Number(cfg.Longitude)) }
: function (d) { return clockElevation(d, cfg.SunriseTime, cfg.SunsetTime) }
var elevation = elevationAt(date)
var rising = elevationAt(new Date(date.getTime() + 600000)) > elevation
var phase = weights(elevation, rising)
var result = blend(phase.weights.map(function (w) { return [cfg[key(w[0], "Theme")], w[1]] }))
result.patterns = phase.weights.map(function (w) { return [cfg[key(w[0], "Pattern")], w[1]] })
result.label = phase.label
result.elevation = elevation
return result
}
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
6.7 KiB · text
History
be2c05b