Merge branch 'main' of git.fenja.ai:aru/fenja-project-bifrost
This commit is contained in:
commit
072896b126
2 changed files with 134 additions and 55 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)` |
|
||||
|
|
@ -291,73 +291,78 @@
|
|||
// Initial paint
|
||||
updateTopographyParallax();
|
||||
|
||||
// ─── Sticky-scroll damping ───────────────────────────────────
|
||||
// ─── Sticky-scroll (wheel-multiplier attenuation) ────────────
|
||||
//
|
||||
// When a scene's vertical center is close to the viewport center,
|
||||
// damp Lenis's velocity briefly — scenes "catch" the eye instead
|
||||
// of flying past at full scroll speed. Applied only to non-pinned
|
||||
// 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.
|
||||
// Previous implementation multiplied Lenis's `.velocity` per scroll
|
||||
// event — ineffective, because Lenis overwrites velocity from the
|
||||
// next wheel event before that damped value is consumed.
|
||||
//
|
||||
// Implementation: on each scroll tick, if the active scene is in
|
||||
// the "damp zone" (±20% of viewport height from viewport center),
|
||||
// scale Lenis's next frame velocity by a damping factor (0.82).
|
||||
// When outside the zone, pass through unchanged. Keeps power-users
|
||||
// happy — they can still scroll fast, just feel a soft hold at
|
||||
// scene boundaries.
|
||||
const STICKY_SCENES = new Set([
|
||||
'hero', 'words-scene', 'bifrost', 'bifrost-meaning', 'bifrost-join',
|
||||
]); // note: 'stack-scene' deliberately excluded (already pinned)
|
||||
const DAMP_ZONE_VH = 0.20; // ±20% of viewport height from center
|
||||
const DAMP_FACTOR = 0.82; // multiply velocity while in zone
|
||||
const DAMP_MAX_MS = 300; // cap: never damp for longer than this per scene-crossing
|
||||
// New approach: directly reduce Lenis's `wheelMultiplier` when the
|
||||
// viewport center is near a sticky target. The user's wheel input
|
||||
// is attenuated at the source — their next scroll produces less
|
||||
// delta, so the scene noticeably holds. When the zone is exited,
|
||||
// the multiplier is restored.
|
||||
//
|
||||
// Targets are:
|
||||
// - Non-pinned scenes (hero, words-scene, bifrost, bifrost-join)
|
||||
// - The treasure map (bifrost-meaning) AND each of its three
|
||||
// stops individually — previously the whole 300vh section was
|
||||
// one target, so users flew through the individual stops.
|
||||
//
|
||||
// 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
|
||||
let lastDampedScene = null; // id of the scene we last entered
|
||||
|
||||
function updateStickyDamping() {
|
||||
const now = performance.now();
|
||||
const midY = window.innerHeight * 0.5;
|
||||
const zone = window.innerHeight * DAMP_ZONE_VH;
|
||||
let inZone = false, zoneScene = null;
|
||||
for (const id of STICKY_SCENES) {
|
||||
// Collect sticky targets. IDs of scenes + DOM refs for the three
|
||||
// individual treasure-map stops. Cached once — the list doesn't
|
||||
// change after init.
|
||||
function collectStickyTargets() {
|
||||
const targets = [];
|
||||
const sceneIds = ['hero', 'words-scene', 'bifrost', 'bifrost-join'];
|
||||
sceneIds.forEach(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();
|
||||
// Use the scene's vertical midpoint as the "attractor."
|
||||
const sceneMid = (r.top + r.bottom) * 0.5;
|
||||
if (Math.abs(sceneMid - midY) < zone) {
|
||||
const elMid = (r.top + r.bottom) * 0.5;
|
||||
if (Math.abs(elMid - midY) < zone) {
|
||||
inZone = true;
|
||||
zoneScene = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inZone) {
|
||||
// New scene entered the zone → arm a fresh damping window.
|
||||
// Reusing the same scene (still in zone, already damped) doesn't
|
||||
// extend — DAMP_MAX_MS is a hard cap per entry to avoid an
|
||||
// indefinite hold.
|
||||
if (zoneScene !== lastDampedScene) {
|
||||
lastDampedScene = zoneScene;
|
||||
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;
|
||||
}
|
||||
const nextMult = inZone ? STICKY_WHEEL_MULT : BASE_WHEEL_MULT;
|
||||
if (nextMult !== wheelMultState) {
|
||||
wheelMultState = nextMult;
|
||||
// Lenis reads options.wheelMultiplier on every wheel event —
|
||||
// re-assigning takes effect immediately.
|
||||
lenis.options.wheelMultiplier = nextMult;
|
||||
lenis.options.touchMultiplier = inZone ? STICKY_TOUCH_MULT : BASE_TOUCH_MULT;
|
||||
}
|
||||
}
|
||||
lenis.on('scroll', updateStickyDamping);
|
||||
// Initial paint (we likely start at hero which is itself a target).
|
||||
requestAnimationFrame(updateStickyDamping);
|
||||
|
||||
// ─── Site-2 scene animations ─────────────────────────────────
|
||||
// (transplanted verbatim; all ScrollTriggers below automatically
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue