diff --git a/.gitignore b/.gitignore
index da61ed1..364fdec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-*.jpg
public/
diff --git a/config.toml b/config.toml
index 0370fdf..fba0b72 100644
--- a/config.toml
+++ b/config.toml
@@ -1,4 +1,5 @@
-name = "From the Office of the Chief Sundries Officer and Head of R&D"
+name = "Office of the Chief Sundries Officer and Head of R&D"
+title = "From the Desk of the Chief Sundries Officer and Head of R&D, NebCorp Heavy Industries and Sundries"
# The URL the site will be built for
base_url = "https://proclamations.nebcorp-hias.com"
@@ -16,6 +17,8 @@ taxonomies = [
]
theme = "apollo"
+mathjax = true
+mathjax_dollar_inline_enable = true
[markdown]
# Whether to do syntax highlighting
diff --git a/content/_index.md b/content/_index.md
index 30f06c2..fb170c9 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -1,8 +1,9 @@
+++
-title = "From the Desk of the Head of R&D and Chief Sundries Officer"
+title = "Latest Proclamations"
sort_by = "date"
generate_feed = true
toc = true
+template = "home.html"
[extra]
toc = true
+++
diff --git a/content/about/index.md b/content/about.md
similarity index 100%
rename from content/about/index.md
rename to content/about.md
diff --git a/content/rnd/a_serialized_mystery/index.md b/content/rnd/a_serialized_mystery/index.md
new file mode 100644
index 0000000..29c35ba
--- /dev/null
+++ b/content/rnd/a_serialized_mystery/index.md
@@ -0,0 +1,535 @@
++++
+title = "A One-Part Serialized Mystery"
+slug = "one-part-serialized-mystery"
+date = "2023-06-29"
+updated = "2023-06-29"
+[taxonomies]
+tags = ["software", "rnd", "proclamation", "upscm", "rust"]
++++
+
+# *Mise en Scene*
+
+I recently spent a couple days moving from [one type of universally unique
+identifier](https://commons.apache.org/sandbox/commons-id/uuid.html) to a [different
+one](https://github.com/ulid/spec), for an in-progress [database-backed
+web-app](https://gitlab.com/nebkor/ww). The [initial
+work](https://gitlab.com/nebkor/ww/-/commit/be96100237da56313a583be6da3dc27a4371e29d#f69082f7433f159d627269b207abdaf2ad52b24c)
+didn't take very long, but debugging the [serialization and
+deserialization](https://en.wikipedia.org/wiki/Serialization) of the new IDs took another day and a
+half, and in the end, the alleged mystery of why it wasn't working was a red herring due to my own
+stupidity. So come with me on an exciting voyage of discovery, and [once again, learn from my
+folly](@/sundries/a-thoroughly-digital-artifact/index.md)!
+
+# Keys, primarily
+
+Most large distributed programs that people interact with daily via HTTP are, in essence, a fancy
+facade for some kind of database. Facebook? That's a database. Gmail? That's a database.
+
+![that's a database][thats_a_database]
+
wikipedia? that's a database.
+
+In most databases, each entry ("row") has a field that acts as a [primary
+key](https://en.wikipedia.org/wiki/Primary_key), used to uniquely identify that row inside the table
+it's in. Since databases typically contain multiple tables, and primary keys have to be unique only
+within their own table, you could just use a simple integer that's automatically incremented every
+time you add a new record, and in many databases, if you create a table without specifying a primary
+key, they will [automatically and implicitly use a
+mechanism](https://www.sqlite.org/lang_createtable.html#rowid) like that. You may also recognize the
+idea of "serial numbers", which is what these sorts of IDs are.
+
+This is often totally fine! If you only ever have one copy of the database, and never have to worry
+about inserting rows from a different instance of the database, then you can just use those simple
+values and move on your merry way.
+
+However, if you ever think you might want to have multiple instances of your database running, and
+want to make sure they're eventually consistent with each other, then you might want to use a
+fancier identifier for your primary keys, to avoid collisions between them.
+
+## UUIDs
+
+A popular type for fancy keys is called a
+[v4 UUIDs](https://datatracker.ietf.org/doc/html/rfc4122#page-14). These are 128-bit random
+numbers[^uuidv4_random], and when turned into a string, usually look something like
+`1c20104f-e04f-409e-9ad3-94455e5f4fea`; this is called the "hyphenated" form, for fairly obvious
+reasons. Although sometimes they're stored in a DB in that form directly, that's using 36 bytes to
+store 16 bytes' worth of data, which is more than twice as many bytes as necessary. And if you're
+a programmer, this sort of conspicuous waste is unconscionable.
+
+You can cut that to 32 bytes by just dropping the dashes, but then that's still twice as many bytes
+as the actual data requires. If you never have to actually display the ID inside the database, then
+the simplest thing to do is just store it as a blob of 16 bytes[^blob-of-bytes]. Finally, optimal
+representation and efficiency!
+
+## Indexes?
+
+And at first, that's what I did. The [external library](https://docs.rs/sqlx/latest/sqlx/) I'm using
+to interface with my database automatically writes UUIDs as a sequence of sixteen bytes, if you
+specified the type in the database[^sqlite-dataclasses] as "[blob](https://www.sqlite.org/datatype3.html)", which [I
+did](https://gitlab.com/nebkor/ww/-/commit/65a32f1f20df6c572580d796e1044bce807fd3b6#f1043d50a0244c34e4d056fe96659145d03b549b_0_5).
+
+But then I saw a [blog post](https://shopify.engineering/building-resilient-payment-systems) where
+the following tidbit was mentioned:
+
+> We prefer using an Universally Unique Lexicographically Sortable Identifier (ULID) for these
+> idempotency keys instead of a random version 4 UUID. ULIDs contain a 48-bit timestamp followed by
+> 80 bits of random data. The timestamp allows ULIDs to be sorted, which works much better with the
+> b-tree data structure databases use for indexing. In one high-throughput system at Shopify we’ve
+> seen a 50 percent decrease in INSERT statement duration by switching from UUIDv4 to ULID for
+> idempotency keys.
+
+Whoa, that sounds great! But [this youtube
+video](https://www.youtube.com/watch?v=f53-Iw_5ucA&t=590s) tempered my expectations a bit, by
+describing the implementation-dependent reasons for that dramatic
+improvement. Still, switching from UUIDs to ULIDs couldn't *hurt*[^no-stinkin-benches], right? Plus,
+by encoding the time of creation (at least to the nearest millisecond), I could remove a "created
+at" field from every table that used them as primary keys. Which, in my case, would be all of them,
+and I'm worried less about the speed of inserts than I am about keeping total on-disk size down
+anyway.
+
+I was actually already familiar with the idea of using time-based sortable IDs, from
+[KSUIDs](https://github.com/segmentio/ksuid). It's an attractive concept to me, and I'd considered
+using them from the get-go, but discarded that for two main reasons:
+
+ - they're **FOUR WHOLE BYTES!!!** larger than UUIDs
+ - I'd have to manually implement serialization/deserialization, since SQLx doesn't
+ have native support for them
+
+In reality, neither of those are real show-stoppers; 20 vs. 16 bytes is probably not that
+significant, and I'd have to do the manual serialization stuff for anything besides a
+less-than-8-bytes number or a normal UUID. Still, four bytes is four bytes, and all other things
+being equal, I'd rather go for the trimmer, 64-bit-aligned value.
+
+Finally, I'd recently finished with adding some ability to actually interact with data in a
+meaningful way, and to add new records to the database, which meant that it was now or never for
+standardizing on a type for the primary keys. I was ready to do this thing.
+
+# Serial problems
+
+"Deserilization" is the act of converting a static, non-native representation of some kind of
+datatype into a dynamic, native computer programming object, so that you can do the right computer
+programming stuff to it. It can be as simple as when a program reads in a string of digit characters
+and parses it into a real number, but of course the ceiling on complexity is limitless.
+
+In my case, it was about getting those sixteen bytes out of the database and turning them into
+ULIDs. Technically, I could have let Rust [handle that for me](https://serde.rs/derive.html) by
+automatically deriving that functionality. There were a couple snags with that course, though:
+
+ - the default serialized representation of a ULID in the library I was using to provide them [is as
+26-character strings](https://docs.rs/ulid/latest/ulid/serde/index.html), and I wanted to use only
+16 bytes in the database
+ - you could tell it to serialize as a [128-bit
+number](https://docs.rs/ulid/latest/ulid/serde/ulid_as_u128/index.html), but that merely kicked the
+problem one step down the road since SQLite can only handle up to 64-bit numbers, as previously
+discussed, so I'd still have to manually do something for them
+
+This meant going all-in on fully custom serialization and deserialization, something I'd never done
+before, but how hard could it be? (spoiler: actually not that hard!)
+
+## Great coders steal
+
+Something I appreciate about the [Rust programming language](https://www.rust-lang.org/) is that
+because of the way the compiler works[^rust-generics], the full source code almost always has to be
+available to you, the end-user coder. The culture around it is also very biased toward open source,
+and so all the extremely useful libraries are just sitting there, ready to be studied and copied. So
+the first thing I did was take a look at how [SQLx handled
+UUIDs](https://github.com/launchbadge/sqlx/blob/main/sqlx-sqlite/src/types/uuid.rs):
+
+``` rust
+impl Type for Uuid {
+ fn type_info() -> SqliteTypeInfo {
+ SqliteTypeInfo(DataType::Blob)
+ }
+
+ fn compatible(ty: &SqliteTypeInfo) -> bool {
+ matches!(ty.0, DataType::Blob | DataType::Text)
+ }
+}
+
+impl<'q> Encode<'q, Sqlite> for Uuid {
+ fn encode_by_ref(&self, args: &mut Vec>) -> IsNull {
+ args.push(SqliteArgumentValue::Blob(Cow::Owned(
+ self.as_bytes().to_vec(),
+ )));
+
+ IsNull::No
+ }
+}
+
+impl Decode<'_, Sqlite> for Uuid {
+ fn decode(value: SqliteValueRef<'_>) -> Result {
+ // construct a Uuid from the returned bytes
+ Uuid::from_slice(value.blob()).map_err(Into::into)
+ }
+}
+```
+
+There's not a ton going on there, as you can see. To "encode" it just gets the bytes out of the
+UUID, and to "decode" it just gets the bytes out of the database. I couldn't use that exactly as
+done by the SQLx authors, as they were using datatypes that were private to their crate, but it was
+close enough; here's mine:
+
+``` rust
+impl sqlx::Type for DbId {
+ fn type_info() -> ::TypeInfo {
+ <&[u8] as sqlx::Type>::type_info()
+ }
+}
+
+impl<'q> Encode<'q, Sqlite> for DbId {
+ fn encode_by_ref(&self, args: &mut Vec>) -> IsNull {
+ args.push(SqliteArgumentValue::Blob(Cow::Owned(self.bytes().to_vec())));
+ IsNull::No
+ }
+}
+
+impl Decode<'_, Sqlite> for DbId {
+ fn decode(value: SqliteValueRef<'_>) -> Result {
+ let bytes = <&[u8] as Decode>::decode(value)?;
+ let bytes: [u8; 16] = bytes.try_into().unwrap_or_default();
+ Ok(u128::from_ne_bytes(bytes).into())
+ }
+}
+```
+
+(In order to implement the required methods from SQLx, I had to wrap the ULID in a new, custom type,
+which I called `DbId`, to comply with the [orphan rules](https://github.com/Ixrec/rust-orphan-rules).)
+
+That's only half the story, though. If all I had to worry about was getting data in and out of the
+database, that would be fine, but because I'm building a web app, I need to be able to include my
+new ID type in messages sent over a network or as part of a web page, and for that, it needed to
+implement some functionality from a different library, called [Serde](https://serde.rs/). My
+original implementation for *deserializing* looked like this:
+
+``` rust
+struct DbIdVisitor;
+
+impl<'de> Visitor<'de> for DbIdVisitor {
+ type Value = DbId;
+
+ // make a DbId from a slice of bytes
+ fn visit_bytes(self, v: &[u8]) -> Result
+ where
+ E: serde::de::Error,
+ {
+ ...
+ }
+
+ // make a DbId from a Vec of bytes
+ fn visit_byte_buf(self, v: Vec) -> Result
+ where
+ E: serde::de::Error,
+ {
+ ...
+ }
+
+ // you get the picture
+ fn visit_string() ...
+ fn visit_u128() ...
+ fn visit_i128() ...
+}
+```
+
+In my mind, the only important pieces were the `visit_bytes()` and `visit_byte_buf()` methods,
+which worked basically the same as the `decode()` function for SQLx. I mean, as far as I could tell,
+the only time something would be encountering a serialized `DbId` would be in the form of raw bytes
+from the database; no one else would be trying to serialize one as something else that I didn't
+anticipate, right?
+
+RIGHT???
+
+(wrong)
+
+## A puzzling failure
+
+As soon as my code compiled, I ran my tests. Everything passed... except for one, that tested
+logging in.
+
+This was very strange. All the other tests were passing, and basically every operation requires
+getting one of these IDs into or out of the database. But at this point, it was late, and I set it
+down until the next day.
+
+# When in doubt, change many things at once
+
+The next day I sat back down to get back to work, and in the course of examining what was going on,
+realized that I'd missed something crucial: these things were supposed to be *sortable*. But the way
+I was inserting them meant that they weren't, because of endianness.
+
+## More like shmexicographic, amirite
+
+"ULID" stands for "Universally Unique Lexicographically Sortable
+Identifier"[^uulsid]. "[Lexicographic order](https://en.wikipedia.org/wiki/Lexicographic_order)"
+basically means, "like alphabetical, but for anything with a defined total order". Numbers have a
+defined total order; bigger numbers always go after smaller.
+
+But sometimes numbers get sorted out of order, if they're not treated as numbers. Like say you had a
+directory with twelve files in it, called "1.txt" up through "12.txt". If you were to ask to see
+them listed out in lexicographic order, it would go like:
+
+``` text
+$ ls
+10.txt
+11.txt
+12.txt
+1.txt
+2.txt
+3.txt
+4.txt
+5.txt
+6.txt
+7.txt
+8.txt
+9.txt
+```
+
+This is because '10' is "less than" '2' (and '0' is "less than" '.', which is why "10.txt" is before "1.txt"). The solution, as all
+data-entering people know, is to pad the number with leading '0's:
+
+``` text
+$ ls
+01.txt
+02.txt
+03.txt
+04.txt
+05.txt
+06.txt
+07.txt
+08.txt
+09.txt
+10.txt
+11.txt
+12.txt
+```
+
+Now the names are lexicographically sorted in the right numerical order[^confusing-yes].
+
+So, now that we're all expert lexicographicographers, we understand that our IDs are just
+supposed to naturally sort themselves in the correct order, based on when they were created; IDs
+created later should sort after IDs created earlier.
+
+The implementation for my ULIDs only guaranteed this property for the string form of them, but I was
+not storing them in string from. Fundamentally, the ULID was a simple [128-bit primitive
+integer](https://doc.rust-lang.org/std/primitive.u128.html), capable of holding values between 0 and
+340,282,366,920,938,463,463,374,607,431,768,211,455.
+
+But there's a problem: I was storing the ID in the database as a sequence of 16 bytes. I was asking
+for those bytes in "native endian", which in my case, meant "little endian". If you're not familiar
+with endianness, there are two varieties: big, and little. "Big" makes the most sense for a lot of
+people; if you see a number like "512", it's big-endian; the end is the part that's left-most, and
+"big" means that it is the most-significant-digit. This is the same as what westerners think of as
+"normal" numbers. In the number "512", the "most significant digit" is `5`, which correspends to
+`500`, which is added to the next-most-significant digit, `1`, corresponding to `10`, which is added
+to the next-most-significant digit, which is also the least-most-significant-digit, which is `2`,
+which is just `2`, giving us the full number `512`.
+
+If we put the least-significant-digit first, we'd write the number `512` as "215"; the order when
+written out would be reversed. This means that the lexicographic sort of `512, 521` would have "125"
+come before "215", which is backwards.
+
+Little-endianness is like that. If a multibyte numeric value is on a little-endian system, the
+least-significant bytes will come first, and a lexicographic sorting of those bytes would be
+non-numeric.
+
+The solution, though, is simple: just write them out in big-endian order! This was literally a
+one-line change in the code, to switch from `to_ne_bytes()` ("ne" for "native endian") to
+`to_be_bytes()`. I confirmed that the bytes written into were being written in the correct
+lexicographic order:
+
+``` sql
+sqlite> select hex(id), username from users order by id asc;
+018903CDDCAAB0C6872A4509F396D388|first_user
+018903D0E591525EA42202FF461AA5FA|second_user
+```
+
+Note the first six characters are the same, for these two users created some time apart[^ulid-timestamps].
+
+Boom. "Sorted".
+
+## The actual problem
+
+Except that the logins were still broken; it wasn't just the test. What was even stranger is that
+with advanced debugging techniques[^advanced-debugging], I confirmed that the login *was*
+working. By which I mean, when the user submitted a login request, the function that handled the
+request was:
+
+ - correctly confirming password match
+ - retrieving the user from the database
+
+The second thing was required for the first. It was even creating a session in the session table:
+
+``` sql
+sqlite> select * from async_sessions;
+..|..|{"id":"ZY...","expiry":"...","data":{"_user_id":"[1,137,3,205,220,170,176,198,135,42,69,9,243,150,211,136]","_auth_id":"\"oM..."}}
+```
+
+I noticed that the ID was present in the session entry, but as what looked like an array of decimal
+values. The less not-astute among you may have noticed that the session table seemed to be using
+JSON to store information. This wasn't my code, but it was easy enough to find the
+[culprit](https://github.com/http-rs/async-session/blob/d28cef30c7da38f52639b3d60fc8cf4489c92830/src/session.rs#L214):
+
+``` rust
+ pub fn insert(&mut self, key: &str, value: impl Serialize) -> Result<(), serde_json::Error> {
+ self.insert_raw(key, serde_json::to_string(&value)?);
+ Ok(())
+ }
+```
+
+This was in the [external library](https://docs.rs/async-session/latest/async_session/) I was using
+to provide cookie-based sessions for my web app, and was transitively invoked when I called the
+`login()` method in my own code. Someone else was serializing my IDs, in a way I hadn't anticipated!
+
+The way that Serde decides what code to call is based on its [data
+model](https://serde.rs/data-model.html). And wouldn't you know it, the following words are right
+there, hiding in plain sight, as they had been all along:
+
+> When deserializing a data structure from some format, the Deserialize implementation for the data
+> structure is responsible for mapping the data structure into the Serde data model by passing to
+> the Deserializer a Visitor implementation that can receive the various types of the data model...
+>
+> [...]
+>
+> * seq
+> - A variably sized heterogeneous sequence of values, for example Vec<T> or HashSet<T>. ...
+>
+> [...]
+>
+> The flexibility around mapping into the Serde data model is profound and powerful. When
+> implementing Serialize and Deserialize, be aware of the broader context of your type that may make
+> the most instinctive mapping not the best choice.
+
+Well, when you put it that way, I can't help but understand: I needed to implement a `visit_seq()`
+method in my deserialization code.
+
+![fine, fine, I see the light][see_the_light]
+fine, fine, i see the light
+
+You can see that
+[here](https://gitlab.com/nebkor/ww/-/blob/656e6dceedf0d86e2805e000c9821e931958a920/src/db_id.rs#L194-216)
+if you'd like, but I'll actually come back to it in a second. The important part was that my logins
+were working again; time to party!
+
+## Wait, why *isn't* it broken?
+
+I'd just spent the day banging my head against this problem, and so when everything worked again, I
+committed and pushed the change and signed off. But something was still bothering me, and the next
+day, I dove back into it.
+
+
+All my serialization code was calling a method called
+[`bytes()`](https://gitlab.com/nebkor/ww/-/blob/656e6dceedf0d86e2805e000c9821e931958a920/src/db_id.rs#L18),
+which simply called another method that would return an array of 16 bytes, in big-endian order, so
+it could go into the database and be sortable, as discussed.
+
+But all[^actually_not_all] my *deserialization* code was constructing the IDs as [though the bytes
+were
+*little*-endian](https://gitlab.com/nebkor/ww/-/blob/656e6dceedf0d86e2805e000c9821e931958a920/src/db_id.rs#L212). Which
+lead me to ask:
+
+what the fuck?
+
+Like, everything was *working*. Why did I need to construct from a different byte order? I felt like
+I was losing my mind, so I reached out to the [Recurse Center](https://www.recurse.com) community
+and presented my case.
+
+Basically, I showed that bytes were written correctly, resident in the DB in big-endian form, but
+then were "backwards" coming out and "had to be" cast using little-endian constructors
+("`from_ne_bytes()`").
+
+What had actually happened is that as long as there was agreement about what order to use for reconstructing the
+ID from the bytes, it didn't matter if it was big or little-endian, it just had to be the same on
+both the
+[SQLx](https://gitlab.com/nebkor/ww/-/commit/84d70336d39293294fd47b4cf115c70091552c11#ce34dd57be10530addc52a3273548f2b8d3b8a9b_106_105)
+side and on the
+[Serde](https://gitlab.com/nebkor/ww/-/commit/84d70336d39293294fd47b4cf115c70091552c11#ce34dd57be10530addc52a3273548f2b8d3b8a9b_210_209)
+side. This is also irrespective of the order they were written out in, but again, the two sides must
+agree on the convention used. Inside the Serde method, I had added some debug printing of the bytes
+it was getting, and they were in little-endian order. What I had not realized is that that was
+because they were first passing through the SQLx method which reversed them.
+
+Mmmmm, delicious, delicous red herring.
+
+Two people were especially helpful, Julia Evans and Nicole Tietz-Sokolskaya; Julia grabbed a copy of
+my database file and poked it with Python, and could not replicate the behavior I was seeing, and
+Nicole did the same but with a little Rust program she wrote. Huge thanks to both of them (but not
+just them) for the extended [rubber ducking](https://en.wikipedia.org/wiki/Rubber_duck_debugging)!
+And apologies for the initial gas-lighting; Julia was quite patient and diplomatic when pushing back
+against "the bytes are coming out of the db backwards".
+
+
+# Lessons learned
+
+Welp, here we are, the end of the line; I hope this has been informative, or barring that, at least
+entertaining. Or the other way around, I'm not that fussy!
+
+Obviously, the biggest mistake was to futz with being clever about endianness before understanding
+why the login code was now failing. Had I gotten it working correctly first, I would have been able to
+figure out the requirement for agreement on convention between the two different serialization
+systems much sooner, and I would not have wasted mine and others' time on misunderstanding.
+
+On the other hand, it's hard to see these things on the first try, especially when you're on your
+own, and are on your first fumbling steps in a new domain or ecosystem; for me, that was getting
+into the nitty-gritty with Serde, and for that matter, dealing directly with serialization-specific
+issues. Collaboration is a great technique for navigating these situations, and I definitely need to
+focus a bit more on enabling that[^solo-yolo-dev].
+
+In the course of debugging this issue, I tried to get more insight via
+[testing](https://gitlab.com/nebkor/ww/-/commit/656e6dceedf0d86e2805e000c9821e931958a920#ce34dd57be10530addc52a3273548f2b8d3b8a9b_143_251),
+and though that helped a little, it was not nearly enough; the problem was that I misunderstood how
+something worked, not that I had mistakenly implemented something I was comfortable with. Tests
+aren't a substitute for understanding!
+
+And of course, I'm now much more confident and comfortable with Serde; reading the Serde code for
+other things, like [UUIDs](https://github.com/uuid-rs/uuid/blob/main/src/external/serde_support.rs),
+is no longer an exercise in eye-glaze-control. Maybe this has helped you with that too?
+
+----
+
+[^uuidv4_random]: Technically, most v4 UUIDs have only 122 random bits, as six out of 128 are
+ reserved for version information.
+
+[^blob-of-bytes]: Some databases have direct support for 128-bit primitive values (numbers). The
+ database I'm using, SQLite, only supports up to 64-bit primitive values, but it does support
+ arbitrary-length sequences of bytes called "blobs".
+
+[^sqlite-dataclasses]: I'm using [SQLite](https://www.sqlite.org/index.html) for reasons that I plan
+ to dive into in a different post, but "blob" is specific to it. In general, you'll probably want
+ to take advantage of implementation-specific features of whatever database you're using, which
+ means that your table definitions won't be fully portable to a different database. This is fine
+ and good, actually!
+
+[^no-stinkin-benches]: You may wonder: have I benchmarked this system with UUIDs vs. ULIDs? Ha ha,
+ you must have never met a programmer before! No, of course not. But, that's coming in a
+ follow-up.
+
+[^rust-generics]: If the code you're using has
+ [generics](https://doc.rust-lang.org/book/ch10-01-syntax.html) in it, then the compiler needs to
+ generate specialized versions of that generic code based on how you use it; this is called
+ "[monomorphization](https://doc.rust-lang.org/book/ch10-01-syntax.html#performance-of-code-using-generics)",
+ and it requires the original generic source to work. That's also true in C++, which is why most
+ templated code is [header-only](https://isocpp.org/wiki/faq/templates#templates-defn-vs-decl),
+ but Rust doesn't have header files.
+
+[^uulsid]: I guess the extra 'U' and 'S' are invisible.
+
+[^confusing-yes]: Is this confusing? Yes, 100%, it is not just you. Don't get discouraged.
+
+[^ulid-timestamps]: The 6 most-significant bytes make up the timestamp in a ULID, which in the hex
+ dump form pasted there would be the first twelve characters, since each byte is two hex
+ digits.
+
+[^advanced-debugging]: "adding `dbg!()` statements in the code"
+
+[^actually_not_all]: Upon further review, I discovered that the only methods that were constructing
+ with little-endian order were the SQLx `decode()` method, and the Serde `visit_seq()` method,
+ which were also the only ones that were being called at all. The
+ [`visit_bytes()`](https://gitlab.com/nebkor/ww/-/blob/656e6dceedf0d86e2805e000c9821e931958a920/src/db_id.rs#L152)
+ and `visit_byte_buf()` methods, that I had thought were so important, were correctly treating
+ the bytes as big-endian, but were simply never actually used. I fixed [in the next
+ commit](https://gitlab.com/nebkor/ww/-/commit/84d70336d39293294fd47b4cf115c70091552c11#ce34dd57be10530addc52a3273548f2b8d3b8a9b)
+
+[^solo-yolo-dev]: I've described my current practices as "solo-yolo", which has its plusses and
+ minuses, as you may imagine.
+
+
+[thats_a_database]: ./thats_a_database.png "simpsons that's-a-paddling guy"
+
+[see_the_light]: ./seen_the_light.png "jake blues seeing the light"
diff --git a/content/rnd/a_serialized_mystery/seen_the_light.png b/content/rnd/a_serialized_mystery/seen_the_light.png
new file mode 100644
index 0000000..96a5702
Binary files /dev/null and b/content/rnd/a_serialized_mystery/seen_the_light.png differ
diff --git a/content/rnd/a_serialized_mystery/thats_a_database.png b/content/rnd/a_serialized_mystery/thats_a_database.png
new file mode 100644
index 0000000..a98b5af
Binary files /dev/null and b/content/rnd/a_serialized_mystery/thats_a_database.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/blending_california.png b/content/sundries/a-thoroughly-digital-artifact/blending_california.png
new file mode 100644
index 0000000..04e8e02
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/blending_california.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/blurred_scaled_hm_3.png b/content/sundries/a-thoroughly-digital-artifact/blurred_scaled_hm_3.png
new file mode 100644
index 0000000..6f9edf5
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/blurred_scaled_hm_3.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/ca_topo_crappy_test_print.png b/content/sundries/a-thoroughly-digital-artifact/ca_topo_crappy_test_print.png
new file mode 100644
index 0000000..7d22d1a
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/ca_topo_crappy_test_print.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/crappy_test_print_close_up.jpg b/content/sundries/a-thoroughly-digital-artifact/crappy_test_print_close_up.jpg
new file mode 100644
index 0000000..36d4ed9
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/crappy_test_print_close_up.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/exponential_plot.png b/content/sundries/a-thoroughly-digital-artifact/exponential_plot.png
new file mode 100644
index 0000000..c89adb3
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/exponential_plot.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/final_ca_topo_blend.png b/content/sundries/a-thoroughly-digital-artifact/final_ca_topo_blend.png
new file mode 100644
index 0000000..64d14bd
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/final_ca_topo_blend.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/final_heightmap.png b/content/sundries/a-thoroughly-digital-artifact/final_heightmap.png
new file mode 100644
index 0000000..a32756d
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/final_heightmap.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/final_printed.jpg b/content/sundries/a-thoroughly-digital-artifact/final_printed.jpg
new file mode 100644
index 0000000..68cb034
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/final_printed.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/final_shasta.png b/content/sundries/a-thoroughly-digital-artifact/final_shasta.png
new file mode 100644
index 0000000..bab1ad7
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/final_shasta.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/geotiff-files.png b/content/sundries/a-thoroughly-digital-artifact/geotiff-files.png
new file mode 100644
index 0000000..ae73afc
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/geotiff-files.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/index.md b/content/sundries/a-thoroughly-digital-artifact/index.md
new file mode 100644
index 0000000..94974b1
--- /dev/null
+++ b/content/sundries/a-thoroughly-digital-artifact/index.md
@@ -0,0 +1,900 @@
++++
+title = "A Thoroughly Digital Artifact"
+slug = "a-thoroughly-digital-artifact"
+date = "2023-01-19"
+updated = "2023-01-21"
+[taxonomies]
+tags = ["3dprinting", "CAD", "GIS", "CNC", "art", "sundry", "proclamation", "research"]
++++
+
+![A plywood slab carved with CNC into a topographic representation of California][main_image]
+
+# A birthday wish
+
+Last summer, I wanted to get my wife something nice for her birthday. For many years, she had
+expressed an occasional and casual desire for a topographic carving of the state of California,
+where we live, and I thought it might be something I could figure out how to get her. In the end,
+after many dozens of hours of work, five weeks, and several hundred dollars paid to a professional
+CNC machine shop, I had the artifact shown in the picture above. This is the story of its creation,
+starting from knowing almost nothing about GIS, cartography, or CNC machining.
+
+# First steps
+
+Before you ask, I did not do a ton of research before embarking on this. As I write this, about six
+months later, it only now occurred to me to do a basic search for an actual physical thing I could
+buy, and luckily it seems that CNC-carved wooden relief maps of the whole state are not trivially
+easy to come by, so, *phew!*
+
+No, my first step was to see if there were any shops in the area that could carve something out of
+nice plywood, about a week before the intended recipient's birthday. I found one that was less than
+ten minutes away, and filled out their web contact form. They had a field for material, and I said,
+"some nice plywood between 0.75 and 1.0 inches thick or similar" (I didn't know exactly what was
+available and wanted to give broad acceptable parameters), and under "project description", I wrote,
+
+> A relief map of California, carved from wood. Height exaggerated enough
+to visibly discern the Santa Monica mountains. I can provide an STL file if needed.
+
+For some [incorrect] reason that I only later examined[^introspection], I just sort of assumed that the shop would
+have a library of shapes available for instantiating into whatever material medium you might
+need. But just in case, I included that hedge about being able to provide an STL file. Needless to
+say, that was a bluff.
+
+![the programmer's creed: we do these things not because they are easy, but because we thought they
+were going to be easy -- from twitter user @unoservix, 2016-08-05][programmers_creed]
+*me, every. single. time.*
+
+Also needless to say, my bluff was immediately called, and I had the following exchange with the
+shop:
+
+> *CNC Shop*: STL can work but I can’t manipulate it, which could save some money. If possible can it
+>be exported to an .igs or .iges or .stp format?
+>
+> *Me*: Yeah, STP should be no problem. Can you give a rough estimate of the cost for 1x2-foot relief carving?
+>
+> *Shop*: Without seeing the drawings, I can’t give even a close price but in the past they range from
+>a few hundred dollars to several thousand dollars.
+>
+> *Me*: That's totally fair! I'll get you some files in a few days.
+
+"STP should be no problem ... I'll get you some files in a few days," was an even harder lean into
+the bluff; my next communication with the shop was nearly four weeks later. But that's getting ahead
+of things.
+
+# Meshes and solid bodies
+
+First off, let's talk about file formats and how to represent shapes with a
+computer.[^math-computers] I first said I could provide an *STL
+file*. [STL](https://en.wikipedia.org/wiki/STL_(file_format)) is a pretty bare-bones format that
+describes the outside surface of a shape as a mesh of many, many triangles, each of which is
+described by three 3D points, where each point (but not necessarily each edge) of the triangle lies
+on the surface of the shape of the thing you're modeling. This format is popular with 3D printers,
+which is how I became familiar with it.
+
+STL is simple to implement and easy for a computer to read, but if you have a model in that format
+that you need to manipulate, like you want to merge it with another shape, you won't have a good
+time. In order to actually do things like that, it needs to be converted into a CAD program's native
+representation of a "solid body", which is pretty much what it sounds like: a shape made of a finite
+volume of "stuff", and NOT just an infinitesimally thin shell enclosing an empty volume, which is
+what a mesh is.
+
+In order for the CAD program to convert a mesh into a solid body, the mesh must be *manifold*,
+meaning, no missing faces (triangles), and with a clearly-defined interior and exterior (all
+triangles are facing in one direction relative to their interior). When there are no missing faces,
+it's called "water tight". You can still have "holes" in a mesh, like if you have a model of a
+donut[^manifold_holes], but the surface of the donut can't have any missing faces. A valid STL
+file's meshes are manifold.
+
+The CNC shop had requested a model in a format called
+[ST**P**](https://www.fastradius.com/resources/everything-you-need-to-know-about-step-files/). `.stp`
+is the extension for a "STEP" file; STEP is supposed to be short for "standard for the exchange of
+product data", so someone was playing pretty fast and loose with their initialisms, but I
+digress. The main thing about STEP files is that CAD programs can really easily convert them into
+their native internal solid body representation, which allows easy manipulation. Another thing about them is that a CAD program can usually turn a manifold mesh
+into an STP file, unless the mesh is too complicated and your computer doesn't have enough RAM
+(*note: foreshadowing*[^chekhovs-ram]).
+
+![an overly-complicated mesh of a cube][meshy-cube]
+*this cube's mesh has too many vertices and edges, I hope my computer has enough
+RAM to work with it*
+
+But at that moment, I had nothing at all. Time to get some data and see if I can turn it into a model.
+
+# Public data
+
+My first impulse was to search [USGS](https://usgs.gov)'s website for [digital elevation
+map](https://en.wikipedia.org/wiki/Digital_elevation_model) data, but I wound up not finding
+anything appropriate. Searching now with the wisdom of experience and hindsight, I found this, which
+would have been perfect:
+
+
+
+Did I just accidentally miss it then? Did I find it and not recognize its utility because I didn't
+know what I was doing *at all*? The world may never know, but at least now you can benefit from my
+many, many missteps.
+
+## From space?
+
+Anyway, having not found anything I could really use from the USGS through all fault of my own, I
+found [this site](https://portal.opentopography.org/raster?opentopoID=OTSRTM.082015.4326.1), from
+OpenTopography, an organization run by the UCSD Supercomputer Center, under a grant from the
+National Science Foundation. So, still hooray for public data!
+
+That particular page is for a particular dataset; in this case, "[SRTM
+GL1](http://www2.jpl.nasa.gov/srtm/) Global 30m". "SRTM" stands for "[Shuttle Radar Topography
+Mission](https://en.wikipedia.org/wiki/Shuttle_Radar_Topography_Mission)", which was a Space Shuttle
+mission in February, 2000, where it did a [fancy radar
+scan](https://en.wikipedia.org/wiki/Interferometric_synthetic-aperture_radar) of most of the land on
+Earth. Though, it's hard to verify that the data was not synthesized with other datasets of more
+recent, non-space origin, especially in places like California. But probably space was involved in
+some way.
+
+## In Australia, it's pronounced "g'dal"
+
+Anyway, I'd found an open source of public data. This dataset's [horizontal resolution is 1 arc
+second](https://gisgeography.com/srtm-shuttle-radar-topography-mission/) (which is why it's
+"GL**1**"), or roughly 30x30 meters, and the height data is accurate to within 16 meters. Not too
+shabby!
+
+They provided the data in the form of [GeoTIFF](https://en.wikipedia.org/wiki/GeoTIFF)s, which are
+basically an image where each pixel represents one data point (so, a 30x30 square meter plot)
+centered at a particular location on the Earth's surface. It's a monochrome image, where absolute
+height is mapped to absolute brightness of each pixel, and each pixel represents an exact location
+in the world.
+
+The only problem was that you could only download data covering up to 450,000 square kilometers at a
+time, so I had had to download a bunch of separate files and then mosaic them together. Luckily,
+there's a whole suite of open source tools called
+[GDAL](https://gdal.org/faq.html#what-does-gdal-stand-for). Among that suite is a tool called
+`gdal_merge.py` (yes, the `.py` is part of the name of the tool that gets installed to your system
+when you install the GDAL tools), which does exactly what I wanted:
+
+> `gdal_merge.py -o ca_topo.tif norcal_topo.tif centcal_topo.tif socal_topo.tif so_cent_cal_topo.tif norcal_topo_redux.tif last_bit.tif east_ca.tif`
+
+This produced a file called `ca_topo.tif`. It was very large, in every sense:
+
+![listing of tif files with sizes][geotiff-files]
+*last_little_piece_i_swear_final_final2.tif*
+
+Using [another tool](https://gdal.org/programs/gdalinfo.html) called `gdalinfo`, we can examine the
+metadata of the mosaic we just created:
+
+``` text
+$ gdalinfo -mm ca_topo.tif
+Driver: GTiff/GeoTIFF
+Files: ca_topo.tif
+Size is 40757, 35418
+Coordinate System is:
+GEOGCRS["WGS 84",
+ DATUM["World Geodetic System 1984",
+ ELLIPSOID["WGS 84",6378137,298.257223563,
+ LENGTHUNIT["metre",1]]],
+ PRIMEM["Greenwich",0,
+ ANGLEUNIT["degree",0.0174532925199433]],
+ CS[ellipsoidal,2],
+ AXIS["geodetic latitude (Lat)",north,
+ ORDER[1],
+ ANGLEUNIT["degree",0.0174532925199433]],
+ AXIS["geodetic longitude (Lon)",east,
+ ORDER[2],
+ ANGLEUNIT["degree",0.0174532925199433]],
+ ID["EPSG",4326]]
+Data axis to CRS axis mapping: 2,1
+Origin = (-125.109583333326071,42.114305555553187)
+Pixel Size = (0.000277777777778,-0.000277777777778)
+Metadata:
+ AREA_OR_POINT=Area
+Image Structure Metadata:
+ INTERLEAVE=BAND
+Corner Coordinates:
+Upper Left (-125.1095833, 42.1143056) (125d 6'34.50"W, 42d 6'51.50"N)
+Lower Left (-125.1095833, 32.2759722) (125d 6'34.50"W, 32d16'33.50"N)
+Upper Right (-113.7881944, 42.1143056) (113d47'17.50"W, 42d 6'51.50"N)
+Lower Right (-113.7881944, 32.2759722) (113d47'17.50"W, 32d16'33.50"N)
+Center (-119.4488889, 37.1951389) (119d26'56.00"W, 37d11'42.50"N)
+Band 1 Block=40757x1 Type=Int16, ColorInterp=Gray
+ Computed Min/Max=-130.000,4412.000
+```
+
+If I may draw your attention to a couple things there, the image is 40,757 pixels wide and 35,418
+pixels tall. The "pixel size" is 0.000277777777778 by 0.000277777777778; the units, given by the
+"angleunit", is degrees; 1 arc second is 1/3600th of a degree, which is 0.01754... They're degrees
+of arc along the surface of the Earth[^wgs-ellipsoid], at a distance measured from the center of the
+planet. As previously mentioned, that translates into a size of roughly 30 meters. So if you were
+ever curious about how many 100-ish-foot squares you'd need to fill a rectangle that fully enclosed
+the entire border of California, then one billion, four-hundred-forty-three million,
+five-hundred-thirty-one thousand, and four-hundred-twenty-six (40,757 times 35,418) is pretty close.
+
+The other units in there are under the "Coordinate System is" section, and are meters relative to
+the [World Geodetic System 1984](https://en.wikipedia.org/wiki/World_Geodetic_System) vertical datum
+(distances from this reference surface in the dataset are within 16 meters of the true distance in
+reality); the very last line is the lowest and highest points in file, which are -130 meters and 4,412 meters respectively, relative to the baseline height
+defined by the WGS84 ellipsoid. If you were to view the file with an image viewer, it would look
+like this:
+
+![the ca_topo image; it's hard to make out details and very dark][small_ca_topo]
+*if you squint, you can kinda see the mountains*
+
+It's almost completely black because the highest possible value an image like that could have for a
+pixel is 65,535[^16-bit-ints], and the highest point in our dataset is only 4,412, which is not that
+much in comparison. Plus, it includes portions of not-California in the height data, and ideally, we
+want those places to not be represented in our dataset; we have a little more processing to do
+before we can use this.
+
+## Cartography is complicated
+
+The first order of business is to mask out everything that's not California, and the first thing I
+needed for that was a [shapefile](https://en.wikipedia.org/wiki/Shapefile) that described the
+California state border. Luckily, [that exact
+thing](https://data.ca.gov/dataset/ca-geographic-boundaries) is publicly available from the state's
+website; thank you, State of California!
+
+There was only one issue: the shapefile was in a different [map
+projection](https://en.wikipedia.org/wiki/Map_projection) than the data in our geotiff file. A "map
+projection" is just the term for how you display a curved, 3D shape (like the border of a state on the
+curved surface of the Earth) on a flat, 2D surface, like a map. If you look at the line in the
+output of `gdalinfo` above that says, `ID["EPSG",4326]`, that is telling us the particular
+projection used. [EPSG 4326](https://en.wikipedia.org/wiki/EPSG_Geodetic_Parameter_Dataset) uses
+latitude and longitude, expressed in degrees, covers the entire Earth including the poles, and
+references the WGS84 ellipsoid as the ground truth.
+
+The shapefile was in a projection called [EPSG
+3857](https://en.wikipedia.org/wiki/Web_Mercator_projection), or "Web Mercator". This is similar to
+EPSG 4326, except instead of using the WGS84 ellipsoid, it pretends the Earth is a perfect
+sphere. It only covers +/- 85-ish degrees of latitude (so not the poles), and it uses meters instead
+of degrees of lat/long. It's popular with online map services (like Google Maps and Open Street
+Maps) for displaying maps, hence the name, "Web Mercator", so you'd probably recognize the shapes of
+things in it.
+
+Once again, there's a [handy GDAL tool](https://gdal.org/programs/gdalwarp.html), `gdalwarp`, which
+is for reprojecting geotiffs. So all we have to do is take our 4326-projected geotiff, use
+`gdalwarp` to project it to 3857/Web Mercator, and then we can use the shapefile to mask off all
+other height data outside the border of California.
+
+It's almost *too* easy.
+
+> gdalwarp -t_srs EPSG:3857 ca_topo.tif ca_topo_mercator.tif
+
+This gives us a 3857-projected file called `ca_topo_mercator.tif`. It still has over a billion
+pixels in it (it's a little bigger overall, but the aspect is
+much wider, with the different projection); scaling it down will be a very last step, since at that
+point, it will no longer be a digital elevation map, it will just be an image. We'll get there,
+just not yet.
+
+Cracking open `gdalinfo`, we get:
+
+``` text
+$ gdalinfo ca_topo_mercator.tif
+Driver: GTiff/GeoTIFF
+Files: ca_topo_mercator.tif
+Size is 36434, 39852
+Coordinate System is:
+PROJCRS["WGS 84 / Pseudo-Mercator",
+ BASEGEOGCRS["WGS 84",
+ ENSEMBLE["World Geodetic System 1984 ensemble",
+ MEMBER["World Geodetic System 1984 (Transit)"],
+ MEMBER["World Geodetic System 1984 (G730)"],
+ MEMBER["World Geodetic System 1984 (G873)"],
+ MEMBER["World Geodetic System 1984 (G1150)"],
+ MEMBER["World Geodetic System 1984 (G1674)"],
+ MEMBER["World Geodetic System 1984 (G1762)"],
+ MEMBER["World Geodetic System 1984 (G2139)"],
+ ELLIPSOID["WGS 84",6378137,298.257223563,
+ LENGTHUNIT["metre",1]],
+ ENSEMBLEACCURACY[2.0]],
+ PRIMEM["Greenwich",0,
+ ANGLEUNIT["degree",0.0174532925199433]],
+ ID["EPSG",4326]],
+ CONVERSION["Popular Visualisation Pseudo-Mercator",
+ METHOD["Popular Visualisation Pseudo Mercator",
+ ID["EPSG",1024]],
+ PARAMETER["Latitude of natural origin",0,
+ ANGLEUNIT["degree",0.0174532925199433],
+ ID["EPSG",8801]],
+ PARAMETER["Longitude of natural origin",0,
+ ANGLEUNIT["degree",0.0174532925199433],
+ ID["EPSG",8802]],
+ PARAMETER["False easting",0,
+ LENGTHUNIT["metre",1],
+ ID["EPSG",8806]],
+ PARAMETER["False northing",0,
+ LENGTHUNIT["metre",1],
+ ID["EPSG",8807]]],
+ CS[Cartesian,2],
+ AXIS["easting (X)",east,
+ ORDER[1],
+ LENGTHUNIT["metre",1]],
+ AXIS["northing (Y)",north,
+ ORDER[2],
+ LENGTHUNIT["metre",1]],
+ USAGE[
+ SCOPE["Web mapping and visualisation."],
+ AREA["World between 85.06°S and 85.06°N."],
+ BBOX[-85.06,-180,85.06,180]],
+ ID["EPSG",3857]]
+Data axis to CRS axis mapping: 1,2
+Origin = (-13927135.110024485737085,5178117.270359318703413)
+Pixel Size = (34.591411839078859,-34.591411839078859)
+Metadata:
+ AREA_OR_POINT=Area
+Image Structure Metadata:
+ INTERLEAVE=BAND
+Corner Coordinates:
+Upper Left (-13927135.110, 5178117.270) (125d 6'34.50"W, 42d 6'51.50"N)
+Lower Left (-13927135.110, 3799580.326) (125d 6'34.50"W, 32d16'33.21"N)
+Upper Right (-12666831.611, 5178117.270) (113d47'17.10"W, 42d 6'51.50"N)
+Lower Right (-12666831.611, 3799580.326) (113d47'17.10"W, 32d16'33.21"N)
+Center (-13296983.361, 4488848.798) (119d26'55.80"W, 37d21'21.69"N)
+Band 1 Block=36434x1 Type=Int16, ColorInterp=Gray
+```
+
+You can see that the `PROJCRS[ID]` value is `"EPSG,3857"`, as expected. The "pixel size" is
+"34.591411...." and the "lengthunit" is "metre". But the number of pixels is different, and the
+shape is different, yet the coordinates of the bounding corners are the same as the original file's
+(the latitude and longitude given as the second tuple). This is all from the Web Mercator's different
+projection causing the aspect ratio to stretch horizontally, but it still represents the same area
+of the planet.
+
+## The one custom script
+
+Now that we had our geotiff in the right projection, the next step was use the shapefile to mask out
+the California border in it. Here is where GDAL failed me, and looking around now as I
+write this, I still can't find a specific GDAL tool for doing it. Given how useful I found all the
+other tools, I can't really complain, so I won't! It wasn't that hard to write something that would
+do it with other open source tools; I didn't even bother checking this into a git repo or anything:
+
+``` python
+#!/usr/bin/env python3
+
+import fiona # for reading the shapefile
+import rasterio # for working with the geotiff
+import rasterio.mask as rmask
+
+import sys
+
+def main():
+ tif = sys.argv[1]
+ msk = sys.argv[2]
+ out = sys.argv[3]
+
+ print("input: {tif}\nmask: {msk}\noutput: {out}".format(tif=tif, msk=msk, out=out))
+ if input("Enter 'y' to continue: ").lower() != 'y': # double-check I don't stomp something I wanted to keep
+ print("See ya.")
+ return
+
+ with fiona.open(msk, "r") as shapefile:
+ shapes = [feature["geometry"] for feature in shapefile]
+
+ with rasterio.open(tif) as in_tif:
+ out_image, out_xform = rmask.mask(in_tif, shapes, filled=True, crop=True)
+ out_meta = in_tif.meta
+ out_meta.update({"driver": "GTiff",
+ "height": out_image.shape[1],
+ "width": out_image.shape[2],
+ "transform": out_xform})
+ for k, v in out_meta.items():
+ print("{}: {}".format(k, v)) # just outta curiosity
+
+ with rasterio.open(out, "w", **out_meta) as dest:
+ dest.write(out_image)
+
+ print("Wrote masked tif to {}".format(out))
+
+ return
+
+if __name__ == "__main__":
+ main()
+```
+
+I include that just in case anyone else ever needs to do this, and doesn't find one of the hundreds
+of other examples out there already. This one is nice because you don't need to pre-process the
+shapefile into [GeoJSON](https://geojson.org/) or anything, the
+[Fiona](https://pypi.org/project/Fiona/1.4.2/) package handles things like that transparently for
+you, but don't think this is great Python or anything; it's the dumbest, quickest thing I could crap
+out to do the task I needed to be done[^the-real-treasure-is-the-gd-treasure].
+
+After running that script, I had a Web Mercator-projected geotiff that included data only for places
+inside the state border of California. It was still enormous; the mask didn't change the shape and
+you can't have non-rectangular images anyway, but at this point, I had the final desired
+dataset. It was time to turn it into a heightmap that we could use to make a mesh.
+
+## A usable heightmap
+
+I've been trying to be careful about referring to the image file as a "dataset" or "geotiff", vs. a
+"heightmap". A geotiff file is not a regular image file, it includes particular metadata and data
+that is meant to be interpreted as a real map of the land; each pixel in it says something about an exact,
+actual location in the real world. In our geotiff, a mountain would have to be more than twelve
+miles high before it appeared as bright white[^zero-pixel-value].
+
+A "heightmap" is an image file, like a geotiff, where each pixel's monochromatic intensity is meant
+to represent height above some lowest plane. The difference is that the height values are *normalized*
+so that the lowest height is 0, and the highest is the maximum possible value in the format's value
+range. For geotiff digital elevation maps, which use 16-bit numbers as previously mentioned, that
+maximum possible value is 65,535. But unlike a geotiff, a generic heightmap has no exact
+correspondence with anything else; it's not necessarily an accurate dataset, and won't include the
+GIS stuff like what projection it is, what the coordinate bounding boxes are, etc. But it *is*
+useful for turning into a mesh.
+
+And here I get to the [final GDAL tool](https://gdal.org/programs/gdal_translate.html) I used,
+`gdal_translate`. This is something that can read in a geotiff, and write out a different image
+format. When in doubt, [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) is fine, I
+always say. It's a simple format that nearly everything can read, and is compressed so it should be
+a much smaller file on disk, even if it's the same number of pixels. Smaller file size is always
+easier.
+
+> `gdal_translate -of PNG -ot UInt16 -scale -130 4412 0 65535 masked_ca_topo.tif heightmap.png`
+
+Like we saw earlier, the lowest point we had in our data was -130
+meters, and the highest was 4,412. The `-scale -130 4412 0 65535` arguments are saying, "anything
+with a height of -130 should be totally dark, and anything with a height of 4,412 should be as
+bright as possible, and anything in-between should be set proportionately." This is a linear
+mapping, and preserves the relationships between vertical features (that is, if something is twice
+as tall as another thing, that will still be true after being scaled), so in a sense, it's
+"accurate", but would it be good, was the question (*note: more foreshadowing*).
+
+Once I had the PNG file, I used the [ImageMagick](https://imagemagick.org/script/convert.php) `convert`
+command to resize the file down to a reasonable size. Finally, I had something I could use to make a
+mesh:
+
+![the heightmap made by doing a linear scale of height to brightness][scaled_heightmap]
+
+Pretty cool, right? I thought so! The detail is pretty great; that bright spot near the top is
+[Mt. Shasta](https://en.wikipedia.org/wiki/Mount_Shasta), for example;
+[Mt. Whitney](https://en.wikipedia.org/wiki/Mount_Whitney) is slightly taller, but not by much, and
+is part of a range so it doesn't stand out the way Shasta does. It was time to start making some 3D
+geometry with the heightmap[^time-to-mesh]!
+
+# A mesh is born
+
+My next step was to figure out how exactly to turn that heightmap into a mesh. Some searching
+assured me that [Blender](https://www.blender.org/), a free and open source 3D modeling package that
+I'd dabbled with before, would work well. For example, here's a pretty high-level walk-through of
+[how to use a heightmap to displace a mesh
+plane](https://alanedwardes.com/blog/posts/create-meshes-from-height-maps-using-blender/), which is
+almost exactly what I first wanted to do. Before too long, I had something that looked like this:
+
+![a very pointy california topo][pointy-california]
+
+At first glance, it looks OK, but there's so. much. detail. And it's very, very pointy; it just
+looks jagged. Check out this close-up detail of Mt. Shasta:
+
+![a very pointy mt shasta][pointy-shasta]
+*witch's hat-assed mountain*
+
+You can tell it wounldn't be nice to touch, and being able to run your fingers along the shape
+was a huge part of the appeal of having the physical object.
+
+## Back to the realm of images
+
+Given that it seemed like there were at least a couple semi-related problems from too much detail,
+my first instinct was to blur the heightmap, and then reduce the size of it. I used the ImageMagick
+`convert` command again [to blur the image](https://legacy.imagemagick.org/Usage/blur/) a couple
+rounds, and then resized it down:
+
+![first attempt at blurring the heightmap][blurry-linear-hm]
+
+A little better, but still not great. A few more rounds of blurring and shrinking got me
+this:
+
+![second round of blurring the heightmap][blurry-linear-hm-smaller]
+
+With that version, I was able to produce some reasonable-looking geometry in Blender:
+
+![a slightly smoother mesh][smoother-california-mesh]
+
+Or so I thought.
+
+It may have been smoother, it was still very pointy. A lot of the high-frequency detail has been removed, which
+means it's not rough and jagged, but Shasta still looks ridiculous.
+
+## A matter of scale
+
+The problem was that I was doing a linear scaling of the height of features in the data, and the
+required factors were so enormous that it distorted the geometry in an ugly way.
+
+The State of California is very large, but for the sake of argument, let's pretend it's exactly 700
+miles tall, from the southern tip to the northern border's latitude, going straight north; the real
+length is close to that. Also for the sake of argument, let's say that the tallest mountain is 3
+miles tall; the actual height is a little less than that, but that's OK, the argument holds more
+strongly at lower height. That means the ratio of height to length is 3/700, or 0.0043-ish.
+
+If you had a physically accurate topographic carving of California that was a foot long, the tallest
+peak on the carving would be 0.0043 feet high, which is about 1/20th of an inch, or about 1.3
+millimeters. You'd probably be able to see and feel where Shasta was, and see that there was a faint
+line from the Sierra Nevadas, but that would be it. That's why it's so hard to see the details in
+the raw elevation data geotiff.
+
+In order to be able to see any detail, and to meet expectations about what a topographic carving is
+supposed to look like, the height of the highest peaks needs to be scaled up by something like
+10-20x. My problem was that I was doing a linear scale; I was making *everything* 10-20x taller than
+it "should" be, which was causing everything to look stretched and weird.
+
+And even with that amount of exaggeration, some low-elevation features were still not showing
+up. For example, [Sutter Buttes, a 2,000-foot tall mound in the Sacramento
+Valley](https://en.wikipedia.org/wiki/Sutter_Buttes), which is faintly visible in the heightmap, is
+almost not there in the resulting mesh. It's about 1/7th the height of Shasta, which is not all that
+much, when Shasta was represented by something 0.75 inches tall.
+
+What I really needed was some non-linear way to scale the height, some way to exaggerate lower
+altitudes more than higher ones. The highest points should stay as high as they were; they determine
+the ultimate overall height, but lower points should be given a relative boost. An easy way to do
+this is to take some fractional root (raise a number to a power between 0.0 and 1.0) of the linear
+scaling factor, and use that new value instead. For example, the graph of *x* raised to the
+0.41th[^zero-forty-oneth] power looks like this:
+
+![y = x^0.41 between 0 and 1][exp-plot]
+
+Notice how values *at* 0 and 1 are the same as they would be with linear scaling, values *near* 0
+rapidly get scaled upward, and by the time you get near 1, it looks almost linear again. The linear
+scaling function we'd initially used would just look like a straight line from the lower left corner
+to the upper right.
+
+Luckily, `gdal_translate` has an option to do this kind of scaling, so it was a quick
+
+> `gdal_translate -of PNG -ot UInt16 -scale -130 4412 0 65535 -exponent 0.41 ca_topo.tif
+exponentially_scaled_heightmap.png`
+
+and a couple rounds of blurring, and I had the following heightmap:
+
+![a non-linearly scaled heightmap][lo-rez_exp_blurred]
+
+which resulted in a mesh that looked something like this inside Blender:
+
+![3D viewport in Blender showing a topo-displaced mesh that looks like
+California][exp-scaled-blending]
+
+Doesn't that look nicer? Notice how a bunch of things that were nearly invisible before, like Sutter
+Buttes, are easily visible. Check out the [Channel
+Islands](https://en.wikipedia.org/wiki/Channel_Islands_(California)) now plain as day! I was feeling
+pretty good about having this whole thing wrapped up shortly, only a little late for the birthday.
+
+# A dark age
+
+What followed was two frustrating weeks attempting to get a manifold mesh out of Blender that was
+small enough, by which I mean number of vertices and edges, so that
+[FreeCAD](https://www.freecadweb.org/) could turn it into an STP file. Unfortunately, FreeCAD is not
+a good tool for doing fancy things with meshes, like creating them from a heightmap, so I had to use
+two different tools.
+
+This also meant that I would run into surprising limits when going between them. Let me explain. I'd
+get a mesh in Blender, export it to a neutral mesh format like
+[OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) that both programs understand well, and it
+would be a 60 megabyte file. My computer has 32 **giga**bytes, more than 500 times more memory than
+that, so you'd think it would not be a problem.
+
+The act of asking FreeCAD to import that OBJ file as a *mesh*, and not even as a solid body, caused
+the memory use to go to 21 gigabytes. This is a lot, but the computer still had plenty of room left
+in memory for things like "responding to the keyboard and mouse" or "redrawing the
+screen". Everything at this point is still perfectly usable.
+
+When I attempted to convert that mesh into a solid body, though, memory use ballooned up to
+encompass all available RAM, and my system immediately came to a nearly imperceptible crawl until my
+frantic `ctrl-c`s were finally registered by the [signal
+handlers](https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html) in FreeCAD
+before I could use it again. This happened *a lot*. At last, the prophecy
+had come to pass.
+
+I went through many rounds of attempting to clean up the mesh and reduce its complexity, but I don't
+have many notes or intermediate artifacts from this time. A lot of that was being a beginner at both
+Blender **and** FreeCAD, though there's so much educational material that I was rarely held back by
+not knowing how to do a particular thing inside each program. A lot was inexperience in the domain;
+I did not know how much detail was essential, and I did not have a lot of experience with digital
+modeling in the first place. The workflow was very manual, and cycles were fairly long, which made
+it hard to try a bunch of things in quick succession as experiments. All those things and more
+conspired to make this portion of the process a total slog with very little to show off.
+
+# Test prints
+
+Eventually, after a couple weeks of trying and failing to get something into FreeCAD that I could then
+work with (like merging it with a thick base and trimming that base to follow the shape of the
+state), I had had enough. I was just going to send the shop an STL file and forget about trying to
+get an STP file. I have some notes from then, right after I'd started my first test print:
+
+> I'm finally printing something out. I've given up on converting it into [something CAD-friendly];
+> it seems this is a Hard Problem, but I'm not sure why. My goal with doing that was to give a
+> CAD-friendly file to a local CNC milling shop, per their request, since when I suggested a
+> mesh-based file (STL), the guy was like "I can't do much manipulation with that to make it more
+> manufacturable, so a real CAD file would be best".
+>
+> But at least with an STL file, I can print it myself. So that's going now, we'll see how it turns
+> out in no less than eight hours.
+>
+> I haven't really done anything else with my computer besides this for a while.
+
+When that print was done, here's what it looked like:
+
+![a piece of literal dogshit][crappy_test_print]
+*don't look at me, I'm hideous*
+
+In case you were not revolted enough, then please allow me to direct your gaze toward this eldritch
+abomination:
+
+![close-up of extremely bad print results][crappy-close-up]
+*what did I just say about looking at me*
+
+As bad as it looked, it felt even worse to touch. Setting aside the hideous base with its weird
+visual artifacts due to those areas not being a single flat polygon, but rather several polygons
+that were not parallel, there was still just too much high-frequency detail in the terrain, and it
+was a total mismatch with the 3D printed medium.
+
+The real thing was going to be carved out of wood by a [CNC
+mill](https://all3dp.com/2/what-is-cnc-milling-simply-explained/), which uses a drill-like component
+to carve away pieces of the material you're working with. This means that there's a tiny spinning
+bit with a definite, finite size, and any detail in the model smaller than the end of that spinning
+bit would likely be impossible to carve with it. This meant that all that high-frequency detail was
+not only ugly, it was also completely unnecessary.
+
+## Just try harder
+
+I was very eager to get something into the CNC shop's hands at this point, but I also knew that this
+model was not acceptable. So, I resolved to brutally simplify the geometry until I got something
+that was workable inside FreeCAD.
+
+First off, I made the heightmap even smaller, only 500 pixels wide. Fewer pixels means fewer details
+for turning into a displacement map for a mesh! I also removed the Channel Islands from the
+heightmap, resulting in this final mesh displacement input:
+
+![it's the final heightmap][final-heightmap]
+*it's the final heightmap (doot-doot-doot-doot,
+doot-doot-doot-doot-doot)*
+
+Inside Blender, I'd gotten quite proficient at running through the steps to generate a mesh from a
+heightmap, and once I'd done that, I went through several rounds of [mesh
+simplification](https://graphics.stanford.edu/courses/cs468-10-fall/LectureSlides/08_Simplification.pdf);
+the geometry was practically homeopathic.
+
+![the final model in blender][final-model]
+*by the principles of homeopathy, the fewer the vertices, the more potent the mesh*
+
+Check out this close-up of Mt Shasta:
+
+![close-up of Shasta in the final model][final-shasta]
+*a chonkier, less lonesome Mt Shasta*
+
+Present, but not obnoxious. I printed out a second test print to make sure it looked as good in
+physical reality:
+
+![the final test print of the final model][final-print]
+
+Verdict: yes. If you want, you can visit
+ and download the 3D printer file to
+print it yourself at home. If you don't have a 3D printer, you can still look at and interact with a
+3D model of it in the browser on that site, so it's still kind of neat. A couple different strangers
+uploaded pictures of their prints of it, which I thought was cool!
+
+I brought the mesh into FreeCAD and finally was able to create the STP[^fancy-iges] file the shop had
+asked for, a mere twenty-five days after I'd last spoken with them.
+
+# Final cut
+
+I emailed the file to the shop, and said,
+
+> As modeled, there's probably more high-frequency detail in the mountains than is necessary, as I'm
+> going for something that feels nice to the touch so smoother is better. It's also modeled at a
+> slightly larger scale than necessary, though not too far off (it's 500x577mm, and I'm interested
+> in the 400-500mm range for width; the relief height is in the 20-30mm range depending on scale). I
+> was imagining it would be carved with contour cuts in some thick nice ply, though I'm happy to
+> hear better ideas; I have literally no experience with making something like this.
+
+The shop came back with,
+
+> I can’t smooth out the cuts, I can only cut what is there. That being said, if I use a rounded cutter, it will round out the valleys but not the peaks as it won’t go into areas that it can’t reach.
+>
+> Hope that makes sense.
+>
+> Let me know if this will work for you or not. If you think it will, I will try to program the toolpaths and see what it will look like.
+
+I definitely didn't want to lose the sharp seams in the bottoms of the valleys!
+
+> Me: I guess what I was really saying is that if some detail is lost due to using a larger cutting
+> head that's probably fine. I wouldn't necessarily want the valleys to be made more concave than
+> they already are, though. Does that make sense?
+>
+> Shop: Yes, that makes sense. I can use a Vee cutter and it will cut the sharp edges in the
+> valleys."
+
+It felt nice to be understood! Next came the issue of cost:
+
+> I ran the numbers on both sizes using a .01” step-over cut, meaning that is how far apart the
+> finish cuts will be from each other.
+>
+> You will probably see some tool marks depending on what type of material is used.
+>
+> The larger one is coming in at around $850.00 and the 12” one at $350.00.
+>
+> I can go tighter, say .005” step-over and it will probably not show many marks but I won’t know
+> until I run it.
+>
+> If I do that it will double the cut time so close to doubling the price.
+
+One of the things that my wife had said she wanted to do with the carving of California was sand and
+finish it herself, so the coarser 0.01-inch step-over cut was not really a problem. Even the
+0.005-inch cut would still require a final sanding before staining or sealing.
+
+The "larger one" the shop referred to was for a 20-inch wide carving, which would be way too huge
+anyway; 12 inches was fine. Still, $350 was at the top of what I had hoped/expected to spend. I
+hoped it was worth it!
+
+After a few more back-and-forths and days, I got a message from the shop saying it was ready. They
+also said,
+
+> I decided to run these with half the original step-over, which means it takes twice as long but
+> the finish is almost smooth. I think you will be pleased with it.
+
+Whoa! This meant he had used the 0.005-inch cutting resolution, and the job had taken twice as long
+as originally quoted. Like the [kind and generous tailor from *The Hudsucker
+Proxy*](https://getyarn.io/yarn-clip/0f78e11f-df94-42e4-8bdf-b11c39326f7c), he had given me the
+double-stitch anyway, even though I had insisted that single stitch was fine. I was very excited and
+grateful, and couldn't wait to see it.
+
+## Pics or it didn't happen
+
+When I got there, it was almost exactly what I had imagined and hoped it would be. Obviously, you've
+seen the photo at the top of the page, but please enjoy this CNC-carved topographic California porn.
+
+![portrait of the whole state][wood-portrait]
+*some nice soft lighting*
+
+![our old friend, the Sutter Buttes][wood-buttes]
+*sutter buttes, we meet again*
+
+![down low view, like the shot from Blender][wood-blender]
+*recognize this angle, from blender?*
+
+![close up of Shasta][wood-shasta]
+*lookin' good, shasta*
+
+I wasn't the only one pleased with it; my wife was delighted when she saw it.
+
+MISSION ACCOMPLISHED, HAPPY *belated* BIRTHDAY!
+
+# Thank yous
+
+Obviously, I have tons of people to thank for their help with this, either directly or
+indirectly. First and foremost, my wife, for everything, but especially for the inspiration and also
+patience with me during this process.
+
+A close second for this project goes to Steve at [Triumph CNC](https://www.triumphcnc.com/). He
+asked me what I was going to do with it, and when I said give it to my wife as a gift, he said, "Oh,
+that's great! I feel even better about using the smaller step-over now." If you need some CNC
+milling done in Los Angeles, maybe give them a call!
+
+Along the way during this journey I got a lot of feedback and suggestions from friends and
+colleagues, so thank you, 'rades[^short-for-comrades]!
+
+Of course, this would all have been unthinkably difficult not so long ago, but thanks to things like
+NASA's missions and public GIS datasets, almost anyone can do something like this.
+
+And not just public, government data and organizations, but private, passion-driven free software
+projects like Blender and FreeCAD that rival functionality found in multi-thousand-dollar commercial
+packages. I'm in awe of their accomplishments; they are true wonders of the modern world.
+
+# Things I learned, and some lessons
+
+I said early on that I knew basically nothing about any of this, and that was true. I had had some
+earlier casual experience with both Blender and FreeCAD, and many, many years ago I had taken a
+semester of engineering drafting my first year of college. But I knew basically nothing about GIS,
+about the different map projections, about shapefiles, about any of the tools or jargon. Likewise,
+I have no experience or instruction in any kind of CNC milling; my scant 3D printing experience
+doesn't really apply.
+
+This article is as close as I could get to serializing nearly everything I had to learn and do to
+create that carving.
+
+And at the time it was happening, it didn't feel like I was retaining all of it, or that I really,
+truly understood everything I had done; I was hurrying as fast as I could toward a particular
+goal. But in the course of writing this, I was basically retracing my steps, and found that I really
+did have a pretty good handle on it. One of my favorite things to do is learn stuff, so this was a
+great outcome for me!
+
+If I were to do this again, or if I were starting for the first time with the benefit of someone
+else's experience, there are obviously a few things I would do differently. First off, I'd see if I
+could find a lower-resolution dataset. One arc second is way overkill; at the scale of a topo
+carving that you can hold in your hands, a resolution of several arc *minutes* (one arc minute is
+one [nautical mile](https://en.wikipedia.org/wiki/Nautical_mile), which is about 1.1 regular
+(terrestrial?) miles) would probably be enough.
+
+I'd also use the USGS [national map downloader](https://apps.nationalmap.gov/downloader/) site to
+get just the California data; you can upload a shapefile and it'll give you back a masked
+geotiff. If I had started from that, it would have shaved at least two weeks off the time it took me
+to make the thing; I could have jumped immediately into being frustrated in Blender and FreeCAD.
+
+Speaking of, I wish I could give some guidance on effectively using Blender and FreeCAD, but that's
+a journey only you can plot. That's probably not true, but I still feel like a doofus in those
+tools, so I don't feel like it's worth anyone's time to hear from me about them. Good luck in your
+quest!
+
+---
+
+[wood-portrait]: wood-full-portrait.jpg "portrait shot of the whole carving"
+
+[wood-buttes]: wood_sutter_buttes.jpg "our old friend, the Sutter Buttes"
+
+[wood-blender]: wooden_like_blender.jpg "down low view, like the shot from Blender"
+
+[wood-shasta]: wooden_shasta_close-up.jpg "close up of Shasta"
+
+[final-print]: final_printed.jpg "the final test print of the final model"
+
+[final-shasta]: final_shasta.png "close-up of Shasta in the final model"
+
+[final-model]: final_ca_topo_blend.png "the final model in Blender"
+
+[final-heightmap]: final_heightmap.png "it's the final heightmap (sick synthesizer riff blasts)"
+
+[crappy-close-up]: crappy_test_print_close_up.jpg "close-up of extremely bad print results"
+
+[main_image]: wood_ca_on_table.jpg "A plywood slab carved with CNC into a topographic representation of California"
+
+[programmers_creed]: /images/programmers_creed.jpg "jfk overlaid with the programmer's creed: we do these things not because they are easy, but because we thought they were going to be easy"
+
+[meshy-cube]: meshy-cube.png "an overly-complicated mesh of a cube"
+
+[geotiff-files]: geotiff-files.png "the input geotiff files and the resulting 'ca_topo.tif' output file, which is 2.7 gigabytes"
+
+[small_ca_topo]: small_ca_topo.png "a 'raw' heightmap of california and parts of nevada, arizona, and mexico"
+
+[scaled_heightmap]: scaled_heightmap.png "the heightmap made by doing a linear mapping of height to brightness"
+
+[pointy-california]: pointy_california_blending.png "the displaced mesh plane made from the first heightmap"
+
+[pointy-shasta]: pointy_shasta_close-up.png "a very pointy mt shasta"
+
+[blurry-linear-hm]: blurred_scaled_hm_3.png "first attempt at blurred heightmap"
+
+[blurry-linear-hm-smaller]: lo-rez_blurred_hm3.png "second round of blurring the heightmap"
+
+[smoother-california-mesh]: blending_california.png "slightly smoother mesh in blender"
+
+[exp-plot]: exponential_plot.png "a graph of the function `y = x^0.41` between 0 and 1"
+
+[lo-rez_exp_blurred]: lo-rez_exp_blurred.png "nearly final heightmap, using exponential scaling to exaggerate lower altitudes"
+
+[exp-scaled-blending]: non-linear_scaling_of_ca_height_data.png "You can see how Shasta doesn't stick out so much when the other hills are brought up a bit relatively speaking"
+
+[crappy_test_print]: ca_topo_crappy_test_print.png "a piece of literal dogshit"
+
+[^introspection]: The conclusion upon examination was, "I just wasn't thinking".
+
+[^math-computers]: I'm pretty sure this is more "represent shapes with math" than with a computer, but
+the computer is helping us do the math and it's more relatable.
+
+[^manifold_holes]: I *think* you could also have a 2D sheet with a hole cut out of it represented by
+a mesh that is manifold, as long as the connectivity was correct in terms of how many shared edges
+and vertices there were (though this would not be a valid STL file). Imagine a cloth sheet with a
+hole cut out in the middle, and the edge of the hole hemmed or otherwise "sealed", which is then a
+*manifold boundary*. See [this powerpoint
+deck](https://pages.mtu.edu/~shene/COURSES/cs3621/SLIDES/Mesh.pdf) for a pretty math-y overview of
+"mesh basics" (but not really that basic, that's just academics trolling us, don't let it bother
+you). If I'm wrong about a 2D sheet with a hole being possibly manifold, I invite correction!
+
+[^chekhovs-ram]: A textbook example of *Chekhov's Scarce Computational Resource*.
+
+[^wgs-ellipsoid]: Technically, it's an arc along the WGS84 ellipsoid, which is a perfectly smooth
+*smushed* sphere, which more closely matches the real shape of the Earth vs. a perfectly round sphere.
+
+[^16-bit-ints]: Each pixel is 16 bits, so the possible values are from 0 to 2^16 - 1. 2^16 is 65536,
+so there you go.
+
+[^the-real-treasure-is-the-gd-treasure]: A friend posited at one point that my circuitous journey to
+the end product was the point, but I assured him that every step I took was trying to get to the end
+product as quickly and straightforwardly as possible. Still, I did in fact wind up learning a whole
+shitload of stuff, which is nice, I GUESS.
+
+[^zero-pixel-value]: I'm not actually sure what the "0" altitude pixel value is. It can't actually
+be 0, because the numbers in the file can't be negative, and there are deep valleys on the earth's
+surface. But it's clearly not that high a value, otherwise, when you viewed the geotiff as an image,
+it would be closer to white or gray than black.
+
+[^time-to-mesh]: Based on the timestamps of the files in the directory where I was working on this
+project, it took about ten days from the time I first downloaded a geotiff dataset to having the
+heightmap shown above, so you can imagine all the dead-ends I went down and did not share in this
+write-up.
+
+[^zero-forty-oneth]: I think this was just the first fractional value that I tried, and it was fine.
+
+[^fancy-iges]: I actually produced an [IGES](https://en.wikipedia.org/wiki/IGES) file; STP is basically
+fancy IGES, and my model didn't include the extra info in STP files like material and color anyway.
+
+[^short-for-comrades]: pronounced "rads", and is short for "comrades".
diff --git a/content/sundries/a-thoroughly-digital-artifact/lo-rez_blurred_hm3.png b/content/sundries/a-thoroughly-digital-artifact/lo-rez_blurred_hm3.png
new file mode 100644
index 0000000..a3adb8b
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/lo-rez_blurred_hm3.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/lo-rez_exp_blurred.png b/content/sundries/a-thoroughly-digital-artifact/lo-rez_exp_blurred.png
new file mode 100644
index 0000000..b52e50d
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/lo-rez_exp_blurred.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/meshy-cube.png b/content/sundries/a-thoroughly-digital-artifact/meshy-cube.png
new file mode 100644
index 0000000..3e145ff
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/meshy-cube.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/non-linear_scaling_of_ca_height_data.png b/content/sundries/a-thoroughly-digital-artifact/non-linear_scaling_of_ca_height_data.png
new file mode 100644
index 0000000..ad14fa0
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/non-linear_scaling_of_ca_height_data.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/notes.txt b/content/sundries/a-thoroughly-digital-artifact/notes.txt
new file mode 100644
index 0000000..f284c76
--- /dev/null
+++ b/content/sundries/a-thoroughly-digital-artifact/notes.txt
@@ -0,0 +1,143 @@
+inital comms with CNC shop:
+------------------------------
+Me: "project description": A relief map of California, carved from wood. Height exaggerated enough
+to visibly discern the Santa Monica mountains. I can provide an STL file if needed.
+
+Shop: STL can work but I can’t manipulate it, which could save some money. If possible can it be
+exported to an .igs or .iges or .stp format?
+
+Me: Yeah, STP should be no problem. Can you give a rough estimate of the cost for 1x2-foot relief carving?
+
+Shop: Without seeing the drawings, I can’t give even a close price but in the past they range from a
+few hundred dollars to several thousand dollars.
+
+Me: That's totally fair! I'll get you some files in a few days.
+------------------------------
+
+next comms with shop three weeks later:
+------------------------------
+
+Hi Steve, I'm sorry for taking so long to get back to you! I had a harder time producing the IGES
+file than I thought I would, but I think this should be OK:
+
+(snip url to file)
+
+It's 51 megabytes, otherwise I'd attach here.
+
+As modeled, there's probably more high-frequency detail in the mountains than is necessary, as I'm
+going for something that feels nice to the touch so smoother is better. It's also modeled at a
+slightly larger scale than necessary, though not too far off (it's 500x577mm, and I'm interested in
+the 400-500mm range for width; the relief height is in the 20-30mm range depending on scale). I was
+imagining it would be carved with contour cuts in some thick nice ply, though I'm happy to hear
+better ideas; I have literally no experience with making something like this.
+
+(NOTE: go back to email thread and summarize the back-and-forth over tooling)
+---------------------------------
+
+Note that the shop did the extra work anyway just because they were nice, and that he was glad when
+I told him it was a gift for my wife.
+
+
+Zulip dump from 10 days after initial contact:
+-----------------------------------
+It IS Mt. Shasta!
+
+After I made the mosaic out of the tiles I downloaded to cover the area, I masked it with an outline
+of the state that I downloaded from a California gov geo site, then used a program called
+gdal_translate to turn the image, a "geotiff" file with height data encoded, into that heightmap png
+with the lowest value mapped to 0 and the highest to maxint.
+
+I also had to reproject the geotiff with the height data into the same coordinate system as the
+state outline was in. The height data was in a system using lat/long called "EPSG:4326", while the
+state outline was made from line segments with 2d vertices in a projected coordinate system called
+"EPSG:3857" with units of "meters". 3857 is "web Mercator", and is the coordinate system used by
+Google and Open Street Map for their map tiles and other shapes.
+
+It may or may not be surprising that cartography is very complicated!
+
+My next step is to turn this heightmap into solid geometry that I can 3d print and/or send to a
+local CNC shop to have them carve a relief of California out of wood, which is close to the final
+step of producing an artifact as a present for my partner.
+
+There are a bunch of python packages for working in this domain, but they're all just wrappers
+around various GDAL libraries or tools.
+
+The raw topo data I got from
+https://portal.opentopography.org/raster?opentopoID=OTSRTM.082015.4326.1 (that was the epsg 4326
+geocoded tiff; actually, several of them because you can only download up to 450km^2 at a time,
+hence having to mosaic them with the gdal_merge.py command (the '.py' is in the name of the command
+that gets installed when you do apt install gdal-bin)), then use gdalwarp to re-project to 3857,
+then I had to write a python program to mask it for some reason, then gdal_translate (no .py on that
+one, but they're all just python scripts) to convert to the png heightmap. I'm leaving out a couple
+details in the workflow, but that's the main shape of it.
+
+OK, actually, now that all that context is established, here's the actual command that produced that
+file from the geocoded tiff:
+
+gdal_translate -of PNG -ot UInt16 -scale -130 4412 0 65535 cropped_ca_topo.tif heightmap_cropped.png
+
+and then I used convert (the imagemagick program) to scale the png from 33,311x38,434 to
+2,000x2,2308 pixels.
+
+the -scale -130 4412 0 65535 is mapping the height data min/max to the png whiteness in the output
+file.
+---------------------------------------
+
+Zulip musings from a few days after that, still working on the heightmap:
+---------------------------------------
+(re: non-linear scaling of height to reduce pointiness)
+ok, it was easier than I thought it would be. gdal_translate has a -exponent flag you can use with
+-scale, so I remade the heightmap with an exponential scaling, using 0.41 as the exponent.
+
+funny enough, I'm still working on this, since even when I drastically scale the size of the mesh
+down in Blender (which I export to OBJ for import by FreeCAD), doing anything like modelling (eg,
+extruding downward or joining with a solid base, or cutting the shape so it's CA-shaped and not a
+rectangle) requires tens of gigabytes of resident memory and I keep having to kill the program and
+start over.
+
+a 60-megabyte OBJ file turns into 21 GB of resident data in the modelling software.
+
+I have 32GB of RAM installed
+
+that 21GB expands to 30 when I try manipulating it
+------------------------------------------
+
+Zulip from two weeks later (July 7):
+--------------------------------------
+
+Two weeks later I'm finally printing something out. I've given up on converting it into a parametric
+CAD-like object; it seems this is a Hard Problem, but I'm not sure why. My goal with doing that was
+to give a parametric CAD file to a local CNC milling shop, per their request, since when I suggested
+a mesh-based file (STL), the guy was like "I can't do much manipulation with that to make it more
+manufacturable, so a real CAD file would be best".
+
+But at least with an STL file, I can print it myself. So that's going now, we'll see how it turns
+out in no less than eight hours.
+
+I haven't really done anything else with my computer besides this for a while.
+
+(next day)
+ok, I got something printed out, but I'm not super stoked on it. Also, I'm still chasing the elusive
+dream of turning this into a parametric solid for easier CNCing. Vape pen for scale:
+(insert shitty print photo)
+
+(next day after that, the 9th)
+I've finally "finished": I've created a mesh that has no missing faces, is not too crazy, and can be
+converted into a parametric solid, and sent that off to a local CNC shop for a quote on having it
+routed out of wood. I'll also do another 3D print, since the base is now a larger version of the
+coastline instead of a rectangle, and the high frequency detail is a little diminished.
+
+----------------------------------------
+
+Links:
+
+https://data.ca.gov/
+
+https://portal.opentopography.org/raster?opentopoID=OTSRTM.082015.4326.1
+
+https://www.printables.com/model/240867-topographic-california
+
+https://touchterrain.geol.iastate.edu/
+
+https://en.wikipedia.org/wiki/Shuttle_Radar_Topography_Mission
+
diff --git a/content/sundries/a-thoroughly-digital-artifact/plot.gnuplot b/content/sundries/a-thoroughly-digital-artifact/plot.gnuplot
new file mode 100644
index 0000000..8dd7638
--- /dev/null
+++ b/content/sundries/a-thoroughly-digital-artifact/plot.gnuplot
@@ -0,0 +1,9 @@
+f(x) = x**0.41
+set terminal png size 600,600
+set output 'exp_scaling.png'
+set grid
+
+set xrange [0:1]
+set yrange [0:1]
+
+plot f(x) lw 3 notitle
diff --git a/content/sundries/a-thoroughly-digital-artifact/pointy_california_blending.png b/content/sundries/a-thoroughly-digital-artifact/pointy_california_blending.png
new file mode 100644
index 0000000..c1ae135
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/pointy_california_blending.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/pointy_shasta_close-up.png b/content/sundries/a-thoroughly-digital-artifact/pointy_shasta_close-up.png
new file mode 100644
index 0000000..f390864
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/pointy_shasta_close-up.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/scaled_heightmap.png b/content/sundries/a-thoroughly-digital-artifact/scaled_heightmap.png
new file mode 100644
index 0000000..7520a48
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/scaled_heightmap.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/small_ca_topo.png b/content/sundries/a-thoroughly-digital-artifact/small_ca_topo.png
new file mode 100644
index 0000000..bf06855
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/small_ca_topo.png differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/wood-full-portrait.jpg b/content/sundries/a-thoroughly-digital-artifact/wood-full-portrait.jpg
new file mode 100644
index 0000000..ded9fae
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/wood-full-portrait.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/wood_ca_on_table.jpg b/content/sundries/a-thoroughly-digital-artifact/wood_ca_on_table.jpg
new file mode 100644
index 0000000..8dccf10
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/wood_ca_on_table.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/wood_sutter_buttes.jpg b/content/sundries/a-thoroughly-digital-artifact/wood_sutter_buttes.jpg
new file mode 100644
index 0000000..839a55b
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/wood_sutter_buttes.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/wooden_like_blender.jpg b/content/sundries/a-thoroughly-digital-artifact/wooden_like_blender.jpg
new file mode 100644
index 0000000..ef61b8c
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/wooden_like_blender.jpg differ
diff --git a/content/sundries/a-thoroughly-digital-artifact/wooden_shasta_close-up.jpg b/content/sundries/a-thoroughly-digital-artifact/wooden_shasta_close-up.jpg
new file mode 100644
index 0000000..5fed889
Binary files /dev/null and b/content/sundries/a-thoroughly-digital-artifact/wooden_shasta_close-up.jpg differ
diff --git a/content/sundries/a-very-digital-artifact/index.md b/content/sundries/a-very-digital-artifact/index.md
deleted file mode 100644
index b16ed8b..0000000
--- a/content/sundries/a-very-digital-artifact/index.md
+++ /dev/null
@@ -1,9 +0,0 @@
-+++
-title = "A Very Digital Artifact"
-slug = "a-very-digital-artifact"
-date = "2022-11-11"
-[taxonomies]
-tags = ["3dprinting", "CAD", "GIS", "CNC", "art", "sundries"]
-+++
-
-![a CNC-carved exaggerated relief of California made of plywood](PXL_20220723_214758454.jpg)
diff --git a/content/sundries/shit-code/birth_of_freedomdates.png b/content/sundries/shit-code/birth_of_freedomdates.png
new file mode 100644
index 0000000..5bef07c
Binary files /dev/null and b/content/sundries/shit-code/birth_of_freedomdates.png differ
diff --git a/content/sundries/shit-code/freedoms_birthday.png b/content/sundries/shit-code/freedoms_birthday.png
new file mode 100644
index 0000000..f90df2f
Binary files /dev/null and b/content/sundries/shit-code/freedoms_birthday.png differ
diff --git a/content/sundries/shit-code/index.md b/content/sundries/shit-code/index.md
new file mode 100644
index 0000000..c18c062
--- /dev/null
+++ b/content/sundries/shit-code/index.md
@@ -0,0 +1,312 @@
++++
+title = "Shit-code and Other Performance Arts"
+slug = "shit-code-and-performance-art"
+date = "2023-02-08"
+updated = "2023-02-09"
+[taxonomies]
+tags = ["software", "art", "sundry", "proclamation", "chaos"]
+[extra]
+toc = false
++++
+
+# A sundry collection of intellectual property, some less intellectual than other
+
+Something I firmly believe is that it's important to make jokes in any medium. Here at NebCorp Heavy
+Industries & Sundries, despite occasional dabbling with the
+[physical](@/sundries/a-thoroughly-digital-artifact/index.md), we work primarily with software, and
+software is one of our primary corporate humor channels. Below is just some of our work there,
+from least to most useless.
+
+## *katabastird, a graphical countdown timer*
+
+[katabastird](https://crates.io/crates/katabastird) is, in its own words, "a simple countdown timer
+that is configured and launched from the commandline." It looks like this when it's running:
+
+![katabastird running normally][katabastird_normal]
+
+It was created for a couple reasons:
+ - I wanted to make a GUI program to learn how to use a [particular library called
+ "egui"](https://github.com/emilk/egui);
+ - I had signed up to give a five-minute talk to demonstrate the latest release of a [commandline
+ argument parsing library called "clap"](https://docs.rs/clap/4.0.0/clap/), which I had titled,
+ "Clap for Clap Four", and I needed a program to showcase it.
+
+Obviously the best way to showcase a commandline-parsing library is to incorporate it into a
+graphical program. Other commandline-mission-critical features included changing the color of the
+background to get more and more red as less time remained
+
+![katabastird almost done counting down][katabastird_ending]
+
+and using the font used by the alien in *Predator*
+
+![get to the choppah][katabastird_predator]
+
+But by far its greatest feature is an undocumented option, `-A`, that will play an [airhorn
+salvo](https://gitlab.com/nebkor/katabastird/-/blob/4ccc2e4738df3f9d3af520e2d3875200534f4f6f/resources/airhorn_alarm.mp3)
+when it's done. This option is visible in the program's help text, but it's not described.
+
+Truly honestly, this is not a great program. Once it's launched, it only understands two keyboard
+inputs, `ESC` and `q`, both of which simply cause it to exit. Using the mouse, you can pause,
+restart, and reset. And that's it, that's all the interaction you get.
+
+In spite of this, I find myself using it all the time. It's easy to launch with different times (the
+commandline parsing understands things like `-h` for hours, `-m` for minutes, etc.), and its last
+invocation is just an up-arrow in my terminal away. The airhorn cracks me up every time.
+
+At some point, I plan on changing it to something that uses the GPU to run a fire simulation on the
+numbers, and have the flame intensity get higher as the time remaining gets lower. I'll save that
+for when I want to get slightly more serious about graphics and shaders, though; it would basically
+be a total re-write.
+
+
+As for the name, it's just a perversion of "katabasis", which means, "descent to the Underworld". I
+guess a bastardized "bastard" is in there, too. Listen, I'm gonna level with you: I'm not wild about the name, but
+what's done is done.
+
+## *randical, a commandline program for generating random values*
+
+Some time ago, I was [trying to work out some ways to pick random points in a
+sphere](https://blog.joeardent.net/2018/07/right-and-wrong-ways-to-pick-random-points-inside-a-sphere/),
+and during that exploration, I found myself wanting to just be able to generate random values
+outside of any program in particular. So, I wrapped a primitive interface around [the random value
+generation library](https://docs.rs/rand/0.8.0/rand/index.html) I was using. I wound up using it
+selfishly and in a limited fashion for that project, but afterward, decided to expand it a bit and
+release it, as my first [real Rust crate](https://crates.io/crates/randical).
+
+I'll reproduce the help text here, since it's fairly comprehensive:
+
+``` text
+$ randical -h
+Radical Random Value Generator 1.618033
+
+Generates arbitrary numbers of uniformly distributed random values.
+
+USAGE:
+ randical [FLAGS] [OPTIONS]
+
+FLAGS:
+ --buel Prints either 'Here.' or 'Um, he's sick. My best friend's sister's boyfriend's brother's girlfriend
+ heard from this guy who knows this kid who's going with the girl who saw Ferris pass out at 31
+ Flavors last night. I guess it's pretty serious.', with equal probability. Not compatible with `-t`
+ or `--bule`.
+ --bule Prints either 'true' or 'false', with equal probability. Not compatible with `-t` or `--buel`.
+ -e, --exit With equal probability, exit with either status 0, like /bin/true, or status 1, like /bin/false.
+ Technically compatible with all other options, but exit status will have no relation to any
+ generated output. Sets default number of values to print to 0.
+ -h, --help Prints help information
+ -V, --version Prints version information
+
+OPTIONS:
+ -n, --num-vals Number of random values to print out. Defaults to 1.
+ -t, --type Type of random value to print. Defaults to 'bool'.
+ Possible values are 'b'ool, 'f'loat64, 'U'UIDv4, 'u'nsigned64, 's'igned64, and 'k'suid
+ with millisecond precision.
+```
+
+The [README](https://github.com/nebkor/randical/blob/main/README.md) contains some examples of using
+it to do various things, like simulate a fair coin toss, or an *unfair* coin toss, or "a *Sliding
+Doors*-style garden of forking paths alternate timeline for Ferris Bueller's presence or absence on
+that fateful day."
+
+I have one actual non-shithead usecase for this program: in my [.emacs file, I use it to
+generate](https://gitlab.com/nebkor/dotfiles/-/blob/3aaf06fc66cdb541b76dfd44d25c369c4762301f/.emacs#L113-116)
+[ksuids](https://segment.com/blog/a-brief-history-of-the-uuid/). But I don't *really* use it.
+
+I include it mostly because, by most measurable metrics, this is my most popular program with end
+users that I can specifically identify:
+
+![randical's popularity is baffling][randical_downloads]
+
+Who is downloading my software, and why? I don't know, and more importantly, I don't care or need to
+know. It's truly better for everyone that way.
+
+## *freedom-dates, a library neither wanted nor needed*
+
+When I started writing this post, "freedom-dates" existed strictly as a shit-head idea of mine about
+the dumbest possible way to represent dates as a string. In fact, I had had it about a month before,
+while chatting with some friends on Discord.
+
+![the birth of the birth of freedom][freedomdates-discord]
+*actually i did ask if i should*
+
+As usual, I thought tossing a small crate together to realize this joke would take, at most, one
+hour, and be maybe ten lines long. At least this time, it only took five or six times as long as I
+thought it would. In its own words, `freedom-dates`
+
+> provides a convenient suite of affordances for working with dates in *freedom format*. That is, it
+> takes representations of dates in Communinst formats like "2023-02-08", and liberates them into a
+> Freedom format like "2/8/23".
+
+For something like this, where I would not want to actually be accused of punching down or being a
+jingoistic moron, it's important to be as multidimensionally absurd as possible; I really needed to
+commit to the bit and provide maximum, richly-textured incongruity.
+
+Luckily, using the [Rust language](https://www.rust-lang.org/) helps with that in a lot of
+ways. After I [published it to the official package
+repository](https://crates.io/crates/freedom-dates), the official documentation site built and
+published the [autogenerated documentation](https://docs.rs/freedom-dates/latest/freedom_dates/) for
+it. This leads to the creation of content that looks like this:
+
+![this is history][freedoms-birthday]
+
+The slick-looking defaults and basically frictionless process for participating in the Rust
+ecosystem make it easy for culture-jamming like this. All I had to do was diligently comment, test,
+and document my code[^just-do-lots-of-work], and the larger systems took care of the rest.
+
+Rust also provides a lot of different fancy programming tools, like
+[`Traits`](https://docs.rs/freedom-dates/latest/freedom_dates/trait.FreedomTime.html), that allow
+you to dress up deeply unserious content in deeply serious costume.
+
+In all real seriousness, though, I hope that seeing how easy it is to get something this silly
+published in the official channels inspires you to overcome any trepidation about doing that
+yourself, if you have something you want to share!
+
+## *bad_print, a silly program*
+
+A few years ago, someone at the [Recurse Center](https://recurse.com/)[^rc-link] started a chat
+thread titled "bad print", and opened it with,
+
+> you probably didn't know that you needed a bad print function, one that spawns a thread for each
+> character in your string and prints the single character before quitting... well, now that you
+> know that you needed this, i've written one for you
+
+and then pasted a 3-line program in Haskell, and asked for other implementations of "bad print", in
+any language. I whipped one up using [Rayon](https://github.com/rayon-rs/rayon), a library for doing
+some things in parallel really easily, but eventually settled on the following, which uses a much
+smaller and more focused external library called
+[threadpool](https://github.com/rust-threadpool/rust-threadpool):
+
+``` rust
+use std::io::Write;
+use std::{env, io};
+
+use threadpool::ThreadPool;
+
+fn main() {
+ let args: Vec = env::args().collect();
+ if args.len() < 2 {
+ panic!("Please supply a phrase to be badly printed.")
+ }
+ let string = args[1..].join(" ");
+ let num_threads = string.len();
+
+ println!(--------);
+ let pool = ThreadPool::new(num_threads);
+ for c in string.chars() {
+ pool.execute(move || {
+ print!("{}", c);
+ let _ = io::stdout().flush();
+ });
+ }
+ pool.join();
+ println!();
+}
+```
+
+I noted about it, relative to earlier versions,
+
+> It appears to output strings with an even larger edit distance from the arguments given to it,
+> presumably due to the chaos inherent to harnessing the power of one full tpc (thread per char).
+
+Indeed, witness it for yourself:
+
+``` text
+$ bad_print Behold the awesome power of one full Thread Per Character.
+--------
+Bwoesmd elpoh or onh eu Thread earPCh ceearfofelwtter la.
+```
+
+By far the most impressive was a bash script that did *Matrix*-style cascading text in your
+terminal, called, appropriately enough, `bad_matrix`; that particular one was by someone [who's a
+bit of a shell
+wizard](https://www.evalapply.org/posts/shell-aint-a-bad-place-to-fp-part-2-functions-as-unix-tools/index.html#main).
+
+# Other peformance arts
+
+An artist's medium is all of reality and all of time, so every piece of the work is eligible for
+expression; the frame is also part of the work. Software in my culture is still embedded in a
+context that is a bit stuffy, a bit up its ass about things like "copyright" and "semantic
+versioning"[^smegver], and so they're things I enjoy playing with, too.
+
+At the bottom of the [readme for
+freedom-dates](https://github.com/nebkor/misfit_toys/blob/master/freedom-dates/README.md), I have
+the following about the version:
+
+> Freedom *STARTS* at number 1, baby! And every release is ten times the last, so second release is
+> 10, then 100, etc. FREEDOM!
+
+and indeed it is at version 1.0.0; the two `.0`s after the `1` are there to satisfy Cargo's
+requirements about semver[^smegver].
+
+## goldver
+
+When I version software for public consumption, I tend to use a scheme I call
+"[goldver](https://gitlab.com/nebkor/katabastird/-/blob/main/VERSIONING.md)", short for "Golden
+Versioning". It works like this:
+
+> When projects are versioned with goldver, the first version is "1". Note that it is not "1.0", or,
+> "1.0-prealpha-release-preview", or anything nonsensical like that. As new versions are released,
+> decimals from *phi*, the [Golden Ratio](https://en.wikipedia.org/wiki/Golden_ratio), are appended
+> after an initial decimal point. So the second released version will be "1.6", the third would be
+> "1.61", etc., and on until perfection is asymptotically approached as the number of released
+> versions goes to infinity.
+
+In order to be compliant with the semver version structure, the following rule is applied to
+projects published to the official Rust package repository:
+
+> Once there have been at least three releases, the version string in the Cargo.toml file will
+> always be of the form "1.6.x", where x is at least one digit long, starting with "1". Each
+> subsequent release will append the next digit of phi to x. The number of releases can be
+> calculated by counting the number of digits in x and adding 2 to that.
+
+I sincerely believe that this is *better than [semver](https://semver.org/)* for plenty of non-library
+software. It was Windows 95 and then Windows 2000; obviously there was a lot of change. I don't care
+about arguing about the whether or not this is a "patch release" or a "minor release" or a "major
+change". There are no downstream dependents who need to make sure they don't accidentally upgrade to
+the latest release. If someone wants to update it, they know what they're getting into, and they do
+it in an inherently manual way.
+
+## chaos license
+
+Anything that I can[^chaos-software], I license under the Chaos License, which states,
+
+> This software is released under the terms of the Chaos License. In cases where the terms of the
+license are unclear, refer to the [Fuck Around and Find Out
+License](https://git.sr.ht/~boringcactus/fafol/tree/master/LICENSE-v0.2.md).
+
+This is about as
+[business-hostile](https://blog.joeardent.net/2017/01/say-no-to-corporate-friendly-licenses/) as I
+can imagine, far worse even than the strong copyleft licenses that terrified the lawyers at ILM when
+I was there. It oozes uncertainty and risk; you'd have to be deranged to seriously engage with
+it. But if you're just a person? Dive right in, it doesn't really matter!
+
+---
+That's about all I have for now, my droogs. Go take what you know and do something weird with it; it
+may amuse you! You might learn something! You might make someone laugh!
+
+---
+
+[katabastird_normal]: ./katabastird_normal.png "counting down with one hour, twenty-nine minutes, and forty-three seconds remaining"
+
+[katabastird_ending]: ./katabastird_almost_done.png "counting down with one second remaining"
+
+[katabastird_predator]: ./katabastird_predator.png "get to the choppah"
+
+[randical_downloads]: ./randical_installs.png "who the hell are these people?"
+
+[freedomdates-discord]: ./birth_of_freedomdates.png "a screencap of a conversation where I suggest 'freedom-formatted' dates are 'seconds since july 4 1776'"
+
+[freedoms-birthday]: ./freedoms_birthday.png "Freedom was born at noon on the Fourth of July, '76, Eastern Time. This is History."
+
+[^just-do-lots-of-work]: I did more test-writing and documenting for that useless joke project
+than for most other software I ever write.
+
+[^rc-link]: See also the link at the bottom of the page here.
+
+[^smegver]: "semantic versioning" sounds like it could be a good idea: "the versions should be
+meaningful, and when they change, they should be changed in a way that means something
+consistent". As usual with these things, it's turned into a prescriptivist cult whose adherents
+insist that all software be released according to its terms. This is annoying.
+
+[^chaos-software]: This is basically anything I write by me, for me, as opposed to contributing to
+someone else's project.
diff --git a/content/sundries/shit-code/katabastird_almost_done.png b/content/sundries/shit-code/katabastird_almost_done.png
new file mode 100644
index 0000000..501906c
Binary files /dev/null and b/content/sundries/shit-code/katabastird_almost_done.png differ
diff --git a/content/sundries/shit-code/katabastird_normal.png b/content/sundries/shit-code/katabastird_normal.png
new file mode 100644
index 0000000..6faa4b1
Binary files /dev/null and b/content/sundries/shit-code/katabastird_normal.png differ
diff --git a/content/sundries/shit-code/katabastird_predator.png b/content/sundries/shit-code/katabastird_predator.png
new file mode 100644
index 0000000..58edd86
Binary files /dev/null and b/content/sundries/shit-code/katabastird_predator.png differ
diff --git a/content/sundries/shit-code/randical_installs.png b/content/sundries/shit-code/randical_installs.png
new file mode 100644
index 0000000..3dd5384
Binary files /dev/null and b/content/sundries/shit-code/randical_installs.png differ
diff --git a/sass/main.scss b/sass/main.scss
index daf0dbf..f1b65b8 100644
--- a/sass/main.scss
+++ b/sass/main.scss
@@ -46,4 +46,4 @@ html {
html {
font-size: 18px;
}
-}
\ No newline at end of file
+}
diff --git a/sass/parts/_cards.scss b/sass/parts/_cards.scss
index a23fd91..9ecdada 100644
--- a/sass/parts/_cards.scss
+++ b/sass/parts/_cards.scss
@@ -1,6 +1,6 @@
.cards {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(500px, 1fr));
grid-template-rows: auto;
gap: 24px;
padding: 12px 0;
@@ -41,4 +41,4 @@
.cards {
gap: 18px;
}
-}
\ No newline at end of file
+}
diff --git a/sass/parts/_header.scss b/sass/parts/_header.scss
index 31fc388..85ee937 100644
--- a/sass/parts/_header.scss
+++ b/sass/parts/_header.scss
@@ -1,3 +1,8 @@
+.hias-footer {
+ text-align: center;
+ font-size: 0.4rem;
+}
+
.page-header {
font-size: 3em;
line-height: 100%;
@@ -28,15 +33,33 @@ header .main {
display: flex;
flex-direction: row;
flex-wrap: wrap;
- justify-content: space-between;
+ justify-content: flex-end;
align-items: flex-start;
gap: 12px;
- font-size: 1.5rem;
+ font-size: 1.2rem;
/* Otherwise header and menu is too close on small screens*/
margin-bottom: 10px;
}
+header .config-title {
+ line-height: 1.3;
+}
+
+.nav-header {
+ /* flex-child */
+ flex-grow: 0;
+ /* flex-container */
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ align-items: flex-even;
+ gap: 4px;
+}
+
+
+
.socials {
/* flex-child */
flex-grow: 0;
@@ -67,6 +90,12 @@ header .main {
letter-spacing: -0.5px;
}
+
+h1 { font-size: 1.5rem }
+h2 { font-size: 1.2rem }
+
+
+/*
h1,
h2,
h3,
@@ -76,39 +105,34 @@ h6 {
font-size: 1.2rem;
margin-top: 2em;
}
+ */
h1::before {
color: var(--primary-color);
- content: "# ";
}
h2::before {
color: var(--primary-color);
- content: "## ";
}
h3::before {
color: var(--primary-color);
- content: "### ";
}
h4::before {
color: var(--primary-color);
- content: "#### ";
}
h5::before {
color: var(--primary-color);
- content: "##### ";
}
h6::before {
color: var(--primary-color);
- content: "###### ";
}
@media (prefers-color-scheme: dark) {
.social>img {
filter: invert(1);
}
-}
\ No newline at end of file
+}
diff --git a/sass/parts/_image.scss b/sass/parts/_image.scss
index 447f930..4cf64b8 100644
--- a/sass/parts/_image.scss
+++ b/sass/parts/_image.scss
@@ -1,6 +1,10 @@
img {
border: 3px solid #ececec;
max-width: 100%;
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ margin-bottom: 3px;
}
figure {
@@ -31,5 +35,5 @@ figure h4::before {
}
svg {
- max-height: 15px;
-}
\ No newline at end of file
+ max-height: 30px;
+}
diff --git a/sass/parts/_tags.scss b/sass/parts/_tags.scss
index 461530c..cefb3e9 100644
--- a/sass/parts/_tags.scss
+++ b/sass/parts/_tags.scss
@@ -1,5 +1,6 @@
.tags li::before {
- content: "🏷 ";
+ content: "🏷 ";
+ font-size: 0.5rem;
}
.tags a {
@@ -9,4 +10,4 @@
.tags a:hover {
color: var(--hover_color);
background-color: var(--primary-color);
-}
\ No newline at end of file
+}
diff --git a/sass/parts/_misc.scss b/sass/parts/misc.scss
similarity index 91%
rename from sass/parts/_misc.scss
rename to sass/parts/misc.scss
index cecb254..10a3049 100644
--- a/sass/parts/_misc.scss
+++ b/sass/parts/misc.scss
@@ -11,6 +11,12 @@
background-color: var(--primary-color);
}
+.caption {
+ font-size: 0.5em;
+ font-style: italic;
+ text-align: center;
+}
+
::-moz-selection {
background: var(--primary-color);
color: var(--hover-color);
@@ -60,4 +66,4 @@ time {
margin: 0;
padding: 0;
}
-}
\ No newline at end of file
+}
diff --git a/static/images/programmers_creed.jpg b/static/images/programmers_creed.jpg
new file mode 100644
index 0000000..dae2572
Binary files /dev/null and b/static/images/programmers_creed.jpg differ
diff --git a/static/js/count.js b/static/js/count.js
deleted file mode 100644
index 7c504bc..0000000
--- a/static/js/count.js
+++ /dev/null
@@ -1,270 +0,0 @@
-// GoatCounter: https://www.goatcounter.com
-// This file (and *only* this file) is released under the ISC license:
-// https://opensource.org/licenses/ISC
-;(function() {
- 'use strict';
-
- if (window.goatcounter && window.goatcounter.vars) // Compatibility with very old version; do not use.
- window.goatcounter = window.goatcounter.vars
- else
- window.goatcounter = window.goatcounter || {}
-
- // Load settings from data-goatcounter-settings.
- var s = document.querySelector('script[data-goatcounter]')
- if (s && s.dataset.goatcounterSettings) {
- try { var set = JSON.parse(s.dataset.goatcounterSettings) }
- catch (err) { console.error('invalid JSON in data-goatcounter-settings: ' + err) }
- for (var k in set)
- if (['no_onload', 'no_events', 'allow_local', 'allow_frame', 'path', 'title', 'referrer', 'event'].indexOf(k) > -1)
- window.goatcounter[k] = set[k]
- }
-
- var enc = encodeURIComponent
-
- // Get all data we're going to send off to the counter endpoint.
- var get_data = function(vars) {
- var data = {
- p: (vars.path === undefined ? goatcounter.path : vars.path),
- r: (vars.referrer === undefined ? goatcounter.referrer : vars.referrer),
- t: (vars.title === undefined ? goatcounter.title : vars.title),
- e: !!(vars.event || goatcounter.event),
- s: [window.screen.width, window.screen.height, (window.devicePixelRatio || 1)],
- b: is_bot(),
- q: location.search,
- }
-
- var rcb, pcb, tcb // Save callbacks to apply later.
- if (typeof(data.r) === 'function') rcb = data.r
- if (typeof(data.t) === 'function') tcb = data.t
- if (typeof(data.p) === 'function') pcb = data.p
-
- if (is_empty(data.r)) data.r = document.referrer
- if (is_empty(data.t)) data.t = document.title
- if (is_empty(data.p)) data.p = get_path()
-
- if (rcb) data.r = rcb(data.r)
- if (tcb) data.t = tcb(data.t)
- if (pcb) data.p = pcb(data.p)
- return data
- }
-
- // Check if a value is "empty" for the purpose of get_data().
- var is_empty = function(v) { return v === null || v === undefined || typeof(v) === 'function' }
-
- // See if this looks like a bot; there is some additional filtering on the
- // backend, but these properties can't be fetched from there.
- var is_bot = function() {
- // Headless browsers are probably a bot.
- var w = window, d = document
- if (w.callPhantom || w._phantom || w.phantom)
- return 150
- if (w.__nightmare)
- return 151
- if (d.__selenium_unwrapped || d.__webdriver_evaluate || d.__driver_evaluate)
- return 152
- if (navigator.webdriver)
- return 153
- return 0
- }
-
- // Object to urlencoded string, starting with a ?.
- var urlencode = function(obj) {
- var p = []
- for (var k in obj)
- if (obj[k] !== '' && obj[k] !== null && obj[k] !== undefined && obj[k] !== false)
- p.push(enc(k) + '=' + enc(obj[k]))
- return '?' + p.join('&')
- }
-
- // Show a warning in the console.
- var warn = function(msg) {
- if (console && 'warn' in console)
- console.warn('goatcounter: ' + msg)
- }
-
- // Get the endpoint to send requests to.
- var get_endpoint = function() {
- var s = document.querySelector('script[data-goatcounter]')
- if (s && s.dataset.goatcounter)
- return s.dataset.goatcounter
- return (goatcounter.endpoint || window.counter) // counter is for compat; don't use.
- }
-
- // Get current path.
- var get_path = function() {
- var loc = location,
- c = document.querySelector('link[rel="canonical"][href]')
- if (c) { // May be relative or point to different domain.
- var a = document.createElement('a')
- a.href = c.href
- if (a.hostname.replace(/^www\./, '') === location.hostname.replace(/^www\./, ''))
- loc = a
- }
- return (loc.pathname + loc.search) || '/'
- }
-
- // Run function after DOM is loaded.
- var on_load = function(f) {
- if (document.body === null)
- document.addEventListener('DOMContentLoaded', function() { f() }, false)
- else
- f()
- }
-
- // Filter some requests that we (probably) don't want to count.
- goatcounter.filter = function() {
- if ('visibilityState' in document && document.visibilityState === 'prerender')
- return 'visibilityState'
- if (!goatcounter.allow_frame && location !== parent.location)
- return 'frame'
- if (!goatcounter.allow_local && location.hostname.match(/(localhost$|^127\.|^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\.|^0\.0\.0\.0$)/))
- return 'localhost'
- if (!goatcounter.allow_local && location.protocol === 'file:')
- return 'localfile'
- if (localStorage && localStorage.getItem('skipgc') === 't')
- return 'disabled with #toggle-goatcounter'
- return false
- }
-
- // Get URL to send to GoatCounter.
- window.goatcounter.url = function(vars) {
- var data = get_data(vars || {})
- if (data.p === null) // null from user callback.
- return
- data.rnd = Math.random().toString(36).substr(2, 5) // Browsers don't always listen to Cache-Control.
-
- var endpoint = get_endpoint()
- if (!endpoint)
- return warn('no endpoint found')
-
- return endpoint + urlencode(data)
- }
-
- // Count a hit.
- window.goatcounter.count = function(vars) {
- var f = goatcounter.filter()
- if (f)
- return warn('not counting because of: ' + f)
-
- var url = goatcounter.url(vars)
- if (!url)
- return warn('not counting because path callback returned null')
-
- var img = document.createElement('img')
- img.src = url
- img.style.position = 'absolute' // Affect layout less.
- img.style.bottom = '0px'
- img.style.width = '1px'
- img.style.height = '1px'
- img.loading = 'eager'
- img.setAttribute('alt', '')
- img.setAttribute('aria-hidden', 'true')
-
- var rm = function() { if (img && img.parentNode) img.parentNode.removeChild(img) }
- img.addEventListener('load', rm, false)
- document.body.appendChild(img)
- }
-
- // Get a query parameter.
- window.goatcounter.get_query = function(name) {
- var s = location.search.substr(1).split('&')
- for (var i = 0; i < s.length; i++)
- if (s[i].toLowerCase().indexOf(name.toLowerCase() + '=') === 0)
- return s[i].substr(name.length + 1)
- }
-
- // Track click events.
- window.goatcounter.bind_events = function() {
- if (!document.querySelectorAll) // Just in case someone uses an ancient browser.
- return
-
- var send = function(elem) {
- return function() {
- goatcounter.count({
- event: true,
- path: (elem.dataset.goatcounterClick || elem.name || elem.id || ''),
- title: (elem.dataset.goatcounterTitle || elem.title || (elem.innerHTML || '').substr(0, 200) || ''),
- referrer: (elem.dataset.goatcounterReferrer || elem.dataset.goatcounterReferral || ''),
- })
- }
- }
-
- Array.prototype.slice.call(document.querySelectorAll("*[data-goatcounter-click]")).forEach(function(elem) {
- if (elem.dataset.goatcounterBound)
- return
- var f = send(elem)
- elem.addEventListener('click', f, false)
- elem.addEventListener('auxclick', f, false) // Middle click.
- elem.dataset.goatcounterBound = 'true'
- })
- }
-
- // Add a "visitor counter" frame or image.
- window.goatcounter.visit_count = function(opt) {
- on_load(function() {
- opt = opt || {}
- opt.type = opt.type || 'html'
- opt.append = opt.append || 'body'
- opt.path = opt.path || get_path()
- opt.attr = opt.attr || {width: '200', height: (opt.no_branding ? '60' : '80')}
-
- opt.attr['src'] = get_endpoint() + 'er/' + enc(opt.path) + '.' + enc(opt.type) + '?'
- if (opt.no_branding) opt.attr['src'] += '&no_branding=1'
- if (opt.style) opt.attr['src'] += '&style=' + enc(opt.style)
- if (opt.start) opt.attr['src'] += '&start=' + enc(opt.start)
- if (opt.end) opt.attr['src'] += '&end=' + enc(opt.end)
-
- var tag = {png: 'img', svg: 'img', html: 'iframe'}[opt.type]
- if (!tag)
- return warn('visit_count: unknown type: ' + opt.type)
-
- if (opt.type === 'html') {
- opt.attr['frameborder'] = '0'
- opt.attr['scrolling'] = 'no'
- }
-
- var d = document.createElement(tag)
- for (var k in opt.attr)
- d.setAttribute(k, opt.attr[k])
-
- var p = document.querySelector(opt.append)
- if (!p)
- return warn('visit_count: append not found: ' + opt.append)
- p.appendChild(d)
- })
- }
-
- // Make it easy to skip your own views.
- if (location.hash === '#toggle-goatcounter') {
- if (localStorage.getItem('skipgc') === 't') {
- localStorage.removeItem('skipgc', 't')
- alert('GoatCounter tracking is now ENABLED in this browser.')
- }
- else {
- localStorage.setItem('skipgc', 't')
- alert('GoatCounter tracking is now DISABLED in this browser until ' + location + ' is loaded again.')
- }
- }
-
- if (!goatcounter.no_onload)
- on_load(function() {
- // 1. Page is visible, count request.
- // 2. Page is not yet visible; wait until it switches to 'visible' and count.
- // See #487
- if (!('visibilityState' in document) || document.visibilityState === 'visible')
- goatcounter.count()
- else {
- var f = function(e) {
- if (document.visibilityState !== 'visible')
- return
- document.removeEventListener('visibilitychange', f)
- goatcounter.count()
- }
- document.addEventListener('visibilitychange', f)
- }
-
- if (!goatcounter.no_events)
- goatcounter.bind_events()
- })
-})();
-
diff --git a/static/js/footnoter.js b/static/js/footnoter.js
new file mode 100644
index 0000000..138b9b9
--- /dev/null
+++ b/static/js/footnoter.js
@@ -0,0 +1,27 @@
+// The DOMContentLoaded event fires when the initial HTML
+// document has been completely loaded and parsed, without
+// waiting for stylesheets, images, and subframes to finish loading.
+document.addEventListener('DOMContentLoaded', (_event) => {
+ const references = document.getElementsByClassName('footnote-reference')
+ // For each footnote reference, set an id so we can refer to it from the definition.
+ // If the definition had an id of 'some_id', then the reference has id `some_id_ref`.
+ for (const reference of references) {
+ const link = reference.firstChild
+ const id = link.getAttribute('href').slice(1) // skip the '#'
+ link.setAttribute('id', `${id}_ref`)
+ }
+
+ const footnotes = document.getElementsByClassName('footnote-definition-label')
+ // For each footnote-definition, add an anchor element with an href to its corresponding reference.
+ // The text used for the added anchor is 'Leftwards Arrow with Hook' (U+21A9).
+ for (const footnote of footnotes) {
+ const pid = footnote.parentElement.getAttribute('id')
+ const num = footnote.textContent;
+ footnote.textContent = '';
+
+ const backReference = document.createElement('a')
+ backReference.setAttribute('href', `#${pid}_ref`)
+ backReference.textContent = `${num} ⬑`
+ footnote.append(backReference)
+ }
+});
diff --git a/templates/base.html b/templates/base.html
index 14f5056..d531830 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -13,7 +13,7 @@
Nothing here?!
{% endblock main_content %}
- _
+