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
52 changes: 16 additions & 36 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ lost-satellite.html - Lost Satellite Studios breakdown (1 project + screensh
css/
shared.css - All shared styles (variables, nav, layout, components)
project.css - Project page-specific styles (hero, sections, gallery)
cards.css - Card and hand container styles (base card 220x320, theme overrides)
js/
shared.js - Scroll reveal + theme switcher with localStorage persistence
destruction.js - Text destruction system (plane mode shatter + sequential reform)
card-hand.js - Card hand system (fan layout, drag, proximity detection, play zone)
page-transition.js - Card play animation + SPA page transitions (fly clones, page fade-in)
css/
cards.css - Card and hand container styles (base card 220×320, theme overrides)
```

## Detailed Documentation
- **CSS** (themes, line ranges, selectors): See [css/CLAUDE.md](css/CLAUDE.md)
- **JS** (functions, constants, APIs): See [js/CLAUDE.md](js/CLAUDE.md)
- **Tests** (scenarios, thresholds, setup): See [tests/CLAUDE.md](tests/CLAUDE.md)

## Key Details
- **Fonts**: Google Fonts `<link>` tags in every page's `<head>` load all theme fonts:
- Bold: Archivo Black, Archivo, IBM Plex Mono
Expand All @@ -28,48 +32,24 @@ css/
- Retro: Shrikhand, Bitter, IBM Plex Mono
- Neon: Sora, Space Mono
- Font CSS variables (`--font-serif`, `--font-body`, `--font-mono`) are redefined per theme in `shared.css`
- **Theme system**: Five themes cycle via button: `bold cinematic brutalist retro neon bold`
- **Theme system**: Five themes cycle via button: `bold -> cinematic -> brutalist -> retro -> neon -> bold`
- `data-theme` attribute on `<html>` controls the active theme (`bold` default)
- Each theme defines its own CSS variable block in `shared.css` (`:root` for bold, `[data-theme="cinematic"]`, etc.)
- Theme-specific overrides (hardcoded colours, border-radius, font weights, etc.) go in `[data-theme="<name>"]` selector blocks after the variable blocks
- `js/shared.js` has `const themes = ['bold', 'cinematic', 'brutalist', 'retro', 'neon']` — the switcher button cycles through and persists to `localStorage`
- **Persistence**: inline `<script>` in `<head>` of every page reads `localStorage.getItem('portfolio-theme')` and sets `data-theme` before CSS loads (prevents flash). Also migrates stored `coral`/`slate` values to `bold`.
- **Theme design notes**:
- **bold** (default): Warm light theme. Archivo Black headings, Archivo body, IBM Plex Mono labels. Orange accent (#FF6123). Sharp corners (0px radius). Dark about section, orange contact section. Hero accent circle.
- **cinematic**: Dark tech-noir theme. Syne headings, system body, Commit Mono labels. Red accent (#D6001C). Scanline overlay in hero, red glow, film grain on body. Sharp corners.
- **brutalist**: Typographic grid theme. Space Mono headings, Literata body. Electric blue accent (#0038FF). Status bar below nav. 2-column hero grid, data-row about section. Heavy black borders (2px). Uses alternate HTML layouts (.hero-brutalist, .about-brutalist).
- **retro**: 1970s warm theme. Shrikhand headings, Bitter body, IBM Plex Mono labels. Red accent (#D94230) + mustard secondary (#E8A825). Split hero with teal panel. Deep teal about section. Rounded corners (10px). Uses .hero-panel HTML element.
- **neon**: Dark techy theme. Sora headings/body, Space Mono labels. Lime-green accent (`#c9f059`). Sharp corners (6-12px radius). Grain overlay on `body::before`. Grid pattern in hero `::before`. Doodles hidden.
- See [css/CLAUDE.md](css/CLAUDE.md) for per-theme design notes and line ranges.
- **Nav**: Removed from all pages. Navigation is handled by the card-hand system only. Nav CSS is archived in `backup/nav-styles.css`.
- **Alternate HTML layouts**:
- `.hero-default` / `.hero-brutalist` — dual-layout hero (brutalist has alternate grid)
- `.about-default` / `.about-brutalist` — dual-layout about (brutalist has data rows)
- `.hero-panel` — retro teal panel (right side of split hero)
- `.hero-default` / `.hero-brutalist` — dual-layout hero
- `.about-default` / `.about-brutalist` — dual-layout about
- `.hero-panel` — retro teal panel
- `.status-bar` — brutalist status bar (hidden by default)
- **Work cards**: `<a>` links in `index.html` that navigate to individual project pages; hover highlight effect (accent border + lift + shadow)
- **JS features**: Scroll reveal (IntersectionObserver), theme switcher with localStorage
- **Text Destruction** (`js/destruction.js`): "Plane mode" feature — a paper airplane projectile shatters text on collision, then characters reform.
- **Dependencies**: GSAP core, SplitText plugin, Physics2DPlugin (all loaded externally, no npm)
- **How it works**: `TextDestruction.init()` splits all destructible text elements (headings, paragraphs, labels, etc.) into word and char `<span>`s via `SplitText` (classes `destruct-word` and `destruct-char`). Word spans are `inline-block; white-space: nowrap` to prevent mid-word breaks. When plane mode is active, `onProjectileAt(x, y)` finds chars within `BLAST_RADIUS` and shatters them.
- **Scatter phase**: Chars fly away from impact using `physics2D` (velocity, angle, gravity) with a brief accent-colour flash, fading to `opacity: 0` over `SCATTER_DURATION` (1.2s).
- **Reform phase** (sequential typing drop-in): After a `REFORM_PAUSE` (1.0s), chars re-enter left-to-right in DOM reading order. Each char is pre-positioned 16px above its slot (`DROP_DISTANCE`), then drops into place with `power2.out` (no bounce) over `CHAR_LAND_DURATION` (0.12s). Consecutive chars are staggered by `CHAR_STAGGER` (0.055s) with an extra `WORD_EXTRA_STAGGER` (0.05s) pause at word boundaries (detected by `parentElement` change).
- **Lifecycle**: `TextDestruction.onThemeChange()` destroys and re-inits on theme switch. Resize is debounced to re-split text. A `charRectCache` (invalidated on scroll/resize) accelerates hit detection.
- **Selector list** (`DESTRUCTIBLE_SELECTOR`): targets headings, hero text, work cards, about section, chips, contact, project pages, footer, brutalist layout elements, retro panel elements — excludes theme switcher, buttons.
- **Mobile-gated constants**: `_isMob` (viewport ≤768 OR touch+coarse) gates performance-sensitive values. Desktop is completely unchanged. Mobile overrides: `MAX_SHATTERED` 100 (vs 300), `REFORM_PAUSE` 1.0s (vs 0.8s), `CHAR_STAGGER` 0.035s (vs 0.055s), `WORD_EXTRA_STAGGER` 0.03s (vs 0.05s), `MAX_VELOCITY` 350 (vs 500), `MAX_ROTATION` 360° (vs 720°). Color flash tween is skipped on mobile. Impact coalescing on mobile batches same-frame `onProjectileAt()` calls via RAF.
- **Impact throttle**: `IMPACT_THROTTLE` in `plane.js` is 80ms on mobile, 0ms on desktop. The scroll speed gate (`SCROLL_SPEED_THRESHOLD`) was removed — it unnecessarily limited desktop destruction.
- **Design fallback**: If optimisation doesn't resolve plane mode scroll+fire lag, the fallback is to **disable page scrolling while plane mode is active** (e.g. CSS `overflow: hidden` on `<html>` when `.plane-active`). This eliminates scroll-triggered cache invalidation and the compound scroll+destruction cost entirely.
- **Card-hand system** (`js/card-hand.js`, `js/page-transition.js`, `css/cards.css`): Cards are held in a fan layout at the bottom of the viewport. Dragging a card into the play zone triggers a page transition.
- **Card constants**: `CARD_W` = 220, `CARD_H` = 320, `HAND_W` = 700, `PLAY_ZONE_HALF_W` = 385, `PLAY_ZONE_HALF_H` = 289
- **Play animation phases**: center (0.5s) → wriggle (1.0s, increasing amplitude) → expand to fill viewport (0.5s) → decompose card elements → fly title/art clones to target page positions → page fade-in → rebuild hand
- **Expand scaling**: Uses `Math.min(vw / CARD_W, vh / CARD_H)` ("contain" behavior) so the card never exceeds viewport bounds. No `devicePixelRatio` multiplication needed — `window.innerWidth`/`innerHeight` return CSS pixels, which is what GSAP operates in.
- **Proximity effects**: As a dragged card approaches the viewport center, a blur overlay and card glow intensify. Calculated via distance from card center to viewport center.
- **SPA navigation**: `page-transition.js` fetches project pages via `fetch()`, caches them, and injects content into `#pageContainer` without full page reload. Title and art elements fly from the expanded card position to their target positions on the new page.
- **Title matching convention**: Card titles (in `card-hand.js` CARDS array) must match the project page `<h1 class="project-hero-title">` text exactly. Descriptive subtitles (e.g. "Casino Games", "Eve of Destruction") go in `.project-hero-badge`. Project title font styling (italic, weight, line-height) matches card title styling so the flying clone transition is seamless.
- **`.reveal` vs card handoff**: Any element that a fly clone hands off to must have `.reveal` removed from itself **and all `.reveal` ancestors** before the wrapper fades in. Otherwise the clone disappears but the real element is still `opacity: 0` (waiting for IntersectionObserver → `.visible`), causing a visible gap. This is done in `transitionToProject()` after measuring targets — currently handled for `.project-hero-title` and the first `.work-image`'s parent. **When adding new fly targets** (e.g. a second image, a badge, etc.), find the target element, call `el.closest('.reveal')`, and remove the class before the fly timeline starts.
- **Known issue — clone fly alignment**: The card title is `text-align: center` but the project page title is left-aligned. During the fly animation, the clone inherits center alignment from the card. For shorter titles (e.g. "Coffin-Likker"), the text visibly snaps from center to left when the clone is swapped for the real title. Longer titles (e.g. "Lost Satellite Studios") mask this. Fix options: left-align the clone and offset its start position, center the page title, or left-align the card title.
- **Char-fly landing bugs (resolved)**: Two issues caused the flying chars to land at the wrong position:
1. **Home page: `.measuring` removed too early** — `transitionToHome()` removed `.measuring` from `#page-home` before calling `fly()`. Without `.measuring`, the page reverted to `.spa-page { display:none }`, so all `getBoundingClientRect()` calls in `measureDirect()` returned zeros. Fix: remove `.measuring` after `fly()`, matching `transitionToProject()`.
2. **All pages: inline vs inline-block glyph offset + line-height mismatch** — `measureDirect()` measures inline `<span>` chars whose `getBoundingClientRect()` returns glyph bounds (ascenders extend above the line box). Flying chars are `display:inline-block` where the box top IS the positioned coordinate. Additionally, flying chars had hardcoded `line-height:1` but project titles use `line-height:1.2`. Fix in `text-rearrange.js`: (a) y-correction shifts target positions by `elRect.top - minCharY` so inline-block boxes align with the element box, (b) line-height is captured per-char and interpolated from source to target during the animation.
- **Text Destruction**: See [js/CLAUDE.md](js/CLAUDE.md) for detailed destruction.js documentation (mechanics, constants, mobile-gated values, impact throttle, design fallback).
- **Card-hand system**: See [js/CLAUDE.md](js/CLAUDE.md) for card-hand.js and page-transition.js documentation (play animation phases, SPA navigation, title matching, .reveal vs card handoff, known issues).
- **Fly-swap alignment pitfall**: Character positions differ between inline text and `display: inline-block` spans (`.destruct-char`). Any code that measures character positions for the fly animation (`measureDirect` in text-rearrange.js) must use `charsClass: 'destruct-char'` so measurements match the final split state. Similarly, `onThemeChange()` must run before the target title is revealed during cleanup — see [js/CLAUDE.md](js/CLAUDE.md) "transitionToHome Cleanup Ordering" and "Measurement Invariant" sections.
- **Project pages**: Shared template - theme switcher, back link, project hero, repeatable sub-project sections, footer

## Serving Locally
Expand Down Expand Up @@ -130,7 +110,7 @@ Seven scenarios:
- **scatter_spike**: Single impact on dense text (#about). Isolates the 1.2s physics2D scatter window. Thresholds: maxFrameMs > 40, p95 > 30, avg > 22.
- **cache_rebuild**: Forces `cacheStale = true` then impacts to trigger `rebuildCharCache()`. Measures the `getBoundingClientRect()` loop cost. Threshold: maxFrameMs > 50.
- **dense_burst**: 6 rapid-fire impacts at 100ms intervals across #about. Measures overlapping physics2D tweens. Thresholds: p95 > 35, droppedFrames > 30%, ScriptDurationMs > 800.
- **overlap_scatter_reform**: 3 staggered impacts on hero (h1 hero-desc tidbits) creating triple wave overlap (scatter + reform simultaneously). Thresholds: maxFrameMs > 50, p95 > 35.
- **overlap_scatter_reform**: 3 staggered impacts on hero (h1 -> hero-desc -> tidbits) creating triple wave overlap (scatter + reform simultaneously). Thresholds: maxFrameMs > 50, p95 > 35.
- **high_count_reform**: 8-10 impacts to shatter near MAX_SHATTERED chars, then measures the reform animation window. Thresholds: maxFrameMs > 60, p95 > 30.
- **figure8_scroll_fire**: Simultaneous scrolling + destruction across the full page height (figure-8 Lissajous pattern). Tests scroll-triggered cache invalidation, overlapping scatter+reform across viewport changes, and compound scroll+destruction cost. Thresholds: maxFrameMs > 60, p95 > 35, avg > 25, droppedFrames > 40%.
- **sustained_annihilation**: Destroys all text in #about every 0.3s for 6 cycles using a 60px impact grid. Measures overlapping scatter+reform waves at extreme frequency. Thresholds: overlap maxFrameMs > 70, p95 > 40, avg > 25, droppedFrames > 40%.
Expand All @@ -145,7 +125,7 @@ Verify that the expanded card stays within the viewport at multiple sizes. These
3. Wait for the expand phase (~2s into the animation timeline)
4. Check `document.documentElement.scrollWidth <= window.innerWidth` and `document.documentElement.scrollHeight <= window.innerHeight` — if either is false, the card overflows
5. Take a screenshot at the expanded state for visual verification
6. Repeat at different viewport sizes: 1920×1080 (desktop), 1024×768 (tablet), 375×812 (mobile portrait), 812×375 (mobile landscape)
6. Repeat at different viewport sizes: 1920x1080 (desktop), 1024x768 (tablet), 375x812 (mobile portrait), 812x375 (mobile landscape)

**Test script pattern** (to be implemented in `tests/card-bounds-test.js`):
```js
Expand Down
Loading
Loading