Compare commits
1 commit
main
...
posts/lond
| Author | SHA1 | Date | |
|---|---|---|---|
| 0040cd0960 |
5
.gitattributes
vendored
|
|
@ -1,5 +0,0 @@
|
|||
*.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
|
||||
11
.gitignore
vendored
|
|
@ -1,12 +1,3 @@
|
|||
LightroomExports/
|
||||
node_modules/
|
||||
public/
|
||||
/documentation/mirrors/
|
||||
/resources/
|
||||
resources/
|
||||
.hugo_build.lock
|
||||
*.log
|
||||
*.orig
|
||||
*~
|
||||
|
||||
# Backup files for Markdown files processed in-place with sed
|
||||
*.md-e
|
||||
|
|
|
|||
21
.gitmodules
vendored
|
|
@ -1,18 +1,3 @@
|
|||
[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
|
||||
[submodule "themes/paper"]
|
||||
path = themes/paper
|
||||
url = ssh://git@github.com:erynofwales/hugo-paper.git
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-- Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
vim.bo.shiftwidth = 2
|
||||
vim.bo.softtabstop = 2
|
||||
|
|
@ -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/**")
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-- Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
local root = gitTopLevelDirectory()
|
||||
vim.opt_local.path:prepend(root .. "/assets/scripts/**")
|
||||
|
|
@ -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 ×
|
||||
]]
|
||||
|
|
@ -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",
|
||||
})
|
||||
45
Makefile
|
|
@ -1,44 +1,9 @@
|
|||
# Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
BUILD_DIR=public
|
||||
DEPLOY_LOCATION=eryn@nutmeg.erynwells.me:/srv/www/erynwells.me/html
|
||||
|
||||
CONTENT_PATH=content
|
||||
.PHONY: deploy
|
||||
deploy:
|
||||
hugo
|
||||
rsync -avz --no-times --no-perms --delete public/ $(DEPLOY_LOCATION)
|
||||
|
||||
DEPLOY_USER=eryn
|
||||
DEPLOY_HOSTNAME=nutmeg.erynwells.me
|
||||
DEPLOY_PATH=/srv/www/erynwells.me/html
|
||||
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_DATA_FILE=data/nethack/logfile/$(HOSTNAME).json
|
||||
|
||||
.PHONY: site deploy clean
|
||||
|
||||
site:
|
||||
@echo "Building site"
|
||||
hugo --buildFuture --enableGitInfo --destination "$(BUILD_DIR)"
|
||||
|
||||
deploy: site
|
||||
@echo "Removing .DS_Store files from $(DEPLOY_LOCATION)"
|
||||
find "$(BUILD_DIR)" -name .DS_Store -print -delete
|
||||
@echo "Deploying to $(DEPLOY_LOCATION)"
|
||||
rsync -avz --no-times --no-perms --delete "$(BUILD_DIR)/" "$(DEPLOY_LOCATION)"
|
||||
git tag -f $(shell ./scripts/website deployment next-tag)
|
||||
|
||||
deployall: nethack deploy
|
||||
|
||||
nethack: nethack-logfile nethack-commit
|
||||
|
||||
nethack-logfile: $(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)/"
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
slug: link-{{ .Name }}
|
||||
date: {{ .Date }}
|
||||
categories: links
|
||||
draft: true
|
||||
tags: []
|
||||
---
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
title: "{{ replace .Name "-" " " | title }}"
|
||||
date: {{ .Date }}
|
||||
draft: true
|
||||
---
|
||||
|
||||
{{< figures/p5 id="sketch" >}}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
const sketch = p => {
|
||||
p.setup = () => {
|
||||
const sketchContainer = document.querySelector('#sketch');
|
||||
const canvasWidth = parseFloat(getComputedStyle(sketchContainer).width);
|
||||
let canvas = p.createCanvas(canvasWidth, canvasWidth);
|
||||
canvas.canvas.removeAttribute('style');
|
||||
sketchContainer.appendChild(canvas.canvas);
|
||||
|
||||
p.pixelDensity(p.displayDensity());
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
p.background(255);
|
||||
p.strokeWeight(4);
|
||||
p.stroke(0, 0, 255);
|
||||
p.circle(100, 100, 100);
|
||||
};
|
||||
};
|
||||
|
||||
new p5(sketch, 'sketch');
|
||||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 464 KiB |
|
|
@ -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');
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import rr from "scripts/lib/railroad.js";
|
||||
|
||||
class RailroadDiagramManager {
|
||||
constructor() {
|
||||
this.figures = new Map();
|
||||
this.isCurrentlyNarrow = undefined;
|
||||
|
||||
this.diagramBreakpoint = window.matchMedia("(max-width: 450px)");
|
||||
this.diagramBreakpoint.addEventListener("change", () => {
|
||||
this.updateVisiblity();
|
||||
});
|
||||
}
|
||||
|
||||
add(svg) {
|
||||
const parent = svg.parentElement;
|
||||
if (!this.figures.has(parent)) {
|
||||
this.figures.set(parent, []);
|
||||
}
|
||||
|
||||
this.figures.get(parent).push(svg);
|
||||
|
||||
parent.removeChild(svg);
|
||||
}
|
||||
|
||||
updateVisiblity() {
|
||||
const isNarrow = this.diagramBreakpoint.matches;
|
||||
|
||||
if (isNarrow === this.isCurrentlyNarrow) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCurrentlyNarrow = isNarrow;
|
||||
|
||||
for (let [figure, svgs] of this.figures.entries()) {
|
||||
for (let svg of svgs) {
|
||||
const svgHasNarrowClass = svg.classList.contains("narrow");
|
||||
if (isNarrow && svgHasNarrowClass)
|
||||
figure.appendChild(svg);
|
||||
else if (!isNarrow && !svgHasNarrowClass)
|
||||
figure.appendChild(svg);
|
||||
else if (svg.parentElement === figure)
|
||||
figure.removeChild(svg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let railroadDiagramManager = new RailroadDiagramManager();
|
||||
|
||||
export function railroadDiagram(builder, elementID, isNarrow) {
|
||||
const diagram = builder(rr);
|
||||
const svg = diagram.addTo(document.getElementById(elementID));
|
||||
|
||||
if (isNarrow) {
|
||||
svg.classList.add("narrow");
|
||||
}
|
||||
|
||||
railroadDiagramManager.add(svg);
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
railroadDiagramManager.updateVisiblity();
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/* site.js
|
||||
* Eryn Wells <eryn@erynwells.me>
|
||||
*/
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
function getInlineSketchWidth() {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue("--body-width");
|
||||
}
|
||||
|
||||
function convertRemToPx(rem) {
|
||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
}
|
||||
|
|
@ -12,13 +12,6 @@ name = 'Eryn Wells'
|
|||
[languages.es]
|
||||
weight = 2
|
||||
|
||||
[markup]
|
||||
[markup.highlight]
|
||||
anchorLineNos = true
|
||||
lineNos = true
|
||||
lineNumbersInTable = false
|
||||
noClasses = false
|
||||
|
||||
[mediaTypes]
|
||||
[mediaTypes.'application/rss+xml']
|
||||
delimiter = '.'
|
||||
|
|
@ -29,16 +22,10 @@ name = 'Eryn Wells'
|
|||
identifier = 'blog'
|
||||
name = 'Blog'
|
||||
url = '/blog/'
|
||||
weight = 10
|
||||
[[menu.main]]
|
||||
identifier = 'photos'
|
||||
name = 'Photos'
|
||||
url = '/photos/'
|
||||
weight = 20
|
||||
[[menu.main]]
|
||||
identifier = 'about'
|
||||
name = 'About'
|
||||
url = '/about/'
|
||||
identifier = 'resume'
|
||||
name = 'Résumé'
|
||||
url = '/resume/'
|
||||
|
||||
[outputFormats]
|
||||
[outputFormats.RSS]
|
||||
|
|
@ -48,7 +35,7 @@ name = 'Eryn Wells'
|
|||
|
||||
[outputs]
|
||||
home = ['HTML', 'RSS']
|
||||
page = ['HTML', 'JSON']
|
||||
page = ['HTML']
|
||||
|
||||
[params]
|
||||
twitter = 'erynofwales'
|
||||
|
|
@ -58,11 +45,9 @@ description = 'Home page of Eryn Wells'
|
|||
|
||||
[permalinks]
|
||||
blog = 'blog/:year/:month/:slug/'
|
||||
photos = 'photos/:year/:month/:slug/'
|
||||
|
||||
[taxonomies]
|
||||
category = 'categories'
|
||||
location = 'locations'
|
||||
series = 'series'
|
||||
tag = 'tags'
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
baseURL: https://erynwells.me/
|
||||
languageCode: en-US
|
||||
title: ~eryn
|
||||
copyright: Copyright © 2020—2024 Eryn Wells
|
||||
defaultContentLanguage: en
|
||||
enableEmoji: true
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
en:
|
||||
languageName: English
|
||||
weight: 1
|
||||
es:
|
||||
languageName: Español
|
||||
weight: 2
|
||||
jp:
|
||||
languageName: 日本語
|
||||
weight: 3
|
||||
tok:
|
||||
languageName: toki pona
|
||||
weight: 4
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
goldmark:
|
||||
renderer:
|
||||
unsafe: true
|
||||
parser:
|
||||
attribute:
|
||||
block: true
|
||||
title: true
|
||||
highlight:
|
||||
anchorLineNos: true
|
||||
lineNos: false
|
||||
lineNumbersInTable: false
|
||||
noClasses: false
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
application/rss+xml:
|
||||
delimiter: .
|
||||
suffixes: [rss]
|
||||
application/atom+xml:
|
||||
delimiter: .
|
||||
suffixes: [atom, xml]
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
main:
|
||||
- identifier: blog
|
||||
name: Blog
|
||||
url: /blog/
|
||||
weight: 10
|
||||
- identifier: photos
|
||||
name: Photos
|
||||
url: /photos/
|
||||
weight: 20
|
||||
- identifier: about
|
||||
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
|
||||
weight: 10
|
||||
params:
|
||||
shortName: mst
|
||||
- identifier: github
|
||||
name: Github
|
||||
url: https://github.com/erynofwales
|
||||
weight: 20
|
||||
params:
|
||||
shortName: gh
|
||||
- identifier: instagram
|
||||
name: Instagram
|
||||
url: https://instagram.com/erynofwales
|
||||
weight: 30
|
||||
params:
|
||||
shortName: ig
|
||||
- identifier: feed
|
||||
name: feed
|
||||
url: /feed.atom
|
||||
weight: 40
|
||||
params:
|
||||
shortName: feed
|
||||
targetBlank: false
|
||||
about:
|
||||
- identifier: resume
|
||||
name: Résumé
|
||||
url: /resume/
|
||||
- identifier: whereAmI
|
||||
name: Where Am I
|
||||
url: /where-am-i/
|
||||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
RSS:
|
||||
mediatype: application/rss+xml
|
||||
baseName: feed
|
||||
suffixes: [rss]
|
||||
Atom:
|
||||
mediatype: application/atom+xml
|
||||
baseName: feed
|
||||
suffixes: [atom, xml]
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
home: [HTML, Atom]
|
||||
section: [HTML, Atom]
|
||||
taxonomy: [HTML]
|
||||
term: [HTML]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
blog: blog/:year/:month/:slug/
|
||||
photos: photos/:year/:month/:slug/
|
||||
twitter: twitter/:year/:month/:slug/
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
x:
|
||||
enableDNT: true
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
x:
|
||||
disableInlineCSS: true
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
category: categories
|
||||
location: locations
|
||||
series: series
|
||||
tag: tags
|
||||
|
|
@ -1,20 +1,48 @@
|
|||
---
|
||||
layout: single
|
||||
draft: true
|
||||
params:
|
||||
renderHeadingAnchors: false
|
||||
---
|
||||
# Hola! 👋🏻
|
||||
|
||||
¡Hola! Me llamo Eryn Wells. Este es mi sitio web. ¡Bienvenidos!
|
||||
Me llamo Eryn. Mis pronombres son [ella/ella][p]. Esta es me página personal. Bienvenide.
|
||||
|
||||
Soy una mujer trans y queer. Vivo en San Francisco con mis [dos gatos][cats]. Nací en Seattle, WA, y crecía en Phoenix, AZ. Asistí [Oberlin College][ob] donde obtuve un títolo en informática. {{< tess >}} es mi novia.
|
||||
|
||||
## Personal
|
||||
Mi lengua nativa es inglés, y también hablo español pero siempre necesito practicar más.
|
||||
|
||||
Soy mujer queer,
|
||||
## Pasatiempos
|
||||
|
||||
Tengo un gran apetito por probar y aprender pasatiempos nuevos, especialmente
|
||||
las que combinan varias de mis intereses.
|
||||
|
||||
## Profesional
|
||||
He sido música para el gran parte de mi vida. Empezé tocar el piano cuando tuve
|
||||
siete años, toqué la trompeta en colegio, y he tocado algunos otros instrumentos
|
||||
por el camino. Todavía toco el piano, pero mi obsesión musical hoy en día es mi
|
||||
sintetizador modular. De vez en cuando, grabo música y espero compartirla aquí,
|
||||
en este sitio. Por el momento, puedes escuchar mi música en [Bandcamp][bc] o
|
||||
[SoundCloud][sc].
|
||||
|
||||
Desde 2011, he trabajado como ingeniera de software en varios companías alrededor del área
|
||||
del Bahía de San Francisco. En 2016, me uní a [Apple][]. Hoy, trabajo en
|
||||
tecnologías que
|
||||
Otros pasatiempos de los años recients incluyen: cerámica, caligrafía, tejido,
|
||||
horneado pan, ciclismo, fotografía, y astronomía. ¿Uno no puede tener demasiados
|
||||
pasatiemos, de verdad?
|
||||
|
||||
## Trabajo
|
||||
|
||||
He trabajado como ingeniera de software desde 2011 por varias companías basadas
|
||||
en San Francisco y Silicon Valley. Hace seis años que he trabajado en Apple,
|
||||
primero en el equipo iOS Accessibility, y ahora en el equipo Authentication
|
||||
Experience.
|
||||
|
||||
Echa un vistazo a mi [resumen][r] para más detalles.
|
||||
|
||||
## Decirme Hola
|
||||
|
||||
Puedes encontrarme en muchos rincones del Internet. Estoy más activa en
|
||||
[Twitter][t] y [Instagram][i]. Publico música en [SoundCloud][sc] y
|
||||
[Bandcamp][bc]. Y para los proyectos de software, estoy en [GitHub][gh].
|
||||
|
||||
[p]: http://pronoun.is/she
|
||||
[cats]: {{< ref "/cats" >}}
|
||||
[ob]: https://www.oberlin.edu
|
||||
[r]: {{< ref "/resume" >}}
|
||||
[t]: https://twitter.com/erynofwales
|
||||
[i]: https://www.instagram.com/erynofwales/
|
||||
[sc]: https://soundcloud.com/purlsnbeeps
|
||||
[bc]: https://erynwells.bandcamp.com/releases
|
||||
[gh]: https://github.com/erynofwales
|
||||
|
|
@ -1,66 +1,56 @@
|
|||
---
|
||||
layout: single
|
||||
params:
|
||||
renderHeadingAnchors: false
|
||||
---
|
||||
# Hi! 👋🏻
|
||||
|
||||
Hi, I'm Eryn Wells. This is my website. Welcome!
|
||||
I'm Eryn. My pronouns are [she/her][p]. This is my personal page. Welcome!
|
||||
|
||||
I'm a queer trans woman. I live in San Francisco, CA, on the unceded ancestral
|
||||
lands of the Ramaytush Ohlone people, with my [two cats][cats]. I was born in
|
||||
Seattle, WA and grew up in Phoenix, AZ. I attended [Oberlin College][ob] where I
|
||||
got a degree in Computer Science. {{< tess >}} is my girlfriend.
|
||||
|
||||
## Latest
|
||||
I speak English natively, and Spanish too, though I always need more practice.
|
||||
|
||||
Here are some of my most recent posts.
|
||||
## Hobbies
|
||||
|
||||
{{< home/latest >}}
|
||||
I have a big appetite for learning new skills, especially things that combine
|
||||
multiple of my interests.
|
||||
|
||||
I've been a musician for most of my life. I started on the piano at age seven,
|
||||
played trumpet in high school, and have picked up a smattering of other
|
||||
instruments along the way. I still play piano occasionally, but my current
|
||||
musical obsession is my modular synthesizer. I occasionally record things, and
|
||||
I'm hoping to share them here on this site, but for now, they're on
|
||||
[SoundCloud][sc] and [Bandcamp][bc].
|
||||
|
||||
## Personal
|
||||
|
||||
I'm a queer 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][]
|
||||
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].
|
||||
I do try to spend my non-work time away from the computer, but every so often I
|
||||
get inspired to hack on something. Those projects usually end up on
|
||||
[GitHub][gh].
|
||||
|
||||
Other things I've picked up over the years include: ceramics, calligraphy,
|
||||
knitting, bread baking, bicycling, photography, and astronomy. You can never
|
||||
have too many hobbies, right?
|
||||
|
||||
## Professional
|
||||
|
||||
I've worked as a software engineer since 2011 for a variety of companies around
|
||||
the San Francisco Bay Area. I joined [Apple][] in 2016, where I currently work
|
||||
on password management and authentication technologies.
|
||||
the San Francisco Bay Area. I've been at Apple since early 2016, first on the
|
||||
iOS Accessibility team, and now on the Authentication Experience team.
|
||||
|
||||
My [résumé][r] has all the details.
|
||||
Check out my [résumé][r] for more details.
|
||||
|
||||
## Say Hello
|
||||
|
||||
## Hobbies
|
||||
You can find me in lots of other corners of the Internet. I'm most active on
|
||||
[Twitter][t] and [Instagram][i]. I post music on [SoundCloud][sc] and
|
||||
[Bandcamp][bc]. I'm on [GitHub][gh] for coding projects. You can also send me an
|
||||
[email][eml].
|
||||
|
||||
When I'm not working, you can reliably find me hacking on this website or [some
|
||||
other coding project][src]. I'm also a musician: I play piano, Irish tin
|
||||
whistle, and modular synthesizer. Occasionally I [record things][sc] or offer
|
||||
them [for sale][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
|
||||
[iaidō][mjer], a traditional Japanese sword art, at [Nishi Kaigan Iaidō
|
||||
Dōjō][nkid] since early 2024. Other things I've been into include: bread baking,
|
||||
bicycling, calligraphy, ceramics, and knitting.
|
||||
|
||||
|
||||
[Apple]: https://apple.com
|
||||
[ab]: {{< ref "/about" >}}
|
||||
[b]: {{< ref "/blog" >}}
|
||||
[bc]: https://erynwells.bandcamp.com/releases
|
||||
[p]: http://pronoun.is/she
|
||||
[cats]: {{< ref "/cats" >}}
|
||||
[eml]: mailto:Eryn%20Wells<eryn@erynwells.me>
|
||||
[src]: https://source.erynwells.me/eryn
|
||||
[ig]: https://www.instagram.com/erynofwales
|
||||
[m]: https://mastodon.social/@erynofwales
|
||||
[n]: {{< ref "/now" >}}
|
||||
[Oberlin College]: https://www.oberlin.edu
|
||||
[p]: {{< ref "/photos" >}}
|
||||
[pronouns]: http://pronoun.is/she
|
||||
[ob]: https://www.oberlin.edu
|
||||
[r]: {{< ref "/resume" >}}
|
||||
[t]: https://twitter.com/erynofwales
|
||||
[i]: https://www.instagram.com/erynofwales/
|
||||
[sc]: https://soundcloud.com/purlsnbeeps
|
||||
[where-am-i]: {{< ref "/about/where-am-i" >}}
|
||||
[mjer]: https://en.wikipedia.org/wiki/Mus%C5%8D_Jikiden_Eishin-ry%C5%AB
|
||||
[nkid]: https://iaido.org
|
||||
[bc]: https://erynwells.bandcamp.com/releases
|
||||
[gh]: https://github.com/erynofwales
|
||||
[eml]: mailto:Eryn%20Wells<eryn@erynwells.me>
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
---
|
||||
title: "Hola! 👋🏻"
|
||||
draft: false
|
||||
slug: sobre
|
||||
resources:
|
||||
- name: me
|
||||
src: me.jpeg
|
||||
---
|
||||
|
||||
{{< circular_image id=me name=me class="float-right" width=200
|
||||
alt="Una foto de me, con sombrero, sentando en frente de un fondo de piedra">}}
|
||||
|
||||
Me llamo Eryn. Mis pronombres son [ella/ella][p]. Esta es me página personal.
|
||||
Bienvenide.
|
||||
|
||||
Soy una mujer trans y queer. Vivo en San Francisco con mis [dos gatos][cats].
|
||||
Nací en Seattle, WA, y crecía en Phoenix, AZ. Asistí [Oberlin College][ob] donde
|
||||
obtuve un títolo en informática. {{< tess >}} es mi novia.
|
||||
|
||||
Mi lengua nativa es inglés, y también hablo español pero siempre necesito
|
||||
practicar más.
|
||||
|
||||
## Pasatiempos
|
||||
|
||||
Tengo un gran apetito por probar y aprender pasatiempos nuevos, especialmente
|
||||
las que combinan varias de mis intereses.
|
||||
|
||||
He sido música para el gran parte de mi vida. Empezé tocar el piano cuando tuve
|
||||
siete años, toqué la trompeta en colegio, y he tocado algunos otros instrumentos
|
||||
por el camino. Todavía toco el piano, pero mi obsesión musical hoy en día es mi
|
||||
sintetizador modular. De vez en cuando, grabo música y espero compartirla aquí,
|
||||
en este sitio. Por el momento, puedes escuchar mi música en [Bandcamp][bc] o
|
||||
[SoundCloud][sc].
|
||||
|
||||
Otros pasatiempos de los años recients incluyen: cerámica, caligrafía, tejido,
|
||||
horneado pan, ciclismo, fotografía, y astronomía. ¿Uno no puede tener demasiados
|
||||
pasatiemos, de verdad?
|
||||
|
||||
## Trabajo
|
||||
|
||||
He trabajado como ingeniera de software desde 2011 por varias companías basadas
|
||||
en San Francisco y Silicon Valley. Hace seis años que he trabajado en Apple,
|
||||
primero en el equipo iOS Accessibility, y ahora en el equipo Authentication
|
||||
Experience.
|
||||
|
||||
Echa un vistazo a mi [resumen][r] para más detalles.
|
||||
|
||||
## Decirme Hola
|
||||
|
||||
Puedes [encontrarme en muchos rincones del Internet][where-am-i]. Estoy más
|
||||
activa en [Twitter][t] y [Instagram][i]. Publico música en [SoundCloud][sc] y
|
||||
[Bandcamp][bc]. Y para los proyectos de software, estoy en [GitHub][gh].
|
||||
|
||||
[p]: http://pronoun.is/she
|
||||
[cats]: {{< ref "/cats" >}}
|
||||
[ob]: https://www.oberlin.edu
|
||||
[r]: {{< ref path="/resume" >}}
|
||||
[where-am-i]: {{< ref path="about/where-am-i" lang="es" >}}
|
||||
[t]: https://twitter.com/erynofwales
|
||||
[i]: https://www.instagram.com/erynofwales/
|
||||
[sc]: https://soundcloud.com/purlsnbeeps
|
||||
[bc]: https://erynwells.bandcamp.com/releases
|
||||
[gh]: https://github.com/erynofwales
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
---
|
||||
title: "Hi! 👋🏻"
|
||||
layout: single
|
||||
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 %}}
|
||||
|
||||
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,
|
||||
with my [two cats][cats]. I was born in Seattle, WA and grew up in Phoenix, AZ.
|
||||
I attended [Oberlin College][ob] where I got a degree in Computer Science.
|
||||
{{< tess >}} is my girlfriend.
|
||||
|
||||
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,
|
||||
played trumpet in high school, and have picked up a smattering of other
|
||||
instruments along the way. I still play piano occasionally, but my current
|
||||
musical obsession is my modular synthesizer. I occasionally record things, and
|
||||
I'm hoping to share them here on this site, but for now, they're on
|
||||
[SoundCloud][sc] and [Bandcamp][bc].
|
||||
|
||||
I do try to spend my non-work time away from the computer, but every so often I
|
||||
get inspired to hack on something. Those projects usually end up on
|
||||
[GitHub][gh].
|
||||
|
||||
Other things I've picked up over the years include: ceramics, calligraphy,
|
||||
knitting, bread baking, bicycling, photography, and astronomy. You can never
|
||||
have too many hobbies, right?
|
||||
|
||||
## Professional
|
||||
|
||||
I've worked as a software engineer since 2011 for a variety of companies around
|
||||
the San Francisco Bay Area. I've been at Apple since early 2016, first on the
|
||||
iOS Accessibility team, and now on the Authentication Experience team.
|
||||
|
||||
Check out my [résumé][r] for more details.
|
||||
|
||||
## Say Hello
|
||||
|
||||
You can find me in [lots of other corners of the Internet][where-am-i]. I'm most
|
||||
active on [Twitter][t] and [Instagram][i]. I post music on [SoundCloud][sc] and
|
||||
[Bandcamp][bc]. I'm on [GitHub][gh] for coding projects. You can also send me an
|
||||
[email][eml].
|
||||
|
||||
[p]: http://pronoun.is/she
|
||||
[cats]: {{< ref "/cats" >}}
|
||||
[ob]: https://www.oberlin.edu
|
||||
[r]: {{< ref "/resume" >}}
|
||||
[t]: https://twitter.com/erynofwales
|
||||
[i]: https://www.instagram.com/erynofwales/
|
||||
[sc]: https://soundcloud.com/purlsnbeeps
|
||||
[bc]: https://erynwells.bandcamp.com/releases
|
||||
[gh]: https://github.com/erynofwales
|
||||
[eml]: mailto:Eryn%20Wells<eryn@erynwells.me>
|
||||
[where-am-i]: {{< ref "/about/where-am-i" >}}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bd1145d48109dd4cb948c0f53c0dea540741ef9c95ff0c895c9b0d5ee831c510
|
||||
size 1286058
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
@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%);
|
||||
width: min(200px, 25%);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
title: "Dónde encontrarme"
|
||||
date: 2022-11-11T08:35:26-08:00
|
||||
slug: donde-encontrarme
|
||||
---
|
||||
|
||||
Aquí está una lista de dónde se puede encontrarme en línea.
|
||||
|
||||
## Redes Sociales
|
||||
|
||||
- Cohost: [@eryn](https://cohost.org/eryn)
|
||||
- Instagram: [@erynofwales](https://instagram.com/erynofwales)
|
||||
- Mastodon: [@erynofwales](https://mastodon.social/@erynofwales)
|
||||
- Twitter: [@erynofwales](https://twitter.com/erynofwales)
|
||||
|
||||
## Contenido
|
||||
|
||||
- Bandcamp: [erynwells](https://erynwells.bandcamp.com/releases)
|
||||
- Soundcloud: [purlsnbeeps](https://soundcloud.com/purlsnbeeps)
|
||||
- YouTube: [Eryn Wells](https://www.youtube.com/channel/UCWb2pTDlC27R1PucyUPrypA)
|
||||
- GitHub: [erynofwales](https://github.com/erynofwales)
|
||||
|
||||
## La Manera Antigua
|
||||
|
||||
- Email: [eryn@erynwells.me](mailto:Eryn%20Wells<eryn@erynwells.me>)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
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.
|
||||
|
||||
## 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)
|
||||
- Instagram: [@erynofwales](https://instagram.com/erynofwales)
|
||||
- Mastodon: [@erynofwales](https://mastodon.social/@erynofwales)
|
||||
- Twitter: [@erynofwales](https://twitter.com/erynofwales)
|
||||
|
||||
## 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)
|
||||
|
||||
## The Old Fashioned Way
|
||||
|
||||
- Email: [eryn@erynwells.me](mailto:Eryn%20Wells<eryn@erynwells.me>)
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
---
|
||||
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"]
|
||||
tags: ["Tech"]
|
||||
---
|
||||
|
||||
In order to do this, I modified the [EEPROM bootloader][eeprom] bootloader
|
||||
according to the instructions in the Raspberry Pi documentation. That page is
|
||||
also on [GitHub][eeprom-gh] which might be a more stable location. On Raspbian
|
||||
on the Raspberry Pi:
|
||||
on the RPi:
|
||||
|
||||
{{< figures/code >}}
|
||||
```sh
|
||||
```bash
|
||||
fw=/lib/firmware/raspberrypi/bootloader/stable/pieeprom-2020-09-03.bin
|
||||
rpi-eeprom-config $fw > ~/bootconf.txt
|
||||
vi ~/bootconf.txt
|
||||
|
|
@ -21,12 +19,10 @@ rpi-eeprom-config --out ~/pieeprom-new.bin --config ~/bootconf.txt $fw
|
|||
sudo rpi-eeprom-update -d -f ~/pieeprom-new.bin
|
||||
sudo reboot
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
My updated `bootconf.txt` is:
|
||||
My updated bootconf.txt is:
|
||||
|
||||
{{< figures/code >}}
|
||||
```cfg
|
||||
```conf
|
||||
[all]
|
||||
BOOT_UART=1
|
||||
WAKE_ON_GPIO=0
|
||||
|
|
@ -38,7 +34,6 @@ ENABLE_SELF_UPDATE=1
|
|||
DISABLE_HDMI=0
|
||||
BOOT_ORDER=0xf412
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
I enabled UART debugging, and set the boot order to be: network `0x2`, SD card
|
||||
`0x1`, USB mass storage `0x4`, and finally reboot `0xf`. These steps need to be
|
||||
|
|
@ -46,51 +41,45 @@ repeated if the bootloader is updated via apt.
|
|||
|
||||
I [enabled the TFTP server][mac-tftp] on my Mac:
|
||||
|
||||
{{< figures/code >}}
|
||||
```sh
|
||||
```bash
|
||||
sudo launchctl load -F /System/Library/LaunchDaemons/tftp.plist
|
||||
sudo launchctl enable System/com.apple.tftpd
|
||||
sudo launchctl start com.apple.tftpd
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
I’m not sure if the `enable` command is actually necessary. This doesn't
|
||||
actually start the `tftpd` daemon. Instead, macOS starts the daemon on demand
|
||||
when it notices an incoming tftp request on the network. Don't be alarmed!
|
||||
|
||||
The tftp server looks for files to serve out of **`/private/tftpboot`**, and those
|
||||
The tftp server looks for files to serve out of **/private/tftpboot**, and those
|
||||
things need to be world `rwx`, i.e. `777`. By default (this is configurable) the
|
||||
Raspberry Pi queries for a directory named by its serial number.
|
||||
RPi queries for a directory named by its serial number.
|
||||
|
||||
{{< figures/code >}}
|
||||
```sh
|
||||
```bash
|
||||
mkdir /private/tftpboot/$raspberry_pi_serial
|
||||
chmod 777 /private/tftpboot
|
||||
chmod -R 777 /private/tftpboot/*
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
Raspberry Pi looks for files of various names in that directory, one in
|
||||
particular by the name of **`start.elf`**.
|
||||
RPi looks for files of various names in that directory, one in particular by the
|
||||
name of **start.elf**.
|
||||
|
||||
Next, I had to update my Ubiquiti router's DHCP server configuration (on the
|
||||
command line) to pass a `tftp-server` parameter in the DHCP payload. This step
|
||||
may be optional because you can also set `TFTP_IP` in the **`bootconf.txt`** above
|
||||
may be optional because you can also set `TFTP_IP` in the **bootconf.txt** above
|
||||
to specify the IP directly. On my router:
|
||||
|
||||
{{< figures/code >}}
|
||||
```sh
|
||||
```bash
|
||||
configure
|
||||
set service dhcp-server shared-network-name LAN subnet $lan_cidr_subnet tftp-server-name $ip_of_mac
|
||||
commit
|
||||
save
|
||||
exit
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
I also gave my Mac a static IP, and renewed the DHCP lease so it took the new IP
|
||||
to make the whole process a little more smooth. Now, it appears the Raspberry
|
||||
Pi will attempt a TFTP boot, and I see queries in the logs on my Mac.
|
||||
to make the whole process a little more smooth. Now, it appears the RPi will
|
||||
attempt a TFTP boot, and I see queries in the logs on my Mac.
|
||||
|
||||
## Further Reading
|
||||
|
||||
|
|
@ -99,4 +88,4 @@ Pi will attempt a TFTP boot, and I see queries in the logs on my Mac.
|
|||
|
||||
[eeprom]: https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711_bootloader_config.md
|
||||
[eeprom-gh]: https://github.com/raspberrypi/documentation/blob/master/hardware/raspberrypi/bcm2711_bootloader_config.md
|
||||
[mac-tftp]: https://www.unixfu.ch/start-a-tftp-server-on-your-mac/
|
||||
[mac-tftp]: https://www.unixfu.ch/start-a-tftp-server-on-your-mac/
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: 2020
|
||||
date: 2020-01-01
|
||||
---
|
||||
|
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 3.4 MiB |
|
|
@ -11,12 +11,10 @@ resources:
|
|||
- name: "ramen"
|
||||
src: "images/ramen.jpg"
|
||||
title: "A bowl of ramen from Marufuku"
|
||||
categories: ["Travel"]
|
||||
locations: ["San Francisco", "California", "United States"]
|
||||
tags: ["Staycation", "Food"]
|
||||
tags: ["Travel"]
|
||||
---
|
||||
|
||||
{{< figures/image name=buchanan >}}
|
||||
{{< post_figure name=buchanan >}}
|
||||
|
||||
This past weekend, {{< tess >}} and I took a short, one-night staycation in San
|
||||
Francisco's Japantown. With all the flurry of things happening in our lives, it
|
||||
|
|
@ -43,7 +41,7 @@ walking around the neighborhood, eating, and relaxing. For only being just over
|
|||
|
||||
Tess also [wrote](https://tess.oconnor.cx/2021/09/japantown) about our trip.
|
||||
|
||||
{{< figures/image name=ramen >}}
|
||||
{{< post_figure name=ramen >}}
|
||||
|
||||
[hk]: https://www.jdvhotels.com/hotels/california/san-francisco/hotel-kabuki
|
||||
[ks]: https://kabukisprings.com
|
||||
[ks]: https://kabukisprings.com
|
||||
|
|
@ -3,11 +3,10 @@ title: "Oskitone Scout"
|
|||
date: 2021-09-11T16:47:07-07:00
|
||||
description: A timelapse video of me building an Oskitone Scout set to music produced using the Scout itself.
|
||||
draft: false
|
||||
categories: ["Music"]
|
||||
tags: ["Synthesizers", "Electronics", "DIY", "Compositions"]
|
||||
tags: ["Music", "Synthesizers", "DIY"]
|
||||
---
|
||||
|
||||
{{< youtube id="gCSwWsxzy_c" title="A timelapse video of me building an Oskitone Scout, set to music produced using the Scout itself" >}}
|
||||
{{< youtube_figure 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
|
||||
|
|
@ -17,4 +16,4 @@ building the kit, set to music I wrote and produced with the completed Scout.
|
|||
Enjoy!
|
||||
|
||||
[oskitone]: https://www.oskitone.com
|
||||
[scout]: https://www.oskitone.com/product/scout-synth
|
||||
[scout]: https://www.oskitone.com/product/scout-synth
|
||||
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 328 KiB |
|
|
@ -7,19 +7,10 @@ resources:
|
|||
- name: friends
|
||||
src: "friends.jpg"
|
||||
title: ""
|
||||
categories: ["Travel"]
|
||||
locations:
|
||||
- "Portland, OR"
|
||||
- "Oregon"
|
||||
- "Olympic Peninsula"
|
||||
- "Seattle, WA"
|
||||
- "Washington"
|
||||
- "Pacific Northwest"
|
||||
- "United States"
|
||||
tags: ["Pandemic", "Friends"]
|
||||
tags: ["Travel", "Pandemic", "Friends"]
|
||||
---
|
||||
|
||||
{{< figures/image name=friends >}}
|
||||
{{< post_figure name=friends >}}
|
||||
|
||||
I'm writing this in the airport having just spend a week with a group of friends
|
||||
who mean the world to me. For the past 18 months, we have been spending our
|
||||
|
|
@ -41,4 +32,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 1447951049076056071 >}}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: 2021
|
||||
date: 2021-01-01
|
||||
---
|
||||
|
|
@ -3,11 +3,10 @@ title: "Colorado Modular Synth Society Three Module Challenge"
|
|||
date: 2022-01-23T09:21:35-07:00
|
||||
description: In which I submit a video of me playing a small Eurorack system to CMSS
|
||||
draft: false
|
||||
categories: ["Music"]
|
||||
tags: ["Eurorack", "Synthesizers", "Recordings", "Performances", "Compositions"]
|
||||
tags: ["Music", "Eurorack", "Synthesizers", "Recordings", "Performances"]
|
||||
---
|
||||
|
||||
{{< 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." >}}
|
||||
{{< youtube_figure 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
|
||||
|
|
@ -20,4 +19,4 @@ performing, without the stage fright that comes with doing it live in front of a
|
|||
crowd. I encourage you to give it a go if you're interested in performing!
|
||||
|
||||
[3mc]: https://youtu.be/sglQv_fV4FQ?t=1930
|
||||
[hosts]: https://youtu.be/sglQv_fV4FQ?t=2382
|
||||
[hosts]: https://youtu.be/sglQv_fV4FQ?t=2382
|
||||
|
|
@ -3,8 +3,7 @@ title: "Profiling ZSH"
|
|||
date: 2022-01-23T11:35:38-08:00
|
||||
description: In which I learn about how to profile my ZSH init files.
|
||||
draft: false
|
||||
categories: ["Tech"]
|
||||
tags: ["ZSH", "Dotfiles"]
|
||||
tags: ["Tech", "ZSH"]
|
||||
---
|
||||
|
||||
I've been hacking on my [dotfiles][dotfiles] a lot lately. One of the things
|
||||
|
|
@ -14,11 +13,9 @@ undertook the same project.
|
|||
|
||||
TIL, ZSH has a profiler built in. You can start it by calling the following.
|
||||
|
||||
{{< figures/code >}}
|
||||
```zsh
|
||||
zmodload zsh/zprof
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
Then, once you're done, you call `zprof` to get a report that tells you where
|
||||
ZSH is spending most of its time. I put the line above at the top of my
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
title: Ay, Ella Se Ha Vuelto Adicta a Nethack
|
||||
date: 2022-04-24T17:36:33-07:00
|
||||
description: In which I get hooked on that one roguelike (God help me)
|
||||
categories: ["Games"]
|
||||
draft: false
|
||||
tags: ["Nethack", "Video Games"]
|
||||
---
|
||||
|
||||
Hace unas semanas que conecté a mi VPS para averiguar algo. Hay [un
|
||||
parte][list-tmux-sessions] de [mis dotfiles][zprofile] que ejecuta durante la
|
||||
iniciación de las sesiones de inicio de ZSH que imprime las sesiones existidos
|
||||
de `tmux` asociados con mi cuenta. Me dio cuenta que había una sesión y me la
|
||||
adjunté.
|
||||
de `tmux` asociados con mi cuenta. Realizé que había una sesión y me la adjunté.
|
||||
|
||||
En esa sesión encontré un juego de [Nethack][nethack] que empezaba en enero y
|
||||
nunca lo completé. Lo completé, me mató un duende, y, pues, el daño ya se me
|
||||
|
|
@ -18,7 +17,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
|
||||
|
|
@ -35,4 +34,4 @@ web. Echa un vistazo a mi [logfile](/nethack) de Nethack para esa.
|
|||
[nethack]: https://www.nethack.org
|
||||
[nethackwiki]: https://nethackwiki.com/wiki/Main_Page
|
||||
[priceid]: https://nethackwiki.com/wiki/Price_identification
|
||||
[over9000]: https://www.youtube.com/watch?v=ITWMoS2L1oo
|
||||
[over9000]: https://www.youtube.com/watch?v=ITWMoS2L1oo
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
title: Oh Dear, She Got Hooked on Nethack Again
|
||||
date: 2022-04-24T17:36:33-07:00
|
||||
description: In which I get hooked on that one roguelike (God help me)
|
||||
categories: ["Games"]
|
||||
tags: ["Nethack", "Roguelikes"]
|
||||
draft: false
|
||||
tags: ["Nethack", "Video Games", "Roguelikes"]
|
||||
---
|
||||
|
||||
A couple weeks ago, I connected to my VPS to check on something. There's [a
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 804 KiB |
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
title: "Roguelikes I Like"
|
||||
date: 2022-05-09T08:37:23-07:00
|
||||
description: Some roguelikes I’ve enjoyed recently.
|
||||
draft: false
|
||||
resources:
|
||||
- name: nethack
|
||||
|
|
@ -13,7 +12,6 @@ resources:
|
|||
- name: dcss
|
||||
src: "dcss.png"
|
||||
title: ""
|
||||
categories: ["Games"]
|
||||
tags: ["Roguelikes", "Video Games"]
|
||||
---
|
||||
|
||||
|
|
@ -28,7 +26,7 @@ Most recently, it's what got me hooked on the genre all over again. Famously, it
|
|||
you start. It's up to you to figure out everything based on what you -- the player -- know about the game. It's like a
|
||||
puzzle!
|
||||
|
||||
{{< figures/image name=nethack >}}
|
||||
{{< post_figure name=nethack >}}
|
||||
|
||||
### Brogue
|
||||
|
||||
|
|
@ -37,7 +35,7 @@ awe. It does a really great job of making use of the terminal to render a beauti
|
|||
(there are lakes!) animates, there are fields of mossy grass and plants, there are blobs of goo that explode and spread
|
||||
noxious pink gas everywhere.
|
||||
|
||||
{{< figures/image name=brogue >}}
|
||||
{{< post_figure name=brogue >}}
|
||||
|
||||
### Dungeon Crawl Stone Soup
|
||||
|
||||
|
|
@ -45,7 +43,7 @@ My friend David introduced me to [Dungeon Crawl Stone Soup][dcss] on Twitter. I
|
|||
the complexities of many roguelikes into a simpler form that's still rich and engaging. I really like how easy it is to
|
||||
drop in on people and observe games too!
|
||||
|
||||
{{< figures/image name=dcss >}}
|
||||
{{< post_figure name=dcss >}}
|
||||
|
||||
[nh]: https://www.nethack.org
|
||||
[b]: https://sites.google.com/site/broguegame/
|
||||
|
|
|
|||
|
|
@ -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"><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 |
|
|
@ -1,204 +0,0 @@
|
|||
---
|
||||
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.
|
||||
categories: ["Tech"]
|
||||
tags: ["P5.js", "Programming", "Web", "Art"]
|
||||
resources:
|
||||
- src: sketch.js
|
||||
params:
|
||||
is_module: false
|
||||
---
|
||||
|
||||
{{< audio id="amen" mp3="amen.mp3" >}}
|
||||
|
||||
This is a quick write-up to share with y'all a small project I've been working
|
||||
on using [P5.js][p5] and [Web Audio][webaudio] to implement some audio
|
||||
visualizations. By the end, we'll have something like this:
|
||||
|
||||
{{< figures/p5 id="oscilloscopeFinal" bordered=1 >}}
|
||||
|
||||
## Embedding an Audio File
|
||||
|
||||
HTML has the ability to [embed audio][mdn-audio-tag] in a page with the
|
||||
`<audio>` tag. This one declares a single MP3 file as a source.
|
||||
|
||||
{{< figures/code >}}
|
||||
```html
|
||||
<audio id="amen">
|
||||
<source src="amen.mp3" type="audio/mpeg">
|
||||
</audio>
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
In this form, the `<audio>` element doesn't do anything except declare some
|
||||
audio that can be played. It's invisible and the user can't interact with it or
|
||||
control playback. That fine because because I'm going to implement my own
|
||||
playback control as part of my sketch below.
|
||||
|
||||
## Processing Audio with Web Audio
|
||||
|
||||
Web Audio uses a node-based paradigm to process audio. Audio flows from source
|
||||
nodes, through a web of interconnected processing nodes, and out through
|
||||
destination nodes.
|
||||
|
||||
Sources can be `<audio>` tags or realtime waveform generators; processing nodes
|
||||
might be filters, gain adjustments, or more complex effects like reverb; and
|
||||
destinations could be your computer's speakers or a file.
|
||||
|
||||
Here's the entire code snippet that sets up the audio processing I need for the
|
||||
sketch:
|
||||
|
||||
{{< figures/code >}}
|
||||
```js {linenostart=2}
|
||||
let analyzerNode = null;
|
||||
let samples = null;
|
||||
|
||||
let audioElement = (() => {
|
||||
return document.querySelector('audio#amen');
|
||||
})();
|
||||
|
||||
let audioContext = (() => {
|
||||
const audioContext = new AudioContext();
|
||||
const track =
|
||||
audioContext.createMediaElementSource(audioElement);
|
||||
analyzerNode = audioContext.createAnalyser();
|
||||
|
||||
track.connect(analyzerNode)
|
||||
.connect(audioContext.destination);
|
||||
|
||||
return audioContext;
|
||||
})();
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
The [`AudioContext`][mdn-audio-context] is the object that encapsulates the
|
||||
entire node graph. On line 10, I create a new `AudioContext`.
|
||||
|
||||
On line 11, I create a [`MediaElementSourceNode`][mdn-webaudio-media-source-tag]
|
||||
with the `<audio>` element I declared on this page.
|
||||
|
||||
Next, line 13 creates an [AnalyzerNode][mdn-analyzer-node]. Analyzer nodes don't
|
||||
affect the audio that flows through them. Instead, this node gives the sketch
|
||||
access to the raw audio samples as they're passing through the AudioContext.
|
||||
We'll use this to plot the waveform as the audio is playing!
|
||||
|
||||
Line 15 hooks up the nodes in the graph. We connect the output of the source
|
||||
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:
|
||||
|
||||

|
||||
|
||||
By itself the AudioContext doesn't actually play any audio. I'll tackle that
|
||||
next.
|
||||
|
||||
## Playing Audio
|
||||
|
||||
Next up is starting playback. The following snippet creates a Play button using
|
||||
P5.js's DOM manipulation API, and hooks up the button's `click` event to start
|
||||
and stop playback.
|
||||
|
||||
{{< figures/code >}}
|
||||
```js {linenostart=29}
|
||||
const playPauseButton = p.createButton('Play');
|
||||
playPauseButton.position(10, 10);
|
||||
|
||||
const playPauseButtonElement = playPauseButton.elt;
|
||||
playPauseButtonElement.dataset.playing = 'false';
|
||||
playPauseButtonElement.addEventListener('click', function() {
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
|
||||
if (this.dataset.playing === 'false') {
|
||||
audioElement.play();
|
||||
this.dataset.playing = 'true';
|
||||
this.innerHTML = '<span>Pause</span>';
|
||||
} else if (this.dataset.playing === 'true') {
|
||||
audioElement.pause();
|
||||
this.dataset.playing = 'false';
|
||||
this.innerHTML = '<span>Play</span>';
|
||||
}
|
||||
});
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
Something I found odd while working with these audio components is there isn't a
|
||||
way to ask any of them if audio is playing back at any given moment. Instead it
|
||||
is up to the script to listen for the appropriate [events][mdn-audio-tag-events]
|
||||
and track playback state itself.
|
||||
|
||||
If this snippet looks a little convoluted, that's why.
|
||||
|
||||
To track playback status, I decided to set a `playing` property on the button's
|
||||
`dataset` indicating whether to call `audioElement.play()` or
|
||||
`audioElement.pause()` and to set the label of the button appropriately.
|
||||
|
||||
The last bit of playback state tracking to do is to listen for when playback
|
||||
ends because it reached the end of the audio file. I did that with the `ended`
|
||||
event:
|
||||
|
||||
{{< figures/code >}}
|
||||
```js {linenostart=53}
|
||||
audioElement.addEventListener('ended', function() {
|
||||
playPauseButtonElement.dataset.playing = 'false';
|
||||
playPauseButtonElement.innerHTML = '<span>Play</span>';
|
||||
}, false);
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
This handler resets the `playing` flag and the label of the button.
|
||||
|
||||
## The Sketch
|
||||
|
||||
Now it's time to draw some waveforms! The main part of a P5 sketch is the `draw` method. Here's mine:
|
||||
|
||||
{{< figures/code >}}
|
||||
```js {linenostart=57}
|
||||
const amplitude = p.height / 2;
|
||||
const axis = p.height / 2;
|
||||
|
||||
const blue = p.color(24, 62, 140);
|
||||
const purple = p.color(255, 0, 255);
|
||||
|
||||
p.background(255);
|
||||
|
||||
if (analyzerNode) {
|
||||
analyzerNode.getFloatTimeDomainData(samples);
|
||||
}
|
||||
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const sampleValue = samples[i];
|
||||
const absSampleValue = Math.abs(sampleValue);
|
||||
|
||||
const weight = p.lerp(2, 12, 1.5 * absSampleValue);
|
||||
p.strokeWeight(sampleValue === 0 ? 1 : weight);
|
||||
p.stroke(p.lerpColor(blue, purple, absSampleValue));
|
||||
|
||||
p.point(i, axis + amplitude * sampleValue);
|
||||
}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
The most interesting part of this function starts at line 66 where we get an array of samples from the analyzer node. The `samples` variable is a JavaScript `Float32Array`, with one element for each pixel of width.
|
||||
|
||||
{{< figures/code >}}
|
||||
```js {linenostart=30}
|
||||
samples = new Float32Array(p.width);
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
Once the sample data is populated from the analyzer, we can render them by
|
||||
plotting them along the X axis, scaling them to the height of the sketch.
|
||||
|
||||
I also manipulate the weight (size) of the point and its color by interpolating sizes and colors based on the value of the sample.
|
||||
|
||||
[p5]: https://p5js.org
|
||||
[webaudio]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API
|
||||
[mdn-audio-tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio
|
||||
[mdn-audio-tag-events]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#events
|
||||
[mdn-audio-context]: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
|
||||
[mdn-webaudio-media-source-tag]: https://developer.mozilla.org/en-US/docs/Web/API/MediaElementAudioSourceNode
|
||||
[mdn-create-media-element-source]: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/createMediaElementSource
|
||||
[mdn-analyzer-node]: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
const oscilloscopeFinal = p => {
|
||||
let analyzerNode = null;
|
||||
let samples = null;
|
||||
|
||||
let audioElement = (() => {
|
||||
return document.querySelector('audio#amen');
|
||||
})();
|
||||
|
||||
let audioContext = (() => {
|
||||
const audioContext = new AudioContext();
|
||||
const track =
|
||||
audioContext.createMediaElementSource(audioElement);
|
||||
analyzerNode = audioContext.createAnalyser();
|
||||
|
||||
track.connect(analyzerNode)
|
||||
.connect(audioContext.destination);
|
||||
|
||||
return audioContext;
|
||||
})();
|
||||
|
||||
p.setup = () => {
|
||||
const sketchContainer = document.querySelector('#oscilloscopeFinal');
|
||||
const canvasWidth = parseFloat(getComputedStyle(sketchContainer).width);
|
||||
let canvas = p.createCanvas(canvasWidth, 250);
|
||||
canvas.canvas.removeAttribute('style');
|
||||
sketchContainer.appendChild(canvas.canvas);
|
||||
|
||||
p.pixelDensity(p.displayDensity());
|
||||
|
||||
samples = new Float32Array(p.width);
|
||||
|
||||
const playPauseButton = p.createButton('Play');
|
||||
playPauseButton.position(10, 10);
|
||||
|
||||
const playPauseButtonElement = playPauseButton.elt;
|
||||
playPauseButtonElement.dataset.playing = 'false';
|
||||
playPauseButtonElement.addEventListener('click', function() {
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume();
|
||||
}
|
||||
|
||||
if (this.dataset.playing === 'false') {
|
||||
audioElement.play();
|
||||
this.dataset.playing = 'true';
|
||||
this.innerHTML = '<span>Pause</span>';
|
||||
} else if (this.dataset.playing === 'true') {
|
||||
audioElement.pause();
|
||||
this.dataset.playing = 'false';
|
||||
this.innerHTML = '<span>Play</span>';
|
||||
}
|
||||
});
|
||||
|
||||
audioElement.addEventListener('ended', function() {
|
||||
playPauseButtonElement.dataset.playing = 'false';
|
||||
playPauseButtonElement.innerHTML = '<span>Play</span>';
|
||||
}, false);
|
||||
};
|
||||
|
||||
p.draw = () => {
|
||||
const amplitude = p.height / 2;
|
||||
const axis = p.height / 2;
|
||||
|
||||
const blue = p.color(24, 62, 140);
|
||||
const purple = p.color(255, 0, 255);
|
||||
|
||||
p.background(255);
|
||||
|
||||
if (analyzerNode) {
|
||||
analyzerNode.getFloatTimeDomainData(samples);
|
||||
}
|
||||
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const sampleValue = samples[i];
|
||||
const absSampleValue = Math.abs(sampleValue);
|
||||
|
||||
const weight = p.lerp(2, 12, 1.5 * absSampleValue);
|
||||
p.strokeWeight(sampleValue === 0 ? 1 : weight);
|
||||
p.stroke(p.lerpColor(blue, purple, absSampleValue));
|
||||
|
||||
p.point(i, axis + amplitude * sampleValue);
|
||||
}
|
||||
};
|
||||
|
||||
p.mouseClicked = () => {
|
||||
p.clear();
|
||||
};
|
||||
};
|
||||
|
||||
new p5(oscilloscopeFinal, 'oscilloscopeFinal');
|
||||
38
content/blog/2022/08/hellas/index.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: "Ελλάς"
|
||||
date: 2022-08-11T08:19:08-07:00
|
||||
draft: true
|
||||
tags: ["Travel", "Greece"]
|
||||
---
|
||||
|
||||
After being in [London]({{< ref "blog/2022/08/london" >}}) for a week, {{< tess >}} and I took a flight to Greece for a
|
||||
week of vacation.
|
||||
|
||||
## Αθήνα
|
||||
|
||||
Our first stop was Athens for a couple of days. We stayed in a cute little hotel in the Κεραμεικός neighborhood. I
|
||||
learned later that it's called that because it's the historical pottery quarter of Athens!
|
||||
|
||||
We took a walk through the nearby neighborhoods to Πλατεία Συντάγματος, the square in front of the Hellenic Parliament,
|
||||
and the Εθνικός Κήπος (National Gardens) next door.
|
||||
|
||||
The next day was our one big day in Athens so we wanted to get an early start. We managed to fall asleep without setting our alarms though, and along with some *exceptional* blackout shades, we didn't wake up until almost noon!
|
||||
|
||||
Despite getting a late start, we had a great day of sightseeing. We explored the Library of Hadrian, the αρχαία αγορά (Ancient Agora), and the Acropolis. It's truly stunning how old so many of these buildings are. Athens was a bustling Roman city, and the remains of the Greeks were ancient even then! Coming from London, it was also abundantly clear how
|
||||
|
||||
On our last night, we met up Lea, Chris, and their kiddo Zoe, for dinner at in an outlying neighborhood of Athens.
|
||||
|
||||
## Δελφοί
|
||||
|
||||
## Σκάλα Ερεσού, Λεσβος
|
||||
|
||||
## Home Again
|
||||
|
||||
Getting home again was thankfully pretty painless. We took flew back to Athens the night before our flight to London,
|
||||
staying overnight in an airport hotel. Twelve hours on a plane from London to San Francisco brought us home again. I
|
||||
still find myself wanting to say "γεια σας" to waitstaff and store clerks, but the [kitties]({{< ref "/cats" >}}) are
|
||||
glad to have us back.
|
||||
|
||||
Tess [wrote about our trip to Greece][tess-greece] too.
|
||||
|
||||
[tess-greece]: https://tess.oconnor.cx/2022/08/greece
|
||||
27
content/blog/2022/08/london/index.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: "London"
|
||||
date: 2022-08-11T08:16:16-07:00
|
||||
draft: true
|
||||
tags: ["Travel", "England"]
|
||||
---
|
||||
|
||||
In late July, I traveled with {{< tess >}} to London. I was tagging along on a work trip of hers, and it was the first
|
||||
time I had traveled abroad since 2015.
|
||||
|
||||
We landed in London on Saturday. We stayed in a hotel facing Russell Square that turned out to be a great location for all of the things we wanted to do. It happened to be only a couple blocks from [Gay's The Word][gay], one of the only if not *the* only LGBTQIA+ bookshops in London.
|
||||
|
||||
## The British Museum
|
||||
|
||||
## Bletchley Park
|
||||
|
||||
## Greenwich
|
||||
|
||||
## Moving On
|
||||
|
||||
After a week in London, we went to [Greece]({{< ref "blog/2022/08/hellas" >}}). It was the second week of vacation for
|
||||
me, and a full week of vacation for her.
|
||||
|
||||
Tess [wrote about our trip to London][tess-london] too.
|
||||
|
||||
[gay]: https://www.gaystheword.co.uk
|
||||
[tess-london]: https://tess.oconnor.cx/2022/07/london
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
---
|
||||
title: "Hugo's Dictionary API"
|
||||
date: 2022-10-13T10:19:02-07:00
|
||||
description: I’ve found Hugo’s API for collections to be difficult to understand. Here’s my attempt to summarize it’s quirks.
|
||||
categories: ["Tech"]
|
||||
tags: ["Hugo", "Web", "API Design"]
|
||||
series: "Erynwells.me Development"
|
||||
---
|
||||
|
||||
Hugo's templating system has support for dictionaries. Unfortunately the API for
|
||||
working with them is, frankly, awful. While working on developing some new
|
||||
templates for this site, I had to figure out how to build up dictionary data
|
||||
structures and it took me a _long_ time to figure out how to do some basic
|
||||
operations with them.
|
||||
|
||||
Here's a quick summary of what I found.
|
||||
|
||||
## Creating Dictionaries
|
||||
|
||||
The function to create a dictionary is called [`dict`][dict] and it takes a
|
||||
variable number of arguments that alternate between keys and values. It reminds
|
||||
me of this [bizarre and backwards NSDictionary API][nsdictionary-init] in Apple's
|
||||
Foundation framework. Keys must be strings (or string slices) and values can be
|
||||
anything. So this:
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{ $d := dict "a" 1 "b" 2 "c" 3 }}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
creates a structure that looks like this JSON object:
|
||||
|
||||
{{< figures/code >}}
|
||||
```json
|
||||
{ "a": 1, "b": 2, "c": 3 }
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
You can also create an empty dictionary by calling `dict` with no arguments.
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{ $d := dict }}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
## Accessing Keys and Values
|
||||
|
||||
Statically, you can get a single item in a dictionary with dot syntax. Below,
|
||||
`$item` will get the value 1.
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{ $item := (dict "a" 1 "b" 2 "c" 3).a }}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
If you want to get a value with a key you get at render time, you can use the
|
||||
[`index`][index] function. In the snippet below, `$item` will get the value of
|
||||
`"b"`, which is 2.
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{ $key := "b" }}
|
||||
{{ $item := index $key (dict "a" 1 "b" 2 "c" 3) }}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
`index` doesn't make much sense to me as a verb for accessing values in a
|
||||
dictionary. It sounds more like an array function, and indeed it's the function
|
||||
that gives you access to items in arrays. I would like to see another function
|
||||
with a more dictionary-sounding name, like `get` or `value` or `item`, even if
|
||||
it were just an alias for `index` underneath.
|
||||
|
||||
## Adding Items to a Dictionary
|
||||
|
||||
This is a bit complex because, as far as I can tell, dictionaries are immutable.
|
||||
So, if you want to update a dictionary, you need to combine two dictionaries and
|
||||
then save it back to the original variable. The [`merge`][merge] function does
|
||||
that. Here's a snippet:
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{ $d := dict "a" 1 "b" 2 "c" 3 }}
|
||||
{{ $d = merge $d (dict "b" 4) }}
|
||||
{{ $item = index "b" $d }}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
`merge` takes a variable number of arguments, and merges dictionaries left to
|
||||
right. So, items in dictionaries later in the argument list will override items
|
||||
in dictionaries earlier in the list.
|
||||
|
||||
Just to underscore, you have to set the update dictionary back to the original
|
||||
variable to complete the update, hence the `$d = ...`.
|
||||
|
||||
All that is to say: at the end of that snippet, `$item` will get the value 4.
|
||||
|
||||
## A Complex Example: A Dictionary of Arrays
|
||||
|
||||
For the previously mentioned template changes I was making, I was updating the
|
||||
`terms` template for my category taxonomy. For each category, I wanted to show
|
||||
one section per tag, and a list of all the posts with that tag underneath.
|
||||
|
||||
My categories are high level groups like "Tech," "Music," and "Travel." Tags are
|
||||
more specific topics for the post like "Web" or "Compositions." Pages only ever
|
||||
have one category but they can have multiple tags.
|
||||
|
||||
A `terms` template lets you access an array of terms, and the pages associated
|
||||
with those terms. You can access the tags attached to a page with the
|
||||
`.GetTerms` function. Here's what I did, and then I'll talk through it:
|
||||
|
||||
{{< figures/code >}}
|
||||
```go-html-template
|
||||
{{- $pagesByTag := dict -}}
|
||||
{{- range $page := .Pages -}}
|
||||
{{- range $tag := .GetTerms "tags" -}}
|
||||
{{- $tagName := $tag.Name -}}
|
||||
{{- if not (in $pagesByTag $tagName) -}}
|
||||
{{- $pagesByTag = merge $pagesByTag
|
||||
(dict $tagName (slice $page)) -}}
|
||||
{{- else -}}
|
||||
{{- $pagesForTag := index $pagesByTag $tagName -}}
|
||||
{{- $pagesForTag = $pagesForTag | append $page -}}
|
||||
{{- $pagesByTag = merge $pagesByTag
|
||||
(dict $tagName $pagesForTag) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
```
|
||||
{{< /figures/code >}}
|
||||
|
||||
`$pagesByTag` is my empty dictionary. It will hold tag names as keys, each
|
||||
pointing to a slice (array) of page objects. For each page, I get its list of
|
||||
tags. For each tag, I check `$pagesByTag` to see if it already has a key/value
|
||||
pair for that tag. If not, I create a new entry in `$pagesByTag` with `merge`.
|
||||
If it does already, I get the slice for that tag with `index`, add the Page to
|
||||
the slice with `append`, and then merge the updated slice back into
|
||||
`$pagesByTag` with `merge`.
|
||||
|
||||
It's not too bad once it's all spelled out, but it does feel like more work than
|
||||
it should take for such simple operations.
|
||||
|
||||
I think this API could be improved substantially with some new functions that
|
||||
operate specifically on dictionaries and that have clear names that describe
|
||||
what they do.
|
||||
|
||||
[dict]: https://gohugo.io/functions/dict/
|
||||
[index]: https://gohugo.io/functions/index-function/
|
||||
[merge]: https://gohugo.io/functions/merge/
|
||||
[nsdictionary-init]: https://developer.apple.com/documentation/foundation/nsdictionary/1574181-dictionarywithobjectsandkeys?language=objc
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
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]
|
||||
---
|
||||
|
||||
I shouldn't be surprised (but I am) that the lunar eclipse happening tomorrow
|
||||
morning has [its own Wikipedia page][wp]. It won't be visible at all from most
|
||||
of Europe and Africa, but it will be from most of North America, and on the west
|
||||
coast of North America, we'll be able to see it all. Yay!
|
||||
|
||||
All the times on that page are in UTC. Here are some handy conversions (in 24-hr
|
||||
form) to PST for those of us on the US west coast:
|
||||
|
||||
| Contact | Time (PST) |
|
||||
|:--------:|:----------:|
|
||||
| P1 | 00:02 |
|
||||
| U1 | 01:09 |
|
||||
| U2 | 02:16 |
|
||||
| Max | 02:59 |
|
||||
| U3 | 03:41 |
|
||||
| U4 | 04:49 |
|
||||
| P4 | 04:56 |
|
||||
|
||||
If these times make no sense to you, the eclipse starts at roughly midnight
|
||||
tonight, the total eclipse is between 02:16 and 03:41, and it ends at 04:56.
|
||||
|
||||
More information about contact points for lunar eclipses can be found in the
|
||||
Timing section on the Wikipedia page for [Lunar Eclipse][wp-le].
|
||||
|
||||
[wp]: https://en.wikipedia.org/wiki/November_2022_lunar_eclipse
|
||||
[wp-le]: https://en.wikipedia.org/wiki/Lunar_eclipse#Timing
|
||||
|
|
@ -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" >}}
|
||||
|
Before Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
title: "Week of Soup"
|
||||
date: 2022-11-11T12:42:33-08:00
|
||||
categories: ["Food"]
|
||||
tags: ["Soup", "San Francisco", "Restaurants"]
|
||||
---
|
||||
|
||||
{{< tess >}} and I were both sick this week and had a hankering for soup. We
|
||||
ended up eating soup for lunch every day. (What can I say, we like soup.) All of
|
||||
these places are excellent.
|
||||
|
||||
- Matzo Ball Soup from [Wise Sons][ws]
|
||||
- Pozole and caldo de pollo from [La Espiga de Oro][edo]
|
||||
- Ramen from [Coco's][cocos]
|
||||
- Chicken Noodle Soup and dumplings from [Leleka][leleka]
|
||||
|
||||
[ws]: https://www.wisesonsdeli.com
|
||||
[edo]: https://www.yelp.com/biz/la-espiga-de-oro-san-francisco
|
||||
[cocos]: http://www.cocoramen.com
|
||||
[leleka]: https://lelekasf.com
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
title: "Where Am I"
|
||||
date: 2022-11-20T07:42:27-08:00
|
||||
categories: "Tech"
|
||||
tags: ["Twitter", "Me", "News"]
|
||||
---
|
||||
|
||||
In the wake of Elon Musk taking control of Twitter, a lot of folks have decided
|
||||
it's not as welcoming a place as it once was. In my circles, there's been a huge
|
||||
movement of people to [Mastodon][m] and [cohost][c] mainly. I have accounts on
|
||||
all of those places, though I haven't quite figured out where I'll land yet. If
|
||||
you're interested in following me anywhere else on the internet, I made
|
||||
[a handy list][where].
|
||||
|
||||
[m]: https://mastodon.social/
|
||||
[c]: https://cohost.org
|
||||
[where]: {{< ref "about/where-am-i" >}}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: 2022
|
||||
date: 2022-01-01
|
||||
---
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: 2023
|
||||
date: 2023-01-01
|
||||
---
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:562f6c157c43ef11ae55274b0bd1403c2cbb9a64b82443a2d9f4fb58b32606fd
|
||||
size 148873
|
||||
|
|
@ -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
|
||||
---
|
||||
|
||||
|
|
@ -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" >}}
|
||||
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 429 KiB |
|
|
@ -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. Here’s 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" >}}
|
||||
|
|
@ -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×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×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×60 one and a bigger 240×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
|
||||
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 781 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fa3b6749eb23bff608c4b7233746c6d0577c689786cc69d7354758959e56a9fc
|
||||
size 159168
|
||||
|
|
@ -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). 🙂
|
||||
|
|
@ -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
|
||||