Compare commits

..

1 commit

516 changed files with 2419 additions and 10531 deletions

3
.gitattributes vendored
View file

@ -1,5 +1,2 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.pxm filter=lfs diff=lfs merge=lfs -text
*.mov filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text

5
.gitignore vendored
View file

@ -1,11 +1,6 @@
node_modules/
public/
/documentation/mirrors/
/resources/
.hugo_build.lock
*.log
*.orig
*~
# Backup files for Markdown files processed in-place with sed
*.md-e

18
.gitmodules vendored
View file

@ -1,18 +0,0 @@
[submodule "themes/platters"]
path = themes/platters
url = nutmeg:git/hugo-theme-platters.git
[submodule "themes/termlite"]
path = themes/termlite
url = git@github.com:erynofwales/hugo-theme-termlite.git
[submodule "themes/resource-builders"]
path = themes/resource-builders
url = git@github.com:erynofwales/hugo-resource-builders.git
[submodule "themes/image-utils"]
path = themes/image-utils
url = git@github.com:erynofwales/hugo-image-utilities.git
[submodule "themes/photostream"]
path = themes/photostream
url = git@github.com:erynofwales/hugo-theme-photostream.git
[submodule "themes/feeds"]
path = themes/feeds
url = git@github.com:erynofwales/hugo-theme-feeds.git

View file

@ -1,15 +0,0 @@
snippet jp "lang jp shortcode" w
{{< lang jp >}}$1{{< /lang >}}
endsnippet
snippet jpp "lang jp shortcode with expansion" w
{{% lang jp %}}$1{{% /lang %}}
endsnippet
snippet tess "tess shortcode" w
{{< tess >}}
endsnippet
snippet ruby "ruby shortcode" w
{{< ruby "$1" >}}$2{{< /ruby >}}
endsnippet

View file

@ -1,4 +0,0 @@
-- Eryn Wells <eryn@erynwells.me>
vim.bo.shiftwidth = 2
vim.bo.softtabstop = 2

View file

@ -1,6 +0,0 @@
-- Eryn Wells <eryn@erynwells.me>
local root = gitTopLevelDirectory()
vim.opt_local.path:prepend(root .. "/assets/scripts/**")
vim.opt_local.path:prepend(root .. "/assets/styles/**")
vim.opt_local.path:prepend(root .. "/layouts/**")

View file

@ -1,4 +0,0 @@
-- Eryn Wells <eryn@erynwells.me>
local root = gitTopLevelDirectory()
vim.opt_local.path:prepend(root .. "/assets/scripts/**")

View file

@ -1,11 +0,0 @@
-- Eryn Wells <eryn@erynwells.me>
vim.bo.textwidth = 80
vim.cmd [[
iabbrev tokyo Tōkyō
iabbrev Tokyo Tōkyō
iabbrev kyoto Kyōto
iabbrev Kyoto Kyōto
iabbrev xx &times;
]]

View file

@ -1,8 +0,0 @@
-- Eryn Wells <eryn@erynwells.me>
local filetypedetectGroup = vim.api.nvim_create_augroup("HugoHTMLTemplates", {clear = true})
vim.api.nvim_create_autocmd({"BufRead", "BufNewFile"}, {
pattern = {"**/layouts/**/*.html"},
group = filetypedetectGroup,
command = "set ft=gohtmltmpl",
})

View file

@ -1,9 +1,5 @@
# Eryn Wells <eryn@erynwells.me>
BUILD_DIR=public
CONTENT_PATH=content
DEPLOY_USER=eryn
DEPLOY_HOSTNAME=nutmeg.erynwells.me
DEPLOY_PATH=/srv/www/erynwells.me/html
@ -11,32 +7,24 @@ DEPLOY_LOCATION=$(DEPLOY_USER)@$(DEPLOY_HOSTNAME):$(DEPLOY_PATH)
HOSTNAME=$(shell hostname -s)
NETHACK_LOGFILE=$(shell command nethack --showpaths | grep scoredir | sed 's/.*"\(.*\)".*/\1/g')/logfile
NETHACK_LOGFILE=$(shell nethack --showpaths | grep scoredir | sed 's/.*"\(.*\)".*/\1/g')/logfile
NETHACK_LOGFILE_DATA_FILE=data/nethack/logfile/$(HOSTNAME).json
.PHONY: site deploy clean
.PHONY: deploy clean
site:
site: public/index.html nethack
@echo "Building site"
hugo --buildFuture --enableGitInfo --destination "$(BUILD_DIR)"
hugo
deploy: site
@echo "Deploying to $(DEPLOY_LOCATION)"
rsync -avz --no-times --no-perms --delete "$(BUILD_DIR)/" "$(DEPLOY_LOCATION)"
git tag -f deploy-$(shell date +%Y-%m-%d)
rsync -avz --no-times --no-perms --delete public/ $(DEPLOY_LOCATION)
deployall: nethack deploy
nethack: nethack-logfile nethack-commit
nethack-logfile: $(NETHACK_LOGFILE)
nethack: $(NETHACK_LOGFILE)
ifeq (,$(wildcard $<))
@echo "Importing Nethack logfile from $(NETHACK_LOGFILE)"
scripts/import-nethack-logfile.py -o $(NETHACK_LOGFILE_DATA_FILE) $<
endif
nethack-commit: $(NETHACK_LOGFILE_DATA_FILE)
if ! git diff --quiet $<; then git commit -m "Update Nethack logfile for $(HOSTNAME)" -- $<; fi
clean:
rm -rf "$(BUILD_DIR)/"
rm -rf public/

27
activitypub/private.pem Normal file
View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAl1vLbuE1C76GKX90VTJj3FaXyy7Ri57o7dKvrl8Bh+fk9wi/
uNWxuAA0rA70wihttNvPTXXiBpwbHdV9LQ/48hpmy2IJQWBvaOB+FXxDeUGelnwZ
zFYVH6ELmm+zEev6LUmkI8QtPakcw82LQV/7FeR5tZvAPHXqZmQATSsfKBKDPGje
aesNZ6P2g558DJnf35ksBYUXKCPnuKpJP31gAcpvSUPQGT2/wSPlH6T30U0taOhy
c3HUb2IqTKeZfe3EB2uliBGIGZnr6PL+B1OaXUehjT4M0F3RmsRRqpwnwXuFdekP
4CjWjgU2S8aC7wAVVhQl+pXzH2IxQyTrtoGW4QIDAQABAoIBAFOXw/P591UEJY6X
sMU47kQLowv5UIue+SAX4yUXnX3UyfTRZSmNA/kOTAjWvcDZmTVwzL4IJAvofWVt
uhOAJcp4YFtlSp1LyFwQ2DG/jnhSkGamJY1f4Yy1YfYqSKjWPjZy3G9QcngChTty
sr0paD/ADqQJYBNEQG/KPQxhi3SSY2VoKhnO8bn9sFFXN10kpYCVMm7ry2eLkjmN
z2qLqGYv0XJzWydNUGQ4mEakqCu4/4Yzv2lzV2eQvk7to9An9hmwI5RrTdxMod+h
tOKzH0YJ2ofShEhW8lBWiPpuIRwHWXn1ceWqqyyY2bHF6t4/uVYyrCI6fN9wuJfT
hESAiI0CgYEAyML/x0+zXqKbs+XojJ2eaC2ZYO525f+Xs744hAjAqzxdY9VKynS5
YcvguNfPd06AVqOZC/DOSc/SB3Rpcjad0iK9T3IGLZMs54o8nd7qSLrcmnGksua6
HSAvRMIjg7RQZKX97Q55u/+aZlSq2R686vu+SucCh4wR/el2f9L55jcCgYEAwQD/
yzuQwPYbhGvSi/6Wnnd+v/3z1FNS+og5u5egfG4soqV4NwqfrzFEo/498oWUHQQx
R7T028b6jBq8C0WPULwlJK28Srhe14prD494GBpoQE7ORrfUyR2BV+a3aHnjaApI
p1HzyfBMK8I8C472S0qcyynSjvJwMMzccYhiX6cCgYEAvB6M0lSKfSjdwYqzh9/C
KVhhGoUZAJeprRGXn5EcCNh9oiig9mi9tTf3kP0YOHFkrMqLhLbWdi+4XKfmjtFC
vT80KRdtVpNMRoIwHrZczl3ajRXkAIdW6ifYYxOPd8AkfMtVzyX7SABQy4aWuPCm
kYHnZARnSGAmE8kJ8ujcpa0CgYEAuMCLG/HOFFjCaJWKO8aeSd1bcM/Za6PDp91l
XoS79MDGa394P0AlMeA4s785b4GdEdWkkt7PEd88kYc6AJQ6p28jIyRIPUKWTz/K
Ul/k9zW4G+8UGK/6ufZLjWhmo1UiccJ+X+TcOK+VSsIpFmtoqxQmWXiUfY/oA17n
mqqSeLsCgYBqujxgBzTEWtU5g/MPHORVjXgZ83NWOA+3LOoicho/iaRAhz7mI13Y
d7J7s6CSqlxtCXsfVthzXYF+u5eYblkqnOLIY7X+tfcsx00twQflfRkdwjdChZge
cQk1BW4MWNhsH92iXGmb3B5iktFupXPzLlspAaqKeSRq4+5FPfq7Yg==
-----END RSA PRIVATE KEY-----

9
activitypub/public.pem Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl1vLbuE1C76GKX90VTJj
3FaXyy7Ri57o7dKvrl8Bh+fk9wi/uNWxuAA0rA70wihttNvPTXXiBpwbHdV9LQ/4
8hpmy2IJQWBvaOB+FXxDeUGelnwZzFYVH6ELmm+zEev6LUmkI8QtPakcw82LQV/7
FeR5tZvAPHXqZmQATSsfKBKDPGjeaesNZ6P2g558DJnf35ksBYUXKCPnuKpJP31g
AcpvSUPQGT2/wSPlH6T30U0taOhyc3HUb2IqTKeZfe3EB2uliBGIGZnr6PL+B1Oa
XUehjT4M0F3RmsRRqpwnwXuFdekP4CjWjgU2S8aC7wAVVhQl+pXzH2IxQyTrtoGW
4QIDAQAB
-----END PUBLIC KEY-----

View file

@ -1,9 +0,0 @@
---
title: "{{ replace .Name "-" " " | title }}"
slug: link-{{ .Name }}
date: {{ .Date }}
categories: links
draft: true
tags: []
---

View file

@ -1,10 +0,0 @@
---
title: "Notes on {{ time.Now.Format "2006" }}W%%WEEK_NUMBER%%"
slug: weeknotes-{{ time.Now.Format "2006" }}w%%WEEK_NUMBER%%
date: {{ .Date | time.Format "2006-01-02" }}
categories: weeknotes
tags:
- Weeknotes
draft: true
---

View file

@ -1,18 +0,0 @@
/************************
* PARAGRAPH-SPACED LIST
************************/
p + .paragraph-spaced-list {
margin-block-start: var(--space-paragraph);
}
.paragraph-spaced-list {
li + li {
margin-block-start: var(--space-paragraph);
}
}

View file

@ -1,42 +0,0 @@
.home-latest {
display: grid;
grid-column: main-start / main-end;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: min-content min-content;
grid-template-areas:
"blog1 blog1 blog2 blog2"
"photo1 photo2 photo3 photo4";
.home-latest__blog {
margin-block-end: var(--space-m);
}
.home-latest__blog:nth-of-type(1) {
grid-area: blog1;
border-right: 2px dashed var(--gray6);
padding-inline-end: var(--space-s);
}
.home-latest__blog:nth-of-type(2) {
grid-area: blog2;
padding-inline-start: var(--space-s);
}
.home-latest__photo {
}
}
@media screen and (max-width: 480px) {
.home-latest {
grid-template-columns: 1fr 1fr;
grid-template-rows: repeat(min-content, 4);
grid-template-areas:
"blog1 blog2"
"photo1 photo2"
"photo3 photo4";
}
}
p + .home-latest {
margin-block-start: var(--space-paragraph);
}

View file

