small fixes
This commit is contained in:
parent
8790b6629b
commit
04c665e51c
3 changed files with 168 additions and 59 deletions
74
CHANGES 3.md
Normal file
74
CHANGES 3.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Site update v4 — sticky scroll fix + hero shrink + move-up
|
||||||
|
|
||||||
|
Two issues from v3, two targeted fixes. Everything lands in `protected/`.
|
||||||
|
|
||||||
|
## Files to replace
|
||||||
|
|
||||||
|
| File | What changed |
|
||||||
|
|---|---|
|
||||||
|
| `protected/index.html` | Hero fonts reduced ~13% (title, lede, eyebrow, hero-foot margin); hero block pulled further up |
|
||||||
|
| `protected/bifrost.js` | Sticky-scroll rewritten to use `wheelMultiplier` attenuation; treasure-map stops added as individual sticky targets |
|
||||||
|
| `protected/timeline.js` | **No change** from v3 — copy only if you want to re-ship it alongside the others |
|
||||||
|
|
||||||
|
## The fix
|
||||||
|
|
||||||
|
### Why v3's sticky scroll didn't work
|
||||||
|
|
||||||
|
v3 multiplied Lenis's `.velocity` property once per scroll event. Lenis overwrites that value from the next wheel input before it ever gets consumed by the scroll loop — so the damping had no perceptible effect. No amount of tuning the `DAMP_FACTOR` would have helped.
|
||||||
|
|
||||||
|
### The new approach
|
||||||
|
|
||||||
|
Instead of reacting to the scroll after the fact, we attenuate wheel input **at the source**. Lenis reads `options.wheelMultiplier` every time a wheel event fires. When the viewport center passes a sticky target, we drop it from `1.0` to `0.35` — a 65% reduction. The user's wheel still works, but each flick of the wheel now moves the page 35% as far as before. When the zone is exited, the multiplier snaps back to 1.0.
|
||||||
|
|
||||||
|
This produces a tactile, immediate resistance. Power users can still scroll fast (more wheel strokes); casual users get a natural "hold" on each scene.
|
||||||
|
|
||||||
|
### Treasure-map stops
|
||||||
|
|
||||||
|
v3's sticky targets were scenes only — the whole `bifrost-meaning` section (the treasure map) was one target. Since it's ~300vh tall, "center of section" was only briefly aligned with viewport center, and users flew past the individual stops.
|
||||||
|
|
||||||
|
v4 adds each `.map-stop` element as its own sticky target. The intro card, Community, Advisory Council, and Pilot Projects — each dwells in the viewport as the user reaches it.
|
||||||
|
|
||||||
|
### Hero fonts + move up
|
||||||
|
|
||||||
|
`#page-overview #hero` now has overview-specific sizing that runs ~13% smaller than the base hero tokens:
|
||||||
|
|
||||||
|
| Element | Before | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `.hero-title` | `var(--step-hero)` → `clamp(2.4rem, 6.2vw, 5.4rem)` | `clamp(2.1rem, 5.4vw, 4.7rem)` |
|
||||||
|
| `.hero-lede` | `var(--step-lg)` → `clamp(1.35rem, 3vw, 2.2rem)` | `clamp(1.15rem, 2.6vw, 1.9rem)` |
|
||||||
|
| `.eyebrow` | `var(--step-sm)` → `clamp(0.85rem, 1vw, 0.95rem)` | `0.74rem` |
|
||||||
|
| `.hero-wrap` padding-top | `clamp(3rem, 8vh, 6rem)` | `clamp(1.75rem, 4.5vh, 3rem)` |
|
||||||
|
| `.eyebrow` margin-bottom | `clamp(1.4rem, 4vh, 2.4rem)` | `clamp(0.9rem, 2.5vh, 1.6rem)` |
|
||||||
|
| `.hero-lede` margin-top | `clamp(1.5rem, 4vh, 2.5rem)` | `clamp(1rem, 2.5vh, 1.6rem)` |
|
||||||
|
| `.hero-foot` margin-top | `clamp(2rem, 5vh, 3.5rem)` | `clamp(1.25rem, 3.5vh, 2.25rem)` |
|
||||||
|
|
||||||
|
Line-heights tightened to match (`1.02` → `1.0` on the title). Together these keep the hero block compact and clear of the dot-nav + S2 cards below.
|
||||||
|
|
||||||
|
These overrides are scoped to `#page-overview #hero` only — the timeline and other pages are unaffected.
|
||||||
|
|
||||||
|
## Spot-check after deploy
|
||||||
|
|
||||||
|
- [ ] Overview → scroll through hero. Text sits clearly in upper third of viewport. Visible space below "Supported by" + arrow.
|
||||||
|
- [ ] Hero block does NOT overlap the S2 cards below it on scroll; cards drop in cleanly.
|
||||||
|
- [ ] Scroll into S5 treasure map. Each stop (Community, Advisory Council, Pilot Projects) **slows the scroll** as it passes the viewport center — you feel a real hold, not just a brief damping.
|
||||||
|
- [ ] Scrolling fast still works — the wheel is slower, not locked. Determined scrolling passes through.
|
||||||
|
- [ ] S2 (stack) scrubs at normal speed (not damped — pinned scene is exempt).
|
||||||
|
- [ ] Hero, Words, Bifrost, Join scenes each also feel sticky as they hit center.
|
||||||
|
|
||||||
|
## Tuning knobs
|
||||||
|
|
||||||
|
If the stickiness is too strong or too weak after deploy:
|
||||||
|
|
||||||
|
| What | In `bifrost.js` | Change |
|
||||||
|
|---|---|---|
|
||||||
|
| Less sticky (weaker hold) | `STICKY_WHEEL_MULT = 0.35` | → `0.55` or `0.65` |
|
||||||
|
| More sticky (stronger hold) | `STICKY_WHEEL_MULT = 0.35` | → `0.20` or `0.15` |
|
||||||
|
| Wider trigger zone | `STICKY_ZONE_VH = 0.18` | → `0.25` |
|
||||||
|
| Narrower trigger zone | `STICKY_ZONE_VH = 0.18` | → `0.12` |
|
||||||
|
|
||||||
|
If the hero text is still too large or still bleeds into the icons below:
|
||||||
|
|
||||||
|
| What | In `index.html` | Change |
|
||||||
|
|---|---|---|
|
||||||
|
| Smaller hero title | `clamp(2.1rem, 5.4vw, 4.7rem)` | e.g. `clamp(1.9rem, 4.8vw, 4.2rem)` |
|
||||||
|
| Pull up more | `padding-top: clamp(1.75rem, 4.5vh, 3rem)` | e.g. `clamp(1rem, 3vh, 2rem)` |
|
||||||
|
|
@ -286,73 +286,78 @@
|
||||||
// Initial paint
|
// Initial paint
|
||||||
updateTopographyParallax();
|
updateTopographyParallax();
|
||||||
|
|
||||||
// ─── Sticky-scroll damping ───────────────────────────────────
|
// ─── Sticky-scroll (wheel-multiplier attenuation) ────────────
|
||||||
//
|
//
|
||||||
// When a scene's vertical center is close to the viewport center,
|
// Previous implementation multiplied Lenis's `.velocity` per scroll
|
||||||
// damp Lenis's velocity briefly — scenes "catch" the eye instead
|
// event — ineffective, because Lenis overwrites velocity from the
|
||||||
// of flying past at full scroll speed. Applied only to non-pinned
|
// next wheel event before that damped value is consumed.
|
||||||
// scenes (S1, S3, S4, S5, S6). S2 is already pinned + scrubbed
|
|
||||||
// by GSAP ScrollTrigger; adding damping on top would make its
|
|
||||||
// card-fall feel like a drag.
|
|
||||||
//
|
//
|
||||||
// Implementation: on each scroll tick, if the active scene is in
|
// New approach: directly reduce Lenis's `wheelMultiplier` when the
|
||||||
// the "damp zone" (±20% of viewport height from viewport center),
|
// viewport center is near a sticky target. The user's wheel input
|
||||||
// scale Lenis's next frame velocity by a damping factor (0.82).
|
// is attenuated at the source — their next scroll produces less
|
||||||
// When outside the zone, pass through unchanged. Keeps power-users
|
// delta, so the scene noticeably holds. When the zone is exited,
|
||||||
// happy — they can still scroll fast, just feel a soft hold at
|
// the multiplier is restored.
|
||||||
// scene boundaries.
|
//
|
||||||
const STICKY_SCENES = new Set([
|
// Targets are:
|
||||||
'hero', 'words-scene', 'bifrost', 'bifrost-meaning', 'bifrost-join',
|
// - Non-pinned scenes (hero, words-scene, bifrost, bifrost-join)
|
||||||
]); // note: 'stack-scene' deliberately excluded (already pinned)
|
// - The treasure map (bifrost-meaning) AND each of its three
|
||||||
const DAMP_ZONE_VH = 0.20; // ±20% of viewport height from center
|
// stops individually — previously the whole 300vh section was
|
||||||
const DAMP_FACTOR = 0.82; // multiply velocity while in zone
|
// one target, so users flew through the individual stops.
|
||||||
const DAMP_MAX_MS = 300; // cap: never damp for longer than this per scene-crossing
|
//
|
||||||
|
// stack-scene (S2) is deliberately excluded — it's GSAP-pinned and
|
||||||
|
// scrubbed; damping on top makes its card-fall feel like a drag.
|
||||||
|
const BASE_WHEEL_MULT = 1.0;
|
||||||
|
const BASE_TOUCH_MULT = 1.5;
|
||||||
|
const STICKY_WHEEL_MULT = 0.35; // 65% reduction while in a sticky zone
|
||||||
|
const STICKY_TOUCH_MULT = 0.55;
|
||||||
|
const STICKY_ZONE_VH = 0.18; // ±18% of viewport height from its center
|
||||||
|
|
||||||
let dampedUntil = 0; // timestamp (ms); damping active while > now
|
// Collect sticky targets. IDs of scenes + DOM refs for the three
|
||||||
let lastDampedScene = null; // id of the scene we last entered
|
// individual treasure-map stops. Cached once — the list doesn't
|
||||||
|
// change after init.
|
||||||
function updateStickyDamping() {
|
function collectStickyTargets() {
|
||||||
const now = performance.now();
|
const targets = [];
|
||||||
const midY = window.innerHeight * 0.5;
|
const sceneIds = ['hero', 'words-scene', 'bifrost', 'bifrost-join'];
|
||||||
const zone = window.innerHeight * DAMP_ZONE_VH;
|
sceneIds.forEach(id => {
|
||||||
let inZone = false, zoneScene = null;
|
|
||||||
for (const id of STICKY_SCENES) {
|
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) continue;
|
if (el) targets.push(el);
|
||||||
|
});
|
||||||
|
// Treasure map intro + three stops each get their own sticky zone.
|
||||||
|
const mapSection = document.getElementById('bifrost-meaning');
|
||||||
|
if (mapSection) {
|
||||||
|
// Each .map-stop is a separate card along the path — sticky them
|
||||||
|
// individually so the user dwells on each one.
|
||||||
|
mapSection.querySelectorAll('.map-stop').forEach(el => targets.push(el));
|
||||||
|
}
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
const stickyTargets = collectStickyTargets();
|
||||||
|
|
||||||
|
let wheelMultState = BASE_WHEEL_MULT;
|
||||||
|
function updateStickyDamping() {
|
||||||
|
const midY = window.innerHeight * 0.5;
|
||||||
|
const zone = window.innerHeight * STICKY_ZONE_VH;
|
||||||
|
let inZone = false;
|
||||||
|
for (const el of stickyTargets) {
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
// Use the scene's vertical midpoint as the "attractor."
|
const elMid = (r.top + r.bottom) * 0.5;
|
||||||
const sceneMid = (r.top + r.bottom) * 0.5;
|
if (Math.abs(elMid - midY) < zone) {
|
||||||
if (Math.abs(sceneMid - midY) < zone) {
|
|
||||||
inZone = true;
|
inZone = true;
|
||||||
zoneScene = id;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (inZone) {
|
const nextMult = inZone ? STICKY_WHEEL_MULT : BASE_WHEEL_MULT;
|
||||||
// New scene entered the zone → arm a fresh damping window.
|
if (nextMult !== wheelMultState) {
|
||||||
// Reusing the same scene (still in zone, already damped) doesn't
|
wheelMultState = nextMult;
|
||||||
// extend — DAMP_MAX_MS is a hard cap per entry to avoid an
|
// Lenis reads options.wheelMultiplier on every wheel event —
|
||||||
// indefinite hold.
|
// re-assigning takes effect immediately.
|
||||||
if (zoneScene !== lastDampedScene) {
|
lenis.options.wheelMultiplier = nextMult;
|
||||||
lastDampedScene = zoneScene;
|
lenis.options.touchMultiplier = inZone ? STICKY_TOUCH_MULT : BASE_TOUCH_MULT;
|
||||||
dampedUntil = now + DAMP_MAX_MS;
|
|
||||||
}
|
|
||||||
if (now < dampedUntil && lenis.velocity) {
|
|
||||||
// Lenis exposes .velocity (a number). Multiplying in place
|
|
||||||
// dampens the next-frame step. Small, contained effect — no
|
|
||||||
// chance of interfering with ScrollTrigger.
|
|
||||||
lenis.velocity *= DAMP_FACTOR;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Left the zone — reset so the NEXT time we enter a scene,
|
|
||||||
// we get a fresh damping window.
|
|
||||||
if (lastDampedScene !== null) {
|
|
||||||
lastDampedScene = null;
|
|
||||||
dampedUntil = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lenis.on('scroll', updateStickyDamping);
|
lenis.on('scroll', updateStickyDamping);
|
||||||
|
// Initial paint (we likely start at hero which is itself a target).
|
||||||
|
requestAnimationFrame(updateStickyDamping);
|
||||||
|
|
||||||
// ─── Site-2 scene animations ─────────────────────────────────
|
// ─── Site-2 scene animations ─────────────────────────────────
|
||||||
// (transplanted verbatim; all ScrollTriggers below automatically
|
// (transplanted verbatim; all ScrollTriggers below automatically
|
||||||
|
|
|
||||||
|
|
@ -695,10 +695,40 @@
|
||||||
#page-overview #hero .hero-wrap {
|
#page-overview #hero .hero-wrap {
|
||||||
/* Constrain to the left column so Europe is visible to its right. */
|
/* Constrain to the left column so Europe is visible to its right. */
|
||||||
max-width: 62ch;
|
max-width: 62ch;
|
||||||
/* Reduced from clamp(6rem, 16vh, 12rem) to move hero text up into
|
/* Pulled up further so the hero block sits in the upper third of
|
||||||
the upper half of the viewport. Same intent as the #hero padding
|
the viewport, leaving room for hero-foot (Supported-by +
|
||||||
reduction above — the more-specific selector takes precedence. */
|
Scroll-down arrow) without colliding with the dot-nav. */
|
||||||
padding-top: clamp(3rem, 8vh, 6rem);
|
padding-top: clamp(1.75rem, 4.5vh, 3rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overview-specific hero sizing — about 13% smaller than the base
|
||||||
|
hero tokens, so the entire block fits cleanly above the dot-nav
|
||||||
|
and the hero-foot doesn't get pushed into the scroll-hint / icons.
|
||||||
|
Line-height tightened in proportion so reduced size doesn't open
|
||||||
|
unwanted vertical gaps. */
|
||||||
|
#page-overview #hero .hero-title {
|
||||||
|
font-size: clamp(2.1rem, 5.4vw, 4.7rem);
|
||||||
|
line-height: 1.0;
|
||||||
|
letter-spacing: -0.028em;
|
||||||
|
}
|
||||||
|
#page-overview #hero .hero-lede {
|
||||||
|
font-size: clamp(1.15rem, 2.6vw, 1.9rem);
|
||||||
|
line-height: 1.3;
|
||||||
|
/* Shrink the gap above the lede so the hero text block is more
|
||||||
|
compact. */
|
||||||
|
margin-top: clamp(1rem, 2.5vh, 1.6rem);
|
||||||
|
}
|
||||||
|
#page-overview #hero .eyebrow {
|
||||||
|
font-size: 0.74rem;
|
||||||
|
/* Reduce the gap below the eyebrow — the original 1.4–2.4rem
|
||||||
|
combined with the reduced lede margin was still too tall. */
|
||||||
|
margin-bottom: clamp(0.9rem, 2.5vh, 1.6rem);
|
||||||
|
}
|
||||||
|
/* Hero-foot also tightened — smaller top margin so the whole left
|
||||||
|
column fits above mid-viewport and the scroll indicator doesn't
|
||||||
|
end up overlapping S2 cards below. */
|
||||||
|
#page-overview #hero .hero-foot {
|
||||||
|
margin-top: clamp(1.25rem, 3.5vh, 2.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure scenes don't accidentally inherit `main { position: relative }` */
|
/* Make sure scenes don't accidentally inherit `main { position: relative }` */
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue