levity

background

other colors

sun picker

star charts

sunset slider

A fancy light/dark mode switcher that simulates a sunset.

This page is both a debug tool and a showcase. The arrows on the display gradients indicate the current color value of each variable – drag the slider in the page header to manipulate them.

When I started this website in early 2024, I was about to move out of my much-loved second apartment and into an uncertain future. Establishing this home base on the web – such as it is – was largely an effort to cultivate a stable sense of place during a time of transition and rootlessness. My favorite things about that apartment were the access road behind the neighborhood which led to a small park – the only place I've been able to maintain a running habit – and the west-facing sliding glass doors in my office-turned-bedroom, both of which offered excellent views of the evening sky. Winter and spring are the dry season in south Florida, and that sky was often preternaturally clear: a perfect gradient of glowing, searing color. I spent many, many evenings watching it fade from rose to dusk while I worked on this website, or went out to jog and think and say my silent goodbyes to home.

The code for the slider lives in /script/sunset.js. The entrypoint, attachSlider, is imported into our main script file, script.js, which is loaded on almost every page (it's part of the page template). Among a few other things, script.js calls attachSlider to append the slider to the page's (first) header element, if there is one.

The slider controls the two colors present in the background โ€“ a simple two-color gradient with the midpoint color at 70% โ€“ and a selection of color variables used throughout the site's styling. Each of these variables has its own array of breakpoints, which are spaced evenly along the slider's length. (I considered adding bespoke positioning for the breakpoints, but I actually like it this way โ€“ the constraint makes choosing colors more interesting.) When the slider moves, we identify the breakpoints to the left and right of its position for each variable, then interpolate between those colors using Color.js.

Text is a special case, because transitioning smoothly from dark to light text while the background transitions from light to dark would cause states where the text is grey-on-grey. Instead, we run the text color through an additional calculation that checks the intended color's contrast against the background and turns the text white if that would be better, effectively flipping the text from dark to white instantly at the most appropriate point.

The stars have their [own script](/script/stars.js), though the overall effect is a tag-team effort between that script and the stars' [dedicated CSS file](/style/stars.css). The script creates and places the stars, while the CSS file calculates their size, color and opacity from CSS variables that are controlled by the script. First, the script determines how many of the smallest size of star to render based on the screen area. For each larger size, up to `MAX_SIZE` (currently 6), we render an exponentially decreasing number of stars. By way of example, here are the actual numbers for my current screen: - 5 stars of size 6 - 8 stars of size 5 - 15 stars of size 4 - 31 stars of size 3 - 86 stars of size 2 - 490 stars of size 1 For each of these, it then creates the star itself (which is just a div with class `star`), sets an assortment of CSS variables on it, assigns it a random position, and appends it to the parent `stars` element. The CSS variables are: - `--color` – the color of the star, determined at random based on a set of weighted hue-ranges (kindly donated by ChatGPT) that more or less approximates the distribution of real visible star colors - `--star-size` – the size from 1 to `MAX_SIZE` - `--min-value` – the minimum **slider value** (0 is midday, 1 is full night) _at which the star appears_. Below this, the star's opacity will be 0; above, it will gradually fade in as the slider moves right. This is a function of the star's size, where bigger stars have a lower `--min-value` and thus appear sooner. - The math is `(MAX_SIZE + R - size) / (MAX_SIZE + R)`, where `R` is currently 3 — in other words, the largest stars (size 6) will appear at `3/9` or `.333` value, and the smallest stars (size 1) will appear at `8/9` or `0.889` value. - `--anim-offset` – determines the delay and speed of the star's "glowing" animation. Its value is the star's index modulo how many different offsets there are (currently 7); this ensures that stars of the same size will have different animation cycles, since we create all the stars of a given size in a row. Most importantly, **every time the slider moves, [`stars.js`](/script/stars.js) updates the CSS variable `--value` to the slider's current value.** This allows us to control the stars' opacity dynamically with the slider even though the rendering is pure CSS. From there, it's some simple math on opacity in the CSS file: we set each star's opacity to `calc(pow(var(--value) - var(--min-value), 0.5))`, and the animation tweens back and forth between that value halved (at one end) and doubled (at the other). All the stars share the same animation keyframes, but their `animation-duration` is modified by their `--anim-offset` variable, causing them to animate out of sync. There are a lot of magic numbers in the CSS; the bulk of my time spent on it was just fiddling with these to get it to feel right. For example, "raising" the star's opacity to the power of 0.5 (ie taking its square root) was an effort to artificially enhance the brightness of small/dim stars, but it's really not ideal — as you can see from the "star chart" graphs, this creates a convex curve where the star pops in very quickly but doesn't ultimately gain much brightness in full night, whereas a concave curve would have been much better. I'll fix it someday! ๐ŸŒป