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
|
// 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.
|
// 0.7 of the screen's pixels, at 4K about a third.
|
||||||
const MAX_PIXELS = 1.2e6;
|
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.
|
// /usr/share/gpupaper/shaders/light_line.frag, as is.
|
||||||
const LIGHT_LINE_FRAG = `// Lightning
|
const LIGHT_LINE_FRAG = `// Lightning
|
||||||
@@ -232,6 +235,14 @@ const CSS = `
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
pointer-events: none;
|
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
|
/* The name, laid out as the logo lays it out: a box with the logo's
|
||||||
@@ -245,6 +256,7 @@ const CSS = `
|
|||||||
.westra-name {
|
.westra-name {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
animation: westra-in 700ms ease-out;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -308,7 +320,6 @@ const CSS = `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function ensureStyle() {
|
function ensureStyle() {
|
||||||
if (DEBUG.has('nostyle')) return;
|
|
||||||
if (document.getElementById(STYLE_ID)) return;
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = STYLE_ID;
|
style.id = STYLE_ID;
|
||||||
@@ -337,7 +348,7 @@ class WestraHero {
|
|||||||
this.hero = container.closest('.hero') || container.parentElement;
|
this.hero = container.closest('.hero') || container.parentElement;
|
||||||
// Where the module's custom properties go: an element the app
|
// Where the module's custom properties go: an element the app
|
||||||
// never hydrates, so writing to it cannot disturb hydration.
|
// 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.reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||||
this.dark = window.matchMedia('(prefers-color-scheme: dark)');
|
this.dark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
@@ -356,9 +367,9 @@ class WestraHero {
|
|||||||
mark.append(a, b);
|
mark.append(a, b);
|
||||||
this.name.append(mark);
|
this.name.append(mark);
|
||||||
this.mark = 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;
|
this.container = container;
|
||||||
if (!DEBUG.has('nodom')) container.append(this.canvas, this.name);
|
|
||||||
|
|
||||||
this.width = 0;
|
this.width = 0;
|
||||||
this.height = 1;
|
this.height = 1;
|
||||||
@@ -367,7 +378,44 @@ class WestraHero {
|
|||||||
this.t0 = performance.now();
|
this.t0 = performance.now();
|
||||||
this.last = this.t0;
|
this.last = this.t0;
|
||||||
this.raf = 0;
|
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.gl = this.initGl();
|
||||||
this.size();
|
this.size();
|
||||||
|
|
||||||
@@ -583,6 +631,7 @@ class WestraHero {
|
|||||||
stop() {
|
stop() {
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
if (this.raf) cancelAnimationFrame(this.raf);
|
if (this.raf) cancelAnimationFrame(this.raf);
|
||||||
|
if (!this.attached) return;
|
||||||
window.removeEventListener('resize', this._onResize);
|
window.removeEventListener('resize', this._onResize);
|
||||||
window.removeEventListener('scroll', this._onScroll);
|
window.removeEventListener('scroll', this._onScroll);
|
||||||
if (this.observer) this.observer.disconnect();
|
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) {
|
export function mount(container) {
|
||||||
if (DEBUG.has('off')) return { stop() {} };
|
|
||||||
return new WestraHero(container);
|
return new WestraHero(container);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user