To keep things organised put the assets supporting pages into sub-folders in /assets.
130 lines
4.8 KiB
JavaScript
130 lines
4.8 KiB
JavaScript
let heightOffset = 0;
|
|
let lastHeight = window.innerHeight;
|
|
function getWindowHeight(event) {
|
|
|
|
/*
|
|
When scrolling such that the browser toolbar is pushed out of view or
|
|
pulled into view on a mobile device the height of the viewport changes.
|
|
This causes the perspective to shift noticeably when you let go of your
|
|
finger after scrolling. To avoid this, I store the last change of the
|
|
window height if this was previously 0 and apply that to the calculated
|
|
height of the window when the size changes.
|
|
|
|
This causes the heightOffset to toggle between 0 and the value that
|
|
negates the perspective shift when you switch between having the browser
|
|
toolbar visible and not visible on mobile, but stay as 0 in most other
|
|
situations involving window resize.
|
|
*/
|
|
if (event?.type === 'resize') {
|
|
if (heightOffset === 0 && lastHeight !== 0) {
|
|
const newOffset = lastHeight - window.innerHeight;
|
|
// Expect the browser toolbar resize to be between 20 and 100 pixels.
|
|
if (Math.abs(newOffset) > 20 && Math.abs(newOffset) < 100) {
|
|
heightOffset = newOffset;
|
|
} else {
|
|
heightOffset = 0;
|
|
}
|
|
} else {
|
|
heightOffset = 0;
|
|
}
|
|
lastHeight = window.innerHeight;
|
|
}
|
|
return window.innerHeight + heightOffset;
|
|
}
|
|
|
|
/**
|
|
Returns x rescaled such that the values in the range a-b are mapped to the range c-d.
|
|
*/
|
|
function rescaleRange(x, a, b, c, d){
|
|
return c + (x - a) * (d - c) / (b - a);
|
|
}
|
|
|
|
/**
|
|
If value is not undefined return it parsed as a float, otherwise returns undefined.
|
|
*/
|
|
function maybeFloat(value) {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
return Number.parseFloat(value);
|
|
}
|
|
|
|
function setViewportOffset(viewport, windowHeight) {
|
|
|
|
/*
|
|
Ignoring updates if the the bounding height of the viewport element is
|
|
not inside of the visible client area on the page to optimise for when
|
|
it not visible due to being off screen.
|
|
*/
|
|
const viewportRect = viewport.getBoundingClientRect();
|
|
if (viewportRect.top > window.innerHeight || viewportRect.bottom < 0) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
Calculate the distance from the top of the viewport to the center of the
|
|
screen. This is an accurate rendering of perspective shift as you
|
|
scroll.
|
|
|
|
If the the bounds of a viewport element is close to the top or bottom
|
|
of the page the perspective origin is adjusted so that it is still
|
|
contained inside of the bounds of the element to not look out of place.
|
|
endOffset controls how much into the bounds the perspective origin should be
|
|
pushed when the screen in scrolled all the way to the top/bottom.
|
|
*/
|
|
const documentRect = document.documentElement.getBoundingClientRect();
|
|
const endOffset = viewportRect.height * 0.25;
|
|
let origin = Math.min(
|
|
(viewportRect.bottom - documentRect.top) - endOffset,
|
|
Math.max(
|
|
windowHeight / 2,
|
|
windowHeight - (documentRect.bottom - viewportRect.top) + endOffset,
|
|
)
|
|
);
|
|
|
|
/*
|
|
In some cases it's undesirable to let the perspective origin move too far
|
|
away in one or both directions as it causes elements inside the 3d view to
|
|
become obscured by the top/bottom of the viewport. To aid with this
|
|
optionally clamp how far the perspective origin can move relative to the top
|
|
and bottom.
|
|
|
|
This is done by calculating how far the origin can go un-clamped while the
|
|
viewport is visible on the screen and then rescaling the range of the
|
|
perspective origin from it's un-clamped range to its clamped range.
|
|
*/
|
|
const clampTop = maybeFloat(viewport.dataset.clampTop);
|
|
const clampBottom = maybeFloat(viewport.dataset.clampBottom);
|
|
if (clampBottom !== undefined || clampTop !== undefined) {
|
|
const rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
const topOrigin = Math.min(
|
|
viewportRect.bottom - endOffset,
|
|
Math.max(viewportRect.top, documentRect.top + windowHeight) - windowHeight / 2,
|
|
)
|
|
const topClampPosition = rootFontSize * (clampTop ?? -Infinity) + viewportRect.top;
|
|
const topLimit = Math.max(topOrigin, topClampPosition);
|
|
|
|
const bottomOrigin = Math.max(
|
|
viewportRect.top + endOffset,
|
|
Math.min(viewportRect.bottom, documentRect.bottom - windowHeight) + windowHeight / 2,
|
|
)
|
|
const bottomClampPosition = rootFontSize * (clampBottom ?? +Infinity) + viewportRect.bottom;
|
|
const bottomLimit = Math.min(bottomOrigin, bottomClampPosition);
|
|
|
|
origin = rescaleRange(origin, topOrigin, bottomOrigin, topLimit, bottomLimit);
|
|
}
|
|
|
|
const yOffset = origin - viewportRect.top;
|
|
viewport.style.setProperty('perspective-origin', `50% ${yOffset}px`);
|
|
}
|
|
|
|
const viewports = document.querySelectorAll(".viewport");
|
|
function updateViewports(event) {
|
|
const windowHeight = getWindowHeight(event);
|
|
for (const viewport of viewports) {
|
|
setViewportOffset(viewport, windowHeight);
|
|
}
|
|
}
|
|
window.addEventListener("scroll", updateViewports, { passive: true });
|
|
window.addEventListener("resize", updateViewports, { passive: true });
|
|
updateViewports();
|