Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
# Theodore Ouyang

*Quis ego sum?* — Who am I?
This repository contains the source for [theodoreoy.com](https://www.theodoreoy.com/),
a selective personal archive built around four public sections:

I do not think the answer is a fixed biography. It is better understood through
the questions one pursues, the work one chooses to preserve, and the ways one’s
thinking changes over time.
- **Home** — a particle-first opening stage followed by Theodore's profile,
biography, and contact coordinates.
- **Education** — Theodore's academic record and selected coursework.
- **Past Experience** — a five-domain archive generated from a validated source
record.
- **Current Chapter** — a concise account of Theodore's exploration of practical
AI use cases in everyday life.

I created this website to keep that record with continuity: education, past
experience, and the questions shaping my current chapter, each in its proper
context. It is a living archive rather than a conventional portfolio—selective,
honest, and deliberately unfinished.
The Home page gives visual priority to an interactive Three.js particle field.
A same-shape particle fallback makes the first visible frame a stable word;
the canvas then takes over without flashing solid text. The complete identity
and biographical content remains available as static HTML below it.

## Architecture and publishing

The site uses the Next.js App Router, React, and TypeScript, and ships as a
Next.js static export. There is no application server, database, or request-time
API: GitHub Pages serves the generated HTML, CSS, JavaScript, fonts, and images.
Pull requests validate the complete export; pushes to `main` deploy it.

## Development and validation

```sh
npm ci --ignore-scripts
npm run check
npm audit --omit=dev
npm run preview:static
```

`npm run check` runs linting, TypeScript, a production build, and final-artifact
tests. Generated `.next/` and `out/` directories are intentionally not committed.
See [`website-maintenance/`](website-maintenance/) for the source map, design
boundaries, particle-system notes, and release checklist.

## Rollback

Publish changes through a pull request and keep `main` history intact. To undo a
release, create a new `codex/` branch from the current `main`, revert the relevant
merge commit, run the full validation suite, and merge that rollback through a
new pull request. Do not rewrite the published branch.

[Visit the website →](https://www.theodoreoy.com/)
54 changes: 44 additions & 10 deletions app/components/particle-background/ParticleBackground.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,39 @@
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 180ms ease-out;
transition: opacity 120ms ease-out;
}

.fallbackName {
position: absolute;
top: 50%;
left: 50%;
width: min(82vw, 980px);
width: min(70vw, 980px);
margin: 0;
color: rgba(50, 45, 40, 0.7);
font-family: var(--display);
font-size: clamp(2rem, 8vw, 7rem);
color: rgba(55, 49, 44, 0.62);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: clamp(3.2rem, 12.25vw, 16rem);
font-weight: 700;
line-height: 0.92;
letter-spacing: -0.055em;
letter-spacing: 0;
text-align: center;
text-transform: uppercase;
opacity: 1;
transform: translate(-50%, -50%);
transition: opacity 180ms ease-out;
transition: opacity 120ms ease-out;
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
.fallbackName {
color: transparent;
background-image:
radial-gradient(circle, rgba(62, 55, 49, 0.62) 0 0.86px, transparent 1.04px),
radial-gradient(circle, rgba(143, 63, 40, 0.3) 0 0.54px, transparent 0.74px);
background-position: 0 0, 2px 2px;
background-size: 4px 4px, 7px 7px;
-webkit-background-clip: text;
background-clip: text;
}
}

.root[data-state="ready"] .canvas {
Expand All @@ -55,17 +68,38 @@

@media (prefers-reduced-motion: reduce) {
.root[data-state="ready"] .canvas {
opacity: 0.28;
transition-duration: 180ms;
opacity: 1;
transition: none;
}

.root[data-state="ready"] .fallbackName {
opacity: 1;
opacity: 0;
transition: none;
}
}

@media (prefers-contrast: more) {
.fallbackName {
color: var(--ink-strong);
}

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
.fallbackName {
color: transparent;
background-image: radial-gradient(
circle,
rgba(32, 28, 24, 0.96) 0 1.05px,
transparent 1.24px
);
background-size: 4px 4px;
}
}
}

@media (forced-colors: active) {
.fallbackName {
color: CanvasText;
background: none;
-webkit-text-fill-color: currentColor;
}
}
10 changes: 6 additions & 4 deletions app/components/particle-background/ParticleBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ export function ParticleBackground() {
const { ParticleEngine } = await import("./particle-engine");
if (cancelled) return;
engine = new ParticleEngine(canvas, {
onUnavailable: () => setState("fallback"),
onUnavailable: () => {
if (!cancelled) setState("fallback");
},
});
await engine.initialize();
const ready = await engine.initialize();
if (cancelled) {
engine.dispose();
return;
}
setState("ready");
setState(ready ? "ready" : "fallback");
} catch {
engine?.dispose();
if (!cancelled) setState("fallback");
Expand All @@ -46,7 +48,7 @@ export function ParticleBackground() {
<div className={styles.root} data-state={state}>
<canvas className={styles.canvas} ref={canvasRef} aria-hidden="true" />
<span className={styles.fallbackName} aria-hidden="true">
Theodore Ouyang
Theodore
</span>
</div>
);
Expand Down
36 changes: 19 additions & 17 deletions app/components/particle-background/particle-config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { TimelinePhase } from "./particle-types";

export const PARTICLE_CONFIG = {
desktopParticles: 4_000,
mobileParticles: 2_200,
reducedMotionParticles: 900,
initialShape: "theodore",
desktopParticles: 5_000,
mobileParticles: 2_800,
reducedMotionParticles: 1_600,
mobileBreakpoint: 720,
maxPixelRatio: 2,
camera: {
Expand All @@ -15,11 +16,13 @@ export const PARTICLE_CONFIG = {
parallaxY: 3,
},
points: {
size: 1.9,
scatterOpacity: 0.34,
wordOpacity: 0.86,
minimumGray: 0.3,
maximumGray: 0.52,
size: 2.25,
scatterOpacity: 0.38,
wordOpacity: 0.96,
minimumGray: 0.18,
maximumGray: 0.38,
redLift: 0.018,
blueDrop: 0.012,
},
morph: {
maximumDelay: 0.38,
Expand All @@ -37,13 +40,12 @@ export const PARTICLE_CONFIG = {
} as const;

export const PARTICLE_TIMELINE: readonly TimelinePhase[] = [
{ kind: "hold", shape: "scatter", duration: 1.8 },
{ kind: "morph", to: "theodore", duration: 3 },
{ kind: "hold", shape: "theodore", duration: 4 },
{ kind: "morph", to: "scatter", duration: 3 },
{ kind: "hold", shape: "scatter", duration: 1.6 },
{ kind: "morph", to: "ouyang", duration: 3 },
{ kind: "hold", shape: "ouyang", duration: 4 },
{ kind: "morph", to: "scatter", duration: 3 },
{ kind: "hold", shape: "scatter", duration: 1.6 },
{ kind: "hold", shape: "theodore", duration: 5.5 },
{ kind: "morph", to: "scatter", duration: 2.8 },
{ kind: "hold", shape: "scatter", duration: 1.2 },
{ kind: "morph", to: "ouyang", duration: 2.8 },
{ kind: "hold", shape: "ouyang", duration: 4.5 },
{ kind: "morph", to: "scatter", duration: 2.8 },
{ kind: "hold", shape: "scatter", duration: 1.2 },
{ kind: "morph", to: "theodore", duration: 2.8 },
];
Loading
Loading