parent
2f4521dd74
commit
16d4578aa1
@ -0,0 +1,302 @@
|
||||
<script lang="ts">
|
||||
const images = [
|
||||
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200",
|
||||
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200",
|
||||
"https://images.unsplash.com/photo-1493246507139-91e8fad9978e?w=1200",
|
||||
"https://images.unsplash.com/photo-1518837695005-2083093ee35b?w=1200",
|
||||
"https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1200",
|
||||
"https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1200"
|
||||
];
|
||||
|
||||
let section: HTMLElement | undefined;
|
||||
let progress = $state(0);
|
||||
|
||||
const clamp = (value: number, min = 0, max = 1) => {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
};
|
||||
|
||||
const eased = $derived(0.5 - Math.cos(progress * Math.PI) / 2);
|
||||
|
||||
const angle = $derived(72 - eased * 72);
|
||||
const bladeWidth = $derived(70 + eased * 170);
|
||||
const cosA = $derived(Math.cos((angle * Math.PI) / 180));
|
||||
const overlap = $derived(2 * bladeWidth * (1 - cosA));
|
||||
const scale = $derived(0.92 + eased * 0.08);
|
||||
|
||||
$effect(() => {
|
||||
if (!section) return;
|
||||
const scrollSection = section;
|
||||
|
||||
let frame: number | null = null;
|
||||
|
||||
const update = () => {
|
||||
const rect = scrollSection.getBoundingClientRect();
|
||||
const scrollable = scrollSection.offsetHeight - window.innerHeight;
|
||||
|
||||
if (scrollable <= 0) {
|
||||
progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
progress = clamp(-rect.top / scrollable);
|
||||
frame = null;
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
if (frame === null) {
|
||||
frame = requestAnimationFrame(update);
|
||||
}
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
window.addEventListener("resize", update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
window.removeEventListener("resize", update);
|
||||
|
||||
if (frame !== null) {
|
||||
cancelAnimationFrame(frame);
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="scroll-section" bind:this={section}>
|
||||
<div class="sticky-stage">
|
||||
<div class="intro">
|
||||
<span>Cortina de fuelle</span>
|
||||
<h1>Scroll para abrir</h1>
|
||||
<p>
|
||||
Cada imagen ocupa una lama vertical. El scroll controla la apertura,
|
||||
la separación y el ángulo de giro.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="accordion"
|
||||
style="
|
||||
--angle: {angle}deg;
|
||||
--blade-width: {bladeWidth}px;
|
||||
--overlap: {overlap}px;
|
||||
--scale: {scale};
|
||||
"
|
||||
>
|
||||
{#each images as image, index}
|
||||
<article
|
||||
class:left={index % 2 === 0}
|
||||
class:right={index % 2 !== 0}
|
||||
class="blade"
|
||||
style="
|
||||
--image: url('{image}');
|
||||
--z: {images.length - index};
|
||||
"
|
||||
>
|
||||
<div class="blade-face">
|
||||
<div class="shine"></div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="progress-bar">
|
||||
<div style="width: {progress * 100}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
background: #070707;
|
||||
color: white;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.scroll-section {
|
||||
height: 320vh;
|
||||
position: relative;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 255, 255, 0.14), transparent 36rem),
|
||||
linear-gradient(180deg, #111, #050505);
|
||||
}
|
||||
|
||||
.sticky-stage {
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
perspective: 1400px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
position: absolute;
|
||||
top: 7vh;
|
||||
left: clamp(1.5rem, 5vw, 5rem);
|
||||
z-index: 10;
|
||||
max-width: 520px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.intro span {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.75rem;
|
||||
opacity: 0.65;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.intro h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.5rem, 6vw, 6rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.intro p {
|
||||
max-width: 34rem;
|
||||
margin-top: 1.2rem;
|
||||
color: rgba(255, 255, 255, 0.68);
|
||||
font-size: clamp(1rem, 1.5vw, 1.2rem);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: min(94vw, 1320px);
|
||||
min-height: 540px;
|
||||
transform-style: preserve-3d;
|
||||
transform: scale(var(--scale)) rotateX(2deg);
|
||||
}
|
||||
|
||||
.blade {
|
||||
width: var(--blade-width);
|
||||
height: min(64vh, 560px);
|
||||
flex: 0 0 var(--blade-width);
|
||||
position: relative;
|
||||
z-index: var(--z);
|
||||
transform-style: preserve-3d;
|
||||
transition:
|
||||
width 80ms linear,
|
||||
flex-basis 80ms linear,
|
||||
margin-left 80ms linear,
|
||||
transform 80ms linear;
|
||||
}
|
||||
|
||||
.blade.left {
|
||||
transform-origin: left center;
|
||||
transform: rotateY(var(--angle));
|
||||
}
|
||||
|
||||
.blade.right {
|
||||
margin-left: calc(var(--overlap) * -1);
|
||||
transform-origin: right center;
|
||||
transform: rotateY(calc(var(--angle) * -1));
|
||||
}
|
||||
|
||||
.blade-face {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-image: var(--image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
backface-visibility: hidden;
|
||||
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.blade:first-child .blade-face {
|
||||
border-top-left-radius: 24px;
|
||||
border-bottom-left-radius: 24px;
|
||||
box-shadow:
|
||||
0 32px 80px rgba(0, 0, 0, 0.55),
|
||||
inset 1px 0 0 rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.blade:last-child .blade-face {
|
||||
border-top-right-radius: 24px;
|
||||
border-bottom-right-radius: 24px;
|
||||
box-shadow:
|
||||
0 32px 80px rgba(0, 0, 0, 0.55),
|
||||
inset -1px 0 0 rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.blade-face::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, 0.46),
|
||||
transparent 35%,
|
||||
rgba(255, 255, 255, 0.18) 55%,
|
||||
rgba(0, 0, 0, 0.36)
|
||||
);
|
||||
mix-blend-mode: soft-light;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.shine {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
115deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.22) 45%,
|
||||
transparent 62%
|
||||
);
|
||||
opacity: 0.35;
|
||||
transform: translateX(-35%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 6vh;
|
||||
left: 50%;
|
||||
width: min(420px, 70vw);
|
||||
height: 3px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
.progress-bar div {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: white;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.accordion {
|
||||
width: 125vw;
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.blade {
|
||||
height: 52vh;
|
||||
}
|
||||
|
||||
.intro {
|
||||
top: 5vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import CurtainScroll from "./Curtain.svelte";
|
||||
</script>
|
||||
|
||||
<CurtainScroll
|
||||
image="https://images.unsplash.com/photo-1483985988355-763728e1935b?w=2400&q=80"
|
||||
bladeCount={8}
|
||||
maxAngle={82}
|
||||
smoothing={0.12}
|
||||
anchor="right"
|
||||
scrollLength="240vh"
|
||||
>
|
||||
<div class="cta">
|
||||
<div class="cta-content">
|
||||
<span class="cta-eyebrow">Solo este fin de semana</span>
|
||||
<h2>Hasta -40% en toda la colección</h2>
|
||||
<p>
|
||||
Las piezas más buscadas vuelven con descuento. Cuando la cortina cae, no
|
||||
vuelve a abrirse.
|
||||
</p>
|
||||
<a href="/" class="cta-button">Comprar ahora</a>
|
||||
</div>
|
||||
</div>
|
||||
</CurtainScroll>
|
||||
|
||||
<style>
|
||||
.cta {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
background:
|
||||
radial-gradient(circle at 25% 30%, rgba(255, 90, 40, 0.28), transparent 55%),
|
||||
radial-gradient(circle at 75% 70%, rgba(120, 70, 230, 0.22), transparent 55%),
|
||||
#0a0a0c;
|
||||
color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cta-content {
|
||||
max-width: 760px;
|
||||
padding: 0 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cta-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0.42rem 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.cta-content h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(2.5rem, 7vw, 5.5rem);
|
||||
line-height: 1;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.cta-content p {
|
||||
max-width: 38rem;
|
||||
margin: 1.4rem auto 2.4rem;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: clamp(1rem, 1.4vw, 1.18rem);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 1rem 2rem;
|
||||
background: white;
|
||||
color: #0a0a0c;
|
||||
border-radius: 999px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,294 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type CurtainAnchor = "left" | "right";
|
||||
|
||||
interface CurtainProps {
|
||||
image: string;
|
||||
bladeCount?: number;
|
||||
maxAngle?: number;
|
||||
scrollLength?: string;
|
||||
smoothing?: number;
|
||||
anchor?: CurtainAnchor;
|
||||
radius?: string;
|
||||
height?: string;
|
||||
width?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
image,
|
||||
bladeCount = 8,
|
||||
maxAngle = 82,
|
||||
scrollLength = "240vh",
|
||||
smoothing = 0.12,
|
||||
anchor = "right",
|
||||
radius = "24px",
|
||||
height = "min(80vh, 720px)",
|
||||
width = "min(96vw, 1280px)",
|
||||
children
|
||||
}: CurtainProps = $props();
|
||||
|
||||
const blades = $derived(
|
||||
Array.from({ length: bladeCount }, (_, i) => i)
|
||||
);
|
||||
|
||||
let section: HTMLElement | undefined;
|
||||
let progress = $state(0);
|
||||
|
||||
const clamp = (value: number, min = 0, max = 1) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const eased = $derived(0.5 - Math.cos(progress * Math.PI) / 2);
|
||||
const angle = $derived(eased * maxAngle);
|
||||
const cosA = $derived(Math.cos((angle * Math.PI) / 180));
|
||||
const overlapFactor = $derived(2 * (1 - cosA));
|
||||
const fold = $derived(eased);
|
||||
const spread = $derived(1 - eased * eased * 0.78);
|
||||
|
||||
$effect(() => {
|
||||
if (!section) return;
|
||||
const scrollSection = section;
|
||||
|
||||
let target = 0;
|
||||
let rafId: number | null = null;
|
||||
|
||||
const reduced =
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
const lerpFactor = reduced ? 1 : smoothing;
|
||||
|
||||
const tick = () => {
|
||||
const diff = target - progress;
|
||||
|
||||
if (Math.abs(diff) < 0.0005) {
|
||||
progress = target;
|
||||
rafId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
progress += diff * lerpFactor;
|
||||
rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const updateTarget = () => {
|
||||
const rect = scrollSection.getBoundingClientRect();
|
||||
const scrollable = scrollSection.offsetHeight - window.innerHeight;
|
||||
|
||||
target = scrollable <= 0 ? 0 : clamp(-rect.top / scrollable);
|
||||
|
||||
if (rafId === null) {
|
||||
rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
};
|
||||
|
||||
updateTarget();
|
||||
|
||||
window.addEventListener("scroll", updateTarget, { passive: true });
|
||||
window.addEventListener("resize", updateTarget);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", updateTarget);
|
||||
window.removeEventListener("resize", updateTarget);
|
||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="curtain-section"
|
||||
bind:this={section}
|
||||
style="--scroll-length: {scrollLength};"
|
||||
>
|
||||
<div class="sticky-stage">
|
||||
<div class="background-layer">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ground-shadow"
|
||||
class:anchor-right={anchor === "right"}
|
||||
class:anchor-left={anchor === "left"}
|
||||
style="
|
||||
--fold: {fold};
|
||||
--spread: {spread};
|
||||
--curtain-w: {width};
|
||||
--curtain-h: {height};
|
||||
"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="curtain"
|
||||
class:anchor-right={anchor === "right"}
|
||||
class:anchor-left={anchor === "left"}
|
||||
style="
|
||||
--angle: {angle}deg;
|
||||
--overlap-factor: {overlapFactor};
|
||||
--fold: {fold};
|
||||
--n: {bladeCount};
|
||||
--radius: {radius};
|
||||
--curtain-w: {width};
|
||||
--curtain-h: {height};
|
||||
"
|
||||
>
|
||||
{#each blades as i (i)}
|
||||
<article
|
||||
class:left={i % 2 === 0}
|
||||
class:right={i % 2 !== 0}
|
||||
class="blade"
|
||||
style="
|
||||
--image: url('{image}');
|
||||
--bg-pos: {(i / Math.max(1, bladeCount - 1)) * 100}%;
|
||||
--z: {bladeCount - i};
|
||||
"
|
||||
>
|
||||
<div class="blade-face"></div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.curtain-section {
|
||||
height: var(--scroll-length);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-stage {
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
perspective: 1100px;
|
||||
perspective-origin: 50% 35%;
|
||||
}
|
||||
|
||||
.background-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ground-shadow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(var(--curtain-w) * var(--spread));
|
||||
height: calc(var(--curtain-h) * 0.18);
|
||||
transform: translate(-50%, calc(var(--curtain-h) * 0.5 - 10%));
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(0, 0, 0, calc(0.7 + 0.2 * var(--fold))) 0%,
|
||||
rgba(0, 0, 0, 0.4) 35%,
|
||||
rgba(0, 0, 0, 0.15) 60%,
|
||||
transparent 80%
|
||||
);
|
||||
filter: blur(24px);
|
||||
}
|
||||
|
||||
.ground-shadow.anchor-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translate(0, calc(var(--curtain-h) * 0.5 - 10%));
|
||||
}
|
||||
|
||||
.ground-shadow.anchor-left {
|
||||
left: 0;
|
||||
transform: translate(0, calc(var(--curtain-h) * 0.5 - 10%));
|
||||
}
|
||||
|
||||
.curtain {
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
width: var(--curtain-w);
|
||||
height: var(--curtain-h);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
transform-style: preserve-3d;
|
||||
transform: rotateX(2deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.curtain.anchor-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.curtain.anchor-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.blade {
|
||||
flex: 0 0 calc(100% / var(--n));
|
||||
width: calc(100% / var(--n));
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: var(--z);
|
||||
transform-style: preserve-3d;
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.blade.left {
|
||||
transform-origin: left center;
|
||||
transform: rotateY(var(--angle));
|
||||
}
|
||||
|
||||
.blade.right {
|
||||
margin-left: calc(100% / var(--n) * var(--overlap-factor) * -1);
|
||||
transform-origin: right center;
|
||||
transform: rotateY(calc(var(--angle) * -1));
|
||||
}
|
||||
|
||||
.blade-face {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: var(--image);
|
||||
background-size: calc(var(--n) * 100%) 100%;
|
||||
background-position: var(--bg-pos) 0;
|
||||
background-repeat: no-repeat;
|
||||
backface-visibility: hidden;
|
||||
transform: scaleX(1.006);
|
||||
}
|
||||
|
||||
.blade-face::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blade.left .blade-face::after {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, calc(0.08 * var(--fold))) 0%,
|
||||
transparent 35%,
|
||||
transparent 60%,
|
||||
rgba(0, 0, 0, calc(0.65 * var(--fold))) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.blade.right .blade-face::after {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(0, 0, 0, calc(0.65 * var(--fold))) 0%,
|
||||
transparent 40%,
|
||||
transparent 65%,
|
||||
rgba(255, 255, 255, calc(0.08 * var(--fold))) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.blade:first-child .blade-face {
|
||||
border-top-left-radius: var(--radius);
|
||||
border-bottom-left-radius: var(--radius);
|
||||
}
|
||||
|
||||
.blade:last-child .blade-face {
|
||||
border-top-right-radius: var(--radius);
|
||||
border-bottom-right-radius: var(--radius);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue