hero.js: canvaset og navnet venter til appen har hydrert
lint-and-reload / lint (push) Successful in 2s
lint-and-reload / lint (push) Successful in 2s
Det var DOM-en lagt inn i .hero-piece før appen hydrerte som kastet hydreringen av sporet (målt på nettstedet: uten den hydrerte siden hver gang, med den aldri). Stilarket går inn med en gang, canvaset og navnet når portal har adoptert instansen (__heroAdopted), eller etter fire sekunder uten app, og toner inn. De midlertidige ?hero=-bryterne er tatt ut. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 file changed
+53
-11
@@ -44,6 +44,9 @@ const TAU_MS = 140;
|
||||
// browser scales the canvas up to the screen; at 1080p that is about
|
||||
// 0.7 of the screen's pixels, at 4K about a third.
|
||||
const MAX_PIXELS = 1.2e6;
|
||||
// How long to wait for the app to adopt the piece before drawing it
|
||||
// anyway (no wasm, a blocked script): longer than any hydration.
|
||||
const ATTACH_FALLBACK_MS = 4000;
|
||||
|
||||
// /usr/share/gpupaper/shaders/light_line.frag, as is.
|
||||
const LIGHT_LINE_FRAG = `// Lightning
|
||||
@@ -232,6 +235,14 @@ const CSS = `
|
||||
height: 100%;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
animation: westra-in 700ms ease-out;
|
||||
}
|
||||
|
||||
/* They arrive after the app has hydrated (see attachWhenHydrated), so
|
||||
they arrive as a fade rather than a pop. */
|
||||
@keyframes westra-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* The name, laid out as the logo lays it out: a box with the logo's
|
||||
@@ -245,6 +256,7 @@ const CSS = `
|
||||
.westra-name {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
animation: westra-in 700ms ease-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -308,7 +320,6 @@ const CSS = `
|
||||
`;
|
||||
|
||||
function ensureStyle() {
|
||||
if (DEBUG.has('nostyle')) return;
|
||||
if (document.getElementById(STYLE_ID)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = STYLE_ID;
|
||||
@@ -337,7 +348,7 @@ class WestraHero {
|
||||
this.hero = container.closest('.hero') || container.parentElement;
|
||||
// Where the module's custom properties go: an element the app
|
||||
// never hydrates, so writing to it cannot disturb hydration.
|
||||
this.host = DEBUG.has('nobody') ? document.createElement('div') : document.body;
|
||||
this.host = document.body;
|
||||
this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
this.dark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@@ -356,9 +367,9 @@ class WestraHero {
|
||||
mark.append(a, b);
|
||||
this.name.append(mark);
|
||||
this.mark = mark;
|
||||
// Both inside the piece, and nothing outside it (see .westra-line).
|
||||
// Both go inside the piece, and nothing outside it (see
|
||||
// .westra-line) - but not yet: see attachWhenHydrated().
|
||||
this.container = container;
|
||||
if (!DEBUG.has('nodom')) container.append(this.canvas, this.name);
|
||||
|
||||
this.width = 0;
|
||||
this.height = 1;
|
||||
@@ -367,7 +378,44 @@ class WestraHero {
|
||||
this.t0 = performance.now();
|
||||
this.last = this.t0;
|
||||
this.raf = 0;
|
||||
this.attached = false;
|
||||
|
||||
this.attachWhenHydrated();
|
||||
}
|
||||
|
||||
// Portal starts this module at HTML parse time, before its app
|
||||
// has hydrated the page, and hydration walks the DOM as served:
|
||||
// nodes added to the piece before it runs throw it off (verified
|
||||
// on the live site: with the DOM added at once the router never
|
||||
// came up and every click was a full page load; with nothing
|
||||
// added it hydrated every time). So the stylesheet goes in at
|
||||
// once (the sticky header and the hidden wordmark are CSS) and
|
||||
// the canvas and the name wait for the app to adopt this
|
||||
// instance (portal sets __heroAdopted when it does), or for
|
||||
// ATTACH_FALLBACK_MS if no app comes - then they fade in. On a
|
||||
// client-side navigation back here the app mounts the module
|
||||
// itself, after hydration, so they go in at once.
|
||||
attachWhenHydrated() {
|
||||
const ready = () => window.__heroAdopted === true || performance.now() - this.t0 > ATTACH_FALLBACK_MS;
|
||||
const poll = () => {
|
||||
if (this.stopped) return;
|
||||
if (!ready()) {
|
||||
this.raf = requestAnimationFrame(poll);
|
||||
return;
|
||||
}
|
||||
// One more frame after the flag, so the app's own work
|
||||
// for this frame is done before the piece changes.
|
||||
this.raf = requestAnimationFrame(() => {
|
||||
this.raf = 0;
|
||||
if (!this.stopped) this.attach();
|
||||
});
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
attach() {
|
||||
this.attached = true;
|
||||
this.container.append(this.canvas, this.name);
|
||||
this.gl = this.initGl();
|
||||
this.size();
|
||||
|
||||
@@ -583,6 +631,7 @@ class WestraHero {
|
||||
stop() {
|
||||
this.stopped = true;
|
||||
if (this.raf) cancelAnimationFrame(this.raf);
|
||||
if (!this.attached) return;
|
||||
window.removeEventListener('resize', this._onResize);
|
||||
window.removeEventListener('scroll', this._onScroll);
|
||||
if (this.observer) this.observer.disconnect();
|
||||
@@ -602,13 +651,6 @@ class WestraHero {
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary, to find what disturbs hydration on the live site:
|
||||
// ?hero=off mounts nothing, ?hero=nostyle skips the stylesheet,
|
||||
// ?hero=nobody skips the custom properties on <body>, ?hero=nodom
|
||||
// adds nothing to the piece. Removed once known.
|
||||
const DEBUG = new Set((new URLSearchParams(location.search).get('hero') || '').split(','));
|
||||
|
||||
export function mount(container) {
|
||||
if (DEBUG.has('off')) return { stop() {} };
|
||||
return new WestraHero(container);
|
||||
}
|
||||
Reference in new issue
Block a user