diff --git a/src/components/Viewport.tsx b/src/components/Viewport.tsx
index 72973d6..8222998 100644
--- a/src/components/Viewport.tsx
+++ b/src/components/Viewport.tsx
@@ -4,9 +4,17 @@
export default function Viewport(
props: {
children: unknown,
+ clampTop?: number,
+ clampBottom?: number,
}
) {
- return
+ const viewportProps: any = {};
+ if (props.clampTop !== undefined) viewportProps["data-clamp-top"] = String(props.clampTop);
+ if (props.clampBottom !== undefined) viewportProps["data-clamp-bottom"] = String(props.clampBottom);
+ return
{ props.children }
}
diff --git a/web/viewport.js b/web/viewport.js
index a61d374..20aa3c6 100644
--- a/web/viewport.js
+++ b/web/viewport.js
@@ -32,6 +32,23 @@ function getWindowHeight(event) {
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) {
/*
@@ -57,13 +74,46 @@ function setViewportOffset(viewport, windowHeight) {
*/
const documentRect = document.documentElement.getBoundingClientRect();
const endOffset = viewportRect.height * 0.25;
- const origin = Math.min(
+ 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`);
}