diff --git a/style/main.css b/style/main.css index 6f98961..46d6f49 100644 --- a/style/main.css +++ b/style/main.css @@ -153,17 +153,17 @@ main.loading, main.not-found { position: relative; max-width: none; width: 100%; - /* lvh (large viewport height), not svh: svh pins to the *smallest* - viewport (toolbar visible), which never grows once the browser - auto-collapses its toolbar on the first scroll - so the actual - visible screen becomes taller than this box, and the content - below appears to snap into the newly-revealed space the moment - that happens. lvh pins to the toolbar-collapsed size instead, - matching the settled state scrolling actually lands on, so - there's no gap to reveal. (svh is the right choice for the - opposite case - a fixed element that must never be hidden behind - the toolbar - not a hero at the top of a scrolling page.) - Plain-vh fallback declared first - an engine without lvh support + /* 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. + 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 in-flow content and clip the canvas via overflow:hidden below. */ diff --git a/yes.js b/yes.js index bebf45a..9c9ce3c 100644 --- a/yes.js +++ b/yes.js @@ -42,6 +42,7 @@ export class RasterizedYES { this.wiggleAmount = 0.5; this.setupCanvas(); + this.setupResizeObserver(); this.setupMouseTracking(); this.setupClickHandler(); this.rasterizeText(); @@ -51,15 +52,24 @@ export class RasterizedYES { stop() { this.destroyed = true; - if (this._resizeHandler) { - window.removeEventListener('resize', this._resizeHandler); + if (this._resizeObserver) { + this._resizeObserver.disconnect(); } } + // 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. setupCanvas() { const pixelRatio = window.devicePixelRatio || 1; - const width = window.innerWidth; - const height = window.innerHeight; + const rect = this.rasterCanvas.parentElement.getBoundingClientRect(); + const width = rect.width; + const height = rect.height; this.rasterCanvas.style.width = width + 'px'; this.rasterCanvas.style.height = height + 'px'; @@ -77,27 +87,27 @@ export class RasterizedYES { this.displayWidth = width; this.displayHeight = height; this.pixelRatio = pixelRatio; + } - if (!this._resizeHandler) { - // Mobile browsers change window.innerHeight (not width) as - // the address bar hides/shows during scroll, firing a - // `resize` with no real layout change to react to. - // Genuine resizes (orientation change, desktop window - // drag) always change the width too, so gate the redraw - // on that instead of reacting to every resize event. - this._resizeHandler = () => { - if (window.innerWidth === this.displayWidth) return; - this.setupCanvas(); - this.rasterizeText(); - }; - window.addEventListener('resize', this._resizeHandler); - } + // 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; + this.setupCanvas(); + this.rasterizeText(); + }); + this._resizeObserver.observe(this.rasterCanvas.parentElement); } setupMouseTracking() { const updatePosition = (clientX, clientY) => { - this.mouseX = clientX / window.innerWidth; - this.mouseY = clientY / window.innerHeight; + this.mouseX = clientX / this.displayWidth; + this.mouseY = clientY / this.displayHeight; this.containmentStrength = 0.1 + (this.mouseX * 0.9); this.wiggleAmount = 0.1 + (this.mouseY * 1.9); };