用 gsap 重新创建滑块并做出反应
Recreating slider with gsap and react
我正在尝试在此处使用 React 和 gsap 重新创建此代码笔,我已经尝试重新创建它几个小时了,甚至不知道从哪里开始。我不想在那个 codepen 中以相同的方式创建这些部分,而是从一组对象中形成细节,这意味着只创建一次组件使其干燥。我对反应还很陌生,我想知道如何在反应中做类似的事情,从 vanillajs 的角度来看。我已经知道发生了什么,我也在反应中像这样创建了它,但我想要一种情况,我的代码最少,并且对象数组形成一个组件,使其更具动态性,因此我可以单击它以获取有关的更多信息进入的每个部分。
HTML
<div class="slider">
<div class="slider__slide slider__slide--1">
<div class="slider__img slider__img--1"></div>
<div class="slider__text slider__text--1">
<h1 class="slider__header">Rejuvenate your, true self.</h1>
<a href="ign.com" class="cta">discover</a>
</div>
</div>
<div class="slider__slide slider__slide--2">
<div class="slider__img slider__img--2"></div>
<div class="slider__text slider__text--2">
<h1 class="slider__header">Professonial, trust-worthy, and compassionate.</h1>
<a href="google.com" class="cta">learn more</a>
</div>
</div>
<div class="slider__slide slider__slide--3">
<div class="slider__img slider__img--3"></div>
<div class="slider__text slider__text--3">
<h1 class="slider__header">Trust in us.</h1>
<a href="youtube.com" class="cta">learn more</a>
</div>
</div>
<div class="slider__slide slider__slide--4">
<div class="slider__img slider__img--4"></div>
<div class="slider__text slider__text--4">
<h1 class="slider__header">What we do.</h1>
<a href="tsn.ca" class="cta">discover</a>
</div>
</div>
<div class="slider__navigation">
<div class="slider__count slider__count--top">
<p class="count count--top count--top-1">01</p>
<p class="count count--top count--top-2">02</p>
<p class="count count--top count--top-3">03</p>
<p class="count count--top count--top-4">04</p>
</div>
<div class="slider__bar">
<div id="sliderBarDynamic" class="slider__bar--dynamic"></div>
<div class="slider__bar--static"></div>
</div>
<div class="slider__count slider__count--bottom">
<p class="count count--bottom count--bottom-1">02</p>
<p class="count count--bottom count--bottom-2">03</p>
<p class="count count--bottom count--bottom-3">04</p>
<p class="count count--bottom count--bottom-3">01</p>
</div>
</div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Gilda+Display&family=Roboto&display=swap');
*,
*::before,
*::after{
padding:0;
margin:0;
box-sizing:inherit;
}
html{
font-size:16px;
box-sizing:border-box;
}
body{
font-family: 'Roboto', sans-serif;
color:#444444;
font-weight: 300;
line-height: 1.6;
}
img{
max-width:100%;
}
h1{
font-size: 100px;
color: #fff;
font-family: 'Gilda Display', serif;
font-weight: 300;
line-height: 1;
}
h2{
}
h3{
}
P{
color: #fff;
}
a{
text-decoration:none;
color:#ffffff;
font-size: 24px;
}
ul{
list-style-type:none;
}
// Slider
.slider{
width: 100%;
height: 100vh;
overflow: hidden;
position: relative;
&__slide{
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left:0;
&--1{
z-index: 4;
}
&--2{
z-index: 3;
}
&--3{
z-index: 2;
}
&--4{
z-index: 1;
}
}
&__img{
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
&--1{
background-image: url('https://i.postimg.cc/Y0T3F1tc/about-landing.jpg');
}
&--2{
background-image: url('https://i.postimg.cc/FHHyKWyf/i-Stock-1148043788.jpg');
}
&--3{
background-image: url('https://i.postimg.cc/tTqp06QH/i-Stock-1064136816.jpg');
}
&--4{
background-image: url('https://i.postimg.cc/435R13K2/i-Stock-1179976698.jpg');
}
}
&__text{
align-self: flex-end;
padding: 0 0 5vw 15vh;
opacity: 0;
width: 80%;
max-width: 1005px;
.slider__header{
margin-bottom: 40px;
text-transform: capitalize;
}
.cta{
font-weight: 700;
text-transform: uppercase;
letter-spacing: 6px;
margin-left: 65px;
position: relative;
&:before{
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -55px;
width: 40px;
height: 1px;
background-color: white;
}
}
}
// Slider Navigation
&__navigation{
width: 21px;
height: 400px;
position: fixed;
top: 50%;
transform: translateY(-50%);
left: calc(100% - 5vw);
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.count--top{
position: absolute;
top: 0;
left: 0;
// position:
}
.count{
opacity: 0;
}
.count:first-child{
opacity: 1;
}
.count--bottom{
position: absolute;
bottom: 0;
left: 0;
}
&__bar{
width: 2px;
height: 250px;
position: relative;
&--dynamic{
width: 100%;
height: 100%;
background-color: #FF69B4;
transform-origin: top center;
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
&--static{
width: 100%;
height: 100%;
background-color: darkgrey;
position: absolute;
top: 0;
left: 0;
}
}
}
// Slider end
Javascript
/*
*/
// timeline to control animations everytime the timeline restarts/repeats
let tlRepeat = gsap.timeline();
// Need first slides elements - img and text to animate on each repeat of timeline
let repeatBeginning = ()=>{
gsap.set(bgImage[0], {opacity: 0, scale: 1.2, webkitFilter:"blur(" + 6 + "px)"})
tlRepeat
.add("slide1-in")
.fromTo([countTop[0], countBottom[0]], {opacity: 0}, {duration: 0.3, opacity: 1, ease: "Power2.easeIn"}, "slide1-in")
.to(bgImage[0], {duration: 1.8, scale: 1, opacity: 1, webkitFilter:"blur(" + 0 + "px)"}, "slide1-in")
.fromTo(text[0], {opacity: 0, x: -30, ease: "Power2.easeIn"}, {duration: 0.8, opacity: 1, x: 0}, "-=1")
}
// On start animations
// let onStartSlide1Animations = ()=>{
// // gsap.to(text[0], {duration: 0.7, opacity: 1, x: -15, ease: "Power2.easeIn"})
// }
// Variables
let slides = document.querySelectorAll('.slider__slide'),
dynamicBar = document.querySelector('#sliderBarDynamic'),
countTop = document.querySelectorAll(".count--top"),
countBottom = document.querySelectorAll(".count--bottom"),
bgImage = document.querySelectorAll(".slider__img"),
text = document.querySelectorAll(".slider__text"),
tl = gsap.timeline({repeat: 0, delay: 1, paused: false, onRepeat: repeatBeginning});
// Push all text back and only make first one visible
gsap.set(text, {x: -30});
gsap.set(text[0], {opacity: 1});
// Animate slide's elements but not the first one.
// Make first slide's elements animate when timeline is repeating,
// Follow the flow of rest of the slide's animations
slides.forEach((slide, i) =>{
tl
.fromTo(dynamicBar, {scaleY: 0}, {duration: 1.4, scaleY: 1}, "+=2")
.set(dynamicBar, {transformOrigin: "bottom center"})
.to(dynamicBar, {duration: 1, scaleY: 0}, "+=0.4")
.set(dynamicBar, {transformOrigin: "top center"})
.add("elements-in-out")
.to([countTop[i], countBottom[i]], {opacity: 0}, "elements-in-out")
.to([countTop[i+1], countBottom[i+1]], {opacity: 1}, "elements-in-out")
.to(bgImage[i], {duration: 0.2, opacity: 0}, "elements-in-out")
.set(bgImage[i+1], {scale: 1.2, webkitFilter:"blur(" + 6 + "px)"}, "elements-in-out")
.to(bgImage[i+1], {duration: 1.8, scale: 1, webkitFilter:"blur(" + 0 + "px)"}, "elements-in-out")
.to(text[i], {duration: 0.3, opacity: 0}, "elements-in-out")
.to(text[i+1], {duration: 0.8, opacity: 1, x: 0}, "-=1")
})
所以首先,为了 re-imagine ReactJS 的任何 html 片段,尝试在片段中寻找相似之处/重复。一旦你这样做了,你就会明白什么可以作为单独的组件分离出来,以及它们是如何联系在一起的。
现在,只需查看 html 片段,我们就可以看到幻灯片是重复的,因此它们可以作为一个单独的组件保存。我们还看到每张幻灯片都显示一个独特的标题和段落文本,并且还有一个配色方案。因此,为了使我们的 Slide
组件动态化,我们可以将这些作为 props 传递给组件。
所以我们创建整个 Slider
组件(由 Slide
组成)的 data
看起来像这样:
const sliderData = [
{
id: '1',
headerText: `I'm the first Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#500033',
sliderIllustration: '#FF0077',
sliderInner: 'rgba(255, 0, 119, 0.4)',
sliderButton: '#FF0077',
},
},
...
]
接下来我们注意到,一些 UI 是使用 CSS 创建的,而这个 主要是 导致整个代码段是静态的。因此,为了实现 动态性 我们根据获取的数据在组件中渲染了一些样式。像这样:
<style
dangerouslySetInnerHTML={{
__html: [
`
.slider {
display: flex;
width: ${sliderData.length * 100}%; /*(this value in css was static 500%, to rendered only 5 slides)*/
height: 55rem;
transition: all 0.25s ease-in;
transform: translateX(0);
}
.trail {
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: 60%;
display: grid;
grid-template-columns: repeat(${sliderData.length}, 1fr); /*this value was also static in css to display only 5 bars/pages.*/
gap: 1rem;
text-align: center;
font-size: 1.5rem;
}`,
].join('\n'),
}}
></style>
此外,每张幻灯片都分配了静态 class,如 box1, box2... etc
。因此,需要像这样为每张幻灯片呈现这些样式:
<style
dangerouslySetInnerHTML={{
__html: [
`.slider .box${data.id} {
background-color: ${data.colors.sliderBox};
}
.slider .box${data.id} .illustration .inner {
background-color: ${data.colors.sliderIllustration};
}
.slider .box${data.id} .illustration .inner::after, .slider .box${data.id} .illustration .inner::before {
background-color: ${data.colors.sliderInner};
}
.slider .box${data.id} button {
background-color: ${data.colors.sliderButton};
}`,
].join('\n'),
}}
></style>
(请注意,上面代码片段中的 object data
对应于构成整个滑块组件的数组 sliderData
中的 object。
一旦我们 UI 准备就绪,引入 GSAP 动画就很简单了。首先,我们为 GSAP 在动画过程中使用的 UI 个元素创建了 refs
。
接下来,我们触发动画就像您在普通 javascript 中一样。这里唯一的区别是我们在 useEffect
挂钩中执行它们,该挂钩在功能组件加载后运行(空依赖数组)。
此外,还有一些 hard-coded 内容仅对五张幻灯片进行动画处理。我们通过引入 let ratio = 100 / sliderData.length
使其动态化
整个组件:
import React, { useEffect, useRef } from 'react'
import './styleNew.css'
import { gsap } from 'gsap'
const sliderData = [
{
id: '1',
headerText: `I'm the first Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#500033',
sliderIllustration: '#FF0077',
sliderInner: 'rgba(255, 0, 119, 0.4)',
sliderButton: '#FF0077',
},
},
{
id: '2',
headerText: `I'm the second Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#000050',
sliderIllustration: '#0033FF',
sliderInner: 'rgba(0, 51, 255, 0.4)',
sliderButton: '#0033FF',
},
},
{
id: '3',
headerText: `I'm the third Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#00501D',
sliderIllustration: '#00FF44',
sliderInner: 'rgba(0, 255, 68, 0.4)',
sliderButton: '#00FF44',
},
},
{
id: '4',
headerText: `I'm the fourth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#554D00',
sliderIllustration: '#FF4E00',
sliderInner: 'rgba(255, 78, 0, 0.4)',
sliderButton: '#FF4E00',
},
},
{
id: '5',
headerText: `I'm the fifth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#300050',
sliderIllustration: '#8000FF',
sliderInner: 'rgba(128, 0, 255, 0.4)',
sliderButton: '#8000FF',
},
},
{
id: '6',
headerText: `I'm the sixth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#000050',
sliderIllustration: '#0033FF',
sliderInner: 'rgba(0, 51, 255, 0.4)',
sliderButton: '#0033FF',
},
},
{
id: '7',
headerText: `I'm the seventh Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#00501D',
sliderIllustration: '#00FF44',
sliderInner: 'rgba(0, 255, 68, 0.4)',
sliderButton: '#00FF44',
},
},
{
id: '8',
headerText: `I'm the eighth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#554D00',
sliderIllustration: '#FF4E00',
sliderInner: 'rgba(255, 78, 0, 0.4)',
sliderButton: '#FF4E00',
},
},
{
id: '9',
headerText: `I'm the ninth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#300050',
sliderIllustration: '#8000FF',
sliderInner: 'rgba(128, 0, 255, 0.4)',
sliderButton: '#8000FF',
},
},
]
export default function SliderNew() {
const slider = useRef(undefined)
const prevButton = useRef(undefined)
const nextButton = useRef(undefined)
const trail = useRef([])
useEffect(() => {
console.log('sliders', slider)
console.log('trail', trail)
console.log('nextButton.current', nextButton.current)
console.log('prevButton.current', prevButton.current)
startGsapAnimations()
}, [])
const startGsapAnimations = () => {
// Transform value
let value = 0
// trail index number
let trailValue = 0
// interval (Duration)
let interval = 10000
let ratio = 100 / sliderData.length
const tl = gsap.timeline({
defaults: { duration: 0.6, ease: 'power2.inOut' },
})
tl.from('.bg', { x: '-100%', opacity: 0 })
.from('p', { opacity: 0 }, '-=0.3')
.from('h1', { opacity: 0, y: '30px' }, '-=0.3')
.from('button', { opacity: 0, y: '-40px' }, '-=0.8')
// function to restart animation
const animate = () => tl.restart()
const slide = (condition) => {
// CLear interval
clearInterval(start)
// update value and trailValue
condition === 'increase' ? initiateINC() : initiateDEC()
// move slide
move(value, trailValue)
// Restart Animation
animate()
// start interal for slides back
start = setInterval(() => slide('increase'), interval)
}
// function for increase(forward, next) configuration
const initiateINC = () => {
// Remove active from all trails
sliderData.forEach((_item, index) =>
trail[index].classList.remove('active'),
)
// increase transform value
// console.log('initialInc~value', value)
// console.log('initialInc~calc', (sliderData.length - 1) * ratio)
// console.log(
// 'initialInc~eq',
// Math.round(value) === Math.round((sliderData.length - 1) * ratio),
// )
Math.round(value) === Math.round((sliderData.length - 1) * ratio)
? (value = 0)
: (value += ratio)
// update trailValue based on value
trailUpdate()
}
// function for decrease(backward, previous) configuration
const initiateDEC = () => {
// Remove active from all trails
sliderData.forEach((_item, index) =>
trail[index].classList.remove('active'),
)
// decrease transform value
Math.round(value) === 0
? (value = (sliderData.length - 1) * ratio)
: (value -= ratio)
// update trailValue based on value
trailUpdate()
}
// function to transform slide
const move = (S, T) => {
// transform slider
slider.current.style.transform = `translateX(-${S}%)`
//add active class to the current trail
console.log('trail', T)
trail[Math.round(T)].classList.add('active')
}
const trailUpdate = () => {
trailValue = value / ratio
console.log('trailUpdate', trailValue)
}
// Start interval for slides
let start = setInterval(() => slide('increase'), interval)
nextButton.current.addEventListener('click', () => slide('increase'))
prevButton.current.addEventListener('click', () => slide('decrease'))
const clickCheck = (e) => {
// CLear interval
clearInterval(start)
// Get selected trail
const check = e.target
// remove active class from all trails
sliderData.forEach((_item, index) => {
trail[index].classList.remove('active')
if (check === trail[index]) {
value = index * ratio
}
})
// add active class
check.classList.add('active')
// update trail based on value
trailUpdate()
// transfrom slide
move(value, trailValue)
// start animation
animate()
// start interval
start = setInterval(() => slide('increase'), interval)
}
// Add function to all trails
sliderData.forEach((_item, index) =>
trail[index].addEventListener('click', (ev) => clickCheck(ev)),
)
}
return (
<>
<style
dangerouslySetInnerHTML={{
__html: [
`
.slider {
display: flex;
width: ${sliderData.length * 100}%;
height: 55rem;
transition: all 0.25s ease-in;
transform: translateX(0);
}
.trail {
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: 60%;
display: grid;
grid-template-columns: repeat(${sliderData.length}, 1fr);
gap: 1rem;
text-align: center;
font-size: 1.5rem;
}`,
].join('\n'),
}}
></style>
<div className="container">
<div className="slider" ref={slider}>
{sliderData.map((item, index) => (
<Slide key={index} data={item} />
))}
</div>
<Svg
ref={prevButton}
compClass="prev"
compTransform="translate(0 91) rotate(-90)"
/>
<Svg
ref={nextButton}
compClass="next"
compTransform="translate(56.898) rotate(90)"
/>
<div className="trail">
{sliderData.map((item, index) => (
<div
ref={(ref) => {
trail[index] = ref
}}
key={index}
className={index == 0 ? `box${item.id} active` : `box${item.id}`}
>
{item.id}
</div>
))}
</div>
</div>
</>
)
}
function Slide(props) {
const { data } = props
return (
<>
<style
dangerouslySetInnerHTML={{
__html: [
`.slider .box${data.id} {
background-color: ${data.colors.sliderBox};
}
.slider .box${data.id} .illustration .inner {
background-color: ${data.colors.sliderIllustration};
}
.slider .box${data.id} .illustration .inner::after, .slider .box${data.id} .illustration .inner::before {
background-color: ${data.colors.sliderInner};
}
.slider .box${data.id} button {
background-color: ${data.colors.sliderButton};
}`,
].join('\n'),
}}
></style>
<div className={`box${data.id} box`}>
<div className="bg"></div>
<div className="details">
<h1>{data.headerText}</h1>
<p>{data.paragraphText}</p>
<button>{data.buttonText}</button>
</div>
<div className="illustration">
<div className="inner"></div>
</div>
</div>
</>
)
}
const Svg = React.forwardRef((props, ref) => {
console.log('Svg', props)
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width="56.898"
height="91"
className={props.compClass}
viewBox="0 0 56.898 91"
>
<path
fill="#fff"
d="M45.5,0,91,56.9,48.452,24.068,0,56.9Z"
transform={props.compTransform}
></path>
</svg>
)
})
styleNew.css:
*,
*:before,
*:after {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-family: "Roboto", sans-serif;
font-size: 62.5%;
}
@media only screen and (max-width: 800px) {
html {
font-size: 57%;
}
}
body {
background-color: #000;
color: #fff;
padding: 8rem;
}
@media only screen and (max-width: 1000px) {
body {
padding: 0;
}
}
.container {
position: relative;
overflow: hidden;
border-radius: 5rem;
}
@media only screen and (max-width: 1000px) {
.container {
border-radius: 0;
}
}
@media only screen and (max-width: 1000px) {
.slider {
height: 100vh;
}
}
.slider .box {
height: 100%;
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: center;
overflow: hidden;
position: relative;
}
@media only screen and (max-width: 650px) {
.slider .box {
grid-template-columns: 1fr;
grid-template-rows: repeat(2, 1fr);
}
}
.slider .box .bg {
padding: 2rem;
background-color: rgba(0, 0, 0, 0.2);
width: 55%;
transform: skewX(7deg);
position: absolute;
height: 100%;
left: -10%;
padding-left: 20rem;
transform-origin: 0 100%;
}
@media only screen and (max-width: 800px) {
.slider .box .bg {
width: 65%;
}
}
@media only screen and (max-width: 650px) {
.slider .box .bg {
width: 100%;
left: 0;
bottom: 0;
height: 54%;
transform: skewX(0deg);
}
}
.slider .box .bg::before {
content: "";
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: inherit;
pointer-events: none;
transform: skewX(10deg);
}
@media only screen and (max-width: 650px) {
.slider .box .bg::before {
width: 120%;
bottom: 0;
transform: skewX(0deg);
}
}
.slider .box .details {
padding: 5rem;
padding-left: 10rem;
z-index: 100;
grid-column: 1/span 1;
grid-row: 1/-1;
}
@media only screen and (max-width: 650px) {
.slider .box .details {
grid-row: 2/span 1;
grid-column: 1/-1;
text-align: center;
padding: 2rem;
transform: translateY(-9rem);
}
}
.slider .box .details h1 {
font-size: 3.5rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.slider .box .details p {
display: inline-block;
font-size: 1.3rem;
color: #B5B4B4;
margin-bottom: 2rem;
margin-right: 5rem;
}
@media only screen and (max-width: 800px) {
.slider .box .details p {
margin-right: 0;
}
}
.slider .box .details button {
padding: 1rem 3rem;
color: #fff;
border-radius: 2rem;
outline: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.slider .box .details button:hover {
opacity: 0.8;
}
.slider .box .details button:focus {
outline: none;
border: none;
}
.slider .illustration {
grid-column: 2/-1;
grid-row: 1/-1;
justify-self: center;
}
@media only screen and (max-width: 650px) {
.slider .illustration {
grid-row: 1/span 1;
grid-column: 1/-1;
display: flex;
justify-content: center;
align-items: center;
}
}
.slider .illustration div {
height: 25rem;
width: 18rem;
border-radius: 3rem;
background-color: #FF0077;
position: relative;
transform: skewX(-10deg);
}
@media only screen and (max-width: 800px) {
.slider .illustration div {
height: 20rem;
width: 15rem;
}
}
.slider .illustration div::after, .slider .illustration div::before {
content: "";
position: absolute;
height: 100%;
width: 100%;
border-radius: 3rem;
top: 0;
left: 0;
}
.slider .illustration div::after {
transform: translate(4rem, -1rem);
}
.slider .illustration div::before {
transform: translate(2rem, -2rem);
}
.prev,
.next,
.trail {
z-index: 10000;
position: absolute;
}
.prev,
.next {
width: 4rem;
cursor: pointer;
opacity: 0.2;
transition: all 0.3s ease;
}
@media only screen and (max-width: 650px) {
.prev,
.next {
display: none;
}
}
.prev:hover,
.next:hover {
opacity: 1;
}
.prev {
top: 50%;
left: 2%;
transform: translateY(-50%);
}
.next {
top: 50%;
right: 2%;
transform: translateY(-50%);
}
@media only screen and (max-width: 650px) {
.trail {
width: 90%;
bottom: 13%;
}
}
.trail div {
padding: 2rem;
border-top: 3px solid #fff;
cursor: pointer;
opacity: 0.3;
transition: all 0.3s ease;
}
.trail div:hover {
opacity: 0.6;
}
@media only screen and (max-width: 650px) {
.trail div {
padding: 1rem;
}
}
.active {
opacity: 1 !important;
}
我正在尝试在此处使用 React 和 gsap 重新创建此代码笔,我已经尝试重新创建它几个小时了,甚至不知道从哪里开始。我不想在那个 codepen 中以相同的方式创建这些部分,而是从一组对象中形成细节,这意味着只创建一次组件使其干燥。我对反应还很陌生,我想知道如何在反应中做类似的事情,从 vanillajs 的角度来看。我已经知道发生了什么,我也在反应中像这样创建了它,但我想要一种情况,我的代码最少,并且对象数组形成一个组件,使其更具动态性,因此我可以单击它以获取有关的更多信息进入的每个部分。
HTML
<div class="slider">
<div class="slider__slide slider__slide--1">
<div class="slider__img slider__img--1"></div>
<div class="slider__text slider__text--1">
<h1 class="slider__header">Rejuvenate your, true self.</h1>
<a href="ign.com" class="cta">discover</a>
</div>
</div>
<div class="slider__slide slider__slide--2">
<div class="slider__img slider__img--2"></div>
<div class="slider__text slider__text--2">
<h1 class="slider__header">Professonial, trust-worthy, and compassionate.</h1>
<a href="google.com" class="cta">learn more</a>
</div>
</div>
<div class="slider__slide slider__slide--3">
<div class="slider__img slider__img--3"></div>
<div class="slider__text slider__text--3">
<h1 class="slider__header">Trust in us.</h1>
<a href="youtube.com" class="cta">learn more</a>
</div>
</div>
<div class="slider__slide slider__slide--4">
<div class="slider__img slider__img--4"></div>
<div class="slider__text slider__text--4">
<h1 class="slider__header">What we do.</h1>
<a href="tsn.ca" class="cta">discover</a>
</div>
</div>
<div class="slider__navigation">
<div class="slider__count slider__count--top">
<p class="count count--top count--top-1">01</p>
<p class="count count--top count--top-2">02</p>
<p class="count count--top count--top-3">03</p>
<p class="count count--top count--top-4">04</p>
</div>
<div class="slider__bar">
<div id="sliderBarDynamic" class="slider__bar--dynamic"></div>
<div class="slider__bar--static"></div>
</div>
<div class="slider__count slider__count--bottom">
<p class="count count--bottom count--bottom-1">02</p>
<p class="count count--bottom count--bottom-2">03</p>
<p class="count count--bottom count--bottom-3">04</p>
<p class="count count--bottom count--bottom-3">01</p>
</div>
</div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Gilda+Display&family=Roboto&display=swap');
*,
*::before,
*::after{
padding:0;
margin:0;
box-sizing:inherit;
}
html{
font-size:16px;
box-sizing:border-box;
}
body{
font-family: 'Roboto', sans-serif;
color:#444444;
font-weight: 300;
line-height: 1.6;
}
img{
max-width:100%;
}
h1{
font-size: 100px;
color: #fff;
font-family: 'Gilda Display', serif;
font-weight: 300;
line-height: 1;
}
h2{
}
h3{
}
P{
color: #fff;
}
a{
text-decoration:none;
color:#ffffff;
font-size: 24px;
}
ul{
list-style-type:none;
}
// Slider
.slider{
width: 100%;
height: 100vh;
overflow: hidden;
position: relative;
&__slide{
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0;
left:0;
&--1{
z-index: 4;
}
&--2{
z-index: 3;
}
&--3{
z-index: 2;
}
&--4{
z-index: 1;
}
}
&__img{
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
&--1{
background-image: url('https://i.postimg.cc/Y0T3F1tc/about-landing.jpg');
}
&--2{
background-image: url('https://i.postimg.cc/FHHyKWyf/i-Stock-1148043788.jpg');
}
&--3{
background-image: url('https://i.postimg.cc/tTqp06QH/i-Stock-1064136816.jpg');
}
&--4{
background-image: url('https://i.postimg.cc/435R13K2/i-Stock-1179976698.jpg');
}
}
&__text{
align-self: flex-end;
padding: 0 0 5vw 15vh;
opacity: 0;
width: 80%;
max-width: 1005px;
.slider__header{
margin-bottom: 40px;
text-transform: capitalize;
}
.cta{
font-weight: 700;
text-transform: uppercase;
letter-spacing: 6px;
margin-left: 65px;
position: relative;
&:before{
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: -55px;
width: 40px;
height: 1px;
background-color: white;
}
}
}
// Slider Navigation
&__navigation{
width: 21px;
height: 400px;
position: fixed;
top: 50%;
transform: translateY(-50%);
left: calc(100% - 5vw);
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
}
.count--top{
position: absolute;
top: 0;
left: 0;
// position:
}
.count{
opacity: 0;
}
.count:first-child{
opacity: 1;
}
.count--bottom{
position: absolute;
bottom: 0;
left: 0;
}
&__bar{
width: 2px;
height: 250px;
position: relative;
&--dynamic{
width: 100%;
height: 100%;
background-color: #FF69B4;
transform-origin: top center;
position: absolute;
top: 0;
left: 0;
z-index: 2;
}
&--static{
width: 100%;
height: 100%;
background-color: darkgrey;
position: absolute;
top: 0;
left: 0;
}
}
}
// Slider end
Javascript
/*
*/
// timeline to control animations everytime the timeline restarts/repeats
let tlRepeat = gsap.timeline();
// Need first slides elements - img and text to animate on each repeat of timeline
let repeatBeginning = ()=>{
gsap.set(bgImage[0], {opacity: 0, scale: 1.2, webkitFilter:"blur(" + 6 + "px)"})
tlRepeat
.add("slide1-in")
.fromTo([countTop[0], countBottom[0]], {opacity: 0}, {duration: 0.3, opacity: 1, ease: "Power2.easeIn"}, "slide1-in")
.to(bgImage[0], {duration: 1.8, scale: 1, opacity: 1, webkitFilter:"blur(" + 0 + "px)"}, "slide1-in")
.fromTo(text[0], {opacity: 0, x: -30, ease: "Power2.easeIn"}, {duration: 0.8, opacity: 1, x: 0}, "-=1")
}
// On start animations
// let onStartSlide1Animations = ()=>{
// // gsap.to(text[0], {duration: 0.7, opacity: 1, x: -15, ease: "Power2.easeIn"})
// }
// Variables
let slides = document.querySelectorAll('.slider__slide'),
dynamicBar = document.querySelector('#sliderBarDynamic'),
countTop = document.querySelectorAll(".count--top"),
countBottom = document.querySelectorAll(".count--bottom"),
bgImage = document.querySelectorAll(".slider__img"),
text = document.querySelectorAll(".slider__text"),
tl = gsap.timeline({repeat: 0, delay: 1, paused: false, onRepeat: repeatBeginning});
// Push all text back and only make first one visible
gsap.set(text, {x: -30});
gsap.set(text[0], {opacity: 1});
// Animate slide's elements but not the first one.
// Make first slide's elements animate when timeline is repeating,
// Follow the flow of rest of the slide's animations
slides.forEach((slide, i) =>{
tl
.fromTo(dynamicBar, {scaleY: 0}, {duration: 1.4, scaleY: 1}, "+=2")
.set(dynamicBar, {transformOrigin: "bottom center"})
.to(dynamicBar, {duration: 1, scaleY: 0}, "+=0.4")
.set(dynamicBar, {transformOrigin: "top center"})
.add("elements-in-out")
.to([countTop[i], countBottom[i]], {opacity: 0}, "elements-in-out")
.to([countTop[i+1], countBottom[i+1]], {opacity: 1}, "elements-in-out")
.to(bgImage[i], {duration: 0.2, opacity: 0}, "elements-in-out")
.set(bgImage[i+1], {scale: 1.2, webkitFilter:"blur(" + 6 + "px)"}, "elements-in-out")
.to(bgImage[i+1], {duration: 1.8, scale: 1, webkitFilter:"blur(" + 0 + "px)"}, "elements-in-out")
.to(text[i], {duration: 0.3, opacity: 0}, "elements-in-out")
.to(text[i+1], {duration: 0.8, opacity: 1, x: 0}, "-=1")
})
所以首先,为了 re-imagine ReactJS 的任何 html 片段,尝试在片段中寻找相似之处/重复。一旦你这样做了,你就会明白什么可以作为单独的组件分离出来,以及它们是如何联系在一起的。
现在,只需查看 html 片段,我们就可以看到幻灯片是重复的,因此它们可以作为一个单独的组件保存。我们还看到每张幻灯片都显示一个独特的标题和段落文本,并且还有一个配色方案。因此,为了使我们的 Slide
组件动态化,我们可以将这些作为 props 传递给组件。
所以我们创建整个 Slider
组件(由 Slide
组成)的 data
看起来像这样:
const sliderData = [
{
id: '1',
headerText: `I'm the first Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#500033',
sliderIllustration: '#FF0077',
sliderInner: 'rgba(255, 0, 119, 0.4)',
sliderButton: '#FF0077',
},
},
...
]
接下来我们注意到,一些 UI 是使用 CSS 创建的,而这个 主要是 导致整个代码段是静态的。因此,为了实现 动态性 我们根据获取的数据在组件中渲染了一些样式。像这样:
<style
dangerouslySetInnerHTML={{
__html: [
`
.slider {
display: flex;
width: ${sliderData.length * 100}%; /*(this value in css was static 500%, to rendered only 5 slides)*/
height: 55rem;
transition: all 0.25s ease-in;
transform: translateX(0);
}
.trail {
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: 60%;
display: grid;
grid-template-columns: repeat(${sliderData.length}, 1fr); /*this value was also static in css to display only 5 bars/pages.*/
gap: 1rem;
text-align: center;
font-size: 1.5rem;
}`,
].join('\n'),
}}
></style>
此外,每张幻灯片都分配了静态 class,如 box1, box2... etc
。因此,需要像这样为每张幻灯片呈现这些样式:
<style
dangerouslySetInnerHTML={{
__html: [
`.slider .box${data.id} {
background-color: ${data.colors.sliderBox};
}
.slider .box${data.id} .illustration .inner {
background-color: ${data.colors.sliderIllustration};
}
.slider .box${data.id} .illustration .inner::after, .slider .box${data.id} .illustration .inner::before {
background-color: ${data.colors.sliderInner};
}
.slider .box${data.id} button {
background-color: ${data.colors.sliderButton};
}`,
].join('\n'),
}}
></style>
(请注意,上面代码片段中的 object data
对应于构成整个滑块组件的数组 sliderData
中的 object。
一旦我们 UI 准备就绪,引入 GSAP 动画就很简单了。首先,我们为 GSAP 在动画过程中使用的 UI 个元素创建了 refs
。
接下来,我们触发动画就像您在普通 javascript 中一样。这里唯一的区别是我们在 useEffect
挂钩中执行它们,该挂钩在功能组件加载后运行(空依赖数组)。
此外,还有一些 hard-coded 内容仅对五张幻灯片进行动画处理。我们通过引入 let ratio = 100 / sliderData.length
整个组件:
import React, { useEffect, useRef } from 'react'
import './styleNew.css'
import { gsap } from 'gsap'
const sliderData = [
{
id: '1',
headerText: `I'm the first Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#500033',
sliderIllustration: '#FF0077',
sliderInner: 'rgba(255, 0, 119, 0.4)',
sliderButton: '#FF0077',
},
},
{
id: '2',
headerText: `I'm the second Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#000050',
sliderIllustration: '#0033FF',
sliderInner: 'rgba(0, 51, 255, 0.4)',
sliderButton: '#0033FF',
},
},
{
id: '3',
headerText: `I'm the third Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#00501D',
sliderIllustration: '#00FF44',
sliderInner: 'rgba(0, 255, 68, 0.4)',
sliderButton: '#00FF44',
},
},
{
id: '4',
headerText: `I'm the fourth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#554D00',
sliderIllustration: '#FF4E00',
sliderInner: 'rgba(255, 78, 0, 0.4)',
sliderButton: '#FF4E00',
},
},
{
id: '5',
headerText: `I'm the fifth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#300050',
sliderIllustration: '#8000FF',
sliderInner: 'rgba(128, 0, 255, 0.4)',
sliderButton: '#8000FF',
},
},
{
id: '6',
headerText: `I'm the sixth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#000050',
sliderIllustration: '#0033FF',
sliderInner: 'rgba(0, 51, 255, 0.4)',
sliderButton: '#0033FF',
},
},
{
id: '7',
headerText: `I'm the seventh Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#00501D',
sliderIllustration: '#00FF44',
sliderInner: 'rgba(0, 255, 68, 0.4)',
sliderButton: '#00FF44',
},
},
{
id: '8',
headerText: `I'm the eighth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#554D00',
sliderIllustration: '#FF4E00',
sliderInner: 'rgba(255, 78, 0, 0.4)',
sliderButton: '#FF4E00',
},
},
{
id: '9',
headerText: `I'm the ninth Box`,
paragraphText: `Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Integer lacinia dui lectus. Donec scelerisque ipsum
diam, ac mattis orci pellentesque eget. `,
buttonText: 'Check Now',
colors: {
sliderBox: '#300050',
sliderIllustration: '#8000FF',
sliderInner: 'rgba(128, 0, 255, 0.4)',
sliderButton: '#8000FF',
},
},
]
export default function SliderNew() {
const slider = useRef(undefined)
const prevButton = useRef(undefined)
const nextButton = useRef(undefined)
const trail = useRef([])
useEffect(() => {
console.log('sliders', slider)
console.log('trail', trail)
console.log('nextButton.current', nextButton.current)
console.log('prevButton.current', prevButton.current)
startGsapAnimations()
}, [])
const startGsapAnimations = () => {
// Transform value
let value = 0
// trail index number
let trailValue = 0
// interval (Duration)
let interval = 10000
let ratio = 100 / sliderData.length
const tl = gsap.timeline({
defaults: { duration: 0.6, ease: 'power2.inOut' },
})
tl.from('.bg', { x: '-100%', opacity: 0 })
.from('p', { opacity: 0 }, '-=0.3')
.from('h1', { opacity: 0, y: '30px' }, '-=0.3')
.from('button', { opacity: 0, y: '-40px' }, '-=0.8')
// function to restart animation
const animate = () => tl.restart()
const slide = (condition) => {
// CLear interval
clearInterval(start)
// update value and trailValue
condition === 'increase' ? initiateINC() : initiateDEC()
// move slide
move(value, trailValue)
// Restart Animation
animate()
// start interal for slides back
start = setInterval(() => slide('increase'), interval)
}
// function for increase(forward, next) configuration
const initiateINC = () => {
// Remove active from all trails
sliderData.forEach((_item, index) =>
trail[index].classList.remove('active'),
)
// increase transform value
// console.log('initialInc~value', value)
// console.log('initialInc~calc', (sliderData.length - 1) * ratio)
// console.log(
// 'initialInc~eq',
// Math.round(value) === Math.round((sliderData.length - 1) * ratio),
// )
Math.round(value) === Math.round((sliderData.length - 1) * ratio)
? (value = 0)
: (value += ratio)
// update trailValue based on value
trailUpdate()
}
// function for decrease(backward, previous) configuration
const initiateDEC = () => {
// Remove active from all trails
sliderData.forEach((_item, index) =>
trail[index].classList.remove('active'),
)
// decrease transform value
Math.round(value) === 0
? (value = (sliderData.length - 1) * ratio)
: (value -= ratio)
// update trailValue based on value
trailUpdate()
}
// function to transform slide
const move = (S, T) => {
// transform slider
slider.current.style.transform = `translateX(-${S}%)`
//add active class to the current trail
console.log('trail', T)
trail[Math.round(T)].classList.add('active')
}
const trailUpdate = () => {
trailValue = value / ratio
console.log('trailUpdate', trailValue)
}
// Start interval for slides
let start = setInterval(() => slide('increase'), interval)
nextButton.current.addEventListener('click', () => slide('increase'))
prevButton.current.addEventListener('click', () => slide('decrease'))
const clickCheck = (e) => {
// CLear interval
clearInterval(start)
// Get selected trail
const check = e.target
// remove active class from all trails
sliderData.forEach((_item, index) => {
trail[index].classList.remove('active')
if (check === trail[index]) {
value = index * ratio
}
})
// add active class
check.classList.add('active')
// update trail based on value
trailUpdate()
// transfrom slide
move(value, trailValue)
// start animation
animate()
// start interval
start = setInterval(() => slide('increase'), interval)
}
// Add function to all trails
sliderData.forEach((_item, index) =>
trail[index].addEventListener('click', (ev) => clickCheck(ev)),
)
}
return (
<>
<style
dangerouslySetInnerHTML={{
__html: [
`
.slider {
display: flex;
width: ${sliderData.length * 100}%;
height: 55rem;
transition: all 0.25s ease-in;
transform: translateX(0);
}
.trail {
bottom: 5%;
left: 50%;
transform: translateX(-50%);
width: 60%;
display: grid;
grid-template-columns: repeat(${sliderData.length}, 1fr);
gap: 1rem;
text-align: center;
font-size: 1.5rem;
}`,
].join('\n'),
}}
></style>
<div className="container">
<div className="slider" ref={slider}>
{sliderData.map((item, index) => (
<Slide key={index} data={item} />
))}
</div>
<Svg
ref={prevButton}
compClass="prev"
compTransform="translate(0 91) rotate(-90)"
/>
<Svg
ref={nextButton}
compClass="next"
compTransform="translate(56.898) rotate(90)"
/>
<div className="trail">
{sliderData.map((item, index) => (
<div
ref={(ref) => {
trail[index] = ref
}}
key={index}
className={index == 0 ? `box${item.id} active` : `box${item.id}`}
>
{item.id}
</div>
))}
</div>
</div>
</>
)
}
function Slide(props) {
const { data } = props
return (
<>
<style
dangerouslySetInnerHTML={{
__html: [
`.slider .box${data.id} {
background-color: ${data.colors.sliderBox};
}
.slider .box${data.id} .illustration .inner {
background-color: ${data.colors.sliderIllustration};
}
.slider .box${data.id} .illustration .inner::after, .slider .box${data.id} .illustration .inner::before {
background-color: ${data.colors.sliderInner};
}
.slider .box${data.id} button {
background-color: ${data.colors.sliderButton};
}`,
].join('\n'),
}}
></style>
<div className={`box${data.id} box`}>
<div className="bg"></div>
<div className="details">
<h1>{data.headerText}</h1>
<p>{data.paragraphText}</p>
<button>{data.buttonText}</button>
</div>
<div className="illustration">
<div className="inner"></div>
</div>
</div>
</>
)
}
const Svg = React.forwardRef((props, ref) => {
console.log('Svg', props)
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width="56.898"
height="91"
className={props.compClass}
viewBox="0 0 56.898 91"
>
<path
fill="#fff"
d="M45.5,0,91,56.9,48.452,24.068,0,56.9Z"
transform={props.compTransform}
></path>
</svg>
)
})
styleNew.css:
*,
*:before,
*:after {
margin: 0;
padding: 0;
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-family: "Roboto", sans-serif;
font-size: 62.5%;
}
@media only screen and (max-width: 800px) {
html {
font-size: 57%;
}
}
body {
background-color: #000;
color: #fff;
padding: 8rem;
}
@media only screen and (max-width: 1000px) {
body {
padding: 0;
}
}
.container {
position: relative;
overflow: hidden;
border-radius: 5rem;
}
@media only screen and (max-width: 1000px) {
.container {
border-radius: 0;
}
}
@media only screen and (max-width: 1000px) {
.slider {
height: 100vh;
}
}
.slider .box {
height: 100%;
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: center;
overflow: hidden;
position: relative;
}
@media only screen and (max-width: 650px) {
.slider .box {
grid-template-columns: 1fr;
grid-template-rows: repeat(2, 1fr);
}
}
.slider .box .bg {
padding: 2rem;
background-color: rgba(0, 0, 0, 0.2);
width: 55%;
transform: skewX(7deg);
position: absolute;
height: 100%;
left: -10%;
padding-left: 20rem;
transform-origin: 0 100%;
}
@media only screen and (max-width: 800px) {
.slider .box .bg {
width: 65%;
}
}
@media only screen and (max-width: 650px) {
.slider .box .bg {
width: 100%;
left: 0;
bottom: 0;
height: 54%;
transform: skewX(0deg);
}
}
.slider .box .bg::before {
content: "";
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: inherit;
pointer-events: none;
transform: skewX(10deg);
}
@media only screen and (max-width: 650px) {
.slider .box .bg::before {
width: 120%;
bottom: 0;
transform: skewX(0deg);
}
}
.slider .box .details {
padding: 5rem;
padding-left: 10rem;
z-index: 100;
grid-column: 1/span 1;
grid-row: 1/-1;
}
@media only screen and (max-width: 650px) {
.slider .box .details {
grid-row: 2/span 1;
grid-column: 1/-1;
text-align: center;
padding: 2rem;
transform: translateY(-9rem);
}
}
.slider .box .details h1 {
font-size: 3.5rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.slider .box .details p {
display: inline-block;
font-size: 1.3rem;
color: #B5B4B4;
margin-bottom: 2rem;
margin-right: 5rem;
}
@media only screen and (max-width: 800px) {
.slider .box .details p {
margin-right: 0;
}
}
.slider .box .details button {
padding: 1rem 3rem;
color: #fff;
border-radius: 2rem;
outline: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.slider .box .details button:hover {
opacity: 0.8;
}
.slider .box .details button:focus {
outline: none;
border: none;
}
.slider .illustration {
grid-column: 2/-1;
grid-row: 1/-1;
justify-self: center;
}
@media only screen and (max-width: 650px) {
.slider .illustration {
grid-row: 1/span 1;
grid-column: 1/-1;
display: flex;
justify-content: center;
align-items: center;
}
}
.slider .illustration div {
height: 25rem;
width: 18rem;
border-radius: 3rem;
background-color: #FF0077;
position: relative;
transform: skewX(-10deg);
}
@media only screen and (max-width: 800px) {
.slider .illustration div {
height: 20rem;
width: 15rem;
}
}
.slider .illustration div::after, .slider .illustration div::before {
content: "";
position: absolute;
height: 100%;
width: 100%;
border-radius: 3rem;
top: 0;
left: 0;
}
.slider .illustration div::after {
transform: translate(4rem, -1rem);
}
.slider .illustration div::before {
transform: translate(2rem, -2rem);
}
.prev,
.next,
.trail {
z-index: 10000;
position: absolute;
}
.prev,
.next {
width: 4rem;
cursor: pointer;
opacity: 0.2;
transition: all 0.3s ease;
}
@media only screen and (max-width: 650px) {
.prev,
.next {
display: none;
}
}
.prev:hover,
.next:hover {
opacity: 1;
}
.prev {
top: 50%;
left: 2%;
transform: translateY(-50%);
}
.next {
top: 50%;
right: 2%;
transform: translateY(-50%);
}
@media only screen and (max-width: 650px) {
.trail {
width: 90%;
bottom: 13%;
}
}
.trail div {
padding: 2rem;
border-top: 3px solid #fff;
cursor: pointer;
opacity: 0.3;
transition: all 0.3s ease;
}
.trail div:hover {
opacity: 0.6;
}
@media only screen and (max-width: 650px) {
.trail div {
padding: 1rem;
}
}
.active {
opacity: 1 !important;
}