@ -1,100 +0,0 @@
/******************
* NETHACK LOGFILE
******************/
#dungeon-background {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: -1;
filter: brightness(0.3);
}
.nethack-logfile {
margin-inline-start: 0;
padding-inline-start: 0;
.nethack-logentry {
align-items: first baseline;
display: grid;
grid-template-columns: min-content min-content auto min-content;
grid-template-areas:
"list-marker entry-marker entry-date entry-character-descriptor"
". . entry-description entry-description"
". . entry-stats entry-stats";
gap: var(--space-xs);
margin-inline-start: 0;
}
}
.nethack-logentry {
&:not(:last-child) {
margin-block-end: var(--space-l);
}
&::before {
grid-area: list-marker;
}
.nethack-logentry__marker {
grid-area: entry-marker;
}
.nethack-logentry__date {
grid-area: entry-date;
line-height: 1;
margin: 0;
padding: 0;
}
.nethack-logentry__character-descriptor {
font-family: var(--font-family-monospace);
font-size: var(--text-s);
grid-area: entry-character-descriptor;
line-height: 1;
white-space: nowrap;
}
.nethack-logentry__description {
grid-area: entry-description;
margin: 0;
}
.nethack-logentry__stats {
border: 0;
color: var(--text-color-secondary);
font-family: var(--font-family-monospace);
font-size: var(--text-s);
grid-area: entry-stats;
margin-block: 0;
width: 100%;
-webkit-border-horizontal-spacing: 0;
-webkit-border-vertical-spacing: 0;
}
.nethack-logentry__stats {
padding: 0;
text-transform: uppercase;
vertical-align: bottom;
white-space: nowrap;
thead {
font-weight: bolder;
}
.nethack-logentry__score,
.nethack-logentry__hp,
.nethack-logentry__level {
width: 16rem;
text-align: right;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 464 KiB

View file

@ -1,758 +0,0 @@
const NUMBER_OF_ROOMS = 12;
const TUNNEL_PASSES = 3;
class Cell {
static CORRIDOR = "#";
static DOOR_CLOSED = "+";
character;
characterColor;
backgroundColor;
constructor(char, charColor) {
this.character = char;
}
empty() { this.character = " "; }
floor() { this.character = "."; }
upStair() { this.character = "<"; }
downStair() { this.character = ">"; }
corridor() { this.character = Cell.CORRIDOR; }
topLeftWall() { this.character = "┌"; }
topRightWall() { this.character = "┐"; }
bottomLeftWall() { this.character = "└"; }
bottomRightWall() { this.character = "┘"; }
horizontalWall() { this.character = "─"; }
verticalWall() { this.character = "│"; }
doorClosed() { this.character = Cell.DOOR_CLOSED; }
isEmpty() { return !this.character || this.character === " "; }
isDoor() { return this.character === Cell.DOOR_CLOSED; }
isCorridor() { return this.character === Cell.CORRIDOR; }
canBecomeDoor() { return this.character === "─" || this.character === "│" }
}
class Point {
x = 0;
y = 0;
constructor(x, y) {
if (x) { this.x = x; }
if (y) { this.y = y; }
}
*neighbors() {
const x = this.x;
const y = this.y;
yield new Point(x - 1, y - 1);
yield new Point(x, y - 1);
yield new Point(x + 1, y - 1);
yield new Point(x - 1, y);
yield new Point(x + 1, y);
yield new Point(x - 1, y + 1);
yield new Point(x, y + 1);
yield new Point(x + 1, y + 1);
}
equalsPoint(other) { return this.x === other.x && this.y === other.y; }
}
class Size {
width = 0;
height = 0;
constructor(width, height) {
this.width = width;
this.height = height;
}
}
class Rect {
origin = new Point();
size = new Size();
static fromCoordinates(x, y, w, h) {
return new Rect(new Point(x, y), new Size(w, h));
}
constructor(origin, size) {
this.origin = origin;
this.size = size;
}
get minX() { return this.origin.x; }
get minY() { return this.origin.y; }
get maxX() { return this.origin.x + this.size.width; }
get maxY() { return this.origin.y + this.size.height; }
get width() { return this.size.width; }
get height() { return this.size.height; }
get area() { return this.size.width * this.size.height; }
*xCoordinates() {
for (let x = this.minX; x <= this.maxX; x++) {
yield x;
}
}
*yCoordinates() {
for (let y = this.minY; y <= this.maxY; y++) {
yield y;
}
}
insetRect(inset) {
const twiceInset = 2 * inset;
return Rect.fromCoordinates(
this.origin.x + inset,
this.origin.y + inset,
this.size.width - twiceInset,
this.size.height - twiceInset
);
}
intersects(otherRect) {
if (otherRect.minX > this.maxX)
return false;
if (otherRect.maxX < this.minX)
return false;
if (otherRect.minY > this.maxY)
return false;
if (otherRect.maxY < this.minY)
return false;
return true;
}
}
class Grid {
#size;
#cells = [];
#rooms = [];
constructor(width, height) {
this.#size = new Size(width, height);
this.#cells = new Array(width * height);
for (let i = 0; i < this.#cells.length; i++) {
this.#cells[i] = new Cell(" ");
}
}
get bounds() { return new Rect(new Point(), this.#size); }
get width() { return this.#size.width; }
get height() { return this.#size.height; }
get rooms() { return this.#rooms; }
generate(p, roomGeneratorClass, tunnelGeneratorClass) {
this.#generateRooms(p, new roomGeneratorClass(this, NUMBER_OF_ROOMS));
this.#placeStairs();
this.#digCorridors(p, new tunnelGeneratorClass(this, TUNNEL_PASSES));
}
#generateRooms(p, generator) {
generator.generate(p);
this.#rooms = generator.rooms;
}
#placeStairs() {
const indexOfRoomWithUpStairs = randomInt(this.#rooms.length);
const coordinateOfUpStair = this.#rooms[indexOfRoomWithUpStairs].randomPoint();
this.cellAt(coordinateOfUpStair.x, coordinateOfUpStair.y).upStair();
while (true) {
let indexOfRoomForDownStair = randomInt(this.#rooms.length);
if (indexOfRoomForDownStair == indexOfRoomWithUpStairs) {
continue;
}
const coordinateOfDownStair = this.#rooms[indexOfRoomForDownStair].randomPoint();
this.cellAt(coordinateOfDownStair.x, coordinateOfDownStair.y).downStair();
break;
}
}
#digCorridors(p, generator) {
generator.generate(p);
}
pointIsInBounds(pt) { return pt.x >= 0 && pt.x < this.width && pt.y >= 0 && pt.y < this.height; }
cellAt(x, y) { return this.#cells[y * this.width + x]; }
}
class Room {
#rect;
constructor(rect) {
this.#rect = rect;
}
get bounds() { return this.#rect; }
get minX() { return this.#rect.minX; }
get minY() { return this.#rect.minY; }
get maxX() { return this.#rect.maxX; }
get maxY() { return this.#rect.maxY; }
randomPoint() {
return new Point(
this.#rect.minX + 1 + randomInt(this.#rect.size.width - 2),
this.#rect.minY + 1 + randomInt(this.#rect.size.height - 2)
);
}
transformCellAt(pt, cell) {
const minX = this.minX;
const minY = this.minY;
const maxX = this.maxX;
const maxY = this.maxY;
const x = pt.x;
const y = pt.y;
if (y === minY && x === minX) { cell.topLeftWall(); }
else if (y === minY && x === maxX) { cell.topRightWall(); }
else if (y === maxY && x === minX) { cell.bottomLeftWall(); }
else if (y === maxY && x === maxX) { cell.bottomRightWall(); }
else if (y === minY || y === maxY) { cell.horizontalWall(); }
else if (x === minX || x === maxX) { cell.verticalWall(); }
else if ((x > minX && x < maxX) && (y > minY && y < maxY)) { return cell.floor(); }
}
}
class NRandomRoomsGenerator {
static MIN_ROOM_DIMENSION = 7;
static MAX_ROOM_DIMENSION = 12;
#numberOfRooms = 12;
#rooms;
#grid;
constructor(grid, numberOfRooms) {
this.#grid = grid;
if (numberOfRooms) {
this.#numberOfRooms = numberOfRooms;
}
}
get rooms() {
if (!this.#rooms) {
this.#generateRooms();
}
return this.#rooms;
}
get #bounds() { return this.#grid.bounds; }
generate(p) {
this.#generateRooms();
for (let i = 0; i < this.#rooms.length; i++) {
let room = this.#rooms[i];
for (let y = room.minY; y <= room.maxY; y++) {
for (let x = room.minX; x <= room.maxX; x++) {
let point = new Point(x, y);
let cell = this.#grid.cellAt(x, y);
room.transformCellAt(point, cell);
cell.characterColor = p.color(255);
}
}
}
}
#generateRooms() {
let rects = new Array();
const sizeRange = NRandomRoomsGenerator.MAX_ROOM_DIMENSION - NRandomRoomsGenerator.MIN_ROOM_DIMENSION;
while (rects.length < this.#numberOfRooms) {
const randomSize = new Size(
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange),
NRandomRoomsGenerator.MIN_ROOM_DIMENSION + randomInt(sizeRange)
);
const randomOrigin = new Point(
this.#bounds.minX + randomInt(this.#bounds.maxX - randomSize.width),
this.#bounds.minY + randomInt(this.#bounds.maxY - randomSize.height)
);
const proposedRoomRect = new Rect(randomOrigin, randomSize);
// Check that the rect doesn't intersect with any other rects.
if (rects.some(e => e.intersects(proposedRoomRect))) {
continue;
}
rects.push(proposedRoomRect);
}
rects.sort((a, b) => {
if (a.origin.x < b.origin.x) { return -1; }
if (a.origin.x > b.origin.x) { return 1; }
if (a.origin.y < b.origin.y) { return -1; }
if (a.origin.y > b.origin.y) { return 1; }
return 0;
});
this.#rooms = rects.map(r => new Room(r.insetRect(1)));
}
}
class TunnelGenerator {
static MAX_DOORS = 100;
#grid;
#passes = 3;
#numberOfDoorsPlaced = 0;
constructor(grid, passes) {
this.#grid = grid;
if (passes) { this.#passes = passes; }
}
generate(p) {
console.group("Digging tunnels");
if (this.#passes >= 1) {
this.#doPassOne(p);
}
if (this.#passes >= 2) {
this.#doPassTwo(p);
}
if (this.#passes >= 3) {
this.#doPassThree(p);
}
console.groupEnd();
}
#doPassOne(p) {
console.group("Pass 1");
this.#iterateAndConnectRoomsWithOffset(p, 1);
console.groupEnd();
}
#doPassTwo(p) {
console.group("Pass 2");
this.#iterateAndConnectRoomsWithOffset(p, 2);
console.groupEnd();
}
#doPassThree(p) {
console.group("Pass 3");
this.#iterateAndConnectRoomsWithOffset(p, 3);
console.groupEnd();
}
#iterateAndConnectRoomsWithOffset(p, offset) {
let rooms = this.#grid.rooms;
const numberOfRooms = rooms.length;
for (let i = 0; i < numberOfRooms - offset; i++) {
let fromRoom = rooms[i];
let toRoom = rooms[i + offset];
let [fromPoint, toPoint] = this.#findPointFromRoomToRoom(fromRoom, toRoom);
if (!fromPoint || !toPoint) {
continue;
}
for (let neighbor of fromPoint.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if ((neighbor.x === fromPoint.x || neighbor.y === fromPoint.y) && cell.canBecomeDoor()) {
cell.doorClosed();
this.#numberOfDoorsPlaced++;
break;
}
}
const successfullyFoundTargetPoint = this.#digCorridorFromPointToPoint(p, fromPoint, toPoint);
if (successfullyFoundTargetPoint) {
for (let neighbor of toPoint.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if ((neighbor.x === toPoint.x || neighbor.y === toPoint.y) && cell.canBecomeDoor()) {
cell.doorClosed();
this.#numberOfDoorsPlaced++;
break;
}
}
}
}
}
/**
* Dig a corridor from fromPoint to toPoint, assuming that both points are
* adjacent to valid locations for doors on the map.
*
* This is as close a copy of dig_corridor in the Nethack source as I could
* muster. It's not exactly pretty. This method assumed
*/
#digCorridorFromPointToPoint(p, fromPoint, toPoint) {
const MAX_STEPS = 500;
if (!fromPoint || !toPoint) {
return;
}
const fromX = fromPoint.x;
const toX = toPoint.x;
const fromY = fromPoint.y;
const toY = toPoint.y;
let curX = fromX;
let curY = fromY;
let dx = 0;
let dy = 0;
// Set up the initial direction of the dig.
if (toX > curX) {
dx = 1;
} else if (toY > curY) {
dy = 1;
} else if (toX < curX) {
dx = -1;
} else {
dy = -1;
}
curX -= dx;
curY -= dy;
let steps = 0;
while (curX !== toX || curY !== toY) {
if (steps++ > MAX_STEPS) {
return false;
}
curX += dx;
curY += dy;
if (curX >= this.#grid.width - 1 || curX <= 0 || curY <= 0 || curY >= this.#grid.height - 1) {
return false;
}
let cell = this.#grid.cellAt(curX, curY);
if (cell.isEmpty()) {
cell.corridor();
} else if (!cell.isCorridor()) {
return false;
}
let dix = Math.abs(curX - toX);
let diy = Math.abs(curY - toY);
if (dix > diy && diy) {
const random = randomInt(dix - diy + 1);
if (!random) {
dix = 0;
}
} else if (diy > dix && dix) {
const random = randomInt(dix - diy + 1);
if (!random) {
diy = 0;
}
}
if (dy && dix > diy) {
const ddx = curX > toX ? -1 : 1;
let cell = this.#grid.cellAt(curX + ddx, curY);
if (cell.isEmpty() || cell.isCorridor()) {
dx = ddx;
dy = 0;
continue;
}
} else if (dx && diy > dix) {
const ddy = curY > toY ? -1 : 1;
let cell = this.#grid.cellAt(curX, curY + ddy);
if (cell.isEmpty() || cell.isCorridor()) {
dy = ddy;
dx = 0;
continue;
}
}
cell = this.#grid.cellAt(curX + dx, curY + dy);
if (cell.isEmpty() || cell.isCorridor()) {
continue;
}
if (dx) {
dx = 0;
dy = toY < curY ? -1 : 1;
} else {
dy = 0;
dx = toX < curX ? -1 : 1;
}
cell = this.#grid.cellAt(curX + dx, curY + dy);
if (cell.isEmpty() || cell.isCorridor()) {
continue;
}
dy = -dy;
dx = -dx;
}
return true;
}
#findPointFromRoomToRoom(fromRoom, toRoom) {
const fromRoomBounds = fromRoom.bounds;
const toRoomBounds = toRoom.bounds;
let foundFromPoint = false;
let foundToPoint = false;
let fromPoint;
let toPoint;
if (fromRoomBounds.maxX < toRoomBounds.minX) {
// fromRoom is farther left than toRoom
fromPoint = new Point(fromRoomBounds.maxX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let y of fromRoomBounds.yCoordinates()) {
fromPoint.y = y;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(toRoomBounds.minX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let y of toRoomBounds.yCoordinates()) {
toPoint.y = y;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.x += 1;
toPoint.x -= 1;
} else if (fromRoomBounds.minX > toRoomBounds.maxX) {
// fromRoom is farther right than toRoomBounds.
fromPoint = new Point(toRoomBounds.maxX, toRoomBounds.minY + 1 + randomInt(toRoomBounds.height - 2));
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let y of toRoomBounds.yCoordinates()) {
fromPoint.y = y;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(fromRoomBounds.minX, fromRoomBounds.minY + 1 + randomInt(fromRoomBounds.height - 2));
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let y of fromRoomBounds.yCoordinates()) {
toPoint.y = y;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.x -= 1;
toPoint.x += 1;
} else if (fromRoomBounds.maxY < (toRoomBounds.minY - 1)) {
// fromRoom is above toRoom
fromPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.maxY);
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let x of fromRoomBounds.xCoordinates()) {
fromPoint.x = x;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.minY);
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let x of toRoomBounds.xCoordinates()) {
toPoint.x = x;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.y += 1;
toPoint.y -= 1;
} else if (fromRoomBounds.minY > (toRoomBounds.maxY + 1)) {
// fromRoom is below toRoom
fromPoint = new Point(toRoomBounds.minX + 1 + randomInt(toRoomBounds.width - 2), toRoomBounds.maxY);
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
for (let x of toRoomBounds.xCoordinates()) {
fromPoint.x = x;
foundFromPoint = this.#canPlaceDoorAt(fromPoint);
if (foundFromPoint) {
break;
}
}
if (!foundFromPoint) {
return [];
}
toPoint = new Point(fromRoomBounds.minX + 1 + randomInt(fromRoomBounds.width - 2), fromRoomBounds.minY);
foundToPoint = this.#canPlaceDoorAt(toPoint);
for (let x of fromRoomBounds.xCoordinates()) {
toPoint.x = x;
foundToPoint = this.#canPlaceDoorAt(toPoint);
if (foundToPoint) {
break;
}
}
if (!foundToPoint) {
return [];
}
fromPoint.y += 1;
toPoint.y -= 1;
}
return [fromPoint, toPoint];
}
#canPlaceDoorAt(pt) {
if (this.#numberOfDoorsPlaced > TunnelGenerator.MAX_DOORS) {
return false;
}
if (!this.#grid.cellAt(pt.x, pt.y).canBecomeDoor()) {
return false;
}
for (let neighbor of pt.neighbors()) {
if (!this.#grid.pointIsInBounds(neighbor)) {
continue;
}
let cell = this.#grid.cellAt(neighbor.x, neighbor.y);
if (cell.isDoor()) {
return false;
}
}
return true;
}
}
function randomInt(n) {
max = Math.floor(n);
return Math.floor(Math.random() * max);
}
let grid;
new p5(p => {
const CELL_WIDTH = 20;
const CELL_HEIGHT = Math.floor(CELL_WIDTH * 1.3);
p.setup = () => {
const container = document.querySelector('#dungeon-background');
console.assert(container, "Missing #dungeon-background element");
canvasWidth = parseFloat(getComputedStyle(container).width);
canvasHeight = parseFloat(getComputedStyle(container).height);
let canvas = p.createCanvas(canvasWidth, canvasHeight);
canvas.canvas.removeAttribute('style');
container.appendChild(canvas.canvas);
p.pixelDensity(p.displayDensity());
p.textFont("Courier");
const gridBounds = Rect.fromCoordinates(
0, 0,
Math.max(80, Math.ceil(canvasWidth / CELL_WIDTH) - 1),
Math.max(24, Math.ceil(canvasHeight / CELL_HEIGHT) - 1)
);
console.log(`Generating grid with size ${gridBounds.size.width} x ${gridBounds.size.height}`);
grid = new Grid(gridBounds.size.width, gridBounds.size.height);
grid.generate(p, NRandomRoomsGenerator, TunnelGenerator);
};
p.draw = () => {
console.log("Drawing");
p.textSize(CELL_HEIGHT);
for (let y = 0; y < grid.height; y++) {
for (let x = 0; x < grid.width; x++) {
let cell = grid.cellAt(x, y);
let fillColor = cell.characterColor ? cell.characterColor : p.color(255);
p.fill(fillColor);
p.textAlign(p.CENTER, p.CENTER);
p.text(cell.character, x * CELL_WIDTH, y * CELL_HEIGHT, CELL_WIDTH, CELL_HEIGHT);
}
}
p.noLoop();
};
}, '#dungeon-background');

View file

@ -1,4 +1,8 @@
import rr from "scripts/lib/railroad.js";
{{ with resources.Get "scripts/lib/railroad.js" | fingerprint "sha512" }}
import rr from "{{ .RelPermalink }}";
{{ else }}
{{ errorf "Unable to get railroad.js resource" }}
{{ end }}
class RailroadDiagramManager {
constructor() {

View file

@ -1,123 +0,0 @@
class RubiksCubeScrambler extends HTMLElement {
static #RandomMoveHysteresisMaxLength = 2;
#shadowRoot;
#movesListElement;
#numberOfMovesToGenerate = 25;
constructor() {
super();
this.#shadowRoot = this.attachShadow({ mode: "open" });
}
scramble() {
console.log("Randomizing Rubik's cube...");
const movesList = this.#movesListElement;
while (movesList.childElementCount > this.#numberOfMovesToGenerate) {
movesList.removeChild(movesList.lastChild);
}
let randomMoveHysteresis = [];
for (let i = 0; i < this.#numberOfMovesToGenerate; i++) {
const randomMove = this.#randomMove(randomMoveHysteresis);
let moveItem;
if (i < movesList.childElementCount) {
moveItem = movesList.children[i];
} else {
moveItem = document.createElement("li");
movesList.appendChild(moveItem);
}
moveItem.classList.add("scrambler__move");
moveItem.classList.remove("scrambler__move--start", "scrambler__move--end");
if (randomMove.includes("2")) {
moveItem.classList.add("scrambler__move--start");
} else if (randomMove.includes("'")) {
moveItem.classList.add("scrambler__move--end");
}
moveItem.innerText = randomMove;
}
}
#randomMove(hysteresis) {
const faces = "FBLRUD";
let move;
do {
move = faces.charAt(Math.floor(Math.random() * faces.length));
} while (hysteresis && hysteresis.includes(move));
if (hysteresis) {
hysteresis.unshift(move);
while (hysteresis.length > RubiksCubeScrambler.#RandomMoveHysteresisMaxLength) {
hysteresis.pop();
}
}
const modifierFactor = Math.random();
if (modifierFactor < 0.33333) {
move = "2" + move;
} else if (modifierFactor < 0.666666) {
move = move + "'";
}
return move;
}
#removeAllMoves() {
const element = this.#movesListElement;
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
}
// MARK: Custom Element
connectedCallback() {
let template = document.getElementById("rubiks-cube-scrambler-template");
console.assert(template, "Couldn't find RubiksCubeScrambler component template in the document");
const shadowRoot = this.#shadowRoot;
shadowRoot.appendChild(template.content.cloneNode(true));
this.#movesListElement = shadowRoot.querySelector(".scrambler__move-list");
shadowRoot
.querySelector("button[name='scramble']")
.addEventListener("click", () => this.scramble());
const patternLengthInputElement = shadowRoot.querySelector(".scrambler__pattern-length > input");
patternLengthInputElement.value = this.#numberOfMovesToGenerate;
patternLengthInputElement.addEventListener("input", event => {
try {
const integerValue = parseInt(event.target.value);
this.#numberOfMovesToGenerate = integerValue;
} catch (e) {
console.error("Non-integer value of pattern length field", e);
}
});
this.scramble();
}
attributeChangedCallback(name, oldValue, newValue) {
console.debug("RubiksCubeScrambler attribute changed", name, oldValue, newValue);
if (name === "count") {
try {
let newIntValue = parseInt(newValue);
this.#numberOfMovesToGenerate = newIntValue;
} catch (e) {
console.error("`count` attribute should have an integer value.", e);
}
}
}
}
window.customElements.define("rubiks-cube-scrambler", RubiksCubeScrambler);

View file

@ -1,169 +0,0 @@
// Eryn Wells <eryn@erynwells.me>
class RubySwitch extends HTMLElement {
static controlSizeInPixels = 32;
static thumbTransitionDuration = 0.1;
static settings = [
{
id: "ruby-switch-none",
value: "none",
label: "あ"
},
{
id: "ruby-switch-both",
value: "both",
label: "<ruby>あ<rt>a</rt></ruby>",
default: true
},
{
id: "ruby-switch-hidden",
value: "hidden",
label: "ab"
},
];
#root;
#thumb;
constructor() {
super();
this.#updateValue(RubySwitch.settings.find(obj => obj.default).value);
this.addEventListener("RubyStyleChanged", event => {
this.#updateValue(event.detail.style);
});
this.#root = this.attachShadow({ mode: "closed" });
this.#buildShadowDOM();
this.#updateThumbPosition(this.#root.querySelector(".control[data-default]"));
}
#updateValue(style) {
this.setAttribute("value", style);
}
get #stylesheet() {
const controlSize = RubySwitch.controlSizeInPixels;
const halfControlSize = controlSize / 2;
return `
#ruby-controls {
box-sizing: border-box;
display: inline-block;
position: relative;
}
#controls {
border: none;
box-sizing: border-box;
display: inline grid;
grid-template-columns: repeat(3, ${controlSize}px);
margin: 0;
overflow: none;
padding: 0;
}
#thumb {
box-sizing: border-box;
box-shadow: 2px 2px 6px #ccc;
border: 0.5px solid #aaa;
border-radius: ${halfControlSize}px;
position: absolute;
top: 0;
height: ${controlSize}px;
width: ${controlSize}px;
z-index: 50;
transition: left ${RubySwitch.thumbTransitionDuration}s;
}
.control {
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
height: ${controlSize}px;
width: ${controlSize}px;
}
b {
font-weight: normal;
cursor: pointer;
}
label {
z-index: 2;
text-align: center;
}
#ruby-switch-both {
}
`;
}
#buildShadowDOM() {
const root = this.#root;
const style = document.createElement("style");
style.textContent = this.#stylesheet;
root.appendChild(style);
let container = document.createElement("div");
container.id = "ruby-controls";
root.appendChild(container);
let controls = document.createElement("div");
controls.id = "controls";
container.appendChild(controls);
for (const desc of RubySwitch.settings) {
let control = document.createElement("div");
control.classList.add("control")
control.id = desc.id;
control.dataset.value = desc.value;
if (desc.default) {
control.dataset.default = "";
}
controls.appendChild(control);
control.addEventListener("click", event => {
event.stopPropagation();
event.preventDefault();
this.#updateThumbPosition(event.currentTarget);
this.#root.dispatchEvent(new CustomEvent("RubyStyleChanged", {
bubbles: true,
composed: true,
detail: {
style: control.dataset.value,
},
}));
}, { capture: true });
const label = document.createElement("b");
label.innerHTML = desc.label;
control.appendChild(label);
}
const thumb = document.createElement("div");
this.#thumb = thumb;
thumb.id = "thumb";
container.appendChild(thumb);
}
#updateThumbPosition(selectedControl) {
const controls = this.#root.querySelector("#controls");
const trackBoundingRect = controls.getBoundingClientRect();
const controlBoundingRect = selectedControl.getBoundingClientRect();
const offset = controlBoundingRect.left - trackBoundingRect.left;
this.#thumb.style.left = `${offset}px`;
}
}
customElements.define("ruby-switch", RubySwitch);

