YES canvas: size off its own container rect, not window.innerHeight
Deploy / deploy (push) Successful in 34s

setupCanvas() measured window.innerWidth/innerHeight directly, then a
resize listener gated on innerWidth changing (guessing which resize
events were "real" vs mobile Safari's address-bar animation). That
measurement had nothing to do with .hero-yes's actual CSS height, so
the canvas and its box could end up disagreeing - which is what was
producing the observed jump in content below the hero on scroll, not
the choice of viewport unit on its own.

Switched to ResizeObserver on .hero-canvas (which tracks .hero-yes via
inset:0), using its contentRect directly. This also drops the
innerWidth-gating heuristic entirely: a fixed-height box never fires
a ResizeObserver callback during the toolbar animation in the first
place, so there's nothing to gate.

Also updated the mouse/touch position normalization to use the
canvas's own displayWidth/displayHeight instead of
window.innerWidth/innerHeight, for the same reason - and fixed the
.hero-yes comment, which still described the lvh reasoning from a
prior attempt after the height value itself had been changed back to
svh directly on origin/main.
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-06 21:22:00 +02:00
parent cc5174813c
commit d7f280e681
2 changed files with 41 additions and 31 deletions
+11 -11
View File
@@ -153,17 +153,17 @@ main.loading, main.not-found {
position: relative; position: relative;
max-width: none; max-width: none;
width: 100%; width: 100%;
/* lvh (large viewport height), not svh: svh pins to the *smallest* /* svh (small viewport height): a fixed, stable length - computed
viewport (toolbar visible), which never grows once the browser once, never recalculated mid-scroll as mobile Safari's address
auto-collapses its toolbar on the first scroll - so the actual bar hides/shows, unlike plain vh/dvh on some engines. Pinned to
visible screen becomes taller than this box, and the content the *smallest* possible viewport (toolbar visible) so the box
below appears to snap into the newly-revealed space the moment never needs to grow into space the toolbar might reclaim.
that happens. lvh pins to the toolbar-collapsed size instead, yes.js's canvas measures this box's own rendered rect (not
matching the settled state scrolling actually lands on, so window.innerWidth/innerHeight) via ResizeObserver, so the canvas
there's no gap to reveal. (svh is the right choice for the always matches whatever height this resolves to - the two can't
opposite case - a fixed element that must never be hidden behind disagree, which is what was actually causing the content below to
the toolbar - not a hero at the top of a scrolling page.) visibly jump.
Plain-vh fallback declared first - an engine without lvh support Plain-vh fallback declared first - an engine without svh support
ignores the invalid second line rather than falling through to ignores the invalid second line rather than falling through to
auto height, which would collapse this to the height of its auto height, which would collapse this to the height of its
in-flow content and clip the canvas via overflow:hidden below. */ in-flow content and clip the canvas via overflow:hidden below. */
+30 -20
View File
@@ -42,6 +42,7 @@ export class RasterizedYES {
this.wiggleAmount = 0.5; this.wiggleAmount = 0.5;
this.setupCanvas(); this.setupCanvas();
this.setupResizeObserver();
this.setupMouseTracking(); this.setupMouseTracking();
this.setupClickHandler(); this.setupClickHandler();
this.rasterizeText(); this.rasterizeText();
@@ -51,15 +52,24 @@ export class RasterizedYES {
stop() { stop() {
this.destroyed = true; this.destroyed = true;
if (this._resizeHandler) { if (this._resizeObserver) {
window.removeEventListener('resize', this._resizeHandler); 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() { setupCanvas() {
const pixelRatio = window.devicePixelRatio || 1; const pixelRatio = window.devicePixelRatio || 1;
const width = window.innerWidth; const rect = this.rasterCanvas.parentElement.getBoundingClientRect();
const height = window.innerHeight; const width = rect.width;
const height = rect.height;
this.rasterCanvas.style.width = width + 'px'; this.rasterCanvas.style.width = width + 'px';
this.rasterCanvas.style.height = height + 'px'; this.rasterCanvas.style.height = height + 'px';
@@ -77,27 +87,27 @@ export class RasterizedYES {
this.displayWidth = width; this.displayWidth = width;
this.displayHeight = height; this.displayHeight = height;
this.pixelRatio = pixelRatio; this.pixelRatio = pixelRatio;
}
if (!this._resizeHandler) { // ResizeObserver reacts to the container's box actually changing
// Mobile browsers change window.innerHeight (not width) as // size - a fixed-height box (svh, lvh, or plain px) never fires
// the address bar hides/shows during scroll, firing a // this at all during the address-bar animation, so there's no
// `resize` with no real layout change to react to. // need for the old "only redraw if width changed" heuristic that
// Genuine resizes (orientation change, desktop window // guessed at which resize events were real.
// drag) always change the width too, so gate the redraw setupResizeObserver() {
// on that instead of reacting to every resize event. this._resizeObserver = new ResizeObserver((entries) => {
this._resizeHandler = () => { const { width, height } = entries[0].contentRect;
if (window.innerWidth === this.displayWidth) return; if (width === this.displayWidth && height === this.displayHeight) return;
this.setupCanvas(); this.setupCanvas();
this.rasterizeText(); this.rasterizeText();
}; });
window.addEventListener('resize', this._resizeHandler); this._resizeObserver.observe(this.rasterCanvas.parentElement);
}
} }
setupMouseTracking() { setupMouseTracking() {
const updatePosition = (clientX, clientY) => { const updatePosition = (clientX, clientY) => {
this.mouseX = clientX / window.innerWidth; this.mouseX = clientX / this.displayWidth;
this.mouseY = clientY / window.innerHeight; this.mouseY = clientY / this.displayHeight;
this.containmentStrength = 0.1 + (this.mouseX * 0.9); this.containmentStrength = 0.1 + (this.mouseX * 0.9);
this.wiggleAmount = 0.1 + (this.mouseY * 1.9); this.wiggleAmount = 0.1 + (this.mouseY * 1.9);
}; };