yes.js: freeze .hero-yes's height via inline style, don't trust svh alone
Deploy / deploy (push) Successful in 34s
Deploy / deploy (push) Successful in 34s
The container-rect approach (reading .hero-canvas's rendered rect via ResizeObserver) still jumped on the real device: .hero-copy kept sliding down as the address bar collapsed even with svh (then lvh) driving .hero-yes's height, meaning the box's actual rendered height wasn't holding still the way the "stable" viewport units are spec'd to. Reading a rect only helps if what it reads is actually fixed - it wasn't, so no amount of matching the canvas to it would help. yes.js now writes the fix instead of reading around it: it measures window.innerWidth/innerHeight once and sets that literal px height as an inline style on the hero element. Inline style beats the stylesheet's `height: 100svh` in the cascade, so the box's rendered height becomes a fixed number in the DOM rather than something recomputed from a viewport unit on every layout - nothing the browser does with svh afterward can move it. The old width-gated resize listener comes back to re-freeze on a genuine resize (orientation change), since that's still the correct signal for "the address-bar animation is not what's happening right now."
This commit is contained in:
+10
-10
@@ -153,16 +153,16 @@ main.loading, main.not-found {
|
||||
position: relative;
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
/* svh (small viewport height): a fixed, stable length - computed
|
||||
once, never recalculated mid-scroll as mobile Safari's address
|
||||
bar hides/shows, unlike plain vh/dvh on some engines. Pinned to
|
||||
the *smallest* possible viewport (toolbar visible) so the box
|
||||
never needs to grow into space the toolbar might reclaim.
|
||||
yes.js's canvas measures this box's own rendered rect (not
|
||||
window.innerWidth/innerHeight) via ResizeObserver, so the canvas
|
||||
always matches whatever height this resolves to - the two can't
|
||||
disagree, which is what was actually causing the content below to
|
||||
visibly jump.
|
||||
/* svh here is only the pre-JS/no-JS fallback (and first paint before
|
||||
yes.js's constructor runs). Once yes.js mounts, it overwrites this
|
||||
with an inline `height: <px>` frozen from a single measurement -
|
||||
see setupCanvas()'s comment in yes.js for why: on real mobile
|
||||
Safari, content bottom-aligned inside this box kept sliding down
|
||||
as the address bar collapsed even with a spec'd-stable viewport
|
||||
unit here, so the box's actual height can't be trusted to stay
|
||||
put on that unit alone. An inline style set from JS always wins
|
||||
the cascade over this rule, so that frozen number is what
|
||||
actually governs once the page is interactive.
|
||||
Plain-vh fallback declared first - an engine without svh support
|
||||
ignores the invalid second line rather than falling through to
|
||||
auto height, which would collapse this to the height of its
|
||||
|
||||
@@ -41,8 +41,13 @@ export class RasterizedYES {
|
||||
this.containmentStrength = 0.5;
|
||||
this.wiggleAmount = 0.5;
|
||||
|
||||
// .hero-canvas is inset:0 inside this - freezing an inline
|
||||
// height here (below) is what actually locks the box, not
|
||||
// just reading its rect.
|
||||
this.heroEl = this.rasterCanvas.closest('.hero');
|
||||
|
||||
this.setupCanvas();
|
||||
this.setupResizeObserver();
|
||||
this.setupResizeHandler();
|
||||
this.setupMouseTracking();
|
||||
this.setupClickHandler();
|
||||
this.rasterizeText();
|
||||
@@ -52,24 +57,34 @@ export class RasterizedYES {
|
||||
|
||||
stop() {
|
||||
this.destroyed = true;
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
if (this._resizeHandler) {
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Measures the canvases' own containing box (.hero-canvas, which
|
||||
// tracks .hero-yes via inset:0) rather than window.innerWidth/
|
||||
// innerHeight. window.innerHeight is the live viewport - it moves
|
||||
// as mobile Safari's address bar hides/shows on scroll, which
|
||||
// doesn't itself match whatever fixed value .hero-yes's CSS height
|
||||
// is pinned to. Sizing the canvas off the box's own rect instead
|
||||
// means the two can never disagree, whichever viewport unit ends
|
||||
// up governing that box.
|
||||
// Reading .hero-canvas's rendered rect (a prior attempt at this)
|
||||
// only helps if that rect actually stays put - and on real mobile
|
||||
// Safari it didn't: .hero-copy (bottom-aligned via flexbox inside
|
||||
// .hero-yes) kept sliding down as the address bar collapsed, even
|
||||
// with a spec'd-stable viewport unit (svh, then lvh) driving the
|
||||
// box's height. Either the browser isn't holding up its end, or
|
||||
// something in the cascade is still landing on a live value - not
|
||||
// provable from here without the device. Rather than keep
|
||||
// guessing at which CSS viewport unit actually holds still, yes.js
|
||||
// becomes the source of truth instead: it freezes .hero-yes's
|
||||
// rendered height to a literal inline px value once, up front. An
|
||||
// inline style always wins the cascade over the stylesheet's
|
||||
// `height: 100svh`, so once this runs, nothing the browser does
|
||||
// with that unit afterward can move the box - the number is fixed
|
||||
// in the DOM, not recomputed from a unit at all.
|
||||
setupCanvas() {
|
||||
const pixelRatio = window.devicePixelRatio || 1;
|
||||
const rect = this.rasterCanvas.parentElement.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
if (this.heroEl) {
|
||||
this.heroEl.style.height = height + 'px';
|
||||
}
|
||||
|
||||
this.rasterCanvas.style.width = width + 'px';
|
||||
this.rasterCanvas.style.height = height + 'px';
|
||||
@@ -89,19 +104,19 @@ export class RasterizedYES {
|
||||
this.pixelRatio = pixelRatio;
|
||||
}
|
||||
|
||||
// ResizeObserver reacts to the container's box actually changing
|
||||
// size - a fixed-height box (svh, lvh, or plain px) never fires
|
||||
// this at all during the address-bar animation, so there's no
|
||||
// need for the old "only redraw if width changed" heuristic that
|
||||
// guessed at which resize events were real.
|
||||
setupResizeObserver() {
|
||||
this._resizeObserver = new ResizeObserver((entries) => {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
if (width === this.displayWidth && height === this.displayHeight) return;
|
||||
setupResizeHandler() {
|
||||
// Gate on width, not height: mobile Safari's address-bar
|
||||
// animation changes window.innerHeight continuously with no
|
||||
// real layout change to react to (that's the live value this
|
||||
// whole method exists to stop trusting). A genuine resize -
|
||||
// orientation change, desktop window drag - always changes
|
||||
// width too, so that's the real signal to re-freeze on.
|
||||
this._resizeHandler = () => {
|
||||
if (window.innerWidth === this.displayWidth) return;
|
||||
this.setupCanvas();
|
||||
this.rasterizeText();
|
||||
});
|
||||
this._resizeObserver.observe(this.rasterCanvas.parentElement);
|
||||
};
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
}
|
||||
|
||||
setupMouseTracking() {
|
||||
|
||||
Reference in New Issue
Block a user