Snappy CSS 3D avec React et GreenSock —

L'un des principaux mantras de Jhey est de rendre l'apprentissage amusant. Dans cet article, il vous montre comment améliorer vos compétences en donnant vie à vos idées – sans aucune pression et en oubliant le ludisme dans le code. Avec cet état d'esprit, chaque idée devient une opportunité d'essayer quelque chose de nouveau.
Il est difficile de nommer les choses, n'est-ce pas ? Eh bien, "Flippy Snaps" était la meilleure chose que j'ai pu trouver. 😂 J'ai vu un effet comme celui-ci à la télévision un soir et j'ai pris note de faire quelque chose de similaire. Fabriquer. Cela correspond à toute ma position sur « Le jeu dans le code » pour apprendre. Quoi qu'il en soit, quelques jours plus tard, je me suis assis devant le clavier, et quelques heures plus tard, j'avais ceci :
3D CSS Flippy Snaps ✨
Appuyez pour retourner pour une autre image 👇
⚒️ @ reactjs && @greensock
👉 https://t.co/Na14z40tHE via @CodePen pic.twitter.com/nz6pdQGpmd– Jhey 🐻🛠️✨ (@jh3yy) 8 novembre 2021
Ma dernière démo est une application React, mais nous n'avons pas besoin d'approfondir l'utilisation de React pour expliquer les mécanismes de fonctionnement. Nous créerons l'application React une fois que nous aurons déterminé comment faire fonctionner les choses.
Remarque : Avant de commencer. Il convient de noter que les performances de cette démo sont affectées par la taille de la grille et que les démos sont mieux visualisées dans les navigateurs basés sur Chromium.
Commençons par créer une grille. Disons que nous voulons une grille de 10 par 10. C'est 100 cellules (c'est pourquoi React est pratique pour quelque chose comme ça). Chaque cellule sera constituée d'un élément contenant le recto et le verso d'une carte réversible.
Les styles de notre grille sont assez simples. Nous pouvons utiliser display: grid
et utiliser une propriété personnalisée pour la taille de la grille. Ici, nous utilisons par défaut 10
.
.flippy-snap {
affichage : grille ;
espacement de la grille : 1px ;
grid-template-columns: repeat(var(--grid-size, 10), 1fr);
grid-template-rows: repeat(var(--grid-size, 10), 1fr);
}
Nous n'utiliserons pas grid-gap
dans la démo finale, mais c'est bien pour voir les cellules plus facilement pendant le développement.
Voir le stylo [1. Creating a Grid](https://codepen .io/smashingmag/pen/porXNzB) par JHEY
Ensuite, nous devons styliser les côtés de nos cartes et afficher les images. Nous pouvons le faire en tirant parti des propriétés personnalisées CSS en ligne. Commençons par mettre à jour le balisage. Nous avons besoin que chaque carte connaisse sa position x
et y
dans la grille.
Pour la démo, j'utilise Pug
pour générer ceci pour moi. Vous pouvez voir le code HTML compilé en cliquant sur « Afficher le code HTML compilé » dans la démo.
- const GRID_SIZE = 10
- const COUNT = Math.pow(GRID_SIZE, 2)
.flippy-snap
- for(let f = 0; f
Ensuite, nous avons besoin de quelques styles.
.flippy-card {
--current-image: url("https://random-image.com/768");
--next-image: url("https://random-image.com/124");
hauteur : 100 % ;
largeur : 100 % ;
position : relative ;
}
.flippy-card__front,
.flippy-card__rear {
position : absolue ;
hauteur : 100 % ;
largeur : 100 % ;
visibilité au verso : masquée ;
image d'arrière-plan : var(--image-actuelle);
position d'arrière-plan : calc(var(--x, 0) * -100%) calc(var(--y, 0) * -100%);
background-size: calc(var(--grid-size, 10) * 100%);
}
.flippy-card__rear {
image d'arrière-plan : var(--image-suivante);
transformer : rotationY(180deg) rotation(180deg);
}
L'arrière de la carte obtient sa position en utilisant une combinaison de rotations via transform
. Mais, la partie intéressante est de savoir comment nous montrons la partie image pour chaque carte. Dans cette démo, nous utilisons une propriété personnalisée pour définir les URL de deux images. Et puis nous les définissons comme image d'arrière-plan
pour chaque face de carte.
Mais le problème est de savoir comment nous définissons la background-size
et background-position
. En utilisant les propriétés personnalisées --x
et --y
nous multiplions la valeur par -100 %
. Ensuite, nous définissons background-size
sur --grid-size
multiplié par 100%
. Cela affiche la bonne partie de l'image pour une carte donnée.
Voir le stylo [2. Adding an Image](https://codepen.io/smashingmag/pen/gOxNLPz) par JHEY
Vous avez peut-être remarqué que nous avions --current-image
et --next-image
. Mais, actuellement, il n'y a aucun moyen de voir l'image suivante. Pour cela, nous avons besoin d'un moyen de retourner nos cartes. Nous pouvons utiliser une autre propriété personnalisée pour cela.
Introduisons une propriété --count
et définissons une transform
pour nos cartes :
.flippy-snap {
--compte : 0 ;
perspective : 50vmin ;
}
.flippy-card {
transform: rotateX(calc(var(--count) * -180deg));
transition : transformer 0,25 s ;
style de transformation : conserver-3d ;
}
Nous pouvons définir la propriété --count
sur l'élément conteneur. La portée signifie que toutes les cartes peuvent récupérer cette valeur et l'utiliser pour transformer
leur rotation sur l'axe des x. Nous devons également définir transform-style: preserve-3d
pour que nous voyions le dos des cartes. Définir une perspective
nous donne cette perspective 3D.
Cette démo vous permet de mettre à jour la valeur de la propriété --count
afin que vous puissiez voir l'effet qu'elle a.
Voir le stylo [3. Turning Cards](https://codepen.io/smashingmag/pen/LYjKbZW) par JHEY
À ce stade, vous pouvez l'envelopper et définir un simple gestionnaire de clics qui incrémente --count
de un à chaque clic.
const SNAP = document.querySelector('.flippy-snap')
laisser compter = 0
const UPDATE = () => SNAP.style.setProperty('--count', count++)
SNAP.addEventListener('click', UPDATE)
Supprimez le grid-gap
et vous obtiendrez ceci. Cliquez sur le snap pour le retourner.
Voir le stylo [4. Boring Flips](https://codepen.io/smashingmag/pen/eYEwBdN) par JHEY
Maintenant que nous avons élaboré les mécanismes de base, il est temps de transformer cela en une application React. Il y a un peu à décomposer ici.
const App = () => {
const [snaps, setSnaps] = useState([])
const [disabled, setDisabled] = useState(true)
const [gridSize, setGridSize] = useState(9)
const snapRef = useRef (null)
const grabPic = async () => {
const pic = wait fetch ('https://source.unsplash.com/random/1000x1000')
retour pic.url
}
useEffect(() => {
configuration const = async () => {
const url = wait grabPic()
const nextUrl = wait grabPic()
setSnaps([url, nextUrl])
setDisabled(false)
}
mettre en place()
}, [])
const setNewImage = compte async => {
const newSnap = wait grabPic()
setSnaps(
compte.courant % 2 !== 0 ? [newSnap, snaps[1]] : [snaps[0]newSnap]
)
setDisabled(false)
}
const onFlip = compte async => {
setDisabled(true)
setNewImage(count)
}
if (snaps.length !== 2) return Chargement...
revenir (
)
}
Notre composant App
gère la saisie des images et leur transmission à notre composant FlippySnap
. C'est l'essentiel de ce qui se passe ici. Pour cette démo, nous récupérons des images de Unsplash.
const grabPic = async () => {
const pic = wait fetch ('https://source.unsplash.com/random/1000x1000')
retour pic.url
}
// L'effet initial saisit deux snaps à utiliser par FlippySnap
useEffect(() => {
configuration const = async () => {
const url = wait grabPic()
const nextUrl = wait grabPic()
setSnaps([url, nextUrl])
setDisabled(false)
}
mettre en place()
}, [])
S'il n'y a pas deux snaps à afficher, alors nous affichons un message "Chargement…".
if (snaps.length !== 2) return Chargement...
Si nous récupérons une nouvelle image, nous devons désactiver FlippySnap
afin que nous ne puissions pas le spammer.
Nous laissons App
dicte les clichés qui sont affichés par FlippySnap
et dans quel ordre. À chaque retournement, nous récupérons une nouvelle image et, en fonction du nombre de fois que nous avons retourné, nous définissons les bons clichés. L'alternative serait de définir les snaps et de laisser le composant déterminer l'ordre.
const setNewImage = async count => {
const newSnap = wait grabPic() // Saisir le snap
setSnaps(
compte.courant % 2 !== 0 ? [newSnap, snaps[1]] : [snaps[0]newSnap]
) // Définir les snaps en fonction du "compte" actuel que nous obtenons de FlippySnap
setDisabled(false) // Activer à nouveau les clics
}
const onFlip = compte async => {
setDisabled(true) // Désactiver afin que nous ne puissions pas spammer le clic
setNewImage (count) // Prenez un nouveau snap à afficher
}
À quoi pourrait ressembler FlippySnap
? Il n'y a pas grand chose du tout !
const FlippySnap = ({ disabled, gridSize, onFlip, snaps }) => {
const CELL_COUNT = Math.pow(gridSize, 2)
const count = useRef(0)
const flip = e => {
si (désactivé) retour
count.current = count.current + 1
if (onFlip) onFlip(count)
}
revenir (
)
}
Le composant gère le rendu de toutes les cartes et la définition des propriétés personnalisées en ligne. Le gestionnaire onClick
pour le conteneur incrémente le count
. Il déclenche également le rappel onFlip
. Si l'état est actuellement désactivé
il ne fait rien. Ce retournement de l'état désactivé
et la saisie d'un nouveau snap déclenchent le retournement lorsque le composant est rendu à nouveau.
Voir le Pen [5. React Foundation](https://codepen.io/smashingmag/pen/wvqLdGY ) par JHEY
Nous avons un composant React qui va maintenant parcourir les images aussi longtemps que nous voulons continuer à en demander de nouvelles. Mais, cette transition flip est un peu ennuyeuse. Pour le pimenter, nous allons utiliser GreenSock et ses utilitaires. En particulier, l'utilitaire "distribute". Cela nous permettra de répartir le délai de retournement de nos cartes dans une rafale semblable à une grille, quel que soit l'endroit où nous cliquons. Pour ce faire, nous allons utiliser GreenSock pour animer la valeur --count
sur chaque carte.
Il est à noter que nous avons ici le choix. Nous pourrions choisir d'appliquer les styles avec GreenSock. Au lieu d'animer la valeur de la propriété --count
nous pourrions animer rotateX
. Nous pourrions le faire en nous basant sur la référence count
que nous avons. Et cela vaut également pour toutes les autres choses que nous choisissons d'animer avec GreenSock dans cet article. C'est une question de préférence et de cas d'utilisation. Vous pouvez penser que la mise à jour de la valeur de la propriété personnalisée est logique. L'avantage étant que vous n'avez pas besoin de mettre à jour JavaScript pour obtenir un comportement de style différent. Nous pourrions changer le CSS pour utiliser rotateY
par exemple.
Notre fonction mise à jour flip
pourrait ressembler à ceci :
const flip = e => {
si (désactivé) retour
const x = parseInt(e.target.parentNode.getAttribute('data-snap-x'), 10)
const y = parseInt(e.target.parentNode.getAttribute('data-snap-y'), 10)
count.current = count.current + 1
gsap.to(containerRef.current.querySelectorAll('.flippy-card'), {
'--count': count.current,
délai : gsap.utils.distribute({
de : [x / gridSize, y / gridSize],
montant : taille de la grille / 20,
base : 0,
grille : [gridSize, gridSize],
facilité : 'power1.inOut',
}),
durée : 0,2,
onComplete: () => {
// À ce stade, mettez à jour les images
if (onFlip) onFlip(count)
},
})
}
Notez comment nous obtenons une valeur x
et y
en lisant les attributs de la carte cliquée. Pour cette démo, nous avons choisi d'ajouter des attributs data
à chaque carte. Ces attributs communiquent la position d'une carte dans la grille. Nous utilisons également un nouveau ref
appelé containerRef
. C'est ainsi que nous référençons uniquement les cartes pour une instance FlippySnap
lors de l'utilisation de GreenSock.
{new Array(CELL_COUNT).fill().map((cell, index) => {
const x = index % gridSize
const y = Math.floor(index / gridSize)
revenir (
)
})}
Une fois que nous obtenons ces valeurs x
et y
nous pouvons les utiliser dans notre animation. En utilisant gsap.to
nous voulons animer la propriété personnalisée --count
pour chaque .flippy-card
enfant de containerRef
.
Pour répartir le délai à partir duquel nous cliquons, nous définissons la valeur de delay
pour utiliser gsap.utils.distribute
. La valeur from
de la fonction distribute
prend un tableau contenant des rapports le long des axes x et y. Pour obtenir cela, nous divisons x
et y
par gridSize
. La valeur base
est la valeur initiale. Pour cela, nous voulons 0
délai sur la carte sur laquelle nous cliquons. Le montant
est la valeur la plus élevée. Nous avons opté pour gridSize / 20
mais vous pouvez expérimenter avec différentes valeurs. Quelque chose basé sur le gridSize
est une bonne idée cependant. La valeur grid
indique à GreenSock la taille de la grille à utiliser lors du calcul de la distribution. Enfin, le ease
définit la facilité de la distribution delay
.
gsap.to(containerRef.current.querySelectorAll('.flippy-card'), {
'--count': count.current,
délai : gsap.utils.distribute({
de : [x / gridSize, y / gridSize],
montant : taille de la grille / 20,
base : 0,
grille : [gridSize, gridSize],
facilité : 'power1.inOut',
}),
durée : 0,2,
onComplete: () => {
// À ce stade, mettez à jour les images
if (onFlip) onFlip(count)
},
})
Comme pour le reste de l'animation, nous utilisons une durée de retournement de 0,2
secondes. Et nous utilisons onComplete
pour appeler notre rappel. Nous passons le flip count
au callback afin qu'il puisse l'utiliser pour déterminer l'ordre d'accrochage. Des choses comme la durée du retournement pourraient être configurées en passant différents props
si nous le souhaitions.
L'assemblage nous donne ceci :
Voir le stylo [6. Distributed Flips with GSAP](https:// codepen.io/smashingmag/pen/VwzJbpM) par JHEY
Ceux qui aiment pousser un peu les choses ont peut-être remarqué que nous pouvons toujours « spam » cliquer sur le snap. Et c'est parce que nous ne désactivons pas FlippySnap
tant que GreenSock n'est pas terminé. Pour résoudre ce problème, nous pouvons utiliser une référence interne que nous basculons au début et à la fin de l'utilisation de GreenSock.
const flipping = useRef(false) // Nouvelle référence pour suivre l'état de retournement
const flip = e => {
if (désactivé || flipping.current) return
const x = parseInt(e.target.parentNode.getAttribute('data-snap-x'), 10)
const y = parseInt(e.target.parentNode.getAttribute('data-snap-y'), 10)
count.current = count.current + 1
gsap.to(containerRef.current.querySelectorAll('.flippy-card'), {
'--count': count.current,
délai : gsap.utils.distribute({
de : [x / gridSize, y / gridSize],
montant : taille de la grille / 20,
base : 0,
grille : [gridSize, gridSize],
facilité : 'power1.inOut',
}),
durée : 0,2,
onStart : () => {
flipping.current = vrai
},
onComplete: () => {
// À ce stade, mettez à jour les images
flipping.current = false
if (onFlip) onFlip(count)
},
})
}
Et maintenant, nous ne pouvons plus spammer, cliquez sur notre FlippySnap
!
Voir le stylo [7. No Spam Clicks](https://codepen.io/smashingmag/pen/jOLjmXE) de JHEY
Maintenant, il est temps pour quelques touches supplémentaires. Pour le moment, il n'y a aucun signe visuel indiquant que nous pouvons cliquer sur notre FlippySnap
. Et si quand on planait, les cartes se lèvent vers nous ? Nous pourrions utiliser onPointerOver
et utiliser à nouveau l'utilitaire « distribuer ».
const indiquer = e => {
const x = parseInt(e.currentTarget.getAttribute('data-snap-x'), 10)
const y = parseInt(e.currentTarget.getAttribute('data-snap-y'), 10)
gsap.to(containerRef.current.querySelectorAll('.flippy-card'), {
'--hovered' : gsap.utils.distribute({
de : [x / gridSize, y / gridSize],
base : 0,
montant : 1,
grille : [gridSize, gridSize],
facilité : 'power1.inOut'
}),
durée : 0,1,
})
}
Ici, nous définissons une nouvelle propriété personnalisée sur chaque carte nommée --hovered
. Ceci est défini sur une valeur de 0
à 1
. Ensuite, dans notre CSS, nous allons mettre à jour nos styles de cartes pour surveiller la valeur.
.flippy-card {
transform: translate3d(0, 0, calc((1 - (var(--hovered, 1))) * 5vmin))
rotateX(calc(var(--count) * -180deg));
}
Ici, nous disons qu'une carte se déplacera sur l'axe z au plus 5vmin
.
Nous appliquons ensuite cela à chaque carte en utilisant la prop onPointerOver
.[19659014]{new Array(CELL_COUNT).fill().map((cell, index) => {
const x = index % gridSize
const y = Math.floor(index / gridSize)
revenir (
)
})}
Et lorsque notre pointeur quitte notre FlippySnap
nous voulons réinitialiser nos positions de carte.
const réinitialiser = () => {
gsap.to(containerRef.current.querySelectorAll('.flippy-card'), {
'--survolé' : 1,
durée : 0,1,
})
}
Et nous pouvons appliquer cela avec la prop onPointerLeave
.
Rassemblez tout cela et nous obtenons quelque chose comme ça. Essayez de déplacer votre pointeur dessus.
Voir le stylo [8. Visual Inidication with Raised Cards](https://codepen.io/smashingmag/pen/wvqLdZL) par JHEY
Et ensuite ? Que diriez-vous d'un indicateur de chargement pour que nous sachions quand notre App
récupère l'image suivante ? Nous pouvons rendre un spinner de chargement lorsque notre FlippySnap
est désactivé
.
{disabled && }
Il styles pour lesquels pourrait faire un cercle tournant.
.flippy-snap__loader {
rayon de bordure : 50 % ;
bordure : 6px solide #fff ;
border-left-color : #000 ;
border-right-color : #000 ;
position : absolue ;
à droite : 10 % ;
bas : 10 % ;
hauteur : 8 % ;
largeur : 8 % ;
transformer : translate3d(0, 0, 5vmin) rotate(0deg);
animation : spin 1s infini ;
}
@keyframes spin {
à {
transformer : translate3d(0, 0, 5vmin) rotate(360deg);
}
}
Et cela nous donne un indicateur de chargement lors de la saisie d'une nouvelle image.
Voir le stylo [9. Add Loading Indicator](https://codepen.io/smashingmag/pen/qBXzmzx) par JHEY
C'est tout !
C'est ainsi que nous pouvons créer un FlippySnap
avec React et GreenSock. C'est amusant de faire des choses que nous ne pouvons pas créer au quotidien. Des démos comme celle-ci peuvent poser différents défis et améliorer votre jeu de résolution de problèmes.
Je suis allé un peu plus loin et j'ai ajouté un léger effet de parallaxe avec du son. Vous pouvez également configurer la taille de la grille ! (Les grandes grilles affectent cependant les performances.)
Voir le stylo [3D CSS Flippy Snaps v2 (React && GSAP)](https://codepen.io/smashingmag/pen/QWMXgLb) par JHEY
Il convient de noter que cette démo fonctionne mieux dans les navigateurs basés sur Chromium.
Alors, où le prendriez-vous ensuite ? J'aimerais voir si je peux le recréer avec Three.js ensuite. Cela réglerait la performance. 😅
Restez génial ! ʕ•ᴥ•ʔ

Source link