Fix project stack: opaque recede, nav clearance and intro overlap
- The receding card must stay fully opaque; dimming it let the card behind ghost through the title and chips on photo-led cards. - Give the pinned stage top padding so the section label clears the fixed nav, and a gap so the covered card does not clip the intro. - Let the deck size to the space left in the stage, so it can never overflow the viewport.
This commit is contained in:
parent
d01c90265d
commit
21718e5e35
5 changed files with 31 additions and 20 deletions
|
|
@ -104,20 +104,25 @@ export function ProjectStack() {
|
|||
|
||||
return (
|
||||
<div ref={trackRef} className="relative h-[400vh]">
|
||||
<div className="sticky top-0 flex h-[100svh] flex-col items-center justify-center gap-6 overflow-hidden px-5 sm:gap-8 sm:px-8 lg:px-14">
|
||||
{/* The header sits clear of the fixed nav, and the deck takes the rest of
|
||||
the stage, so the card can never overflow the viewport. The gap keeps
|
||||
the covered card — which lifts as it recedes — off the intro copy. */}
|
||||
<div className="sticky top-0 flex h-[100svh] flex-col items-center gap-6 overflow-hidden px-5 pb-10 pt-24 sm:px-8 lg:px-14">
|
||||
<StackHeader />
|
||||
<div className="relative h-[min(26rem,46vh)] w-[min(90vw,24rem)] sm:h-[min(30rem,54vh)] sm:w-[min(92vw,44rem)]">
|
||||
{projects.map((p, i) => (
|
||||
<article
|
||||
key={p.ref}
|
||||
ref={(el) => {
|
||||
cardRefs.current[i] = el
|
||||
}}
|
||||
className="absolute inset-0 opacity-0 will-change-[transform,opacity]"
|
||||
>
|
||||
<CardFace p={p} media={media(p, i, active === i)} />
|
||||
</article>
|
||||
))}
|
||||
<div className="flex min-h-0 w-full flex-1 items-center justify-center">
|
||||
<div className="relative h-[min(26rem,100%)] w-[min(90vw,24rem)] sm:h-[min(30rem,100%)] sm:w-[min(92vw,44rem)]">
|
||||
{projects.map((p, i) => (
|
||||
<article
|
||||
key={p.ref}
|
||||
ref={(el) => {
|
||||
cardRefs.current[i] = el
|
||||
}}
|
||||
className="absolute inset-0 opacity-0 will-change-[transform,opacity]"
|
||||
>
|
||||
<CardFace p={p} media={media(p, i, active === i)} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -44,10 +44,10 @@ test('the first card has landed after one arrival unit', () => {
|
|||
assert.equal(s.rotate, 0)
|
||||
})
|
||||
|
||||
test('a covered card eases back', () => {
|
||||
test('a covered card eases back but stays opaque', () => {
|
||||
const s = cardState(0, ARRIVE + RECEDE)
|
||||
assert.equal(s.yPercent, -7)
|
||||
assert.equal(s.opacity, 0.75)
|
||||
assert.equal(s.opacity, 1)
|
||||
assert.equal(s.scale, 0.92)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ export function cardState(index: number, t: number): CardState {
|
|||
|
||||
return {
|
||||
yPercent: 118 * (1 - u) - 7 * v,
|
||||
opacity: u * (1 - 0.25 * v),
|
||||
// The recede only shifts and shrinks the card — it must stay fully opaque.
|
||||
// A translucent card would let the one behind it ghost through the copy.
|
||||
opacity: u,
|
||||
scale: 0.93 + 0.07 * u - 0.08 * v,
|
||||
rotate: noNegativeZero(tilt * (1 - u)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ test('the first card has landed after one arrival unit', () => {
|
|||
assert.equal(s.rotate, 0)
|
||||
})
|
||||
|
||||
test('a covered card eases back', () => {
|
||||
test('a covered card eases back but stays opaque', () => {
|
||||
const s = cardState(0, ARRIVE + RECEDE)
|
||||
assert.equal(s.yPercent, -7)
|
||||
assert.equal(s.opacity, 0.75)
|
||||
assert.equal(s.opacity, 1)
|
||||
assert.equal(s.scale, 0.92)
|
||||
})
|
||||
|
||||
|
|
@ -211,7 +211,9 @@ export function cardState(index: number, t: number): CardState {
|
|||
|
||||
return {
|
||||
yPercent: 118 * (1 - u) - 7 * v,
|
||||
opacity: u * (1 - 0.25 * v),
|
||||
// The recede must not change opacity: a translucent card lets the card
|
||||
// behind it ghost through the title and chips.
|
||||
opacity: u,
|
||||
scale: 0.93 + 0.07 * u - 0.08 * v,
|
||||
rotate: noNegativeZero(tilt * (1 - u)),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,13 +128,15 @@ u = power2Out(u) // 1 - (1 - x)²
|
|||
v = power1InOut(v) // 2x² below 0.5, else 1 - 2(1-x)²/2
|
||||
|
||||
yPercent = 118 × (1 - u) - 7 × v
|
||||
opacity = u × (1 - 0.25 × v)
|
||||
opacity = u
|
||||
scale = 0.93 + 0.07 × u - 0.08 × v
|
||||
rotate = (i even ? -4° : 4°) × (1 - u)
|
||||
```
|
||||
|
||||
`transform` is composed as `translate3d(0, yPercent%, 0) rotate(rotate) scale(scale)` and written to `element.style.transform`. `opacity` is written to `element.style.opacity`.
|
||||
|
||||
**The recede must not change opacity.** A covered card stays fully opaque — only its scale and position change. A translucent card would let the card behind it ghost through the title and chips, which is very visible on photo-led cards. The reference gets away with it because its cards sit on an opaque panel.
|
||||
|
||||
Cards later in the DOM paint above earlier ones, so the last card dealt ends up on top — the same stacking order as the reference.
|
||||
|
||||
### The active card
|
||||
|
|
|
|||
Loading…
Reference in a new issue