diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml
index 333e053..5443ee7 100644
--- a/.github/FUNDING.yaml
+++ b/.github/FUNDING.yaml
@@ -1,2 +1 @@
github: [rektdeckard]
-
diff --git a/.github/workflows/dreamhost-preview-static.yaml b/.github/workflows/dreamhost-preview-static.yaml
index 93cefeb..25874f8 100644
--- a/.github/workflows/dreamhost-preview-static.yaml
+++ b/.github/workflows/dreamhost-preview-static.yaml
@@ -1,7 +1,6 @@
name: Build and deploy preview site
-on:
- push
+on: push
jobs:
deploy:
@@ -19,7 +18,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
- cache: 'pnpm'
+ cache: "pnpm"
- name: Install dependencies
run: pnpm install
diff --git a/.github/workflows/dreamhost-static.yaml b/.github/workflows/dreamhost-static.yaml
index e7aad98..97c1b6b 100644
--- a/.github/workflows/dreamhost-static.yaml
+++ b/.github/workflows/dreamhost-static.yaml
@@ -21,7 +21,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
- cache: 'pnpm'
+ cache: "pnpm"
- name: Install dependencies
run: pnpm install
diff --git a/.prettierrc.mts b/.prettierrc.mjs
similarity index 72%
rename from .prettierrc.mts
rename to .prettierrc.mjs
index b323a92..65d0091 100644
--- a/.prettierrc.mts
+++ b/.prettierrc.mjs
@@ -1,5 +1,3 @@
-import type { Config } from "prettier";
-
export default {
plugins: ["prettier-plugin-astro"],
overrides: [
@@ -10,4 +8,4 @@ export default {
},
},
],
-} satisfies Config;
+};
diff --git a/README.md b/README.md
deleted file mode 100644
index ff19a3e..0000000
--- a/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Astro Starter Kit: Basics
-
-```sh
-npm create astro@latest -- --template basics
-```
-
-[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
-[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
-[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
-
-> π§βπ **Seasoned astronaut?** Delete this file. Have fun!
-
-![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
-
-## π Project Structure
-
-Inside of your Astro project, you'll see the following folders and files:
-
-```text
-/
-βββ public/
-β βββ favicon.svg
-βββ src/
-β βββ layouts/
-β β βββ Layout.astro
-β βββ pages/
-β βββ index.astro
-βββ package.json
-```
-
-To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
-
-## π§ Commands
-
-All commands are run from the root of the project, from a terminal:
-
-| Command | Action |
-| :------------------------ | :----------------------------------------------- |
-| `npm install` | Installs dependencies |
-| `npm run dev` | Starts local dev server at `localhost:4321` |
-| `npm run build` | Build your production site to `./dist/` |
-| `npm run preview` | Preview your build locally, before deploying |
-| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
-| `npm run astro -- --help` | Get help using the Astro CLI |
-
-## π Want to learn more?
-
-Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/_assets/cert-warning.webp b/_assets/cert-warning.webp
deleted file mode 100644
index 7727db4..0000000
Binary files a/_assets/cert-warning.webp and /dev/null differ
diff --git a/_assets/cockpit-gauges.webp b/_assets/cockpit-gauges.webp
deleted file mode 100644
index 39f13b9..0000000
Binary files a/_assets/cockpit-gauges.webp and /dev/null differ
diff --git a/_assets/cockpit.webp b/_assets/cockpit.webp
deleted file mode 100644
index 820ea41..0000000
Binary files a/_assets/cockpit.webp and /dev/null differ
diff --git a/_assets/helenazhang-spec.png b/_assets/helenazhang-spec.png
deleted file mode 100644
index 7fe500c..0000000
Binary files a/_assets/helenazhang-spec.png and /dev/null differ
diff --git a/_assets/phosphor-site.webp b/_assets/phosphor-site.webp
deleted file mode 100644
index 04dce42..0000000
Binary files a/_assets/phosphor-site.webp and /dev/null differ
diff --git a/_assets/phosphor_site.webp b/_assets/phosphor_site.webp
deleted file mode 100644
index 270eb4a..0000000
Binary files a/_assets/phosphor_site.webp and /dev/null differ
diff --git a/_data/index.tsx b/_data/index.tsx
index d0d919e..d23b852 100644
--- a/_data/index.tsx
+++ b/_data/index.tsx
@@ -53,11 +53,16 @@ export interface Project {
content?: ReactNode;
}
-const IconCount = lazy(
- () => fetch("https://script.google.com/macros/s/AKfycbyFtNDr2e26aHumtDOu780zD1O7ANfRqITkdBc-G3nG2tVG7Qat96Ac7hnsi4XYhDWXkQ/exec?proc=count", { redirect: "follow" })
+const IconCount = lazy(() =>
+ fetch(
+ "https://script.google.com/macros/s/AKfycbyFtNDr2e26aHumtDOu780zD1O7ANfRqITkdBc-G3nG2tVG7Qat96Ac7hnsi4XYhDWXkQ/exec?proc=count",
+ { redirect: "follow" },
+ )
.then((res) => res.json())
- .then((data) => ({ default: () => <>{(data.count * 6).toLocaleString()}> }))
- .catch(() => ({ default: () => <>{9000..toLocaleString()}> }))
+ .then((data) => ({
+ default: () => <>{(data.count * 6).toLocaleString()}>,
+ }))
+ .catch(() => ({ default: () => <>{(9000).toLocaleString()}> })),
);
export const projects: ReadonlyArray = [
@@ -976,12 +981,15 @@ createRoot(document.getElementById('root')!).render();\
When working with large component libraries, it's important to factor
out sources of human error. Mistakes crop up inevitably at scale, and
- with icons and their corresponding implementations β
- packages in 5 javascript frameworks, a Figma plugin and library, and
- other things I'm forgetting β we had a lot of complexity to manage by
- hand. This led me to build custom internal tooling to support our
- efforts from the design process all the way through to production,
- including:
+ with{" "}
+
+
+ {" "}
+ icons and their corresponding implementations β packages in 5
+ javascript frameworks, a Figma plugin and library, and other things
+ I'm forgetting β we had a lot of complexity to manage by hand. This
+ led me to build custom internal tooling to support our efforts from
+ the design process all the way through to production, including:
@@ -1085,7 +1093,8 @@ createRoot(document.getElementById('root')!).render();\
support more use-cases for developers and designers alike. A{" "}
Flutter library
- , along with a number of other third-party ports, have since been
+
+ , along with a number of other third-party ports, have since been
added, and we have plans for more down the road.
- Tobias Fried built the Phosphor Icon Pack apps as
- Commercial apps. These SERVICES are provided by
- Tobias Fried and are intended for
- use as is.
-
-
- This page is used to inform visitors regarding
- my policies with the collection, use, and
- disclosure of Personal Information if anyone decided to use
- my Service.
-
-
- If you choose to use my Service, then you agree
- to the collection and use of information in relation to this
- policy. The Personal Information that I collect is
- used for providing and improving the Service.
- I will not use or share your
- information with anyone except as described in this Privacy
- Policy.
-
-
- The terms used in this Privacy Policy have the same meanings
- as in our Terms and Conditions, which is accessible at
- Phosphor Krypton Icon Pack unless otherwise defined in this Privacy
- Policy.
-
-
Information Collection and Use
-
- For a better experience, while using our Service,
- I may require you to provide us with certain
- personally identifiable information, including but not limited to your Installed Packages. The
- information that I request will be
- retained on your device and is not collected by me in any way.
-
-
- The app does use third party services that may collect
- information used to identify you.
-
-
-
- Link to privacy policy of third party service providers
- used by the app
-
- I want to inform you that whenever
- you use my Service, in a case of an error in the
- app I collect data and information (through third
- party products) on your phone called Log Data. This Log Data
- may include information such as your device Internet
- Protocol (βIPβ) address, device name, operating system
- version, the configuration of the app when utilizing
- my Service, the time and date of your use of the
- Service, and other statistics.
-
-
Cookies
-
- Cookies are files with a small amount of data that are
- commonly used as anonymous unique identifiers. These are
- sent to your browser from the websites that you visit and
- are stored on your device's internal memory.
-
-
- This Service does not use these βcookiesβ explicitly.
- However, the app may use third party code and libraries that
- use βcookiesβ to collect information and improve their
- services. You have the option to either accept or refuse
- these cookies and know when a cookie is being sent to your
- device. If you choose to refuse our cookies, you may not be
- able to use some portions of this Service.
-
-
Service Providers
-
- I may employ third-party companies
- and individuals due to the following reasons:
-
-
-
To facilitate our Service;
-
To provide the Service on our behalf;
-
To perform Service-related services; or
-
To assist us in analyzing how our Service is used.
-
-
- I want to inform users of this
- Service that these third parties have access to your
- Personal Information. The reason is to perform the tasks
- assigned to them on our behalf. However, they are obligated
- not to disclose or use the information for any other
- purpose.
-
-
Security
-
- I value your trust in providing us
- your Personal Information, thus we are striving to use
- commercially acceptable means of protecting it. But remember
- that no method of transmission over the internet, or method
- of electronic storage is 100% secure and reliable, and
- I cannot guarantee its absolute security.
-
-
Links to Other Sites
-
- This Service may contain links to other sites. If you click
- on a third-party link, you will be directed to that site.
- Note that these external sites are not operated by
- me. Therefore, I strongly advise you to
- review the Privacy Policy of these websites.
- I have no control over and assume no
- responsibility for the content, privacy policies, or
- practices of any third-party sites or services.
-
-
Childrenβs Privacy
-
- These Services do not address anyone under the age of 13.
- I do not knowingly collect personally
- identifiable information from children under 13. In the case
- I discover that a child under 13 has provided
- me with personal information,
- I immediately delete this from our servers. If you
- are a parent or guardian and you are aware that your child
- has provided us with personal information, please contact
- me so that I will be able to do
- necessary actions.
-
-
Changes to This Privacy Policy
-
- I may update our Privacy Policy from
- time to time. Thus, you are advised to review this page
- periodically for any changes. I will
- notify you of any changes by posting the new Privacy Policy
- on this page. These changes are effective immediately after
- they are posted on this page.
-
-
Contact Us
-
- If you have any questions or suggestions about
- my Privacy Policy, do not hesitate to contact
- me at friedtm@gmail.com.
-
+ Tobias Fried built the Phosphor Icon Pack apps as Commercial apps. These
+ SERVICES are provided by Tobias Fried and are intended for use as is.
+
+
+ This page is used to inform visitors regarding my policies with the
+ collection, use, and disclosure of Personal Information if anyone
+ decided to use my Service.
+
+
+ If you choose to use my Service, then you agree to the collection and
+ use of information in relation to this policy. The Personal Information
+ that I collect is used for providing and improving the Service. I will
+ not use or share your information with anyone except as described in
+ this Privacy Policy.
+
+
+ The terms used in this Privacy Policy have the same meanings as in our
+ Terms and Conditions, which is accessible at Phosphor Krypton Icon Pack
+ unless otherwise defined in this Privacy Policy.
+
+
Information Collection and Use
+
+ For a better experience, while using our Service, I may require you to
+ provide us with certain personally identifiable information, including
+ but not limited to your Installed Packages. The information that I
+ request will be retained on your device and is not collected by me in
+ any way.
+
+
+ The app does use third party services that may collect information used
+ to identify you.
+
+
+
+ Link to privacy policy of third party service providers used by the
+ app
+
+ I want to inform you that whenever you use my Service, in a case of an
+ error in the app I collect data and information (through third party
+ products) on your phone called Log Data. This Log Data may include
+ information such as your device Internet Protocol (βIPβ) address, device
+ name, operating system version, the configuration of the app when
+ utilizing my Service, the time and date of your use of the Service, and
+ other statistics.
+
+
Cookies
+
+ Cookies are files with a small amount of data that are commonly used as
+ anonymous unique identifiers. These are sent to your browser from the
+ websites that you visit and are stored on your device's internal memory.
+
+
+ This Service does not use these βcookiesβ explicitly. However, the app
+ may use third party code and libraries that use βcookiesβ to collect
+ information and improve their services. You have the option to either
+ accept or refuse these cookies and know when a cookie is being sent to
+ your device. If you choose to refuse our cookies, you may not be able to
+ use some portions of this Service.
+
+
Service Providers
+
+ I may employ third-party companies and individuals due to the following
+ reasons:
+
+
+
To facilitate our Service;
+
To provide the Service on our behalf;
+
To perform Service-related services; or
+
To assist us in analyzing how our Service is used.
+
+
+ I want to inform users of this Service that these third parties have
+ access to your Personal Information. The reason is to perform the tasks
+ assigned to them on our behalf. However, they are obligated not to
+ disclose or use the information for any other purpose.
+
+
Security
+
+ I value your trust in providing us your Personal Information, thus we
+ are striving to use commercially acceptable means of protecting it. But
+ remember that no method of transmission over the internet, or method of
+ electronic storage is 100% secure and reliable, and I cannot guarantee
+ its absolute security.
+
+
Links to Other Sites
+
+ This Service may contain links to other sites. If you click on a
+ third-party link, you will be directed to that site. Note that these
+ external sites are not operated by me. Therefore, I strongly advise you
+ to review the Privacy Policy of these websites. I have no control over
+ and assume no responsibility for the content, privacy policies, or
+ practices of any third-party sites or services.
+
+
Childrenβs Privacy
+
+ These Services do not address anyone under the age of 13. I do not
+ knowingly collect personally identifiable information from children
+ under 13. In the case I discover that a child under 13 has provided me
+ with personal information, I immediately delete this from our servers.
+ If you are a parent or guardian and you are aware that your child has
+ provided us with personal information, please contact me so that I will
+ be able to do necessary actions.
+
+
Changes to This Privacy Policy
+
+ I may update our Privacy Policy from time to time. Thus, you are advised
+ to review this page periodically for any changes. I will notify you of
+ any changes by posting the new Privacy Policy on this page. These
+ changes are effective immediately after they are posted on this page.
+
+
Contact Us
+
+ If you have any questions or suggestions about my Privacy Policy, do not
+ hesitate to contact me at
+ friedtm@gmail.com.
+
+
+
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index c04f360..f86f295 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,11 +1,18 @@
---
-import Welcome from '../components/Welcome.astro';
-import Layout from '../layouts/Layout.astro';
+import { getCollection } from "astro:content";
+import BaseLayout from "@layouts/BaseLayout.astro";
+import StylizedImage from "@components/StylizedImage.astro";
+import VariantImage from "@components/VariantImage.astro";
-// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
-// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
+import testImage from "../assets/images/toby-avatar.png";
+// import avatarG from "../assets/images/toby-avatar-g.png";
+// import avatarP from "../assets/images/toby-avatar-p.png";
+
+const projects = await getCollection("projects");
---
-
-
-
+
+
+
diff --git a/src/projects/cockpit.mdx b/src/projects/cockpit.mdx
new file mode 100644
index 0000000..675d4fa
--- /dev/null
+++ b/src/projects/cockpit.mdx
@@ -0,0 +1,71 @@
+---
+title: "Cockpit"
+description: "An immersive, techy dashboard for Android"
+url: "https://drive.google.com/drive/folders/1-0a62_LKvpX1713hEUAV4s-360yzdqwe?usp=sharing"
+year: 2018
+tags: ["mobile", "android", "frontend"]
+---
+
+import StylizedImage from "@components/StylizedImage.astro";
+import cockpitImage from "@assets/images/cockpit.png";
+import cockpitGaugesImage from "@assets/images/cockpit-gauges.png";
+
+## Digital detox...lite
+
+Our phones are basically digital drugs. Colorful app icons, badges and notifications vying for our attention at every minute. It's tough to resist, and I'd be the first to admit I have a problem. So when I discovered that with an Android phone and some special tools I could entirely replace the addictive homescreen paradigm with whatever I could dream up, I decided to make something that would, hopefully, help institute some harm-reduction with in phone habits. And maybe look kinda cool in the process.
+
+## Heads up
+
+Cockpit is a data-rich homescreen replacement for Android phones inspired by (surprise) aircraft cockpit design. The HUD style interface surfaces useful information like recent messages, nearby transit and weather, and device resource utilization to keep you in the know without hijacking your attention.
+
+
+
+Built using KLWP by [Kustom Industries](https://play.google.com/store/apps/dev?id=5300483087872269403&hl=en_US&gl=US), Cockpit replaces the typical grid of apps and eschews the
+ever-present notification shade in favor of four content areas:
+
+- A *status* section with a clock, local weather, map view, and realtime nearby transit stations.
+- A *notice* section with a customizable quick view into the three messaging or communication apps you need the most.
+- A *vitals* section with like CPU and network usage.
+- A *dock* section containing commonly-used apps, each with a custom icon treatment designed by [my better half](https://helenazhang.com).
+
+> I found that this interface significantly reduced the amount of time I spent mindlessly browsing my phone.
+
+## Functional bits
+
+While the framework API exposes some device vitals and notifications, generating the map view and nearby transit visualizations was a bit more of a challenge. I relied on Google's [Static Maps](https://developers.google.com/maps/documentation/maps-static/overview) and [GTFS Realtime](https://developers.google.com/transit/gtfs-realtime) REST APIs to put it all together. For the map, we would construct a complex query using device GPS coordinates and user-set variables like the current dashboard theme and map zoom level, request a static map image with the appropriate style parameters, then apply post-processing and layer it into the dashboard UI.
+
+```js
+let mapQueryUrl = \`https://maps.googleapis.com/maps/api/staticmap?\` +
+ \`center=\${lat},\${lon}\` +
+ \`&maptype=\${mapType}\` +
+ \`&size=\${Math.round(height)}&scale=\${scale}&zoom=\${zoom}\` +
+ \`&style=feature:all|element:labels|visibility\${
+ labels ? "simplified" : "off"
+ }\` +
+ \`&style=feature:administrative|element:labels|visibility\${
+ labels && cityLabels ? "simplified" : "off"
+ }|color:\${labelColor}\` +
+ \`&style=feature:road.highway|element:geometry.fill|color:\${highwayColor}\` +
+ // ...About 40 more repetitive style parameters...
+ \`&key\${API_KEY}\`;\
+```
+
+The transit stations would be a bit simpler, requiring just a simple API query and some parsing of the response, before displaying the transit modes on screen. Both the map and transit components could be customized to deep-link into the preferred navigation app with some of the navigation context pre-filled.
+
+At the time, I was on a less-than-ideal phone plan with exhorbitant data costs, and I was paranoid of going over my caps β which I did constantly anyway. I figured that placing this quota right on the home screen would keep me honest about my consumption in more ways than one. It turned out to work beautifully!
+
+
+
+I found that this interface significantly reduced the amount of time I spent mindlessly browsing my phone; native notifications were silenced by default with this setup, and the loud, colorful icons and badges were no longer calling to be checked constantly. My data and usage was in check. And it made my feel like a character in Westworld when I used it.
+
+## Variations on a theme
+
+The custom icons designed for Cockpit sowed the seed for a complete set of app icons became a complete set of icons [for Android home screens](https://github.com/phosphor-icons/android) and ultimately an [open-source resource](/projects/phosphor-icons) for designers and developers of all kinds. My partner Helena even wrote [several articles](https://medium.com/@minoraxis) on the subject of icon design and history.
+
+Cockpit got a number of new color schemes, underwent several revisions to adapt to breaking API changes and new OS limitations, but is still alive and well today. I stopped using it on my primary mobile phone, but I know I'll come back to it one day.
diff --git a/src/projects/helena-zhang.mdx b/src/projects/helena-zhang.mdx
new file mode 100644
index 0000000..57c6d97
--- /dev/null
+++ b/src/projects/helena-zhang.mdx
@@ -0,0 +1,110 @@
+---
+title: "Helena Zhang"
+description: "A portfolio website inspired by newspaper design"
+url: "https://helenazhang.com"
+year: 2020
+tags: ["portfolio", "frontend"]
+---
+
+import StylizedImage from "@components/StylizedImage.astro";
+import helenaImage from "@assets/images/helenazhang.png";
+import helenaSpecImage from "@assets/images/helenazhang-spec.webp";
+
+## The brief
+
+My partner Helena and I have a soft spot for the early days of the internet, when sites were unique, experimental, and performant. We wanted to build her a portfolio that was all of these things β but with modern web conventions at the core: responsive design, lazy loading, interactions that felt dynamic and tactile.
+
+Helena put together a design inspired by newspapers, with dense columns of content and explicit hierarchies of information. We mixed in references to broadcast TV and other analog media to make an experience that felt simultaneously nostalgic and modern.
+
+
+
+## Laying the foundation
+
+I knew that using a popular framework like React or Vue.js went against the vibe for the site. It had to load fast, in mere kilobytes, and degrade gracefully in cases of poor bandwidth or lack of JavaScript β none of which are possible with typical SPAs today. This would be a regular old HTML/CSS/JS site. But since it's not 2003 any more, it needed to have some panache, too.
+
+Since most of the content would follow a grid-like pattern, I first laid out a scaffold using CSS `grid` layouts. After some experimentation with different breakpoints (mobile in particular), I pivoted to a `flexbox` based layout to simplify things. I borrowed some ideas (and code) from the [Bulma](https://bulma.io/) CSS framework to achieve good reusability on the column components.
+
+## Arguing semantics
+
+A requirement of the portfolio site was that is should be simple enough for Helena to update herself with new content and projects. HTML5 Semantic Elements helped make the markup read like an outline, and massively aided comprehension. No `<div>` soup for you!
+
+```html
+
+
+ ...
+
+
+
+
Hello, world! I'm a designer/writer...
+
+
+
+
Articles
+ ...
+ ...
+
+ ...
+ ...
+
+
Dribbble
+
+
+
+ ...
+
+
+
+
+
+
+\
+```
+
+Perhaps the two greatest benefits to this approach are the impressive gains to accessibility (screen readers and assistive devices have a much easier time of parsing this type of document), and the boost it gives to webpage SEO and indexing.
+
+## Making some noise
+
+> TODO: StaticField
+
+The design included a small block of TV static at the foot of the page; a purely aesthetic element that was originally intended to be a simple image. For fun, I decided to make it dynamic and wrote a white noise generator and spit the results onto a `<canvas>` element, to great nostalgic effect. The first, naΓ―ve pass used the Canvas API methods `fillRect()` and `clearRect()`, looping through the canvas coordinates and painting 1x1 'pixels' randomly in black or white. Since painting on a canvas is additive, each frame also had to clear the canvas before painting into it again. It was terribly slow.
+
+With some research into optimal data structures in JavaScript, and some performance testing of my own, I came up with a solution that didn't require clearing the canvas, and only needed one paint call per frame (instead of one per pixel!). Using a `Uint32Array` as a shared buffer for both the white noise values and the `ImageData` itself, we would fill the typed array with 32-bit integers representing transparent black (all `0` bits) or solid white (all `1` bits) at random, then paint this `ImageData` in one go with `putImageData()`. The canvas itself was given a black background that peeked through the transparent pixels.
+
+```js
+// static.js
+const canvas = document.getElementById("static");
+
+if (canvas) {
+ const context = canvas.getContext("2d");
+ const { offsetHeight, offsetWidth } = canvas;
+ canvas.width = offsetWidth;
+ canvas.height = offsetHeight;
+
+ const idata = context.createImageData(offsetWidth, offsetHeight);
+ const buffer = new Uint32Array(idata.data.buffer);
+
+ function noise(context) {
+ let len = buffer.length - 1;
+ while (len--) buffer[len] = Math.random() < 0.5 ? 0 : -1 >> 0;
+ context.putImageData(idata, 0, 0);
+ };
+
+ (function loop() {
+ noise(context);
+ requestAnimationFrame(loop);
+ })();
+};\
+```
+
+As a further optimization, I turned to the browser's built-in [Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) to get random bits even faster, with less overhead. The `window.crypto` object exposes a single method, `getRandomValues(typedArray)`, which will fill a passed `TypedArray` with cryptographically strong random bits via a PRNG seeded by a system-level entropy source. Similar to how we went from `n` paint events down to `1`, we were now getting our random bits wholesale.
+
+> **NB:** requesting more than 65,536 bits at a time from `window.crypto.getRandomValues()` will throw a `QuotaExceededError`, due to minimum guaranteed entropy of seed values. If more bits are needed, the buffer can be filled, used, and filled again.
+
+There are certainly less CPU-intensive ways to create the visual effect of TV static: precomputing more noise than needed and choosing a random index into the noise to start painting from each frame, or even just using a GIF. But the fact that this implementation is truly (pseudo)random each frame and still manages to maintain 60 FPS is pretty unique.
+
+## Looking further afield
+
+When I sat down to build this, I was expecting a familiar exercise in static site development. Through the process of playing around with a seemingly mundane aesthetic element, though, I discovered quite a bit about browser APIs, and learned to look for solutions in unexpected places. By architecting the site for future modification and code readability, we got some excellent side-benefits. The end result was exactly the type of experience we wanted to create at the start. And we brought this thing from a sketch to production in one week flat.
diff --git a/src/projects/hey-you.md b/src/projects/hey-you.md
new file mode 100644
index 0000000..537239e
--- /dev/null
+++ b/src/projects/hey-you.md
@@ -0,0 +1,41 @@
+---
+title: "Hey You"
+description: "A subtle reminder to call your loved ones"
+url: "https://hey-you-fullstack.github.io/hey-you-frontend"
+year: 2020
+tags: ["communication", "frontend"]
+---
+
+## The problem these days
+
+...is that despite the million ways to communicate with friends and loved ones, it's harder and harder to do it. The importance of having a strong support network and regular social interaction is underscored especially now in the time of COVID-19 and work-from-home. That's why my friends David & Johan decided to make Hey You, an incentivized reminder to stay in touch with the people who mean the most.
+
+
+
+When you make a commitment with Hey You, we'll send you a reminder once a month, at the time and date of your choice, to call that person. If you have to cancel, or don't feel like it, you donate $5 to one of our charity partners working to combat loneliness, social isolation, and depression.
+
+## Requirements
+
+We needed a site to serve as the face for Hey You, which would allow users to make a commitment and get info about the project and partner charities. We needed a means to send reminders, and hold people to their commitments. And we wanted the experience to seem light and unintrusive, with no app to download or account to create.
+
+As for the reminders, SMS seemed an obvious choice to keep communication lightweight β everyone has it, and there's nothing to download! We decided that once a user had initiated a commitment, we would pick up the process over text, and all further interaction would be through this channel.
+
+## Meat and potatoes
+
+I built a streamlined, mobile-first frontend application in React to handle the signup process and provide contact and FAQ about our partner charities. Particular joy was had in adding polished animations [Framer Motion](https://motion.dev/) and smart forms with the flexible [React Hook Form](https://react-hook-form.com) library.
+
+We designed and implemented a robust microservice-based backend on Firebase that orchestrated registering users (in a Firestore DB), scheduling reminders (via Cloud Functions) and handoffs to the appropriate Twilio SMS flow.
+
+The flows included:
+
+- Receive a hand-off to finishing the sign-up process
+- Send reminders at the requisite time and date
+- Confirm a call was made π
+- Send a donation link if you didn't π€Ή
+- Allow editing existing commitments
+
+## What's left
+
+While yet to launch, Hey You is in the process of securing funding to
+finish development and QA, and to get us to a public release. More
+info will be made available via [the GitHub repository](https://github.com/hey-you-fullstack/hey-you-frontend).
diff --git a/src/projects/huebert.mdx b/src/projects/huebert.mdx
new file mode 100644
index 0000000..d1a1faa
--- /dev/null
+++ b/src/projects/huebert.mdx
@@ -0,0 +1,92 @@
+---
+title: "Huebert"
+description: "A desktop dashboard to control your Philips Hue"
+url: "https://rektdeckard.github.io/huebert"
+year: 2019
+tags: ["iot", "smart home", "frontend", "react"]
+---
+
+import StylizedImage from "@components/StylizedImage.astro";
+import huebertImage from "@assets/images/huebert-controls.svg";
+import certImage from "@assets/images/cert-warning.png";
+
+## Motivation
+
+Smart home lighting is pretty great. The ability to set the mood for a horror movie, gaming session, or party is a new level of immersion. And hey, our screens change color temperature to protect our eyes and sleep patterns, so why shouldn't the lights in our houses?
+
+*Controlling* your smart lights, however, is another story. When I installed Philips Hue bulbs in my apartment, my initial awe was followed shortly by the realization that 'smart' is a relative term. I couldn't touch the wall switches if I wanted the bulbs to stay connected. It always felt like a *process* to simply dim the lights as I worked late into the evening. The desktop apps for managing them were bloated, and my phone was...across the room. I started wondering, could my smart lights and sensors be controlled from a web app? The answer was Yes. Sort of.
+
+
+
+## Hello, Huebert
+
+Enter stage right, Huebert, my idea for a web and desktop client for Philips Hue lighting and home automation. It would provide a clean and lightweight interface to adjust your lights directly from the browser. As far as I knew, it would be the only browser-based app of its kind. As it would turn out, there may be a reason for that.
+
+> I started wondering, could my smart lights and sensors be controlled from a web app?
+>
+> **TL;DR:** Yes. Sort of.
+
+## Getting to know Hue
+
+Philips Hue's [API documentation](https://developers.meethue.com) (account creation required) is thorough and flush with specifications and examples, and shows Philips' deep commitment to making an ecosystem of hackable and open products. They actively welcome development of third-party hardware and software, and even have a lengthy section on light and color theory in the developer pages.
+
+The Hue Bridge operates a local webserver on your WiFi network that listens to requests at several endpoints, providing control of everything from individual lights to scheduling lighting changes to customizing the behavior and triggers of connected switches and sensors. Commands use a declarative language that allow you to describe the state a light or element should be in, and the Bridge handles transitioning to that state.
+
+```js
+// Describe the light state
+const lightState = {
+ bri: 125,
+ hue: 52322,
+ sat: 254,
+};
+
+const options = {
+ method: "POST",
+ body: JSON.stringify(lightState),
+ headers: {
+ "Content-Type": "application/json"
+ },
+};
+
+// Send POST request to the desired light or group endpoint
+fetch("http:///api/groups/", options)
+ .then((res) => res.json())
+ // Update application state accordingly
+ .then((res) => updateLights(res));\
+```
+
+## A deceptively simple implementation
+
+I chose to scaffold out a web application in React, since the declarative nature of the framework seemed a good fit for the API. Talking to the Bridge via the RESTful interface was a breeze, and building an interface around such inherently colorful data was a joy π. To move fast, I leaned on the [Semantic UI](https://react.semantic-ui.com/) toolkit for basic interface components. Other components were custom-made to fit the context. A working prototype came together in a matter of days.
+
+> TODO: SLIDER DEMO THING
+
+Since I was on a roll, I figured why not port the project to a multiplatform Electron app as well. No internet access? No problem! A desktop app could talk to your lights without leaving the local network.
+
+## Pitfalls
+
+The first working web-app build was deployed after a week or two via GitHub Pages. Difficulties arose when I realized that:
+
+- The Hue Bridge does not expose an encrypted endpoint.
+- Modern browsers *really* don't like it when web content from encrypted sources (HTTPS) make requests to unencrypted (HTTP) destinations.
+- Philips *does* provide a Remote API that handles unencrypted traffic without routing communication through the browser, but it requires additional setup. Messages travel over the internet rather than the local network, and therefore incur additional latency.
+
+People were visiting the site, attempting to link to their Hue Bridge, and getting console errors about an `Invalid Certificate`. Their browsers were refusing to talk to unencrypted endpoints, since the origin itself was encrypted β also known as `Mixed Content`. Chrome and other browsers allow you to circumvent this protection, but not without blasting you with some pretty scary-looking warnings:
+
+
+
+Thankfully, this issue wasn't present in the Electron version of the app, since it would be running on a host machine on the local network. Foregoing encryption in this context is fine, since the traffic never leaves your house.
+
+I did the math, and decided the compromise of asking users to occasionally consent to a scary warning (every time the browser is updated or the cache is cleared) while posing litte real security risk was better than the alternatives β hosting the web-app itself on an unencrypted domain and sending all communication in-the-clear, or using the Remote API and requiring a more complicated setup on the user's part.
+
+## Hard truths
+
+Browsers protect us from the vast majority of malicious actors out there, as well as much of our own unsafe behavior. Though working within their security restrictions can present challenges in edge-cases like this one, I'm glad to have them. At least I now know a lot more about the inner workings of [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), [Mixed Content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content), and browser-specific security policies.
+
+Ultimately I was able to provide myself, as well as a creative community of enthusiasts, a much-needed tool. Even if it's a little rough around the edges.
diff --git a/src/projects/phosphor-icons.mdx b/src/projects/phosphor-icons.mdx
new file mode 100644
index 0000000..26648f6
--- /dev/null
+++ b/src/projects/phosphor-icons.mdx
@@ -0,0 +1,92 @@
+---
+title: "Phosphor Icons"
+description: "An open source icon library for React, Vue, and vanilla JS"
+url: "https://phosphoricons.com"
+year: [2020]
+tags: ["icons", "frontend", "oss"]
+---
+
+import StylizedImage from "@components/StylizedImage.astro";
+import phosphorSiteImage from "@assets/images/phosphor_site.png";
+
+## An obsession
+
+A few years ago I made the switch from iPhone to Android. I wanted to escape Apple's walled garden and start using technology that permitted me to modify and customize it to my liking, to make my own. I made a [techy dashboard](/projects/cockpit) for my phone, with design help from my partner [Helena Zhang](https://helenazhang.com). She designed around 40 custom, minimal icons to replace the common utility app icons and fit in with the hacker aesthetic.
+
+Over time that seed grew, eventually becoming a complete set of icons [for Android home screens](https://github.com/phosphor-icons/android), replacing icons for over 800 of the most common apps with clean, minimalistic, monochromatic glyphs. We released different colorways, took icon suggestions, got involved in the small but active community of phone customizers. Helena even wrote [several articles](https://medium.com/@minoraxis) on the subject of icon design and history. We were eating, breathing, and sleeping icons.
+
+> TODO: IconPalette
+
+Eventually we realized that all this effort could serve a much larger audience of digital designers and engineers of all walks, not just the Android enthusiasts. We decided to create a library of general-purpose user interface icons for modern web, desktop, and mobile platforms β and [Phosphor Icons](https://phosphoricons.com) was born.
+
+## Providing great DX
+
+I knew that its success would not come down to the design of the icons alone. We wanted to make an icon library that was as easy to learn as it was to use, covered the vast majority of platforms, and just worked. We needed to provide a top-tier developer experience to those using the icons. This meant not only supporting the most common frameworks, with packages for [React](http://github.com/phosphor-icons/react), [Vue](https://github.com/phosphor-icons/vue), and [vanilla JS](https://github.com/phosphor-icons/web), but also exposing an intuitive API and writing great documentation.
+
+> We wanted to make an icon library that was as easy to learn as it was to use, covered the vast majority of platforms, and just worked.
+
+Taking advantage of React's `Context API` and Vue's `provide/inject` feature, I brought stateful local and global styling to the icon components. You could set the appearance of all icons in a subtree from one place, but unlike CSS it could be dynamic and manipulated programmatically.
+
+```tsx
+import React from "react";
+import { createRoot } from 'react-dom/client';
+import { IconContext, Horse, Heart, Cube } from "phosphor-react";
+
+const App = () => {
+ return (
+
+
+ {/* I'm lime-green, 32px, and bold! */}
+ {/* Me too! */}
+ {/* Me three :) */}
+
+
+ );
+};
+
+createRoot(document.getElementById('root')!).render();
+```
+
+## Automate the fragile stuff, test everything
+
+When working with large component libraries, it's important to factor out sources of human error. Mistakes crop up inevitably at scale, and with `9072` icons and their corresponding implementations β packages in 5 javascript frameworks, a Figma plugin and library, and other things I'm forgetting β we had a lot of complexity to manage by hand. This led me to build custom internal tooling to support our efforts from the design process all the way through to production, including:
+
+- An [icon testbed and live SVG editor](https://testbed.phosphoricons.com) to check visual size and consistency of new icons against known controls
+- A custom SVG sanitizer to slim down our asset sizes
+- Codegen for 99.5% of our framework-specific library code
+- Macro tools for Figma that handle some assembly of the design resources
+- Build-scripts to tie it all together
+
+A small battery of unit and integration tests ensured that our automated approach wouldn't propagate subtle bugs throughout the system. And at the end of it all, we generated example apps to allow thorough inspection of every icon with the good ol'{" "} Mark I Eyeballβ’. This usually consisted of large contact-sheets with multiple instances of each icon in its many variants and sizes, arranged to visually highlight inconsistencies.
+
+> Aside from reducing carpal tunnel risks for my partner and myself, the automation and testing processes gave us a confidence we couldn't get doing all of this manually.
+
+## Giving Phosphor a home
+
+Naturally, we needed a fun and functional site to house the project. We wanted a searchable interface for the icon library that showcased each icon variant, along with code snippets for developers to quickly reference. And to show off a bit with sleek, snappy animations and strong visual demonstrations of the icons in use.
+
+I took it as an opportunity to try out some new tools in the React ecosystem. TypeScript formed the foundation for less buggy code with more compile-time guarantees. I used the (now archived) [Recoil](https://recoiljs.org/), an experimental state management library from Facebook. It was an absolute joy to work with, and eliminates hundreds of lines of Redux boilerplate. I implemented a smart client-size fuzzy search with help from the awesome [Fuse.js](https://fusejs.io/) library, making searching for icons much more forgiving.
+
+
+
+Ultimately, we managed to pack a ton of ergonomic features into a simple and straightforward one-page site, and even received some accolades from sites like:
+
+- [Admire the Web](https://www.admiretheweb.com/inspiration/phosphor-icons)
+- [CSS Nectar](https://cssnectar.com/css-gallery-inspiration/phosphor-icons)
+- [Smashing Magazine](https://twitter.com/smashingmag/status/1323575426501980166)
+- [User Experience Database](https://www.uxdatabase.io/issue-26)
+- [One Page Love](https://onepagelove.com/phosphor-icons)
+
+## Looking ahead
+
+We continue to grow the library, using analytics and feedback from the community to add the most-needed icons first. We hope to expand to support more use-cases for developers and designers alike. A [Flutter library](https://github.com/phosphor-icons/flutter), along with a number of other third-party ports, have since been added, and we have plans for more down the road.
diff --git a/src/projects/qmind.md b/src/projects/qmind.md
new file mode 100644
index 0000000..ba8841f
--- /dev/null
+++ b/src/projects/qmind.md
@@ -0,0 +1,71 @@
+---
+title: "qMind"
+description: "A research tool to map intersections between language and intelligence"
+url: "https://qmind.io"
+year: 2020
+tags: ["ai", "ml", "language", "cognition"]
+---
+
+## What is it?
+
+qMind is a research platform for building a more individual model of intelligence and mental fitness via language. The core concept is that ideas are defined in relation to one another; words can only be defined in terms of other words, therefore our understanding can be represented by a directed graph of word meanings. By asking people to define different phrases in their own terms, we build a weighted network of meaning and word association.
+
+Current language models are based on statistical prediction methods, whereby vast amounts of text are analyzed in advance to form a weighted graph of their likelihood to appear near each other in a sentence. These transformers have an incredible capacity to imitate complex and intelligent behavior.
+
+
+
+Ours is based on self-reported definitions and associations provided by real people in their own words, and in the moment.
+
+## Building realtime tools
+
+One of the requirements was that the portal be not only a means for collecting data, but also serve as an exploration tool for both participants and researchers to look deeply into the data. This meant having interactive data visualizations, search, sort, filter and transforms of the underlying data, and realtime analysis on it.
+
+I implemented algorithms and related visualizations for common graph analysis tools: breadth- and depth-first search, shortest path (first Dijkstra's, then A\*).
+
+```js
+// Implementing Dijkstra's Algorithm for graph traversal
+function shortestPath(source, target) {
+ if (!source || !target) return [];
+ if (source === target) return [source];
+
+ const queue = [source];
+ const visited = { [source]: true };
+ const predecessor = {};
+ let tail = 0;
+
+ while (tail < queue.length) {
+ // Pop vertex off queue
+ let last = queue[tail++];
+ let neighbors = nodeMap[last];
+
+ if (neighbors) {
+ for (let neighbor of neighbors) {
+ if (visited[neighbor]) continue;
+
+ visited[neighbor] = true;
+ if (neighbor === target) {
+ // Check if path is complete. If so, backtrack!
+ const path = [neighbor];
+ while (last !== source) {
+ path.push(last);
+ last = predecessor[last];
+ }
+ path.push(last);
+ path.reverse();
+ return path;
+ }
+ predecessor[neighbor] = last;
+ queue.push(neighbor);
+ }
+ }
+ }
+}
+```
+
+Providing these slices of information on-the-fly is a tough target to hit. The datasets generated by a single session are relatively small, but the analysis on them could be potentially massive. Some tasks lent themselves strongly toward client-side calculation on a per-user basis, and others toward batch or cron jobs on tables or entire databases. That's why we went with a GraphQL-based communication layer, and a hybrid mode of analysis where work was split between server and client, depending on what significance and scope it had.
+
+Finding path lengths between specific nodes, for example, could be done on demand by the client, since it would be relevant only at that time. Calculating the graph's Eigenvector centralities, on the other hand, would be precomputed by batch processes.
+
+## What's next?
+
+The platform will use participant datasets to build language models that are both generalizable, and tuneable. Eventually, the plan is to use the model to help identify cognitive deficiencies in youth and elderly, to recommend areas of focus for school-aged children, and potentially as an early-warning indicator for neurological disease.
diff --git a/src/projects/y-reader.md b/src/projects/y-reader.md
new file mode 100644
index 0000000..6acb87d
--- /dev/null
+++ b/src/projects/y-reader.md
@@ -0,0 +1,107 @@
+
+---
+title: "yReader"
+description: "A modern Hacker News desktop client"
+url: "https://github.com/rektdeckard/y-reade"
+year: 2022
+tags: ["hacker news", "frontend", "dektop", "rust", "oss"]
+---
+
+import StylizedImage from "@components/StylizedImage.astro";
+import phosphorSiteImage from "@assets/images/phosphor_site.png";
+
+## The gap
+
+If you're reading this, you have probably found yourself on [Hacker News](https://news.ycombinator.com) once or twice. It's a simple, beloved site where users share tech-related links and always1 have civil discussions about them. But the UX is dated, and it is not so enjoyable to browse via the official website.
+
+I am far from the first person to come to this conclusion, as there are literally thousands of HN clients out there. But I took this opportunity to explore cross-platform graphical application development in Rust, and resolved to write my own client.
+
+## Decisions
+
+The Rust community has been asking itself for a while now, ["Are we GUI yet?"](https://www.areweguiyet.com)
+and at this point, the answer is decidedly YES. I decided to go with [egui](https://github.com/emilk/egui): an opinionated, declarative, immediate-mode GUI library with backends for both web and native.
+
+
+
+## Limitations and workarounds
+
+The Hacker News website is impressive in this day and age, in that it still [runs on a single, on-prem server](https://news.ycombinator.com/item?id=16076041) and has a very simple architecture. But the official Hacker News API is not great, and is ["essentially a dump of \[their\] in-memory data structures"](https://github.com/HackerNews/API#design) β making certain common actions (like listing entities) quite cumbersome. You cannot fetch a list of posts; instead, you fetch a list of post IDs, then fetch each post in separate requests. Same goes for comments and other entities.
+
+This led me to implement a super-parallelized client that uses an absurd number of threads (one per entity) to hydrate the UI concurrently and efficiently, all at 60FPS.
+
+```rust
+impl YReader {
+ fn init(&self) {
+ let data_top = Arc::clone(&self.data);
+ thread::spawn(move || loop {
+ let client = JsonClient::new();
+ let ids = client.top_stories();
+ if let Ok(ids) = ids {
+ let page;
+ {
+ let data = data_top.lock().unwrap();
+ page = data.top_page;
+ }
+ for (idx, id) in ids.iter().take(WINDOW * (page + 1)).enumerate() {
+ if let Ok(item) = client.item(*id) {
+ let mut data = data_top.lock().unwrap();
+ data.top.insert(idx, item);
+ }
+ }
+ let mut data = data_top.lock().unwrap();
+ data.top_ids = ids;
+ data.top_page = (data.top_page + 1) % 2;
+ }
+ thread::sleep(Duration::from_secs(REFETCH_DELAY_SECONDS));
+ });
+
+ let data_new = Arc::clone(&self.data);
+ thread::spawn(move || loop {
+ let client = JsonClient::new();
+ let ids = client.new_stories();
+ if let Ok(ids) = ids {
+ let page;
+ {
+ let data = data_new.lock().unwrap();
+ page = data.new_page;
+ }
+ for (idx, id) in ids.iter().take(WINDOW * (page + 1)).enumerate() {
+ if let Ok(item) = client.item(*id) {
+ let mut data = data_new.lock().unwrap();
+ data.new.insert(idx, item);
+ }
+ }
+ let mut data = data_new.lock().unwrap();
+ data.new_ids = ids;
+ data.new_page = (data.new_page + 1) % 2;
+ }
+ thread::sleep(Duration::from_secs(REFETCH_DELAY_SECONDS));
+ });
+ }
+}
+```
+
+## Still to come
+
+While the client is already a great way to browse the content, I have yet to implement a few features before considering this project ready for public consumption:
+
+- User authentication
+- Post and comment submission/editing
+- Voting
+- Job boards
+
+## Footnotes
+
+
+