77
assets/styles/blog.css Normal file
View file

@ -0,0 +1,77 @@
:root {
--post-item-highlight-color: #efefef;
}
@media (prefers-color-scheme: dark) {
:root {
--post-item-highlight-color: #121212;
}
}
.blog > .highlight { margin-block-end: var(--body-item-spacing); }
.post-nav {
align-items: baseline;
display: flex;
margin-top: 3.5rem;
}
.post-nav .next {
margin-left: auto;
}
.post-single footer {
margin-block-start: var(--body-item-spacing);
}
.blog.list > ul {
list-style: none;
}
.blog.list > ul > li {
align-items: baseline;
border-radius: 6px;
display: grid;
gap: 1rem;
grid-template-columns: minmax(min-content, 10vh) minmax(min-content, 3vh) auto max-content;
margin-block-end: 0.25rem;
transition: background-color 0.25s;
}
.blog.list > ul > li:hover {
background-color: var(--post-item-highlight-color);
}
.blog.list > ul > li > a { color: inherit; }
.blog.list > ul > li > a:hover { text-decoration: none; }
.blog.list > ul > li > time:nth-child(2) { text-align: end; }
.blog.list > ul > li > .draft { align-self: center; }
@supports (grid-template-columns: subgrid) {
.blog.list {
display: grid;
gap: 1rem;
grid-template-columns: minmax(min-content, 10vh) minmax(min-content, 3vh) auto max-content;
}
.blog.list > ul {
display: grid;
row-gap: 0.25rem;
grid-column: 1 / -1;
grid-template-columns: subgrid;
list-style: none;
}
.blog.list > ul > li {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
margin-block-end: 0;
}
.blog.list > h5 {
grid-column: 1 / -1;
margin-block: 1rem 0;
}
.blog.list > h5:first-child {
margin-block-start: 0;
}
}
.blog.list > ul > li > :first-child {
text-align: end;
}

View file

@ -0,0 +1,67 @@
/* development.css
* Some styles for development UI.
* Eryn Wells <eryn@erynwells.me>
*/
details:has(.photo-params.debug) {
margin-block-end: var(--body-item-spacing);
}
.draft {
color: red;
display: inline-block;
font-family: 'SF Pro', sans-serif;
font-size: 1.5rem;
font-weight: bold;
letter-spacing: 3px;
text-transform: uppercase;
}
.draft:before {
content: "[";
}
.draft:after {
content: "]";
}
#debug-page-info {
background-color: var(--background-color);
border: 1px solid var(--separator-color);
border-radius: 6px;
box-shadow: 4px 5px 5px var(--box-shadow-color);
font-size: 1.75rem;
padding: 0.5rem;
position: fixed;
left: 2rem;
bottom: 2rem;
width: max-content;
}
#debug-page-info > details {
margin: 0.5rem;
}
#debug-page-info > summary {
font-family: var(--font-family-heading);
}
#debug-page-info > details > table {
border-collapse: collapse;
display: block;
margin-block-start: 0.5em;
margin-inline-start: 1em;
}
#debug-page-info > details > table > tbody > tr > td {
border: 1px solid rgb(var(--dk-gray));
padding: 0.3em;
}
#debug-page-info > details > table > tbody > tr:nth-child(even) {
background-color: var(--separator-color);
}
.photo-params.debug {
width: 100%;
}

166
assets/styles/home.css Normal file
View file

@ -0,0 +1,166 @@
:root {
--animation-offset: 6px;
--font-size-max: 12px;
}
body {
display: flex;
flex-direction: column;
font-size: 2rem;
justify-content: center;
margin: auto;
}
h1 {
font-size: 5rem;
line-height: 1.3;
margin: 0;
padding: 0;
}
@media (max-width: 599px) {
h1 { font-size: 4.25rem; }
}
@media (max-width: 599px) {
html {
--font-size-scale-factor: 1.33vmax;
}
}
main {
margin-block-start: 10vh;
width: inherit;
}
main .grid {
align-items: baseline;
display: grid;
gap: 0 2rem;
grid-template-columns: minmax(min-content, 1fr) minmax(min-content, 2fr);
grid-template-rows: repeat(2, max-content);
grid-template-areas:
"title blurb"
"title nav";
padding-inline: 1em;
width: min-content;
}
@media (max-width: 599px) {
main .grid {
gap: 1rem 0;
grid-template-columns: 1fr;
grid-template-rows: repeat(3, max-content);
grid-template-areas:
"title"
"blurb"
"nav";
justify-items: center;
text-align: center;
}
}
main h1 {
letter-spacing: 0.025em;
text-align: end;
}
@media (max-width: 599px) {
main h1 { text-align: center; }
}
nav {
align-items: baseline;
align-self: center;
display: flex;
list-style: none;
text-transform: lowercase;
}
p {
margin: 0;
}
h1 { grid-area: title; }
#content { grid-area: blurb }
nav { grid-area: nav }
h1, #content > p, nav { position: relative; }
h1 {
animation: left-fade-in var(--transition-duration) ease-in-out;
}
@media (max-width: 599px) {
h1 {
animation: top-fade-in var(--transition-duration) ease-in-out;
}
}
#content > p, nav {
animation: right-fade-in var(--transition-duration) ease-in-out;
}
@media (max-width: 599px) {
#content > p, nav {
animation: bottom-fade-in var(--transition-duration) ease-in-out;
}
}
@keyframes left-fade-in {
from {
opacity: 0%;
left: var(--animation-offset);
}
33% {
opacity: 0%;
left: var(--animation-offset);
}
to {
opacity: 100%;
left: 0;
}
}
@keyframes right-fade-in {
from {
opacity: 0%;
left: calc(-1 * var(--animation-offset));
}
33% {
opacity: 0%;
left: calc(-1 * var(--animation-offset));
}
to {
opacity: 100%;
left: 0;
}
}
@keyframes top-fade-in {
from {
opacity: 0%;
bottom: calc(-1 * var(--animation-offset));
}
33% {
opacity: 0%;
bottom: calc(-1 * var(--animation-offset));
}
to {
opacity: 100%;
bottom: 0;
}
}
@keyframes bottom-fade-in {
from {
opacity: 0%;
bottom: var(--animation-offset);
}
33% {
opacity: 0%;
bottom: var(--animation-offset);
}
to {
opacity: 100%;
bottom: 0;
}
}
.platter {
padding: 1.5rem 3rem;
}

