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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Homepage behavior: `assets/js/index-page.js`
- Photography page behavior: `assets/js/photo-page.js`
- Lab loader and navigation: `assets/js/portfolio-labs.js` (lazy-imports `path-playground.js` and `lab-experiments.js`, which pull in three.js)
- Interactive state: `assets/js/state-machine.js`. Every interactive part is an explicit machine mirrored to `<html data-<name>-state>`; `PortfolioState.snapshot()` returns them all. Read and extend the table in `docs/site/STATE.md` instead of adding booleans, timers or your own Escape/focus-trap handlers.

## CSS Layout

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This repo is not React and is not driven by Jekyll templates in its current main

- `index.html` and `photo.html` are the main live surfaces.
- Shared shell behavior lives in `assets/js/site-shell.js`.
- Interactive state lives in explicit state machines (`assets/js/state-machine.js`), mirrored to `<html data-<name>-state>`. See `docs/site/STATE.md` for the full table and the rules.
- Page-specific behavior lives in:
- `assets/js/index-page.js`
- `assets/js/photo-page.js`
Expand Down Expand Up @@ -107,6 +108,7 @@ assets/
lab-experiments.js
lab-models.mjs
fly-brain.js
state-machine.js
vendor/
clippyjs/
three/
Expand Down
2 changes: 1 addition & 1 deletion assets/css/output.css

Large diffs are not rendered by default.

39 changes: 31 additions & 8 deletions assets/js/fly-brain.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,34 @@ export async function mountBrain({ stage, canvas, framing = 'hero' }) {
state.yaw += steps[event.key];
draw();
});
// The loop runs only while the stage is on screen and the GPU context is alive; off screen it costs nothing.
let contextLost = false;
const syncLoop = () => {
const spin = !reduced && state.visible && !contextLost;
renderer.setAnimationLoop(spin ? () => {
if (!state.dragging) state.yaw += 0.0022;
draw();
} : null);
};
new IntersectionObserver((entries) => {
state.visible = entries[0].isIntersecting;
syncLoop();
}).observe(stage);
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault();
contextLost = true;
syncLoop();
stage.classList.remove('is-live');
});
canvas.addEventListener('webglcontextrestored', () => {
contextLost = false;
stage.classList.add('is-live');
draw();
syncLoop();
});

stage.classList.add('is-live');
if (!reduced) {
renderer.setAnimationLoop(() => {
if (!state.visible) return;
if (!state.dragging) state.yaw += 0.0022;
draw();
});
}
syncLoop();

return {
// Additive blending makes black points invisible, so a region can switch off without rebuilding anything.
Expand All @@ -125,7 +141,14 @@ export async function mountBrain({ stage, canvas, framing = 'hero' }) {
const heroStage = document.getElementById('brain-stage');
const heroCanvas = document.getElementById('brain-canvas');
if (heroStage && heroCanvas && 'WebGL2RenderingContext' in window) {
const begin = () => mountBrain({ stage: heroStage, canvas: heroCanvas }).catch(() => {});
// On fly101.html the poster image is the fallback, so failure only needs to be visible to tests and agents.
const hero = window.PortfolioState?.defineLoader('brain-hero');
const begin = () => {
hero?.send('load');
mountBrain({ stage: heroStage, canvas: heroCanvas })
.then(() => hero?.send('resolve'))
.catch(() => hero?.send('reject'));
};
if (document.readyState === 'complete') begin();
else window.addEventListener('load', begin, { once: true });
}
Loading
Loading