Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG.md

## v0.47.0 (unreleased)
- The official homepage's database sculpture now scrolls natively with its section after the opening flight, and section layouts reserve its space with CSS grid.
- For sites that store their pages in a database, checking for page updates no longer makes other pages wait when the database is busy. In a test, another page loaded in under 1 ms instead of about 250 ms.
- Redesigned the official website homepage with live SQLPage examples, an interactive database sculpture, flowing section transitions, responsive layouts, and direct links to documentation and deployment guides.
- Loading embedded card content no longer adds duplicate form submission or file validation handlers to existing fields. Fragment initialization also includes the element announcing the fragment.
Expand Down
2 changes: 1 addition & 1 deletion examples/official-site/assets/landing/js/experience.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function initExperience(root) {
setStatus("error");
}
}
stopScroll = initScrollProgress(section, mount, state);
stopScroll = initScrollProgress(section, state);
syncMotion();
media.addEventListener("change", syncMotion, { signal: events.signal });
root
Expand Down
20 changes: 14 additions & 6 deletions examples/official-site/assets/landing/js/scene/database-scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,17 @@ export function createDatabaseScene({
if (pointers.size) return;
const scale = canvasWidth / 1000;
let { left, right, top, bottom } = screenBounds;
left = clamp(canvasLeft + left * scale - 12, 0, innerWidth);
right = clamp(canvasLeft + right * scale + 12, left, innerWidth);
top = clamp(canvasTop + top * scale - 12, 90, innerHeight);
bottom = clamp(canvasTop + bottom * scale + 12, top, innerHeight);
left = canvasLeft + left * scale - 12;
right = canvasLeft + right * scale + 12;
top = canvasTop + top * scale - 12;
bottom = canvasTop + bottom * scale + 12;
// Clip in viewport space, then keep the target in the canvas host's local
// coordinates. Rotation can otherwise put gesture centers off the screen.
const origin = mount.getBoundingClientRect();
left = clamp(left, -origin.left, innerWidth - origin.left);
right = clamp(right, left, innerWidth - origin.left);
top = clamp(top, 90 - origin.top, innerHeight - origin.top);
bottom = clamp(bottom, top, innerHeight - origin.top);
hitArea.style.left = `${left}px`;
hitArea.style.top = `${top}px`;
hitArea.style.width = `${right - left}px`;
Expand Down Expand Up @@ -367,10 +374,11 @@ export function createDatabaseScene({
pointers.size > 0 ||
!lastOrientation.equals(dragQuaternion) ||
parallax.lengthSq() > 0.000001;
const viewportTop = mount.getBoundingClientRect().top + canvasTop;
const inView =
state.opacity > 0.05 &&
canvasTop + canvasWidth * 0.81 > 0 &&
canvasTop + canvasWidth * 0.22 < window.innerHeight;
viewportTop + canvasWidth * 0.81 > 0 &&
viewportTop + canvasWidth * 0.22 < window.innerHeight;
if (!inView) resetBudget();
if (readySent) {
hitArea.inert = !inView;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,11 @@ function frameAt(width, height, anchor, portrait, bodyWidth = width * 0.48) {

// This small module runs before Three.js downloads, so even the loading layout
// uses the same measured letter, portrait flow, and projection as the live scene.
export function layoutLandingFrame(mount, anchor) {
const width = Math.max(1, mount.clientWidth);
export function layoutLandingFrame(viewport, anchor) {
const width = Math.max(1, viewport.clientWidth);
const screenHeight = window.innerHeight;
const portrait = width < 900 && screenHeight >= width;
const shortLandscape = !portrait && screenHeight < 560;
const viewport = mount.closest(".sqlpage-world").querySelector(".viewport");
const section = viewport.parentElement;
const heading = anchor?.closest(".hero-copy")?.querySelector(".title-from");
const intro = viewport.querySelector(".intro-row");
Expand All @@ -80,7 +79,7 @@ export function layoutLandingFrame(mount, anchor) {
`${-Math.max(0, value - screenHeight)}px`,
);
height =
portrait || shortLandscape ? value : Math.max(1, mount.clientHeight);
portrait || shortLandscape ? value : Math.max(1, viewport.clientHeight);
};
const measureLetter = () => {
if (portrait && heading) {
Expand Down
80 changes: 45 additions & 35 deletions examples/official-site/assets/landing/js/scroll-progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { layoutLandingFrame, SCULPTURE_FRAME } from "./scene/landing-frame.js";

/** One sculpture: an opening flight, then page anchors with fading handoffs. */
export function initScrollProgress(section, mount, state) {
export function initScrollProgress(section, state) {
const root = section.closest(".sqlpage-world");
const preview = root.querySelector(".scene-preview");
const layer = root.querySelector(".scene-layer");
Expand All @@ -28,13 +28,45 @@ export function initScrollProgress(section, mount, state) {
const intro = section.querySelector(".intro-row");
const header = section.querySelector(".site-header");
const ribbon = section.querySelector(".sql-ribbon");
function dock(host) {
if (layer.parentElement === host) return;
const before = layer.getBoundingClientRect();
// Keep focus and pointer capture when supported; older browsers retain focus
// explicitly while moving the same canvas and controls, without a new scene.
if (host.moveBefore) host.moveBefore(layer, null);
else {
const focused = layer.contains(document.activeElement)
? document.activeElement
: null;
host.append(layer);
focused?.focus({ preventScroll: true });
}
layer.toggleAttribute("data-docked", host !== root);
// Preserve the painted frame until the renderer applies the new local pose.
const after = layer.getBoundingClientRect();
const dx = before.left - after.left;
const dy = before.top - after.top;
const canvas = layer.querySelector("canvas");
if (canvas) {
const pose = new DOMMatrix(canvas.style.transform || undefined);
pose.e += dx;
pose.f += dy;
canvas.style.transform = pose.toString();
const input = layer.querySelector(".model-interaction");
input.style.left = `${parseFloat(input.style.left || "0") + dx}px`;
input.style.top = `${parseFloat(input.style.top || "0") + dy}px`;
}
}
function measure() {
// Read authored page positions even when earlier sections are held under a cover.
root.setAttribute("data-measuring-layout", "");
// Measure the authored hero pose, independent of its current text fade/shift.
section.style.setProperty("--progress", "0");
section.style.setProperty("--intro-shift", "0px");
heroFrame = layoutLandingFrame(mount, root.querySelector(".anchor-letter"));
heroFrame = layoutLandingFrame(
viewport,
root.querySelector(".anchor-letter"),
);
anchors = [];
heroRange = Math.max(0, section.offsetHeight - viewport.offsetHeight);
const maxScroll = Math.max(
Expand All @@ -43,39 +75,17 @@ export function initScrollProgress(section, mount, state) {
);
for (const stop of stops) {
const rect = stop.getBoundingClientRect();
const sheet = stop.closest(".landing-section");
const sheetRect = sheet.getBoundingClientRect();
const nextSheet = sheet.nextElementSibling;
// Measure the visible model, not its much larger square canvas.
const size = rect.width / SCULPTURE_FRAME.bodyWidth;
const bodyHeight =
size * (SCULPTURE_FRAME.bounds.bottom - SCULPTURE_FRAME.bounds.top);
const bodyTop = rect.top + scrollY;
const fadeDistance = Math.min(innerHeight * 0.12, bodyHeight * 0.45);
const heldTop = root.hasAttribute("data-section-motion")
? rect.top -
sheetRect.top +
(parseFloat(sheet.style.getPropertyValue("--section-stick-top")) || 0)
: -Infinity;
// A held section can leave part of its sculpture on screen. In that case
// its lifetime ends when the next wave covers it, not at its authored Y.
let exitScroll =
heldTop + bodyHeight > 0 ? Infinity : bodyTop + bodyHeight;
if (nextSheet?.matches(".landing-section")) {
const waveHeight = parseFloat(
getComputedStyle(nextSheet).getPropertyValue("--wave-height"),
);
const coverScroll =
nextSheet.getBoundingClientRect().top +
scrollY -
waveHeight * 0.75 -
(heldTop + bodyHeight * 0.5);
exitScroll = Math.min(exitScroll, coverScroll);
}
anchors.push({
element: stop,
enterScroll: Math.min(bodyTop - innerHeight, maxScroll - fadeDistance),
exitScroll,
// The next section naturally covers a docked sculpture in its own layer.
exitScroll: Infinity,
fadeDistance,
bodyHeight,
bodyTop,
Expand All @@ -86,7 +96,7 @@ export function initScrollProgress(section, mount, state) {
}
heroDockScroll = Math.min(
maxScroll,
anchors[0].bodyTop - innerHeight * 0.25,
Math.floor(anchors[0].bodyTop - innerHeight * 0.25),
);
anchors[0].enterScroll = -Infinity; // The opening flight supplies its entrance.
anchors.at(-1).exitScroll = Infinity; // Keep the closing composition visible.
Expand Down Expand Up @@ -128,11 +138,10 @@ export function initScrollProgress(section, mount, state) {
let sceneOpacity = 1;
if (!heroRange && scroll < heroDockScroll) {
// Reduced motion keeps the opening sculpture attached to the hero layout.
state.frame = {
...heroFrame,
top: heroFrame.top + viewport.getBoundingClientRect().top,
};
dock(viewport);
state.frame = heroFrame;
} else if (heroRange && scroll <= heroDockScroll) {
dock(root);
const bounds = SCULPTURE_FRAME.bounds;
const size = Math.min(
(innerWidth * 0.9) / SCULPTURE_FRAME.bodyWidth,
Expand Down Expand Up @@ -177,8 +186,9 @@ export function initScrollProgress(section, mount, state) {
)
index++;
const anchor = anchors[index];
// Follow the actual page anchor, including its section holding beneath the
// next cover. Only the opening flight interpolates position or size.
dock(anchor.element);
// Native scrolling and section stickiness own placement. Geometry here
// controls fading only; the canvas and input bounds stay local to the host.
const rect = anchor.element.getBoundingClientRect();
if (state.motion) {
sceneOpacity = Math.min(
Expand All @@ -191,8 +201,8 @@ export function initScrollProgress(section, mount, state) {
);
}
state.frame = {
left: rect.left - anchor.size * SCULPTURE_FRAME.bounds.left,
top: rect.top - anchor.size * SCULPTURE_FRAME.bounds.top,
left: -anchor.size * SCULPTURE_FRAME.bounds.left,
top: -anchor.size * SCULPTURE_FRAME.bounds.top,
size: anchor.size,
};
}
Expand Down
15 changes: 3 additions & 12 deletions examples/official-site/assets/landing/styles/frontend.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
}
.deployment-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 0.2fr);
gap: 25px;
}
.deployment-section .eyebrow {
Expand All @@ -22,15 +22,11 @@
margin-bottom: 20px;
}
.deployment-notes {
grid-column: 1 / -1;
align-self: end;
text-align: right;
margin: 0 0 20px;
}
.landing-section .deployment-sculpture {
top: -85px;
right: 5%;
width: 17%;
}
.frontend-tabs,
.frontend-panels {
grid-column: 1 / -1;
Expand Down Expand Up @@ -391,18 +387,13 @@
padding-bottom: 100px;
}
.deployment-grid {
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 0.24fr);
gap: 20px;
}
.deployment-section h2 {
font-size: clamp(36px, 10.8vw, 52px);
max-width: 100%;
}
.landing-section .deployment-sculpture {
width: 19%;
top: -10px;
right: -2%;
}
.deployment-notes {
text-align: left;
margin: 0;
Expand Down
6 changes: 5 additions & 1 deletion examples/official-site/assets/landing/styles/scene.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* One fixed sculpture layer, static fallback, and the opening stage’s pinned lifetime. */
/* One sculpture: a fixed opening flight, then native scrolling in section hosts. */
.sqlpage-world {
position: relative;
overflow-x: clip;
Expand Down Expand Up @@ -39,6 +39,10 @@
position: absolute;
height: 100svh;
}
.scene-layer[data-docked] {
position: absolute;
height: auto;
}
.scene-layer .model-interaction[data-ready="true"] {
pointer-events: auto;
}
Expand Down
Loading
Loading