@manufosela/hero-scroll-animation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/package.json +47 -0
- package/src/hero-scroll-animation.js +670 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 manufosela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @manufosela/hero-scroll-animation
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@manufosela/hero-scroll-animation)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Hero section with scroll-driven parallax animation built with [Lit 3](https://lit.dev/).
|
|
7
|
+
|
|
8
|
+
As the user scrolls, the hero content fades out and slides up while a center image rises and side images slide in from the edges. On mobile, the animation is triggered by an intersection observer instead of continuous scroll tracking.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @manufosela/hero-scroll-animation
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import '@manufosela/hero-scroll-animation';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<hero-scroll-animation>
|
|
24
|
+
<img slot="background" src="bg.jpg" />
|
|
25
|
+
<img slot="center" src="center.png" />
|
|
26
|
+
<img slot="left" src="left.png" />
|
|
27
|
+
<img slot="right" src="right.png" />
|
|
28
|
+
<div slot="content">
|
|
29
|
+
<h1>Hero Title</h1>
|
|
30
|
+
<p>Subtitle text</p>
|
|
31
|
+
</div>
|
|
32
|
+
</hero-scroll-animation>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Attributes
|
|
36
|
+
|
|
37
|
+
| Attribute | Type | Default | Description |
|
|
38
|
+
|-----------|------|---------|-------------|
|
|
39
|
+
| `background-text` | String | `''` | Large decorative text behind the images |
|
|
40
|
+
| `scroll-height` | Number | `450` | Scroll distance in vh for the parallax effect |
|
|
41
|
+
| `overlay-opacity` | Number | `0.5` | Opacity of the dark overlay (0-1) |
|
|
42
|
+
| `scrub` | Number | `1` | Smoothing factor for scroll interpolation |
|
|
43
|
+
| `mobile-breakpoint` | Number | `768` | Viewport width (px) below which mobile mode activates |
|
|
44
|
+
| `mobile-scroll-height` | Number | `220` | Scroll distance in vh for mobile mode |
|
|
45
|
+
|
|
46
|
+
## Slots
|
|
47
|
+
|
|
48
|
+
| Slot | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `content` | Main content (headings, text, CTAs) displayed over the hero |
|
|
51
|
+
| `background` | Hidden `<img>` whose `src` is used as the background image |
|
|
52
|
+
| `center` | Hidden `<img>` whose `src` is used for the center parallax image |
|
|
53
|
+
| `left` | Hidden `<img>` whose `src` is used for the left side image |
|
|
54
|
+
| `right` | Hidden `<img>` whose `src` is used for the right side image |
|
|
55
|
+
|
|
56
|
+
## CSS Custom Properties
|
|
57
|
+
|
|
58
|
+
| Property | Default | Description |
|
|
59
|
+
|----------|---------|-------------|
|
|
60
|
+
| `--hero-accent-color` | `#bfa15f` | Accent color for decorative elements |
|
|
61
|
+
| `--hero-text-color` | `#f0f0f0` | Default text color inside the hero |
|
|
62
|
+
| `--hero-bg-gradient-start` | `#d4af37` | Start color for the background text gradient |
|
|
63
|
+
| `--hero-bg-gradient-end` | `#f4e4b0` | End color for the background text gradient |
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
### With background text
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<hero-scroll-animation background-text="PREMIUM">
|
|
71
|
+
<img slot="background" src="bg.jpg" />
|
|
72
|
+
<img slot="center" src="center.png" />
|
|
73
|
+
<div slot="content">
|
|
74
|
+
<h1>Premium Collection</h1>
|
|
75
|
+
</div>
|
|
76
|
+
</hero-scroll-animation>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Custom overlay and scroll distance
|
|
80
|
+
|
|
81
|
+
```html
|
|
82
|
+
<hero-scroll-animation overlay-opacity="0.7" scroll-height="350">
|
|
83
|
+
<img slot="background" src="bg.jpg" />
|
|
84
|
+
<img slot="center" src="center.png" />
|
|
85
|
+
<img slot="left" src="left.png" />
|
|
86
|
+
<img slot="right" src="right.png" />
|
|
87
|
+
<div slot="content">
|
|
88
|
+
<h1>Dark Hero</h1>
|
|
89
|
+
</div>
|
|
90
|
+
</hero-scroll-animation>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Custom gradient colors
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<hero-scroll-animation
|
|
97
|
+
background-text="EXPLORE"
|
|
98
|
+
style="
|
|
99
|
+
--hero-bg-gradient-start: #e63946;
|
|
100
|
+
--hero-bg-gradient-end: #f4a261;
|
|
101
|
+
"
|
|
102
|
+
>
|
|
103
|
+
<img slot="background" src="bg.jpg" />
|
|
104
|
+
<img slot="center" src="center.png" />
|
|
105
|
+
<div slot="content">
|
|
106
|
+
<h1>Explore</h1>
|
|
107
|
+
</div>
|
|
108
|
+
</hero-scroll-animation>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@manufosela/hero-scroll-animation",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Hero section with scroll-driven parallax animation and image reveal",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "manufosela",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/hero-scroll-animation.js",
|
|
9
|
+
"module": "./src/hero-scroll-animation.js",
|
|
10
|
+
"types": "./src/hero-scroll-animation.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/hero-scroll-animation.d.ts",
|
|
14
|
+
"import": "./src/hero-scroll-animation.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/manufosela/ui-components",
|
|
23
|
+
"directory": "packages/hero-scroll-animation"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"web-components",
|
|
27
|
+
"lit",
|
|
28
|
+
"hero",
|
|
29
|
+
"scroll-animation",
|
|
30
|
+
"parallax"
|
|
31
|
+
],
|
|
32
|
+
"homepage": "https://github.com/manufosela/ui-components/tree/main/packages/hero-scroll-animation#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/manufosela/ui-components/issues"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"lit": "^3.2.1"
|
|
38
|
+
},
|
|
39
|
+
"customElements": "custom-elements.json",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"start": "web-dev-server --node-resolve --open demo/ --watch",
|
|
42
|
+
"test": "web-test-runner",
|
|
43
|
+
"test:watch": "web-test-runner --watch",
|
|
44
|
+
"test:coverage": "web-test-runner --coverage",
|
|
45
|
+
"build:types": "tsc --declaration --declarationMap --emitDeclarationOnly --allowJs --checkJs --outDir ./src ./src/hero-scroll-animation.js"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hero section with scroll-driven parallax animation.
|
|
5
|
+
*
|
|
6
|
+
* As the user scrolls, the hero content fades out and slides up, while
|
|
7
|
+
* a center image rises and side images slide in from the edges. Supports
|
|
8
|
+
* both desktop (scroll-linked) and mobile (intersection-triggered) modes.
|
|
9
|
+
*
|
|
10
|
+
* Images are provided via named slots (`background`, `center`, `left`, `right`)
|
|
11
|
+
* and their `src` attributes are extracted to render internal `<img>` elements.
|
|
12
|
+
* The `content` slot holds arbitrary markup displayed over the hero background.
|
|
13
|
+
*
|
|
14
|
+
* @element hero-scroll-animation
|
|
15
|
+
*
|
|
16
|
+
* @attr {String} background-text - Large decorative text rendered behind the images
|
|
17
|
+
* @attr {Number} scroll-height - Scroll distance in vh units for the parallax effect (default: 450)
|
|
18
|
+
* @attr {Number} overlay-opacity - Opacity of the dark overlay on the background image (default: 0.5)
|
|
19
|
+
* @attr {Number} scrub - Smoothing factor for the scroll interpolation (default: 1)
|
|
20
|
+
* @attr {Number} mobile-breakpoint - Viewport width in px below which mobile mode is used (default: 768)
|
|
21
|
+
* @attr {Number} mobile-scroll-height - Scroll distance in vh units for mobile mode (default: 220)
|
|
22
|
+
*
|
|
23
|
+
* @cssprop [--hero-accent-color=#bfa15f] - Accent color used for decorative elements
|
|
24
|
+
* @cssprop [--hero-text-color=#f0f0f0] - Default text color inside the hero
|
|
25
|
+
* @cssprop [--hero-bg-gradient-start=#d4af37] - Start color for the background text gradient
|
|
26
|
+
* @cssprop [--hero-bg-gradient-end=#f4e4b0] - End color for the background text gradient
|
|
27
|
+
*
|
|
28
|
+
* @slot content - Main content displayed over the hero (headings, text, CTAs)
|
|
29
|
+
* @slot background - Hidden `<img>` whose `src` is used as the hero background image
|
|
30
|
+
* @slot center - Hidden `<img>` whose `src` is used for the center parallax image
|
|
31
|
+
* @slot left - Hidden `<img>` whose `src` is used for the left side parallax image
|
|
32
|
+
* @slot right - Hidden `<img>` whose `src` is used for the right side parallax image
|
|
33
|
+
*/
|
|
34
|
+
export class HeroScrollAnimation extends LitElement {
|
|
35
|
+
static properties = {
|
|
36
|
+
backgroundText: { type: String, attribute: 'background-text' },
|
|
37
|
+
scrollHeight: { type: Number, attribute: 'scroll-height' },
|
|
38
|
+
overlayOpacity: { type: Number, attribute: 'overlay-opacity' },
|
|
39
|
+
scrub: { type: Number },
|
|
40
|
+
mobileBreakpoint: { type: Number, attribute: 'mobile-breakpoint' },
|
|
41
|
+
mobileScrollHeight: { type: Number, attribute: 'mobile-scroll-height' },
|
|
42
|
+
_progress: { type: Number, state: true },
|
|
43
|
+
_isMobile: { type: Boolean, state: true },
|
|
44
|
+
_reducedMotion: { type: Boolean, state: true },
|
|
45
|
+
_mobileTriggered: { type: Boolean, state: true },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
static styles = css`
|
|
49
|
+
:host {
|
|
50
|
+
display: block;
|
|
51
|
+
position: relative;
|
|
52
|
+
--_accent: var(--hero-accent-color, #bfa15f);
|
|
53
|
+
--_text: var(--hero-text-color, #f0f0f0);
|
|
54
|
+
--_grad-start: var(--hero-bg-gradient-start, #d4af37);
|
|
55
|
+
--_grad-end: var(--hero-bg-gradient-end, #f4e4b0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scroller {
|
|
59
|
+
position: relative;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.canvas {
|
|
63
|
+
position: sticky;
|
|
64
|
+
top: 0;
|
|
65
|
+
height: 100vh;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
background-color: #1a1618;
|
|
71
|
+
background-size: cover;
|
|
72
|
+
background-position: center;
|
|
73
|
+
background-repeat: no-repeat;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.overlay {
|
|
77
|
+
position: absolute;
|
|
78
|
+
inset: 0;
|
|
79
|
+
background: rgba(0, 0, 0, var(--_overlay-opacity, 0.5));
|
|
80
|
+
z-index: 1;
|
|
81
|
+
will-change: opacity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.content-wrapper {
|
|
85
|
+
position: relative;
|
|
86
|
+
z-index: 2;
|
|
87
|
+
width: 100%;
|
|
88
|
+
height: 100%;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
text-align: center;
|
|
94
|
+
will-change: transform, opacity;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
::slotted([slot='content']) {
|
|
98
|
+
position: relative;
|
|
99
|
+
z-index: 2;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.bg-text {
|
|
103
|
+
position: absolute;
|
|
104
|
+
bottom: -20%;
|
|
105
|
+
left: 50%;
|
|
106
|
+
transform: translate(-50%, 0);
|
|
107
|
+
font-size: clamp(4rem, 15vw, 12rem);
|
|
108
|
+
font-weight: 700;
|
|
109
|
+
background: linear-gradient(
|
|
110
|
+
135deg,
|
|
111
|
+
var(--_grad-start) 0%,
|
|
112
|
+
var(--_grad-end) 50%,
|
|
113
|
+
var(--_grad-start) 100%
|
|
114
|
+
);
|
|
115
|
+
-webkit-background-clip: text;
|
|
116
|
+
-webkit-text-fill-color: transparent;
|
|
117
|
+
background-clip: text;
|
|
118
|
+
opacity: 0.4;
|
|
119
|
+
z-index: 0;
|
|
120
|
+
white-space: nowrap;
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
will-change: transform, opacity;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.center-img {
|
|
126
|
+
position: absolute;
|
|
127
|
+
bottom: -35%;
|
|
128
|
+
left: 50%;
|
|
129
|
+
transform: translateX(-50%) translateZ(0);
|
|
130
|
+
width: min(60vw, 900px);
|
|
131
|
+
max-width: 90vw;
|
|
132
|
+
z-index: 3;
|
|
133
|
+
will-change: transform;
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.center-img img {
|
|
138
|
+
width: 100%;
|
|
139
|
+
height: auto;
|
|
140
|
+
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.3));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.side-img {
|
|
144
|
+
position: absolute;
|
|
145
|
+
top: 50%;
|
|
146
|
+
z-index: 2;
|
|
147
|
+
width: min(40vw, 700px);
|
|
148
|
+
max-width: 45vw;
|
|
149
|
+
will-change: transform, opacity;
|
|
150
|
+
pointer-events: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.side-img img {
|
|
154
|
+
width: 100%;
|
|
155
|
+
height: auto;
|
|
156
|
+
filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.4));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.side-img--left {
|
|
160
|
+
left: 5%;
|
|
161
|
+
transform: translateY(-50%) translateZ(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.side-img--right {
|
|
165
|
+
right: 5%;
|
|
166
|
+
transform: translateY(-50%) translateZ(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Hide slotted images used as sources */
|
|
170
|
+
::slotted([slot='background']),
|
|
171
|
+
::slotted([slot='center']),
|
|
172
|
+
::slotted([slot='left']),
|
|
173
|
+
::slotted([slot='right']) {
|
|
174
|
+
display: none !important;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Mobile keyframe animations */
|
|
178
|
+
@keyframes fadeOutContent {
|
|
179
|
+
from {
|
|
180
|
+
opacity: 1;
|
|
181
|
+
transform: translateY(0);
|
|
182
|
+
}
|
|
183
|
+
to {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
transform: translateY(-60px);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@keyframes slideUpCenter {
|
|
190
|
+
from {
|
|
191
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
192
|
+
}
|
|
193
|
+
to {
|
|
194
|
+
transform: translateX(-50%) translateY(-55vh) scale(0.75);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes slideInLeft {
|
|
199
|
+
from {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
transform: translateY(-50%) translateX(-200px) scale(0.7);
|
|
202
|
+
}
|
|
203
|
+
to {
|
|
204
|
+
opacity: 1;
|
|
205
|
+
transform: translateY(-50%) translateX(0) scale(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@keyframes slideInRight {
|
|
210
|
+
from {
|
|
211
|
+
opacity: 0;
|
|
212
|
+
transform: translateY(-50%) translateX(200px) scale(0.7);
|
|
213
|
+
}
|
|
214
|
+
to {
|
|
215
|
+
opacity: 1;
|
|
216
|
+
transform: translateY(-50%) translateX(0) scale(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@keyframes fadeBgText {
|
|
221
|
+
from {
|
|
222
|
+
opacity: 0;
|
|
223
|
+
transform: translate(-50%, 0) translateY(0) scale(1.2);
|
|
224
|
+
}
|
|
225
|
+
to {
|
|
226
|
+
opacity: 0.4;
|
|
227
|
+
transform: translate(-50%, 0) translateY(-55vh) scale(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
:host([mobile-triggered]) .content-wrapper {
|
|
232
|
+
animation: fadeOutContent 0.6s ease-out forwards;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
:host([mobile-triggered]) .overlay {
|
|
236
|
+
animation: fadeOutContent 0.6s ease-out forwards;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
:host([mobile-triggered]) .center-img {
|
|
240
|
+
animation: slideUpCenter 0.8s cubic-bezier(0.33, 1, 0.68, 1) 0.3s forwards;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
:host([mobile-triggered]) .side-img--left {
|
|
244
|
+
animation: slideInLeft 0.8s cubic-bezier(0.33, 1, 0.68, 1) 0.35s forwards;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
:host([mobile-triggered]) .side-img--right {
|
|
248
|
+
animation: slideInRight 0.8s cubic-bezier(0.33, 1, 0.68, 1) 0.35s forwards;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
:host([mobile-triggered]) .bg-text {
|
|
252
|
+
animation: fadeBgText 0.8s ease-out 0.35s forwards;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Initial hidden state for mobile side images */
|
|
256
|
+
:host([data-mobile]) .side-img {
|
|
257
|
+
opacity: 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Reduced motion */
|
|
261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
262
|
+
:host {
|
|
263
|
+
/* Show static layout */
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.scroller {
|
|
267
|
+
height: 100vh !important;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.canvas {
|
|
271
|
+
position: relative;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.content-wrapper {
|
|
275
|
+
opacity: 1 !important;
|
|
276
|
+
transform: none !important;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.overlay {
|
|
280
|
+
opacity: 1 !important;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.center-img,
|
|
284
|
+
.side-img,
|
|
285
|
+
.bg-text {
|
|
286
|
+
display: none;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
:host([mobile-triggered]) .content-wrapper,
|
|
290
|
+
:host([mobile-triggered]) .overlay,
|
|
291
|
+
:host([mobile-triggered]) .center-img,
|
|
292
|
+
:host([mobile-triggered]) .side-img--left,
|
|
293
|
+
:host([mobile-triggered]) .side-img--right,
|
|
294
|
+
:host([mobile-triggered]) .bg-text {
|
|
295
|
+
animation: none !important;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Mobile responsive */
|
|
300
|
+
@media (max-width: 768px) {
|
|
301
|
+
.center-img {
|
|
302
|
+
width: 55vw;
|
|
303
|
+
max-width: 85vw;
|
|
304
|
+
bottom: -25%;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.side-img {
|
|
308
|
+
width: 28vw;
|
|
309
|
+
max-width: 35vw;
|
|
310
|
+
top: 60%;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.side-img--left {
|
|
314
|
+
left: 2%;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.side-img--right {
|
|
318
|
+
right: 2%;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.bg-text {
|
|
322
|
+
font-size: min(10vw, 5rem);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
constructor() {
|
|
328
|
+
super();
|
|
329
|
+
this.backgroundText = '';
|
|
330
|
+
this.scrollHeight = 450;
|
|
331
|
+
this.overlayOpacity = 0.5;
|
|
332
|
+
this.scrub = 1;
|
|
333
|
+
this.mobileBreakpoint = 768;
|
|
334
|
+
this.mobileScrollHeight = 220;
|
|
335
|
+
this._progress = 0;
|
|
336
|
+
this._isMobile = false;
|
|
337
|
+
this._reducedMotion = false;
|
|
338
|
+
this._mobileTriggered = false;
|
|
339
|
+
|
|
340
|
+
this._currentProgress = 0;
|
|
341
|
+
this._targetProgress = 0;
|
|
342
|
+
this._rafId = null;
|
|
343
|
+
this._scrollHandler = null;
|
|
344
|
+
this._resizeHandler = null;
|
|
345
|
+
this._observer = null;
|
|
346
|
+
|
|
347
|
+
this._bgSrc = '';
|
|
348
|
+
this._centerSrc = '';
|
|
349
|
+
this._leftSrc = '';
|
|
350
|
+
this._rightSrc = '';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
connectedCallback() {
|
|
354
|
+
super.connectedCallback();
|
|
355
|
+
this._reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
356
|
+
this._checkMobile();
|
|
357
|
+
|
|
358
|
+
this._resizeHandler = this._onResize.bind(this);
|
|
359
|
+
window.addEventListener('resize', this._resizeHandler, { passive: true });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
disconnectedCallback() {
|
|
363
|
+
super.disconnectedCallback();
|
|
364
|
+
this._cleanup();
|
|
365
|
+
if (this._resizeHandler) {
|
|
366
|
+
window.removeEventListener('resize', this._resizeHandler);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
firstUpdated() {
|
|
371
|
+
this._extractSlotSources();
|
|
372
|
+
this._setupAnimation();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_getImgSrc(slotEl) {
|
|
376
|
+
if (!slotEl) return '';
|
|
377
|
+
if (slotEl.tagName === 'PICTURE') {
|
|
378
|
+
const webpSource = slotEl.querySelector('source[type="image/webp"]');
|
|
379
|
+
if (webpSource) return webpSource.getAttribute('srcset') || '';
|
|
380
|
+
const img = slotEl.querySelector('img');
|
|
381
|
+
return img ? img.getAttribute('src') || '' : '';
|
|
382
|
+
}
|
|
383
|
+
return slotEl.getAttribute('src') || '';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_extractSlotSources() {
|
|
387
|
+
const bgSlot = this.querySelector('[slot="background"]');
|
|
388
|
+
const centerSlot = this.querySelector('[slot="center"]');
|
|
389
|
+
const leftSlot = this.querySelector('[slot="left"]');
|
|
390
|
+
const rightSlot = this.querySelector('[slot="right"]');
|
|
391
|
+
|
|
392
|
+
this._bgSrc = this._getImgSrc(bgSlot);
|
|
393
|
+
this._centerSrc = this._getImgSrc(centerSlot);
|
|
394
|
+
this._leftSrc = this._getImgSrc(leftSlot);
|
|
395
|
+
this._rightSrc = this._getImgSrc(rightSlot);
|
|
396
|
+
|
|
397
|
+
this.requestUpdate();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_checkMobile() {
|
|
401
|
+
const wasMobile = this._isMobile;
|
|
402
|
+
this._isMobile = window.innerWidth <= this.mobileBreakpoint;
|
|
403
|
+
|
|
404
|
+
if (this._isMobile) {
|
|
405
|
+
this.setAttribute('data-mobile', '');
|
|
406
|
+
} else {
|
|
407
|
+
this.removeAttribute('data-mobile');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (wasMobile !== this._isMobile && this.hasUpdated) {
|
|
411
|
+
this._cleanup();
|
|
412
|
+
this._mobileTriggered = false;
|
|
413
|
+
this.removeAttribute('mobile-triggered');
|
|
414
|
+
this._setupAnimation();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_onResize() {
|
|
419
|
+
this._checkMobile();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
_cleanup() {
|
|
423
|
+
if (this._rafId) {
|
|
424
|
+
cancelAnimationFrame(this._rafId);
|
|
425
|
+
this._rafId = null;
|
|
426
|
+
}
|
|
427
|
+
if (this._scrollHandler) {
|
|
428
|
+
window.removeEventListener('scroll', this._scrollHandler);
|
|
429
|
+
this._scrollHandler = null;
|
|
430
|
+
}
|
|
431
|
+
if (this._observer) {
|
|
432
|
+
this._observer.disconnect();
|
|
433
|
+
this._observer = null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
_setupAnimation() {
|
|
438
|
+
if (this._reducedMotion) return;
|
|
439
|
+
|
|
440
|
+
if (this._isMobile) {
|
|
441
|
+
this._setupMobileAnimation();
|
|
442
|
+
} else {
|
|
443
|
+
this._setupDesktopAnimation();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
_setupDesktopAnimation() {
|
|
448
|
+
this._scrollHandler = this._onScroll.bind(this);
|
|
449
|
+
window.addEventListener('scroll', this._scrollHandler, { passive: true });
|
|
450
|
+
this._startRafLoop();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
_setupMobileAnimation() {
|
|
454
|
+
const scroller = this.shadowRoot.querySelector('.scroller');
|
|
455
|
+
if (!scroller) return;
|
|
456
|
+
|
|
457
|
+
const scrollerHeight = scroller.offsetHeight;
|
|
458
|
+
const triggerPoint = scrollerHeight * 0.15;
|
|
459
|
+
|
|
460
|
+
this._scrollHandler = () => {
|
|
461
|
+
if (this._mobileTriggered) return;
|
|
462
|
+
const rect = scroller.getBoundingClientRect();
|
|
463
|
+
const scrolled = -rect.top;
|
|
464
|
+
|
|
465
|
+
if (scrolled > triggerPoint) {
|
|
466
|
+
this._mobileTriggered = true;
|
|
467
|
+
this.setAttribute('mobile-triggered', '');
|
|
468
|
+
this._handleMobileA11y();
|
|
469
|
+
window.removeEventListener('scroll', this._scrollHandler);
|
|
470
|
+
this._scrollHandler = null;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
window.addEventListener('scroll', this._scrollHandler, { passive: true });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_handleMobileA11y() {
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
this._setContentA11y(true);
|
|
480
|
+
}, 600);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_onScroll() {
|
|
484
|
+
const scroller = this.shadowRoot.querySelector('.scroller');
|
|
485
|
+
if (!scroller) return;
|
|
486
|
+
|
|
487
|
+
const rect = scroller.getBoundingClientRect();
|
|
488
|
+
const scrollableHeight = scroller.offsetHeight - window.innerHeight;
|
|
489
|
+
|
|
490
|
+
if (scrollableHeight <= 0) {
|
|
491
|
+
this._targetProgress = 0;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const scrolled = -rect.top;
|
|
496
|
+
this._targetProgress = Math.max(0, Math.min(1, scrolled / scrollableHeight));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
_startRafLoop() {
|
|
500
|
+
const loop = () => {
|
|
501
|
+
const lerpFactor = 1 / (1 + this.scrub * 10);
|
|
502
|
+
this._currentProgress += (this._targetProgress - this._currentProgress) * lerpFactor;
|
|
503
|
+
|
|
504
|
+
if (Math.abs(this._currentProgress - this._targetProgress) > 0.0001) {
|
|
505
|
+
this._applyDesktopTransforms(this._currentProgress);
|
|
506
|
+
} else {
|
|
507
|
+
this._currentProgress = this._targetProgress;
|
|
508
|
+
this._applyDesktopTransforms(this._currentProgress);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
this._rafId = requestAnimationFrame(loop);
|
|
512
|
+
};
|
|
513
|
+
this._rafId = requestAnimationFrame(loop);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
_easeOutCubic(t) {
|
|
517
|
+
return 1 - Math.pow(1 - t, 3);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_getSubProgress(progress, start, end) {
|
|
521
|
+
if (progress <= start) return 0;
|
|
522
|
+
if (progress >= end) return 1;
|
|
523
|
+
return (progress - start) / (end - start);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
_applyDesktopTransforms(progress) {
|
|
527
|
+
const contentWrapper = this.shadowRoot.querySelector('.content-wrapper');
|
|
528
|
+
const overlay = this.shadowRoot.querySelector('.overlay');
|
|
529
|
+
const centerImg = this.shadowRoot.querySelector('.center-img');
|
|
530
|
+
const bgText = this.shadowRoot.querySelector('.bg-text');
|
|
531
|
+
const leftImg = this.shadowRoot.querySelector('.side-img--left');
|
|
532
|
+
const rightImg = this.shadowRoot.querySelector('.side-img--right');
|
|
533
|
+
|
|
534
|
+
// Phase 1: 0.0 -> 0.25 - Content fade out
|
|
535
|
+
const phase1 = this._easeOutCubic(this._getSubProgress(progress, 0, 0.25));
|
|
536
|
+
if (contentWrapper) {
|
|
537
|
+
contentWrapper.style.opacity = 1 - phase1;
|
|
538
|
+
contentWrapper.style.transform = `translateY(${-60 * phase1}px)`;
|
|
539
|
+
}
|
|
540
|
+
if (overlay) {
|
|
541
|
+
overlay.style.opacity = 1 - phase1;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// A11y: hide content from tab when scrolled past
|
|
545
|
+
this._setContentA11y(phase1 > 0.9);
|
|
546
|
+
|
|
547
|
+
// Phase 2: 0.3 -> 0.85 - All 3 images move together
|
|
548
|
+
const phase2 = this._easeOutCubic(this._getSubProgress(progress, 0.3, 0.85));
|
|
549
|
+
|
|
550
|
+
// Center image rises
|
|
551
|
+
if (centerImg) {
|
|
552
|
+
const translateY = -65 * phase2; // vh
|
|
553
|
+
const scale = 1 - 0.3 * phase2; // 1 -> 0.7
|
|
554
|
+
centerImg.style.transform = `translateX(-50%) translateY(${translateY}vh) scale(${scale})`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Bg text follows center image (same vertical movement)
|
|
558
|
+
if (bgText) {
|
|
559
|
+
const fontSize = 12 - 5 * phase2; // 12rem -> 7rem
|
|
560
|
+
const translateY = -65 * phase2; // same as center image
|
|
561
|
+
bgText.style.fontSize = `clamp(${4 - 1 * phase2}rem, ${15 - 7 * phase2}vw, ${fontSize}rem)`;
|
|
562
|
+
bgText.style.transform = `translate(-50%, 0) translateY(${translateY}vh)`;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Side images appear together with center (slight delay: 0.35 -> 0.9)
|
|
566
|
+
const phaseSides = this._easeOutCubic(this._getSubProgress(progress, 0.35, 0.9));
|
|
567
|
+
if (leftImg) {
|
|
568
|
+
const x = -200 * (1 - phaseSides);
|
|
569
|
+
const scale = 0.7 + 0.3 * phaseSides;
|
|
570
|
+
leftImg.style.opacity = phaseSides;
|
|
571
|
+
leftImg.style.transform = `translateY(-50%) translateX(${x}px) scale(${scale})`;
|
|
572
|
+
}
|
|
573
|
+
if (rightImg) {
|
|
574
|
+
const x = 200 * (1 - phaseSides);
|
|
575
|
+
const scale = 0.7 + 0.3 * phaseSides;
|
|
576
|
+
rightImg.style.opacity = phaseSides;
|
|
577
|
+
rightImg.style.transform = `translateY(-50%) translateX(${x}px) scale(${scale})`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_setContentA11y(hidden) {
|
|
582
|
+
const contentSlot = this.querySelector('[slot="content"]');
|
|
583
|
+
if (!contentSlot) return;
|
|
584
|
+
|
|
585
|
+
const focusableElements = contentSlot.querySelectorAll('a, button, input, [tabindex]');
|
|
586
|
+
if (hidden) {
|
|
587
|
+
contentSlot.setAttribute('aria-hidden', 'true');
|
|
588
|
+
focusableElements.forEach((el) => el.setAttribute('tabindex', '-1'));
|
|
589
|
+
} else {
|
|
590
|
+
contentSlot.removeAttribute('aria-hidden');
|
|
591
|
+
focusableElements.forEach((el) => el.removeAttribute('tabindex'));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
_getScrollerHeight() {
|
|
596
|
+
if (this._reducedMotion) return '100vh';
|
|
597
|
+
const h = this._isMobile ? this.mobileScrollHeight : this.scrollHeight;
|
|
598
|
+
return `${h}vh`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
render() {
|
|
602
|
+
return html`
|
|
603
|
+
<div
|
|
604
|
+
class="scroller"
|
|
605
|
+
style="height: ${this._getScrollerHeight()}"
|
|
606
|
+
role="region"
|
|
607
|
+
aria-label="Hero animado con scroll"
|
|
608
|
+
>
|
|
609
|
+
<div
|
|
610
|
+
class="canvas"
|
|
611
|
+
style="
|
|
612
|
+
background-image: ${this._bgSrc ? `url('${this._bgSrc}')` : 'none'};
|
|
613
|
+
--_overlay-opacity: ${this.overlayOpacity};
|
|
614
|
+
"
|
|
615
|
+
>
|
|
616
|
+
<div class="overlay"></div>
|
|
617
|
+
|
|
618
|
+
<div class="content-wrapper">
|
|
619
|
+
<slot name="content"></slot>
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
${this.backgroundText ? html`<div class="bg-text">${this.backgroundText}</div>` : ''}
|
|
623
|
+
${this._centerSrc
|
|
624
|
+
? html`
|
|
625
|
+
<div class="center-img">
|
|
626
|
+
<img src="${this._centerSrc}" alt="" loading="eager" width="900" height="506" />
|
|
627
|
+
</div>
|
|
628
|
+
`
|
|
629
|
+
: ''}
|
|
630
|
+
${this._leftSrc
|
|
631
|
+
? html`
|
|
632
|
+
<div class="side-img side-img--left">
|
|
633
|
+
<img
|
|
634
|
+
src="${this._leftSrc}"
|
|
635
|
+
alt=""
|
|
636
|
+
loading="lazy"
|
|
637
|
+
decoding="async"
|
|
638
|
+
width="960"
|
|
639
|
+
height="540"
|
|
640
|
+
/>
|
|
641
|
+
</div>
|
|
642
|
+
`
|
|
643
|
+
: ''}
|
|
644
|
+
${this._rightSrc
|
|
645
|
+
? html`
|
|
646
|
+
<div class="side-img side-img--right">
|
|
647
|
+
<img
|
|
648
|
+
src="${this._rightSrc}"
|
|
649
|
+
alt=""
|
|
650
|
+
loading="lazy"
|
|
651
|
+
decoding="async"
|
|
652
|
+
width="960"
|
|
653
|
+
height="540"
|
|
654
|
+
/>
|
|
655
|
+
</div>
|
|
656
|
+
`
|
|
657
|
+
: ''}
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
<!-- Hidden slots for source images -->
|
|
662
|
+
<slot name="background"></slot>
|
|
663
|
+
<slot name="center"></slot>
|
|
664
|
+
<slot name="left"></slot>
|
|
665
|
+
<slot name="right"></slot>
|
|
666
|
+
`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
customElements.define('hero-scroll-animation', HeroScrollAnimation);
|