192
assets/styles/monokai.css Normal file
View file

@ -0,0 +1,192 @@
:root {
--highlight-background-color: rgb(var(--super-lt-gray));
--highlight-foreground-color: var(--foreground-body-color);
}
@media (prefers-color-scheme: dark) {
:root {
}
}
.chroma {
line-height: var(--body-line-height);
}
.bg {
color: var(--highlight-foreground-color);
background-color: var(--highlight-background-color);
}
.chroma {
color: var(--highlight-foreground-color);
background-color: var(--highlight-background-color);
}
/* Other */ .chroma .x { }
/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
/* CodeLine */ .chroma .cl { }
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* Keyword */ .chroma .k { color: #00a8c8 }
/* KeywordConstant */ .chroma .kc { color: #00a8c8 }
/* KeywordDeclaration */ .chroma .kd { color: #00a8c8 }
/* KeywordNamespace */ .chroma .kn { color: #f92672 }
/* KeywordPseudo */ .chroma .kp { color: #00a8c8 }
/* KeywordReserved */ .chroma .kr { color: #00a8c8 }
/* KeywordType */ .chroma .kt { color: #00a8c8 }
/* Name */ .chroma .n { color: #111111 }
/* NameAttribute */ .chroma .na { color: #75af00 }
/* NameBuiltin */ .chroma .nb { color: #111111 }
/* NameBuiltinPseudo */ .chroma .bp { color: #111111 }
/* NameClass */ .chroma .nc { color: #75af00 }
/* NameConstant */ .chroma .no { color: #00a8c8 }
/* NameDecorator */ .chroma .nd { color: #75af00 }
/* NameEntity */ .chroma .ni { color: #111111 }
/* NameException */ .chroma .ne { color: #75af00 }
/* NameFunction */ .chroma .nf { color: #75af00 }
/* NameFunctionMagic */ .chroma .fm { color: #111111 }
/* NameLabel */ .chroma .nl { color: #111111 }
/* NameNamespace */ .chroma .nn { color: #111111 }
/* NameOther */ .chroma .nx { color: #75af00 }
/* NameProperty */ .chroma .py { color: #111111 }
/* NameTag */ .chroma .nt { color: #f92672 }
/* NameVariable */ .chroma .nv { color: #111111 }
/* NameVariableClass */ .chroma .vc { color: #111111 }
/* NameVariableGlobal */ .chroma .vg { color: #111111 }
/* NameVariableInstance */ .chroma .vi { color: #111111 }
/* NameVariableMagic */ .chroma .vm { color: #111111 }
/* Literal */ .chroma .l { color: #ae81ff }
/* LiteralDate */ .chroma .ld { color: #d88200 }
/* LiteralString */ .chroma .s { color: #d88200 }
/* LiteralStringAffix */ .chroma .sa { color: #d88200 }
/* LiteralStringBacktick */ .chroma .sb { color: #d88200 }
/* LiteralStringChar */ .chroma .sc { color: #d88200 }
/* LiteralStringDelimiter */ .chroma .dl { color: #d88200 }
/* LiteralStringDoc */ .chroma .sd { color: #d88200 }
/* LiteralStringDouble */ .chroma .s2 { color: #d88200 }
/* LiteralStringEscape */ .chroma .se { color: #8045ff }
/* LiteralStringHeredoc */ .chroma .sh { color: #d88200 }
/* LiteralStringInterpol */ .chroma .si { color: #d88200 }
/* LiteralStringOther */ .chroma .sx { color: #d88200 }
/* LiteralStringRegex */ .chroma .sr { color: #d88200 }
/* LiteralStringSingle */ .chroma .s1 { color: #d88200 }
/* LiteralStringSymbol */ .chroma .ss { color: #d88200 }
/* LiteralNumber */ .chroma .m { color: #ae81ff }
/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
/* Operator */ .chroma .o { color: #f92672 }
/* OperatorWord */ .chroma .ow { color: #f92672 }
/* Punctuation */ .chroma .p { color: #111111 }
/* Comment */ .chroma .c { color: #75715e }
/* CommentHashbang */ .chroma .ch { color: #75715e }
/* CommentMultiline */ .chroma .cm { color: #75715e }
/* CommentSingle */ .chroma .c1 { color: #75715e }
/* CommentSpecial */ .chroma .cs { color: #75715e }
/* CommentPreproc */ .chroma .cp { color: #75715e }
/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
/* Generic */ .chroma .g { }
/* GenericDeleted */ .chroma .gd { }
/* GenericEmph */ .chroma .ge { font-style: italic }
/* GenericError */ .chroma .gr { }
/* GenericHeading */ .chroma .gh { }
/* GenericInserted */ .chroma .gi { }
/* GenericOutput */ .chroma .go { }
/* GenericPrompt */ .chroma .gp { }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { }
/* GenericTraceback */ .chroma .gt { }
/* GenericUnderline */ .chroma .gl { }
/* TextWhitespace */ .chroma .w { }
@media (prefers-color-scheme: dark) {
/* Background */ .bg { color: #f8f8f2; background-color: #272822 }
/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; }
/* Other */ .chroma .x { }
/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
/* CodeLine */ .chroma .cl { }
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
/* Keyword */ .chroma .k { color: #66d9ef }
/* KeywordConstant */ .chroma .kc { color: #66d9ef }
/* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
/* KeywordNamespace */ .chroma .kn { color: #f92672 }
/* KeywordPseudo */ .chroma .kp { color: #66d9ef }
/* KeywordReserved */ .chroma .kr { color: #66d9ef }
/* KeywordType */ .chroma .kt { color: #66d9ef }
/* Name */ .chroma .n { }
/* NameAttribute */ .chroma .na { color: #a6e22e }
/* NameBuiltin */ .chroma .nb { color: inherit; }
/* NameBuiltinPseudo */ .chroma .bp { }
/* NameClass */ .chroma .nc { color: #a6e22e }
/* NameConstant */ .chroma .no { color: #66d9ef }
/* NameDecorator */ .chroma .nd { color: #a6e22e }
/* NameEntity */ .chroma .ni { }
/* NameException */ .chroma .ne { color: #a6e22e }
/* NameFunction */ .chroma .nf { color: #a6e22e }
/* NameFunctionMagic */ .chroma .fm { }
/* NameLabel */ .chroma .nl { }
/* NameNamespace */ .chroma .nn { }
/* NameOther */ .chroma .nx { color: #a6e22e }
/* NameProperty */ .chroma .py { }
/* NameTag */ .chroma .nt { color: #f92672 }
/* NameVariable */ .chroma .nv { color: inherit; }
/* NameVariableClass */ .chroma .vc { }
/* NameVariableGlobal */ .chroma .vg { }
/* NameVariableInstance */ .chroma .vi { }
/* NameVariableMagic */ .chroma .vm { }
/* Literal */ .chroma .l { color: #ae81ff }
/* LiteralDate */ .chroma .ld { color: #e6db74 }
/* LiteralString */ .chroma .s { color: #e6db74 }
/* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
/* LiteralStringChar */ .chroma .sc { color: #e6db74 }
/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
/* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
/* LiteralStringEscape */ .chroma .se { color: #ae81ff }
/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
/* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
/* LiteralStringOther */ .chroma .sx { color: #e6db74 }
/* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
/* LiteralNumber */ .chroma .m { color: #ae81ff }
/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
/* Operator */ .chroma .o { color: #f92672 }
/* OperatorWord */ .chroma .ow { color: #f92672 }
/* Punctuation */ .chroma .p { color: inherit; }
/* Comment */ .chroma .c { color: #75715e }
/* CommentHashbang */ .chroma .ch { color: #75715e }
/* CommentMultiline */ .chroma .cm { color: #75715e }
/* CommentSingle */ .chroma .c1 { color: #75715e }
/* CommentSpecial */ .chroma .cs { color: #75715e }
/* CommentPreproc */ .chroma .cp { color: #75715e }
/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
/* Generic */ .chroma .g { }
/* GenericDeleted */ .chroma .gd { color: #f92672 }
/* GenericEmph */ .chroma .ge { font-style: italic }
/* GenericError */ .chroma .gr { }
/* GenericHeading */ .chroma .gh { }
/* GenericInserted */ .chroma .gi { color: #a6e22e }
/* GenericOutput */ .chroma .go { }
/* GenericPrompt */ .chroma .gp { }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { color: #75715e }
/* GenericTraceback */ .chroma .gt { }
/* GenericUnderline */ .chroma .gl { }
/* TextWhitespace */ .chroma .w { }
}

167
assets/styles/photos.css Normal file
View file

@ -0,0 +1,167 @@
:root {
--date-item-background-color: rgb(var(--lt-gray));
--photo-params-background-color: rgb(var(--lt-gray));
--photo-params-container-background-color: rgb(var(--super-lt-gray));
--photo-params-color: rgb(var(--sub-dk-gray));
--photo-params-border-color: rgb(var(--super-lt-gray));
}
@media (prefers-color-scheme: dark) {
:root {
--date-item-background-color: rgb(var(--dk-gray));
--photo-params-background-color: rgb(var(--dk-gray));
--photo-params-container-background-color: rgb(var(--sub-dk-gray));
--photo-params-color: rgb(var(--super-lt-gray));
--photo-params-border-color: rgb(var(--sub-dk-gray));
}
}
.photos.list {
box-sizing: border-box;
max-width: none;
padding: 0 var(--body-item-spacing);
width: 100%;
}
.photos.list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 4px;
}
.photos.page > nav {
margin-block-end: var(--body-item-spacing);
}
.photos.list > a {
display: block;
line-height: 0;
}
.photos.list > a > img {
border-radius: 3px;
image-orientation: from-image;
}
.photos.list > div {
background-color: var(--date-item-background-color);
border-radius: 3px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.photos.list > div > h6 {
display: block;
font-size: 5rem;
margin: 0 auto;
letter-spacing: 0;
}
.photos.list > div > h6 > span {
text-align: end;
width: min-content;
position: relative;
display: inline-block;
padding-inline-end: 20px;
}
.photos.list > div > h6 > span::after {
top: 6px;
right: 0px;
position: absolute;
display: block;
content: "⏵︎";
font-size: 80%;
}
@media (max-width: calc(24px + 400px + 4px)) {
.photos.list > div > h6 > span {
width: max-content;
padding-block: calc(0.25 * var(--body-item-spacing));
}
.photos.list > div > h6 > span::after {
content: "";
}
}
.photo-params {
width: 100%;
}
.photo-params > .container {
display: block;
background-color: var(--photo-params-container-background-color);
border-radius: 10px;
margin: var(--body-item-spacing) auto;
padding: calc(var(--body-item-spacing) / 2);
width: 66%;
}
.photo-params > .container > table {
background-color: var(--photo-params-background-color);
color: var(--photo-params-color);
border-collapse: collapse;
border-radius: 6px;
margin: 0;
table-layout: fixed;
text-align: center;
width: 100%;
}
.photo-params table > thead > tr > td {
border-bottom: 1px solid var(--photo-params-border-color);
font-size: 80%;
font-weight: bold;
}
.photo-params tr.exposure-attributes > td {
border-top: 1px solid var(--photo-params-border-color);
border-left: 1px solid var(--photo-params-border-color);
}
.photo-params tr.exposure-attributes > td:first-child {
border-left: none;
}
.photo-params td {
font-size: 75%;
padding: 1rem;
}
.photo-params td:last-child {
text-align: end;
}
.photo-params td:first-child {
text-align: start;
}
.photo-params .make-model {
font-weight: bold;
}
.photo-params .size {
border-left: 0;
}
.photo-params .location {
border-right: 0;
}
.photo-params.debug thead {
font-size: 2rem;
font-weight: bold;
border-bottom: 2px solid rgb(var(--dk-gray));
}
.photo-params.debug {
width: 100%;
}
.photo-params.debug td {
border: 1px solid var(--photo-params-border-color);
overflow: scroll;
vertical-align: top;
}

View file

@ -0,0 +1,64 @@
:root {
--rect-fill: rgba(var(--mid-blue), 0.1);
}
@media (prefers-color-scheme: dark) {
:root {
--rect-fill: rgb(var(--mid-blue), 0.9);
}
}
svg.railroad-diagram path {
stroke-width: 2;
stroke: var(--html-color);
fill: none;
}
svg.railroad-diagram text {
font-family: var(--font-family-monospace);
text-anchor: middle;
white-space: pre;
fill: var(--html-color);
}
svg.railroad-diagram text.diagram-text {
font-size: 11px;
}
svg.railroad-diagram text.diagram-arrow {
font-size: 16px;
}
svg.railroad-diagram text.label {
text-anchor: start;
}
svg.railroad-diagram text.comment {
font: italic 12px var(--font-family-monospace);
}
svg.railroad-diagram g.non-terminal text {
/*font-style: italic;*/
}
svg.railroad-diagram rect {
stroke-width: 2;
stroke: var(--html-color);
fill: var(--rect-fill);
}
svg.railroad-diagram rect.group-box {
stroke: gray;
stroke-dasharray: 10 5;
fill: none;
}
svg.railroad-diagram path.diagram-text {
stroke-width: 2;
stroke: var(--html-color);
fill: var(--html-background-color);
cursor: help;
}
svg.railroad-diagram g.diagram-text:hover path.diagram-text {
fill: var(--rect-fill);
}

627
assets/styles/root.css Normal file
View file

@ -0,0 +1,627 @@
@layer reset, root, template, page;
:root {
--black: 0, 0, 0;
--sub-dk-gray: 16, 16, 16;
--dk-gray: 32, 32, 32;
--super-dk-gray: 80, 80, 80;
--mid-gray: 128, 128, 128;
--sub-lt-gray: 175, 175, 175;
--lt-gray: 223, 223, 223;
--super-lt-gray: 240, 240, 240;
--white: 255, 255, 255;
--lt-blue: 69, 212, 243;
--mid-blue: 26, 169, 239;
--dk-blue: 63, 46, 231;
--purple: 161, 49, 232;
--lilac: 187, 121, 245;
--site-nav-link-color: rgb(var(--mid-blue));
--separator-color: rgb(var(--lt-gray));
--header-border-color: var(--separator-color);
--footer-border-color: var(--separator-color);
--box-shadow-color: rgba(var(--lt-gray), 0.8);
--header-box-shadow-color: var(--box-shadow-color);
--font-family-body: Verdana, Helvetica, sans-serif;
--font-family-monospace: "Courier New", Courier, monospace;
--font-family-heading: Museo_Slab, Tahoma, sans-serif;
--font-family-site-heading: Museo_Slab, Tahoma, sans-serif;
--font-size-min: 6px;
--font-size-max: 8px;
--body-font-size: 2rem;
--body-item-spacing: 1em;
--body-line-height: 1.5;
--body-header-line-height: 1.1;
--body-code-background-color: rgb(var(--super-lt-gray));
--heading-color: rgb(var(--black));
--header-series-arrow-foreground-color: rgb(var(--sub-dk-gray));
--html-background-color: rgb(var(--white));
--html-color: rgba(var(--black), 0.8);
--nav-bulleted-spacing: 0.75rem;
--platter-background-color: rgba(var(--white), var(--platter-background-opacity));
--platter-background-opacity: 0.6;
--platter-backdrop-filter: blur(10px);
--content-width: 80rem;
--tag-foreground-color: rgb(var(--super-dk-gray));
--tag-background-color: rgb(var(--super-lt-gray));
--tag-spacer-foreground-color: rgb(var(--super-dk-gray));
--tag-hover-background-color: rgb(var(--sub-lt-gray));
--transition-duration: 0.7s;
--social-menu-padding: 1rem;
--menu-icon-size: 20px;
--twitter-icon: url(/icons/twitter.svg);
--github-icon: url(/icons/github.svg);
--instagram-icon: url(/icons/instagram.svg);
--feed-icon: url(/icons/rss.svg);
}
@media (prefers-color-scheme: dark) {
:root {
--separator-color: rgb(var(--dk-gray));
--box-shadow-color: rgba(var(--dk-gray), 0.8);
--body-code-background-color: rgb(var(--dk-gray));
--heading-color: rgb(var(--white));
--header-series-arrow-foreground-color: rgb(var(--super-dk-gray));
--html-background-color: rgb(var(--black));
--html-color: rgba(var(--white), 0.8);
--platter-background-color: rgba(var(--black), var(--platter-background-opacity));
--platter-backdrop-filter: brightness(0.66) blur(10px);
--tag-foreground-color: rgb(var(--sub-lt-gray));
--tag-background-color: rgb(var(--dk-gray));
--tag-spacer-foreground-color: rgb(var(--super-dk-gray));
--tag-hover-background-color: rgb(var(--super-dk-gray));
--tag-hover-foreground-color: rgb(var(--mid-gray));
--twitter-icon: url(/icons/twitter-dark.svg);
--github-icon: url(/icons/github-dark.svg);
--instagram-icon: url(/icons/instagram-dark.svg);
--feed-icon: url(/icons/rss-dark.svg);
}
}
@font-face {
font-family: "Museo_Slab";
src: url("{{ `/fonts/Museo_Slab_500.woff2` | relURL }}") format("woff2"),
url("{{ `/fonts/Museo_Slab_500.woff` | relURL }}") format("woff");
font-weight: normal;
font-style: normal;
}
@layer reset {
body, button, h1, h2, h3, h4, h5, h6, input, ol, ul, p, pre, textarea {
padding: 0; margin: 0;
}
}
a {
color: rgb(var(--mid-blue));
text-decoration: none;
}
a:hover {
text-decoration: underline;
text-underline-offset: 0.12em;
}
a:visited { color: none; }
blockquote {
border-inline-start: 4px solid var(--separator-color);
padding-inline-start: 2rem;
margin-inline-start: 2rem;
}
body {
font-size: var(--body-font-size);
line-height: var(--body-line-height);
}
code {
background-color: var(--body-code-background-color);
border-radius: 0.25rem;
font-family: var(--font-family-monospace);
display: inline-block;
padding-inline: 0.6rem;
}
figcaption {
font-size: 75%;
line-height: var(--body-line-height);
margin-block-start: 0.2em;
text-align: center;
}
figure {
border-radius: 6px;
display: inline-block;
line-height: 1;
margin: 0;
margin-block: 0 var(--body-item-spacing);
margin-inline: 0;
overflow: hidden;
max-width: var(--content-width);
width: 100%;
}
figure.bordered {
padding: 0.5rem;
border: 2px solid #eee;
}
figure a,
figure a:hover {
border: 0;
}
figure img {
border-radius: 6px;
height: auto;
max-width: var(--content-width);
width: 100%;
}
figure svg {
width: 100%;
height: auto;
}
figure .youtube {
border-radius: 6px;
line-height: 1;
overflow: hidden;
max-width: var(--content-width);
width: 100%;
}
footer.site {
align-items: center;
display: flex;
flex-direction: column;
font-size: 1.6rem;
margin-block: var(--body-item-spacing);
text-align: center;
}
footer.site > ul {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
list-style: none;
}
footer.site > .slogans {
margin-block-end: 0;
}
footer.site > .slogans > li {
margin-block-end: 0;
margin-inline-start: 0.5em;
}
footer.site > p {
margin: 0;
}
footer.site p + p {
margin-top: 0.25rem;
}
@media (max-width: 740px) {
footer.site .slogans span {
white-space: nowrap;
}
}
h1 { font-size: 1.6em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.4em; }
h4 { font-size: 1.3em; }
h5 { font-size: 1.3em; }
h6 { font-size: var(--body-font-size); }
h5, h6 { font-family: var(--font-family-body); }
h1, h2, h3, h4, h5, h6 {
color: var(--heading-color);
font-family: var(--font-family-heading);
font-weight: 600;
margin-block: 3.5rem 1rem;
letter-spacing: 0.08em;
line-height: var(--body-header-line-height);
}
h1.site {
color: rgb(var(--mid-blue));
font-size: 2.5em;
}
@supports (background-clip: text) {
h1.site {
background:
radial-gradient(circle at 20% 70%, rgb(var(--purple)), transparent 40%),
radial-gradient(circle at 30% 30%, rgb(var(--lt-blue)), rgb(var(--mid-blue)) 20%, transparent 80%),
radial-gradient(ellipse at 95% 20%, rgb(var(--dk-blue)), rgb(var(--mid-blue)) 70%, transparent 80%),
radial-gradient(circle at 100% 100%, rgb(var(--purple)), rgb(var(--lilac)) 100%),
radial-gradient(circle at 45% 100%, rgb(var(--lilac)), rgb(var(--purple)) 60%),
radial-gradient(ellipse at 50% 50%, rgb(var(--dk-blue)), transparent 80%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
}
h1.site > a { color: inherit; }
h1.site > a:hover { text-decoration: none; }
header.site {
display: flex;
width: 100%;
z-index: 10000;
}
header.site > .grid > h1 {
font-family: var(--font-family-site-heading);
font-size: 2em;
margin: 0;
order: 1;
white-space: nowrap;
}
header.site > .grid {
align-items: baseline;
display: flex;
flex-wrap: wrap;
gap: 0 0.5em;
margin: 0 auto;
max-width: var(--content-width);
padding: 1.5rem 3rem;
width: 100%;
}
header.site > .grid > nav:first-of-type {
justify-content: start;
order: 2;
}
header.site > .grid > nav:last-of-type {
justify-content: end;
order: 3;
}
@media (max-width: 516px) {
header.site > .platter {
border-left: none;
border-radius: 0;
border-right: none;
border-top: none;
}
header.site > .grid {
max-width: none;
}
}
@media (max-width: 435px) {
header.site > .grid > nav:first-of-type {
order: 5;
}
}
html {
--font-size-scale-factor: 1vmax;
background-color: var(--html-background-color);
color: var(--html-color);
font-family: var(--font-family-body);
font-size: clamp(var(--font-size-min), var(--font-size-scale-factor), var(--font-size-max));
}
img {
height: auto;
max-width: 100%;
}
img.circular {
shape-outside: circle(50%);
-webkit-clip-path: circle(50%);
clip-path: circle(50%);
}
main {
max-width: var(--content-width);
margin: var(--body-item-spacing) auto;
padding-inline: var(--body-item-spacing);
width: 100%;
}
main > header {
margin-bottom: var(--body-item-spacing);
}
header.site > .grid > nav {
align-items: center;
display: flex;
font-size: max(1.5rem, 80%);
justify-content: start;
letter-spacing: 0.12em;
list-style: none;
text-transform: lowercase;
}
header.site > .grid > nav > li {
color: var(--site-nav-link-color);
display: block;
}
header.site > .grid > nav > .active { font-weight: bold; }
main > :first-child { margin-block-start: 0; }
main > :last-child { margin-block-end: 0; }
nav.bulleted > li:first-child::before {
color: var(--html-color);
content: "";
margin-inline: 0;
}
nav.bulleted > li::before {
color: var(--heading-color);
content: "•";
font-size: 60%;
font-weight: normal;
opacity: 80%;
margin-inline: var(--nav-bulleted-spacing);
}
nav.social > li { margin-inline-start: var(--nav-bulleted-spacing); }
nav.social > li:first-child { margin-inline-start: 0; }
p {
letter-spacing: 0.025em;
line-height: var(--body-line-height);
margin-block-end: var(--body-item-spacing);
}
ul, ol, dl {
margin-block-end: var(--body-item-spacing);
}
ul,
ol,
dl,
ul > li,
ol > li,
dl > dt,
ul > li > ul,
ul > li > ol,
ol > li > ul,
ol > li > ol {
margin-inline-start: calc(var(--body-item-spacing) + 2px);
}
header > span.series {
font-size: 1.75rem;
letter-spacing: 1px;
margin-inline-start: 0.5em;
}
header > span.series::before {
color: var(--header-series-arrow-foreground-color);
content: "↳";
margin-inline-end: 0.25em;
}
.post-list {
list-style: none;
margin: 0;
padding: 0;
}
@supports (display: subgrid) {
.post-list {
align-items: baseline;
display: grid;
gap: 1em;
grid-template-columns:
minmax(150px, max-content) minmax(20px, max-content) 3px auto;
grid-template-areas: "month day line title";
}
}
.post-list li {
align-items: baseline;
border-radius: 6px;
display: grid;
gap: 1em;
padding: 0.1em;
}
.post-title {
align-items: baseline;
display: flex;
flex-wrap: wrap;
gap: 0 4rem;
}
.post-title h1 {
flex-grow: 1;
font-size: 1.8em;
margin-block: 0;
padding-bottom: 0;
}
.post-title > .post-date {
color: var(--light-dim);
display: inline-block;
font-size: 1.75rem;
inline-size: min-content;
letter-spacing: 1px;
margin-block-start: 0.5rem;
white-space: nowrap;
}
.p5-sketch {
display: block;
position: relative;
width: 100%;
}
.social {
display: block;
letter-spacing: 2px;
margin-left: auto;
order: 2;
}
.social > li > a {
background-image: var(--url);
background-repeat: no-repeat;
background-size: var(--menu-icon-size);
display: block;
height: var(--menu-icon-size);
width: var(--menu-icon-size);
}
.social > li > a > span {
display: none;
}
.social > li + li:before {
color: var(--dark);
padding: 0 0.5rem;
}
footer > .tags {
display: flex;
margin-inline-start: 0;
padding-inline-start: 0;
}
footer > .tags > li {
background-color: var(--tag-background-color);
color: var(--tag-foreground-color);
border-radius: 4px;
display: inline-block;
font-size: 75%;
letter-spacing: 1px;
}
footer > .tags > li:first-child {
margin-inline-start: 0;
}
footer > .tags > li:hover {
background-color: var(--tag-hover-background-color);
}
footer > .tags > li > a {
color: inherit;
display: block;
height: 100%;
width: 100%;
padding: 0.6rem 1rem;
}
footer > .tags > li > a:hover {
color: var(--tag-hover-foreground-color);
text-decoration: none;
}
footer > .tags > li + li { margin-inline-start: 1rem; }
footer > .tags > li.chevron + li { margin-inline-start: 0 }
footer > .tags > .chevron {
align-items: center;
border: 0;
border-radius: 0;
color: var(--tag-spacer-foreground-color);
background-color: inherit;
display: flex;
justify-content: center;
margin-inline-start: 0;
padding-inline-start: 1px;
width: 2rem;
}
footer > .tags > .chevron:hover {
color: var(--tag-spacer-foreground-color);
background-color: inherit;
}
table {
border: 1px solid var(--separator-color);
border-collapse: collapse;
margin-block-end: var(--body-item-spacing);
margin-inline: auto;
width: 50%;
}
td, th {
border: 1px solid var(--separator-color);
padding-inline: 1rem;
}
.youtube iframe {
aspect-ratio: 16 / 9;
margin-bottom: -3px;
width: 100%;
}
/** SYNTAX HIGHLIGHTING **/
.highlight code {
background-color: inherit;
margin-block: 0.5em;
}
.highlight .line {
display: grid;
grid-template-columns: max-content auto;
}
.highlight .ln a {
-webkit-user-select: none;
-moz-use-select: none;
-ms-user-select: none;
user-select: none;
}
.highlight .cl {
white-space: pre-wrap;
}
/** HELPER CLASSES **/
.centered { text-align: center; }
.float-right { float: right; }
.nobreak { white-space: nowrap; }
.noselect {
cursor: default;
-webkit-user-select: none;
-moz-use-select: none;
-ms-user-select: none;
user-select: none;
}
.platter {
-webkit-backdrop-filter: var(--platter-backdrop-filter);
backdrop-filter: var(--platter-backdrop-filter);
background-color: var(--platter-background-color);
border: 1px solid var(--header-border-color);
border-radius: 12px;
box-shadow: 3px 4px 4px var(--header-box-shadow-color);
}
.visible { visibility: visible; }

View file

@ -0,0 +1,2 @@
name: Eryn Wells
email: eryn@erynwells.me

View file

@ -1,6 +1,5 @@
baseURL: https://erynwells.me/
languageCode: en-US
title: ~eryn
copyright: Copyright © 2020—2024 Eryn Wells
title: Erynwells.me
copyright: Copyright © 2020—2022 Eryn Wells
defaultContentLanguage: en
enableEmoji: true

View file

@ -1,12 +1,4 @@
en:
languageName: English
weight: 1
es:
languageName: Español
weight: 2
jp:
languageName: 日本語
weight: 3
tok:
languageName: toki pona
weight: 4

View file

@ -1,12 +1,5 @@
goldmark:
renderer:
unsafe: true
parser:
attribute:
block: true
title: true
highlight:
anchorLineNos: true
lineNos: false
lineNos: true
lineNumbersInTable: false
noClasses: false

View file

@ -11,19 +11,13 @@ main:
name: About
url: /about/
weight: 30
- identifier: feed
name: feed
url: /feed.atom
weight: 40
params:
style: file
social:
- identifier: mastodon
name: Mastodon
url: https://mastodon.social/@erynofwales
- identifier: twitter
name: Twitter
url: https://twitter.com/erynofwales
weight: 10
params:
shortName: mst
shortName: tw
- identifier: github
name: Github
url: https://github.com/erynofwales
@ -37,7 +31,7 @@ social:
params:
shortName: ig
- identifier: feed
name: feed
name: Feed
url: /feed.atom
weight: 40
params:

View file

@ -1,15 +0,0 @@
hugoVersion:
extended: false
min: "0.116.0"
replacements: >-
github.com/erynofwales/hugo-theme-feeds/v2 -> feeds,
github.com/erynofwales/hugo-theme-termlite/v2 -> termlite,
github.com/erynofwales/hugo-theme-photostream/v2 -> photostream,
github.com/erynofwales/hugo-resource-builders/v2 -> resource-builders,
github.com/erynofwales/hugo-image-utilities/v2 -> image-utils
imports:
- path: github.com/erynofwales/hugo-theme-termlite/v2
- path: github.com/erynofwales/hugo-theme-feeds/v2
- path: github.com/erynofwales/hugo-theme-photostream/v2
- path: github.com/erynofwales/hugo-resource-builders/v2
- path: github.com/erynofwales/hugo-image-utilities/v2

View file

@ -1,4 +1,3 @@
home: [HTML, Atom]
section: [HTML, Atom]
taxonomy: [HTML]
term: [HTML]
home: [HTML, Atom, RSS]
page: [HTML, JSON]
section: [HTML, Atom, RSS]

View file

@ -1,20 +1,4 @@
author:
name: Eryn Wells
email: eryn@erynwells.me
shortTitle: Eryn Wells
twitter: erynofwales
github: erynofwales
instagram: erynofwales
description: Home page of Eryn Rachel Wells
blog:
yearLimit: 3
photostream:
yearLimit: 3
photos:
gridSize: 200
thumbnailSize: 600

View file

@ -1,2 +1,2 @@
x:
twitter:
enableDNT: true

View file

@ -1,2 +1,2 @@
x:
twitter:
disableInlineCSS: true

View file

@ -1,6 +1,5 @@
---
title: Eryn Rachel Wells
layout: single
---
{{< nobreak >}}Ingeniera de software,{{< /nobreak >}}

View file

@ -1,58 +1,5 @@
---
layout: single
params:
renderHeadingAnchors: false
title: Eryn Rachel Wells
---
Hi, I'm Eryn Wells. This is my website. Welcome!
## Latest
Here are some of my most recent posts.
{{< home/latest >}}
## Personal
I'm a queer trans woman, {{< tess >}}' partner, and mom of [two cats][cats]. I
was born in Seattle, {{< abbr Washington >}}WA{{< /abbr >}} and grew up in
Phoenix, {{< abbr Arizona >}}AZ{{< /abbr >}}. I attended [Oberlin College][ob]
where I got a degree in Computer Science. My pronouns are [she/her][pronouns].
You can read more about me on my [about][ab] page, or [get in touch][where-am-i].
## Professional
I've worked as a software engineer since 2011 for a variety of companies around
the San Francisco Bay Area. I joined [Apple][a] in 2016, where I currently work
on password management and authentication technologies.
My [résumé][r] has all the details.
## Hobbies
When I'm not working, you can reliably find me hacking on this website or [some
coding other project][gh]. I'm also a musician, and play piano, Irish tin
whistle, and modular synthesizer. Occasionally I [record][bc] [things][sc]. I
love outer space and astronomy; I will always get excited to look at the moon
with you, or check out anything through a telescope. I enjoy [photograhy][p],
mostly as a travel hobby. And I've been practicing iaido, a traditional Japanese
sword art, since early 2024. Other things I've been into include: bread baking,
bicycling, calligraphy, ceramics, and knitting.
[a]: https://apple.com
[ab]: {{< ref "/about" >}}
[b]: {{< ref "/blog" >}}
[bc]: https://erynwells.bandcamp.com/releases
[cats]: {{< ref "/cats" >}}
[eml]: mailto:Eryn%20Wells<eryn@erynwells.me>
[gh]: https://github.com/erynofwales
[ig]: https://www.instagram.com/erynofwales
[m]: https://mastodon.social/@erynofwales
[n]: {{< ref "/now" >}}
[ob]: https://www.oberlin.edu
[p]: {{< ref "/photos" >}}
[pronouns]: http://pronoun.is/she
[r]: {{< ref "/resume" >}}
[sc]: https://soundcloud.com/purlsnbeeps
[where-am-i]: {{< ref "/about/where-am-i" >}}
Software engineer, potter, musician, and overall nerd.

View file

@ -1,16 +1,14 @@
---
title: "Hi! 👋🏻"
layout: single
date: 2022-09-03T12:14:32-07:00
draft: false
resources:
- name: me
src: me.jpeg
params:
alt: >
Me, wearing a hat and smiling slightly, standing in front of a stone
background.
---
{{% section class=content--small-right-column %}}
{{< circular_image id=me name=me class="float-right" width=200
alt="A photo of me, wearing a hat, standing in front of a stone background">}}
I'm Eryn. My pronouns are [she/her][p]. I'm a queer trans woman. I live in San
Francisco, CA, on the unceded ancestral lands of the Ramaytush Ohlone people,
@ -20,10 +18,6 @@ I attended [Oberlin College][ob] where I got a degree in Computer Science.
I speak English natively, and Spanish too, though I always need more practice.
{{< circular_image id=me name=me class="content--right-column" width=200 >}}
{{% /section %}}
## Hobbies
I've been a musician for most of my life. I started on the piano at age seven,

View file

@ -1,17 +1,4 @@
@layer page {
main > section > p:not(:last-child) {
margin-bottom: var(--body-item-spacing);
}
p:has(img#me) {
display: inline;
grid-column: unset;
margin-bottom: 0;
}
img#me {
margin: 0;
shape-outside: circle(55%);
img#me {
width: min(200px, 25%);
}
}

View file

@ -3,16 +3,11 @@ title: "Where to Find Me"
date: 2022-11-11T08:35:26-08:00
---
Here's a list of places you can find me online. You can often find me on
services not listed here with the `erynofwales` or `erynrwells` handles.
Here's a list of places you can find me online.
## Social Media
I'm really only on Instagram and Mastodon these days. My Twitter account is
still live, as an archive, but I don't post on it or look at it. Ditto for
Facebook.
- Facebook: [erynofwales](https://www.facebook.com/erynofwales)
- Cohost: [@eryn](https://cohost.org/eryn)
- Instagram: [@erynofwales](https://instagram.com/erynofwales)
- Mastodon: [@erynofwales](https://mastodon.social/@erynofwales)
- Twitter: [@erynofwales](https://twitter.com/erynofwales)
@ -20,10 +15,9 @@ Facebook.
## Content
- Bandcamp: [erynwells](https://erynwells.bandcamp.com/releases)
- GitHub: [erynofwales](https://github.com/erynofwales)
- Soundcloud: [purlsnbeeps](https://soundcloud.com/purlsnbeeps)
- StoryGraph: [erynrwells](https://app.thestorygraph.com/profile/erynrwells)
- YouTube: [Eryn Wells](https://www.youtube.com/channel/UCWb2pTDlC27R1PucyUPrypA)
- GitHub: [erynofwales](https://github.com/erynofwales)
## The Old Fashioned Way

View file

@ -1,7 +1,7 @@
---
title: "Booting a Raspberry Pi Over TFTP"
date: 2020-10-13T08:31:52-07:00
description: A writeup of how I set up a Raspberry Pi to boot over TFTP to facilitate an operating system development project.
draft: false
series: ["Raspberry Pi OS Development"]
categories: ["Tech"]
tags: ["Raspberry Pi", "Networking"]

View file

@ -1,4 +0,0 @@
---
title: 2020
date: 2020-01-01
---

View file

@ -7,7 +7,7 @@ categories: ["Music"]
tags: ["Synthesizers", "Electronics", "DIY", "Compositions"]
---
{{< youtube id="gCSwWsxzy_c" title="A timelapse video of me building an Oskitone Scout, set to music produced using the Scout itself" >}}
{{< figures/youtube id="gCSwWsxzy_c" title="A timelapse video of me building an Oskitone Scout, set to music produced using the Scout itself">}}
[Oskitone][oskitone] recently released a new synthesizer: the [Scout][scout].
It's a small monophonic keyboard synth built around an Arduino. It was a quick

View file

@ -41,4 +41,4 @@ I'm so grateful for every one of these people. We've been friends for years and
even though our lives have taken us in so many different directions, we've found
each other again and that is so wonderful.
{{< twitter user=erynofwales id=1447951049076056071 >}}
{{< twitter erynofwales 1447951049076056071 >}}

View file

@ -1,4 +0,0 @@
---
title: 2021
date: 2021-01-01
---

View file

@ -7,7 +7,7 @@ categories: ["Music"]
tags: ["Eurorack", "Synthesizers", "Recordings", "Performances", "Compositions"]
---
{{< youtube id="sqr7g4P85aM" title="A top-down video of me operating a small Eurorack system made of only three modules. Lights flash, an incorporeal hand turns knobs to sculpt the sound." >}}
{{< figures/youtube id="sqr7g4P85aM" title="A top-down video of me operating a small Eurorack system made of only three modules. Lights flash, an incorporeal hand turns knobs to sculpt the sound." >}}
This is my submission to the [Three Module Challenge][3mc] show put on by
Colorado Modular Synth Society in late January 2022. This is my first time

View file

@ -18,7 +18,7 @@ estuve hecho.
Es posible que me entusiasme un poco.
{{< twitter user=erynofwales id=1510763278691016705 >}}
{{< twitter erynofwales 1510763278691016705 >}}
He mejorado mucho en las últimas semanas. Mis puntajes han crecidos desde 1,000
hasta el mejor juego hasta ahora en que [obtuve 9401 puntos][over9000]. Quién

View file

@ -23,7 +23,7 @@ packed with [NetHackWiki][nethackwiki] tabs too, including a pinned one for the
I may have gotten a little carried away a time or two.
{{< twitter user=erynofwales id=1510763278691016705 >}}
{{< twitter erynofwales 1510763278691016705 >}}
I've gotten much better in that time. My scores have increased from the
1000-2000 range to my best game so far in which [I scored 9401

View file

@ -1,7 +1,6 @@
---
title: "Roguelikes I Like"
date: 2022-05-09T08:37:23-07:00
description: Some roguelikes Ive enjoyed recently.
draft: false
resources:
- name: nethack

View file

@ -1,36 +0,0 @@
<svg class="railroad-diagram" width="408" height="62" viewBox="0 0 408 62">
<g transform="translate(.5 .5)">
<g>
<path d="M20 21v20m10 -20v20m-10 -10h20"></path>
</g>
<path d="M40 31h10"></path>
<g>
<path d="M50 31h0"></path>
<path d="M358 31h0"></path>
<g class="terminal ">
<path d="M50 31h0"></path>
<path d="M126 31h0"></path>
<rect x="50" y="20" width="76" height="22" rx="10" ry="10"></rect>
<text x="88" y="35">&#60;audio></text>
</g>
<path d="M126 31h10"></path>
<path d="M136 31h10"></path>
<g class="terminal ">
<path d="M146 31h0"></path>
<path d="M230 31h0"></path>
<rect x="146" y="20" width="84" height="22" rx="10" ry="10"></rect>
<text x="188" y="35">Analyzer</text>
</g>
<path d="M230 31h10"></path>
<path d="M240 31h10"></path>
<g class="terminal ">
<path d="M250 31h0"></path>
<path d="M358 31h0"></path>
<rect x="250" y="20" width="108" height="22" rx="10" ry="10"></rect>
<text x="304" y="35">destination</text>
</g>
</g>
<path d="M358 31h10"></path>
<path d="M 368 31 h 20 m -10 -10 v 20 m 10 -20 v 20"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,7 +1,7 @@
---
title: "Making an Audio Scope with P5.js"
date: 2022-08-18T20:48:37-07:00
description: A writeup of a small JavaScript waveform visualizer I made with P5.js.
draft: false
categories: ["Tech"]
tags: ["P5.js", "Programming", "Web", "Art"]
resources:
@ -88,7 +88,22 @@ node to the input of the analyzer node, and the output of the analyzer node to
the audio context's `destination` node that routes to the computer's speakers.
Our audio processing graph looks like this:
![](diagram.svg)
{{< figures/railroad id="audioContextDiagram" >}}
{{< scripts/railroad >}}
return rr.Diagram(
rr.Sequence(
rr.Terminal("<audio>"),
rr.Terminal("Analyzer"),
rr.Terminal("destination")));
{{< /scripts/railroad >}}
{{< scripts/railroad narrow=1 >}}
return rr.Diagram(
rr.Stack(
rr.Terminal("<audio>"),
rr.Terminal("Analyzer"),
rr.Terminal("destination")));
{{< /scripts/railroad >}}
{{< /figures/railroad >}}
By itself the AudioContext doesn't actually play any audio. I'll tackle that
next.

View file

@ -1,7 +1,6 @@
---
title: "Hugo's Dictionary API"
date: 2022-10-13T10:19:02-07:00
description: Ive found Hugos API for collections to be difficult to understand. Heres my attempt to summarize its quirks.
categories: ["Tech"]
tags: ["Hugo", "Web", "API Design"]
series: "Erynwells.me Development"

View file

@ -1,7 +1,6 @@
---
title: "Lunar Eclipse 🌝"
date: 2022-11-07T08:37:45-08:00
description: A quick note about the upcoming lunar eclipse in the morning of 2022-11-08.
categories: Space
tags: [Moon, Lunar Eclipse]
---

View file

@ -1,66 +0,0 @@
---
title: "My Best Nethack Game (So Far)"
date: 2022-11-24T09:13:15-05:00
description: A summary of my best-to-date game of Nethack.
categories: ["Games"]
tags: ["Nethack", "Roguelikes", "Video Games"]
resources:
- name: wishing
src: wishing.png
title:
- name: oracle
src: oracle.png
title:
---
I just finished my best ever game of Nethack. I earned 31,118 points as a level
9 Valkyrie.
Some highlights:
I got all the way to the bottom of the Mines, fought several vampires and
trolls, but somehow completely missed the luckstone. I did pick up a grey stone,
but it ended up being a touchstone.
I got a Wand of Wishing! I was zap testing wands in Minetown and the game asked
me for a wish! I had no idea what to wish for -- it was my first time getting a
wish 😱 -- but I had been watching [Adeon's Nethack speedrun][adeon] at the 2017
Roguelike Celebration and remembered him wishing for some absurdly qualified
dragon scale mail, so I ended up with a *+2 uncursed silver dragon scale mail*
that brought my AC down to -10. It saved my butt later on when I ran into a
bunch of winter wolf pups because it reflected their cold beams.
{{< figures/image name=wishing >}}
Later on in the mines, I stepped on a polymorph trap that transformed me into an
ice dragon. That transformation caused my dragon scale mail to fuse into my
body. When the transformation ended, I was left with simple dragon scales,
which were still silver, but no longer had the big defense bonus. Boo. :( I did
get to lay two eggs that hatched into baby ice dragons. Baby dragons are
ravenous and indiscrimate.
The baby dragons killed the Oracle. Whoops.
{{< figures/image name=oracle >}}
In the oracle level, I tried to dip my long sword into the fountains around the
Oracle, hoping to find Excalibur. Just a few turns before, I'd fought a
gelatinous cube that corroded my sword. I didn't notice at the time though, so
dipping went horribly wrong... Not only did my corroded long sword become
*thoroughly rusted*, it also eventually became cursed. Oddly enough, that sword
was still the best weapon I had, and I used it to the very end. I need to figure
out how to replace weapons that get degraded like that.
I solved Sokoban with some help from the Nethack wiki for the first time. The
puzzles are hard but I enjoyed thinking through them. The monsters in there,
especially in the upper levels are *hard*: multiple elementals, several packs of
winter wolf pups, yetis, apes, and a zruty. I got through the last level and
discovered a mimic blocking the hallway to the zoo. It killed me.
Near the end, I was testing amulets and put on an Amulet of Changing that caused
me to switch genders. Boo.
It's in my [logfile][logfile] now too.
[adeon]: https://www.youtube.com/watch?v=rIB0y_kwFuY
[logfile]: {{< ref "nethack" >}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View file

@ -1,4 +0,0 @@
---
title: 2022
date: 2022-01-01
---

View file

@ -1,4 +0,0 @@
---
title: 2023
date: 2023-01-01
---

View file

@ -1,17 +0,0 @@
---
title: "Atom Feed Bug Fixes"
date: 2023-08-09T08:43:26-07:00
categories: ["Tech"]
tags: ["Erynwells.me", "Meta", "Atom"]
---
A kind reader pointed out to me that my Atom feed was incorrect. There were two
problems. First, I was specifying an incorrect URL in the feed's `<link
rel="self">` -- it was pointing to a nonexistant feed.xml file. Second, I was
omitting a `<link>` tag from the entries entirely.
Thunderbird didn't like this. With no `<link>` for an entry, it would show the
feed's `<link>` in it's UI. And that link left users at a 404 page.
I pushed a fix this morning. You might have to refresh or resubscribe to pick up
the changes.

View file

@ -1,22 +0,0 @@
---
title: "Chess"
date: 2023-11-20T14:58:56-08:00
categories: Chess
tags: ["Games", "Hobbies"]
---
I've been playing a lot of chess lately. {{< tess >}} and I have been watching
[Slow Horses][slow-horses] on Apple TV+, and there was a recent episode in which
a chess game between two characters is a key plot beat. That got me thinking
about playing again.
I learned chess as a kid. My dad taught me. I played in chess clubs in
elementary and middle school. I was really into it for a while!
I have a [Chess.com](chess.com) account now: [erynrwells][chess-com-profile].
I'm also on [lichess.org](lichess.org): I'm [erynrwells][lichess-profile] there too.
Send me a friend request or challenge? :)
[slow-horses]: https://tv.apple.com/us/show/slow-horses/umc.cmc.2szz3fdt71tl1ulnbp8utgq5o
[chess-com-profile]: https://www.chess.com/member/erynrwells
[lichess-profile]: https://lichess.org/@/erynrwells

View file

@ -1,13 +0,0 @@
---
title: "Guide to Computing"
date: 2023-09-23T10:24:35-07:00
categories: "Tech"
tags: ["Retro Computing", "Design"]
---
I really enjoyed looking through the images on [Docubyte's Guide to
Computing][link]. It depicts machines from the early days of modern computing --
think IBM mainframes, PDP-1's, and lots of midcentury modern design -- in a way
I found really intriguing.
[link]: https://www.docubyte.com/projects/guide-to-computing/

View file

@ -1,38 +0,0 @@
---
title: "Hello Chess Friend"
description: I started building a chess engine in Rust. Here it is.
date: 2023-12-29T08:29:00-08:00
series: chess-friend
categories: Tech
tags: [Programming, Chess]
---
I started [playing a lot of chess][chess-post] recently. As often happens with
me, it wasn't very long until I started wondering how I could Do Programming To
It.
I found the mostly excellent, occasionally vague and confusing [Chess
Programming Wiki][cpwiki] and have been using that as a guide. It helpfully says
this on it's [Getting Started][cpgs] page:
> The **very first step** to writing a chess engine is to write a complete, bug
> free board representation that knows every rule of chess.
As a software engineer, the "bug free" bit cracks me up.
My engine is called ChessFriend. It uses [bitboards][cpbb] for its board
representation. As of this post, I've managed to write a board representation
that allows me to place pieces of both colors on any square, and I'm hacking
away at the move generator. I've also written a small command line "board
explorer" utility that can interact with my board representation. Of course, it
has a pile of unit tests, helping me inch ever-so-slowly toward that blissful
bug-free state.
It's written in Rust. I've [_mostly_][rust-bc-toot] avoided fighting with the
borrow checker.
[chess-post]: {{< ref "chess" >}}
[cpwiki]: https://www.chessprogramming.org/Main_Page
[cpgs]: https://www.chessprogramming.org/Getting_Started
[cpbb]: https://www.chessprogramming.org/Bitboards
[rust-bc-toot]: https://mastodon.social/@erynofwales/111637122773195611

View file

@ -1,36 +0,0 @@
---
title: "Less Instagram, More Blog"
description: Resolving, yet again, to blog more and social media less.
date: 2023-12-27T08:56:44-07:00
categories: Meta
tags: [Writing, Resolutions, Habits]
---
I've been thinking the last few days about how to make use of my blog in 2024. I
made some vague noises in this general direction a few days ago in my [What
Should I Blog About?][what-to-blog-about] post too.
My vision since I started posting more here has been to use it as a place to
share all sorts of things: stuff I'm working on or thinking about; photos; and
stories from travel and life.
I often fall into a trap when I sit down to write something in which I feel like
I must first invent the Universe. The need to explain everything from first
principles seriously hampers my ability (and frankly, desire) to write anything.
I don't want to only post carefully thought out, highly edited and polished
pieces, though I certainly hope _some_ of my posts reach that bar. I hope to
also post quick notes and sketches of ideas. I've enjoyed reading some quicker
posts from {{< tess >}} and [Elaine][e] this past year, and I'd like to follow
their example.
{{< youtube zSgiXGELjbc >}}
I'm not setting myself a specific goal here. The idea is just "more" in a
certain general direction. I don't want to commit to a specific frequency or
quality. Instead, I'm hoping this post sets a foundation on which to build a
sustainable thinking-writing-sharing habit.
Thanks for coming along. :)
[what-to-blog-about]: {{< ref "what-to-blog-about" >}}
[e]: https://diplograph.net

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:562f6c157c43ef11ae55274b0bd1403c2cbb9a64b82443a2d9f4fb58b32606fd
size 148873

View file

@ -1,10 +0,0 @@
---
title: "The Long Way to a Small, Angry Planet by Becky Chambers"
slug: long-way-to-a-small-angry-planet-book
date: 2023-02-01T09:16:48-08:00
draft: true
categories: Books
tags: ["Science Fiction"]
series: 2023-books
---

View file

@ -1,15 +0,0 @@
---
title: "Mastodon Icon"
date: 2023-08-11T08:23:25-07:00
categories: ["Tech"]
tags: ["Meta", "Erynwells.me", "Web"]
---
I finally got around to replacing the Twitter icon in the site's header with a
link to my Mastodon page. It was surprisingly tricky because of how I styled and
layed out those icons. I was able to clean up the SVGs a little bit too.
These days I have [way too many social media accounts][where-to-find-me]. I'm
mostly on Mastodon and Instagram.
[where-to-find-me]: {{< ref "/about/where-am-i" >}}

View file

@ -1,14 +0,0 @@
---
title: "Nethack Illustrated Guide"
date: 2023-01-07T08:52:53-08:00
link: https://thinkmoult.com/nethack-illustrated-guide-mazes-of-menace.html
categories: Games
tags: [Nethack, Art, AI]
---
While browsing {{< r nethack >}}, I came across a [post][post] from someone sharing a [collection of AI-generated
images][guide] that illustrate the story arc of a game of Nethack. Between the images and the prose they added around
it, I thought they did a fantastic job of capturing the mood of the game.
[post]: https://www.reddit.com/r/nethack/comments/zx965y/nethack_an_illustrated_guide_to_the_mazes_of/
[guide]: https://thinkmoult.com/nethack-illustrated-guide-mazes-of-menace.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

View file

@ -1,47 +0,0 @@
---
title: "Netscape Meteors: Retrospective"
date: 2023-08-05T17:14:40-07:00
description: Someone shared my Netscape Meteors post on the Orange Website, causing it to be moderately viral for a few days. Heres an update on the web traffic my server received.
categories: ["Tech"]
tags:
- History
- Meta
- Netscape
- Web
- Web Browsers
---
Last week, I published a small blog post about trying to find the original
[Netscape "meteors"][meteors] loading animation. At some point that night,
someone posted it on the orange website, and ... it did some numbers.
As of today, five days later, my server has registered just over 200,000 page
accesses.
{{< figures/code >}}
```txt
% grep "GET /blog/2023/08/netscape-meteors/" \
/var/log/nginx/access.log | wc -l
200201
```
{{< /figures/code >}}
Bandwidth saw a bit of a spike too.
{{< figures/image name=bandwidth-2023-08-05.png shouldShowTitle=false >}}
To my knowledge this is the first time anything I've published on the internet
has been picked up by Hacker News. It's jarring to realize so many people have
visited my website in the last several days, reading this and other things I've
written, listening to music I've published, and looking through photos I've
posted. It's a little like having surprise house guests and realizing you
haven't tidied up in a little while.
I only took a brief look at the comments. I was pleased to see they were civil,
and mostly reminiscing about the days of Netscape and the early web. I had a few
people reach out to tell me they enjoyed my post too.
Thanks, y'all, for reading my little corner of the web, and for your kind
words.
[meteors]: {{< ref "blog/2023/netscape-meteors" >}}

View file

@ -1,97 +0,0 @@
---
title: "Netscape Meteors"
date: 2023-08-01T18:23:33-07:00
description: I went on a hunt to find the "Meteors" loading animation from Netscape back in the 90s, and wrote up my adventure.
resources:
- name: netscape60
title: Netscape Meteor Loading Animation
src: netscape-meteors.gif
- name: netscape-modified60
title: Modified Netscape Meteor Loading Animation, Small
src: netscape-meteors-modified-60.gif
- name: netscape-modified240
title: Modified Netscape Meteor Loading Animation, Large
src: netscape-meteors-modified-240.gif
- name: rectangular-pixels
title: Rectangular Pixels
src: rectangular-pixels.png
alt: "A zoomed in screenshot of an animation frame with pixel grid enabled,
showing rectangular pixels"
categories: Tech
tags: ["Netscape", "History", "Web Browsers", "Web"]
---
I went on a small journey the last couple days to find the original Netscape
Navigator "meteors" animation. This one has a special place in my head and
heart because it is so clearly connected to my memories of discovering the
web as a kid. Here it is in its original 60&times;60 px glory:
{{< figures/image name=netscape60 shouldShowTitle=false size=small >}}
I started out doing some web searches that turned up several versions. One was
promising but far too big: 400&times;400 px. Worse, after some shoddy resize
attempts, the "pixels" had become rectangular.
{{< figures/image name=rectangular-pixels shouldShowTitle=false size=small >}}
This would not do.
I continued searching, hoping to find the original animations. I found someone's
[mirror of Netscape 5.0 on Github][gh-netscape]. Then I found some [very old
versions of Mozilla][moz-netscape] on a Mozilla FTP server. Sadly, the
animations had been stripped out of these archives. :(
Frustrated with hitting several deadends, I complained to {{< tess >}} and
wondered aloud if anyone might have the original images stashed away somewhere.
She quipped that if anyone did, it would be Jamie Zawinski.
A little later, I posted about it on Mastodon.
<iframe
src="https://mastodon.social/@erynofwales/110817133916254596/embed"
class="mastodon-embed"
style="max-width: 100%; border: 0"
width="400"
allowfullscreen="allowfullscreen">
</iframe>
And wouldn't you know it, a friend tagged [`@jwz`][masto-jwz] asking if he had
it, and a few moments later I got a reply from [Jamie][jwz] himself.
<iframe
src="https://mastodon.social/@jwz/110817331045294426/embed"
class="mastodon-embed"
style="max-width: 100%; border: 0"
width="400"
allowfullscreen="allowfullscreen">
</iframe>
If you don't know, Jamie Zawinski is well-know for working on several important
software projects in the '90s. He worked on Netscape Navigator, built and
maintains [Xscreensaver][xscreensaver], and several other things. Nowadays, he
owns and runs [DNA Lounge][dna] in San Francisco.
There are a lot of neat bits of web browser history on the page he linked --
totally worth a quick look over -- but most important to the quest at hand, it
had that Netscape meteors loading animation.
The original one has some small artifacts on the left side of frame 10 that
render as red and orange pixels. These bothered me enough that I made a version
that replaces those pixels with ones that match the surrounding pixels. Here's
the modified 60&times;60 one and a bigger 240&times;240 px one, for good
measure:
{{< content-grid columns=2 >}}
{{< figures/image name="netscape-modified60" shouldShowTitle=false shouldResize=false size=small >}}
{{< figures/image name="netscape-modified240" shouldShowTitle=false shouldResize=false size=small >}}
{{< /content-grid >}}
<script src="https://mastodon.social/embed.js" async="async"></script>
[gh-netscape]: https://github.com/zii/netscape
[moz-netscape]: https://ftp.mozilla.org/pub/mozilla/source/
[masto-jwz]: https://mastodon.social/@jwz
[jwz]: https://www.jwz.org
[xscreensaver]: https://www.jwz.org/xscreensaver/
[dna]: https://www.jwz.org
[about-jwz]: https://www.jwz.org/doc/about-jwz.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 781 KiB

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fa3b6749eb23bff608c4b7233746c6d0577c689786cc69d7354758959e56a9fc
size 159168

View file

@ -1,32 +0,0 @@
---
title: Once Upon a Time I Lived on Mars by Kate Greene
slug: once-upon-a-time-i-lived-on-mars-book
description: A brief book report.
date: 2023-02-20T09:16:48-08:00
date_finished: 2023-02-20T00:00:00-08:00
categories: Books
tags: [Memoirs, Space]
series: 2023-books
resources:
- name: cover
src: cover.jpeg
alt: "The cover of Once Upon a Time I Lived on Mars by Kate Greene, with a subtitle that reads 'Space, Exploration, and Life on Earth'"
title:
---
{{< figures/image name=cover >}}
{{< tess >}} got me this book for Christmas 2022 on a whim at a local bookshop. It's a series of essays -- reflections
and examinations -- of the author's time parcipating in one of NASA's Mars analog missions on Mauna Kea, Hawai'i. She is
a lesbian and, as luck would have it, lived in the same part of town that Tess and I do! It was fun to read little
anecdotes about her and her (ex) wife stopping in at shops that we frequent ourselves.
I enjoyed reading about her experiences working with NASA in the context of an analog mission. It sounds like they went
above and beyond to make the mission as close to a real Mars experience as possible, despite being firmly on Earth.
Communication with mission control was artificially delayed 20 minutes, as it would be on Mars. Going outside required
putting on bulky spacesuits. And participants were isolated together for six months.
She also has several essays in which she reflected on the politics and cost of spaceflight, and what it means for humans
to explore and exist in space.
Support a local bookshop and get it from [Folio Books](https://www.foliosf.com/book/9781250796660). 🙂

View file

@ -1,17 +0,0 @@
---
title: "Pajaro Dunes"
date: 2023-05-30T08:31:34-07:00
tags: [Travel, Beaches, Tess, EJ, Vacations]
---
{{< tess >}}, EJ, and I took a weekend trip down the coast over Memorial Day
weekend this year to stay in a beachside condo in Pajaro Dunes, just west of
Watsonville. We enjoyed hanging out on the beach, playing music and games,
building [Kiwi Crates][kiwi], and just generally being together. I took a couple
photos too. :)
{{< photo "2023/pajaro-dunes" >}}
{{< photo "2023/sunset-over-pajaro-dunes" >}}
[kiwi]: https://www.kiwico.com

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ded6e76e0903c82d1db25e8df3f5b68ac72bd07774648ee2dab2c68c0ddbc77f
size 2616121

View file

@ -1,56 +0,0 @@
---
title: "Tahoe Ski Trip"
date: 2023-01-30T12:40:15-08:00
draft: true
categories: Travel
tags: [Friends, Snowboarding, Snowshoeing]
resources:
- name: cabin
src: cabin.jpg
title:
params:
alt: A cozy cabin living room, with wood panel walls, lots of fuzzy, furry pillows, and a high steeply pitched roof. A fireplace is off to the right, and a large couch occupies the middle of the room. The windows fill the wall, floor to ceiling. Outside, you can see many tall pine trees and snow falling.
- name: snowshoeing
src: snowshoeing.jpg
title:
params:
alt: "A selfie of three people: me and two friends, wearing cold weather gear and standing in the snow. A well-traveled path in the snow meanders through the snow-covered trees."
---
This weekend I took a trip to the north side of Lake Tahoe with a group of coworkers and friends to ski and snowboard,
and enjoy the mountains and each other's company. We stayed in an AirBnb in Truckee, and spent a couple days up at
Northstar.
{{< figures/image name=cabin >}}
We all spent Friday on the mountain. I took an all-day group snowboarding lesson, while the rest did runs all over the
moutain. My lesson was a small group, just five of us, and it was really great. We were all newbies, and very
encouraging of each other. Our instructor pushed us quickly through standing and short glides on the board, to longer J
turns, and traversing the bunny hill. Before lunch, we were doing the Big Easy. I felt like I was starting to get the
hang of it, and the instructor agreed. After lunch, we went up the Arrow Express lift and took Lumberjack all the way
down. It was harrowing--I have bad memories of the first drop from the last time I was at Northstart--but we all made
it! By the end, I was feeling much more confident on the board, though I was also pretty beat up from several rough
falls throughout the day.
We all went to bed pretty sore that night, but the next day we got up and did it all again. There were a _lot_ more
people on the mountain on Saturday, so wait times for lifts were longer. Despite that I got a bunch of good runs in,
including one from the top of the moutain with the rest of my group! I ended the day with two runs from the top of the
moutain, and a sore tail bone, but with much more confidence in my ability to turn and stop on a board. 🤙🏻
The third day was a rest day for me. While some went back up the mountain for another day of skiing, a few others of us
decided to go on a snowshoe hike. This was my first time with snowshoes. They're a bit awkward, but pretty easy to get
the hang of. We took a loop around a small lake near Donner Lake. I always enjoy the peace that being out in nature
brings, and it was great to catch up with two of my friends in a smaller group.
{{< figures/image name=snowshoeing >}}
At the end of the day, we scrambled to amass a stockpile of snowballs for an ambush! The guys in our group had decided
to go skiing a third day, and were on their way back. When they pulled into the driveway, we attacked! Honestly, it
wasn't anywhere near a fair fight. 😅 They fought back though, survived the onslaught, and we all had a great time.
Spending quality time with friends, going on trips like this, means a lot to me. I've gotten to do trips with friends
like this a few times over the last couple years, but it's been a bit since I went with _this_ group. I had so much fun
being up in the moutains, hanging out, playing games, and having long conversations. We're such a good group, and I'm
grateful to have all of them in my life.
Thanks y'all. <3

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00a65714e64f92a4ae67d2d21c8be81341a433f9eabb23c032f835d8cc6a0580
size 3079324

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d82f2760711b2c4b7148bf22a141002a937890b8abb8568b4fd3746d8edf0d0
size 55408

View file

@ -1,26 +0,0 @@
---
title: "The Storyteller by Dave Grohl"
slug: the-storyteller-book
date: 2023-01-31T09:17:09-08:00
date_finished: 2023-01-31T09:17:09-08:00
series: 2023-books
categories: Books
tags: [Memoirs]
resources:
- name: cover
src: cover.jpg
alt: "The cover of The Storyteller by Dave Grohl: a profile photo of Dave Grohl with the title of the book overlaid."
title:
---
{{< figures/image name=cover >}}
I checked out [Dave Grohl's new memoir](https://www.davegrohlstoryteller.com) from the San Francisco Public Library as
an audiobook after a friend recommended it to me. Broadly, it's a series of anecdotes from his life, many of which
include famous celebrities and musicians.
Dave seems like a really genuine person. Throughout the book he expresses his gratitude for the people who've supported
him along the way. He's been through many challenging experiences too, and reflects on them with a positive attitude. I
enjoyed his humor and humility, and the pearls of wisdom he'd earned from those experiences.
[SFPL](https://sfpl.bibliocommons.com/v2/record/S93C4875170)

View file

@ -1,22 +0,0 @@
---
title: Trip to Japan
date: 2023-04-14T21:40:21+09:00
categories: Travel
tags:
- Travel
- japan
---
At the beginning of April, {{< tess >}} and I took a trip to Japan for two
weeks. She had a work meeting to attend in Tōkyō, and we were lucky to be able
to extend the trip to take some vacation before her meeting.
This was my first trip to Japan. I had been wanting to travel there since I was
a kid playing Pokémon Red on my OG Game Boy. To say I was excited is a bit of an
understatement.
You can read all about our trip [on my travel log page][series-page]. Tess also
wrote about it [on her website][tess-post].
[series-page]: {{< ref "/series/2023-japan" >}}
[tess-post]: https://tess.oconnor.cx/2023/04/japan

View file

@ -1,13 +0,0 @@
---
title: What Should I Blog About?
date: 2023-12-20T08:06:34-08:00
link: https://css-irl.info/what-to-blog-about-when-you-dont-know-what-to-blog-about/
categories: Tech
tags: [Writing]
---
I came across this handy list of [things to blog about when you don't know what
to blog about][link]. As someone who often doesn't know what to blog about, it's
nice to have a list of ideas for what to blog about.
[link]: https://css-irl.info/what-to-blog-about-when-you-dont-know-what-to-blog-about/

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7bc98cce772e0c075daed5a6dd9b1c18e6fd19e94ac68197893fd14cb58cf790
size 83946

View file

@ -1,27 +0,0 @@
---
title: "Yerba Buena by Nina LaCour"
date: 2023-11-23T09:50:06-07:00
series: 2023-books
categories: Books
tags: [Romance, Lesbians, Queerness]
resources:
- name: cover
src: cover.jpg
title:
params:
alt: An orange book cover with green leafy sprigs around the edges. Profiles of two women, overlapping and facing opposite directions. "Yerba Buena a novel" is written across the top half.
---
I thought this book was a lesbian romance -- and it is -- but it's so much more
too. It's the story of two women and the difficult pasts they emerge out of. I
really enjoyed how LaCour wove together their processing of that trauma with
growing into young women, making questionable choices, finding themselves, and
ultimately each other. It's heavy at times, but also beautiful. I really enjoyed
it. Thank you, {{< tess >}}, for the gift. ❤️
Get it at [Folio][folio] in San Francisco, or on [Bookshop.org][bookshop].
{{< figures/image name=cover class=content-width >}}
[folio]: https://www.foliosf.com/book/9781250810519
[bookshop]: https://bookshop.org/p/books/yerba-buena-nina-lacour/18721506?ean=9781250810519

View file

@ -1,28 +0,0 @@
---
title: "You Deserve a Tech Union by Ethan Marcotte"
date: 2023-12-16T08:19:41-08:00
draft: true
series: 2023-books
categories: Books
tags: [Unions, Tech]
---
Ethan's book came out in August 2023, and I've been eager to read it since he
announced it. Unionization has been a hot topic in the US over the last several
years. I've rooted for workers at Amazon, Google, Starbucks, and Apple to form
unions and advocate for their rights vis á vis their employers. I think that
work is so important.
This book provides an overview of the history of unions in the US, and in the US
tech industry, plus some helpful thoughts on how to form unions in your
workplace.
I found his arguments about why unions are important, even in an industrial
sector considered to be rather plush compared to many others. Many people think
of unions as organizations that advocate for _more_ privileges and protections
for workers. However, at least as important as that work, they also ensure that
workers retain the privileges they already have.
Get it from [A Book Apart][aba].
[aba]: https://abookapart.com/products/you-deserve-a-tech-union

View file

@ -1,4 +0,0 @@
---
title: 2024
date: 2024-01-01
---

View file

@ -1,56 +0,0 @@
---
title: ¡Ay Carmela!
description: |
Pero nada pueden bombas<br>
Donde sobra corazón
date: 2024-11-06T08:30:22-08:00
categories: Politics
tags:
- Music
- United States
- España
- Guerra Civil Española
---
> ```text
> El ejército del Ebro
> Rumbala rumbala rum-ba-la
> El ejército del Ebro
> Rumbala rumbala rum-ba-la
>
> Una noche el río pasó
> ¡Ay Carmela! ¡Ay Carmela!
> Una noche el río pasó
> ¡Ay Carmela! ¡Ay Carmela!
>
> Pero nada pueden bombas
> Rumbala rumbala rum-ba-la
> Pero nada pueden bombas
> Rumbala rumbala rum-ba-la
>
> Donde sobra corazón
> ¡Ay Carmela! ¡Ay Carmela!
> Donde sobra corazón
> ¡Ay Carmela! ¡Ay Carmela!
>
> Contraataques muy rabiosos
> Rumbala rumbala rum-ba-la
> Contraataques muy rabiosos
> Rumbala rumbala rum-ba-la
>
> <strong>Deberemos resistir</strong>
> ¡Ay Carmela! ¡Ay Carmela!
> Deberemos resistir
> ¡Ay Carmela! ¡Ay Carmela!
>
> Pero igual que combatimos
> Rumbala rumbala rum-ba-la
> Pero igual que combatimos
> Rumbala rumbala rum-ba-la
>
> <strong>Prometemos resistir</strong>
> ¡Ay Carmela! ¡Ay Carmela!
> Prometemos resistir
> ¡Ay Carmela! ¡Ay Carmela!
> ```
{cite="https://music.apple.com/us/album/ay-carmela/1119265269?i=1119265947" caption="Traditional; emphasis mine"}

View file

@ -1,23 +0,0 @@
---
title: B612
date: 2024-03-09T08:38:03-08:00
description: An open source font I rediscovered recently.
categories: Tech
tags: [Fonts]
resources:
- name: specimen
src: specimen.png
title: B612 y B612 Mono
params:
alt: "
Una muestra de B612 y B612 Mono. El mismo pangrama inglés, “quick brown fox,” es escrito en líneas separadas.
"
---
Recientemente, redescubrí la fuente [B612][b612]. Fue engargado por AirBus para
los tableros de instrumentos de sus aviones. Hace unos años que hizen de código
abierto.
{{% figures/image name=specimen class=content-width shouldResize=false %}}
[b612]: https://b612-font.com

View file

@ -1,24 +0,0 @@
---
title: B612
date: 2024-03-09T08:38:03-08:00
description: An open source font I rediscovered recently.
categories: Tech
tags: [Fonts]
resources:
- name: specimen
src: specimen.png
title: B612 and B612 Mono
params:
alt: "
A specimen of B612 and B612 Mono. The same “quick brown fox” pangram sentence is
set in each font on separate lines.
"
---
I recently rediscovered [B612][b612], a font comissioned by AirBus to serve as
the font for their airplanes' instrument panels. They made it open source
several years ago.
{{% figures/image name=specimen class=content-width shouldResize=false %}}
[b612]: https://b612-font.com

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

View file

@ -1,64 +0,0 @@
---
title: Books I Read in 2024
slug: books
date: 2024-12-31
tags:
- Books
- Reading
---
I enjoy reading quite a bit. Nevertheless, it's something I need to be
intentional about incorporating into my life. I usually to finish a modest
number of books in a year. This year I made it to a nice round ten.
A big theme of the year is the [Murderbot Diaries][murderbot]. My friend Jess
recommended them to me a while back, and I finally got around to [All Systems
Red][murderbot1] last year. I was hooked enough that {{< tess >}} bought me the
subsequent three for Christmas last year. So you're gonna see almost all the
Murderbot books on this list.
[Bindle Punk Bruja](https://marthawells.com/networkeffect.htm) by Desideria Mesa
: Mesa's debut novel is set in 1920s Kansas City and centers around Rose, the
daughter of Mexican immigrants who takes on the city's mob bosses with the help
of friends and some witchy magical powers. This was a gift from my sister
[Anna][anna] for Christmas last year.
[The Deep Sky](https://www.yumekitasei.com/projects-6) by Yume Kitasei
: A murder mystery that takes place in space on a one-way mission to colonize a
far-off planet. A gift from my sister.
[Wolfsong](https://www.tjklunebooks.com/wolfsong) by TJ Klune
: Warewolves, magic, queerness, and family. A gift from my sister.
[Fingersmith](https://www.penguinrandomhouse.com/books/348400/fingersmith-by-sarah-waters/) by Sarah Waters
: Mystery, intrigue, gay ladies, and some incredible plot twists. Tess gave me
this one.
[Artificial Condition](https://marthawells.com/murderbot2.htm) by Martha Wells
: Murderbot investigates its past.
[Rogue Protocol](https://marthawells.com/murderbot3.htm) by Martha Wells
: Murderbot goes digging for evidence against its former corporate master,
GrayCris.
[Exit Strategy](https://marthawells.com/murderbot4.htm) by Martha Wells
: Murderbot attempts to save its former owner from GrayCris.
[Network Effect](https://marthawells.com/networkeffect.htm) by Martha Wells
: Continuing the Murderbot obsession, I picked up this one from a [Books
Inc][booksinc] in San Francisco's Marina district.
[I'm Starting to Worry About This Black Box of Doom](https://us.macmillan.com/books/9781250285959/imstartingtoworryaboutthisblackboxofdoom/) by Jason Pargin
: Anna and I decided to read this together, book club style. It's a ridiculous
romp across the US with some deep commentary on the perils of social media: how
it promotes dogpiling in ways that can destroy lives, and the groupthink that
arises from being terminally online.
[The Full Moon Coffee Shop](https://www.penguinrandomhouse.com/books/746333/the-full-moon-coffee-shop-by-mai-mochizuki/) by Mai Mochizuki
: A cute, short read about a mysterious popup coffee shop that appears in Kyōto
during the full moon.
[murderbot]: https://marthawells.com/murderbot.htm
[murderbot1]: https://marthawells.com/murderbot1.htm
[anna]: https://www.instagram.com/anna.e.az/
[booksinc]: https://booksinc.net

Some files were not shown because too many files have changed in this diff Show more