Aguarde...

17 de julho de 2022

Como criar uma transição de página de rosto

Como criar uma transição de página de rosto

Hoje estamos olhando sob o capô de uma transição de página baseada na foto Dribbble de Vitalii Burhonskyi.

Outro dia eu vi essa bela foto do Dribbble de Vitalii Burhonskyi que mostra várias transições adoráveis. Uma delas é o que eu chamaria de “transição de capa” onde uma capa preta é animada para esconder algum conteúdo e então algum novo conteúdo é revelado na parte superior da capa (que é de uma cor diferente da vista anterior).

Adoro o fato de podermos ter uma infinidade de animações diferentes aqui para “desrevelar” ou mostrar o novo conteúdo. Então eu criei uma sequência que vamos explorar hoje em um pequeno tutorial onde veremos alguns destaques da estrutura e animações.

Usaremos o GSAP do GreenSock como biblioteca de animação.

Marcação e estilos

Nossa visão inicial mostrará três itens em uma estrutura de grade:

Como criar uma transição de página de rosto

Vamos dar uma olhada na marcação para isso. Teremos uma divisão para todos os itens:

<div class="content">
	<div class="item">
		<span class="item__meta">2020</span>
		<h2 class="item__title">Alex Moulder</h2>
		<div class="item__img"><div class="item__img-inner" style="background-image:url(img/1.jpg)"></div></div>
		<p class="item__desc">I am only waiting for love to give myself up at last into his hands. That is why it is so late and why I have been guilty of such omissions.</p>
		<a class="item__link">view</a>
	</div>
	<div class="item">
		<span class="item__meta">2021</span>
		<h2 class="item__title">Aria Bennett</h2>
		<div class="item__img"><div class="item__img-inner" style="background-image:url(img/2.jpg)"></div></div>
		<p class="item__desc">They come with their laws and their codes to bind me fast; but I evade them ever, for I am only waiting for love to give myself up at last into his hands.</p>
		<a class="item__link">view</a>
	</div>
	<div class="item">
		<span class="item__meta">2022</span>
		<h2 class="item__title">Jimmy Hughes</h2>
		<div class="item__img"><div class="item__img-inner" style="background-image:url(img/3.jpg)"></div></div>
		<p class="item__desc">Clouds heap upon clouds and it darkens. Ah, love, why dost thou let me wait outside at the door all alone?</p>
		<a class="item__link">view</a>
	</div>
</div>

Primeiro, toda a nossa página terá um layout de grade:

main {
	padding: 1.5rem 2.5rem 3rem;
	height: 100vh;
	display: grid;
	grid-template-columns: 100%;
	grid-template-areas: 'frame' 'content';
	grid-template-rows: min-content 1fr;
	grid-row-gap: 8vh;
}

A divisão de conteúdo terá o seguinte estilo para mostrar os itens em uma grade:

.content {
	grid-area: content;
	max-width: 400px;
}

@media screen and (min-width: 53em) {
	.content {
		max-width: none;
		display: grid;
		grid-template-columns: repeat(3,1fr);
		grid-template-rows: 100%;
		grid-column-gap: 5vw;
	}
}

Queremos apenas mostrar os itens lado a lado quando estamos em uma tela grande. Então, adicionamos uma consulta de mídia.

Para os elementos internos do item teremos o seguinte estilo:

.item {
	margin-bottom: 5rem;
	display: grid;
	grid-template-columns: 100%;
	grid-template-rows: 1rem auto auto 1fr auto;
}

.item__title {
	font-family: kudryashev-d-excontrast-sans, sans-serif;
	font-weight: 300;
	font-size: 2rem;
	margin-bottom: 0.5rem;
}

.item__img {
	position: relative;
	overflow: hidden;
	width: 100%;
	aspect-ratio: 500/333;
}

.item__img-inner {
	background-position: 50% 45%;
	background-size: cover;
	width: 100%;
	height: 100%;
}

.item__desc {
	margin-top: 2.5rem;
	line-height: 1.1;
}

.item__link {
	cursor: pointer;
	text-transform: lowercase;
	width: 100%;
	padding: 1rem;
	color: var(--color-text);
	border: 1px solid var(--color-border);
	border-radius: 2rem;
	text-align: center;
}

.item__link:hover {
	background: var(--color-text);
	border-color: var(--color-text);
	color: var(--color-text-alt);
}

@media screen and (min-width: 53em) {
	.item {
		margin-bottom: 0;
	}
	.item__title {
		font-size: clamp(1.25rem,3vw,2rem);
	}
}

O elemento image tem uma estrutura aninhada que nos permitirá fazer um pequeno efeito de zoom. Uma coisa interessante aqui é a propriedade aspect-ratio que nos permite definir as dimensões da imagem responsiva de acordo com seu tamanho real enquanto usamos a propriedade background-image.

Ao clicar no botão do item, mostraremos uma animação de capa. Estes serão os dois elementos animando sua transformação de escala para cobrir toda a página:

<div class="overlay">
	<div class="overlay__row"></div>
	<div class="overlay__row"></div>
</div>

Vamos adicionar o seguinte estilo:

.overlay {
	position: fixed;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	display: grid;
	grid-template-columns: 100%;
	pointer-events: none;
	grid-template-rows: repeat(2,1fr);
}

.overlay__row {
	background: var(--color-overlay);
	transform: scaleY(0);
	will-change: transform;
}

.overlay__row:first-child {
	transform-origin: 50% 0%;
}

.overlay__row:last-child {
	transform-origin: 50% 100%;
}

Definir as origens de transformação corretas para cada uma das “linhas” garantirá que elas sejam animadas de cima para baixo, “fechando” a visualização atual e ocultando-a.

Em seguida, vamos dar uma olhada na visão que veremos depois. Esta seção será chamada de “visualizações” e adicionaremos o seguinte conteúdo:

<section class="previews">
	<div class="preview">
		<div class="preview__img"><div class="preview__img-inner" style="background-image:url(img/1_big.jpg)"></div></div>
		<h2 class="preview__title oh"><span class="oh__inner">Moulder</span></h2>
		<div class="preview__column preview__column--start">
			<span class="preview__column-title preview__column-title--main oh"><span class="oh__inner">Alex Moulder</span></span>
			<span class="oh"><span class="oh__inner">2020</span></span>
		</div>
		<div class="preview__column">
			<h3 class="preview__column-title oh"><span class="oh__inner">Location</span></h3>
			<p>And if it rains, a closed car at four. And we shall play a game of chess, pressing lidless eyes and waiting for a knock upon the door.</p>
		</div>
		<div class="preview__column">
			<h3 class="preview__column-title oh"><span class="oh__inner">Material</span></h3>
			<p>At the violet hour, when the eyes and back, turn upward from the desk, when the human engine waits.</p>
		</div>
		<button class="unbutton preview__back"><svg width="100px" height="18px" viewBox="0 0 50 9"><path vector-effect="non-scaling-stroke" d="m0 4.5 5-3m-5 3 5 3m45-3h-77"></path></svg></button>
	</div>

	<!-- preview -->

	<!-- preview -->
	
</section>
Como criar uma transição de página de rosto

A imagem grande será animada com uma animação de revelação/revelação que é explicada em detalhes neste tutorial. É por isso que usamos uma estrutura aninhada como na imagem do item. Para os textos que queremos mostrar traduzindo (e sendo cortados pelo pai), usaremos a estrutura .oh > .oh__inner. A ideia por trás disso é traduzir o elemento oh__inner para escondê-lo. Para texto de várias linhas, adicionaremos essa estrutura dinamicamente com JavaScript. Os parágrafos em nossas divisões preview__column serão divididos em linhas usando SplitType .

Vamos adicionar os seguintes estilos para que a magia da linha funcione:

.oh {
	position: relative;
    overflow: hidden;
}

.oh__inner {
	will-change: transform;
    display: inline-block;
}

.line {
	transform-origin: 0 50%;
	white-space: nowrap;
	will-change: transform;
}

Agora, vamos animar esse bebê.

O JavaScript

Vamos definir e instanciar algumas coisas primeiro:

import { gsap } from 'gsap';
import { Item } from './item';
import { Preview } from './preview';

// body element
const body = document.body;

// .content element
const contentEl = document.querySelector('.content');

// frame element
const frameEl = document.querySelector('.frame');

// top and bottom overlay overlay elements
const overlayRows = [...document.querySelectorAll('.overlay__row')];

// Preview instances array
const previews = [];
[...document.querySelectorAll('.preview')].forEach(preview => previews.push(new Preview(preview)));

// Item instances array
const items = [];
[...document.querySelectorAll('.item')].forEach((item, pos) => items.push(new Item(item, previews[pos])));

Agora, quando abrimos um item, primeiro configuramos nosso conteúdo para não ser mais clicável. Isso é feito com uma classe.

Em seguida, ocultamos todas as linhas e elementos que desejamos animar assim que mostramos o conteúdo de visualização. A classe preview-visible nos ajuda a definir algumas cores e eventos de ponteiro. Também o usamos para ocultar nosso pequeno quadro no topo da página, para que possamos mostrá-lo novamente com uma animação quando a capa ocultar a visualização inicial.

A imagem não é revelada ao traduzir o elemento de imagem em uma direção e o elemento interno (que na verdade contém a imagem de fundo) na direção oposta.

Também mostramos todas as linhas e elementos oh__inner finalmente.

const openItem = item => {
    
    gsap.timeline({
        defaults: {
            duration: 1, 
            ease: 'power3.inOut'
        }
    })
    .add(() => {
        // pointer events none to the content
        contentEl.classList.add('content--hidden');
    }, 'start')

    .addLabel('start', 0)
    .set([item.preview.DOM.innerElements, item.preview.DOM.backCtrl], {
        opacity: 0
    }, 'start')
    .to(overlayRows, {
        scaleY: 1
    }, 'start')

    .addLabel('content', 'start+=0.6')

    .add(() => {
        body.classList.add('preview-visible');

        gsap.set(frameEl, {
            opacity: 0
        }, 'start')
        item.preview.DOM.el.classList.add('preview--current');
    }, 'content')
    // Image animation (reveal animation)
    .to([item.preview.DOM.image, item.preview.DOM.imageInner], {
        startAt: {y: pos => pos ? '101%' : '-101%'},
        y: '0%'
    }, 'content')
    
    .add(() => {
        for (const line of item.preview.multiLines) {
            line.in();
        }
        gsap.set(item.preview.DOM.multiLineWrap, {
            opacity: 1,
            delay:0.1
        })
    }, 'content')
    // animate frame element
    .to(frameEl, {
        ease: 'expo',
        startAt: {y: '-100%', opacity: 0},
        opacity: 1,
        y: '0%'
    }, 'content+=0.3')
    .to(item.preview.DOM.innerElements, {
        ease: 'expo',
        startAt: {yPercent: 101},
        yPercent: 0,
        opacity: 1
    }, 'content+=0.3')
    .to(item.preview.DOM.backCtrl, {
        opacity: 1
    }, 'content')

};

Quando fecharmos a visualização, precisaremos fazer algumas animações reversas:

const closeItem = item => {
    
    gsap.timeline({
        defaults: {
            duration: 1, 
            ease: 'power3.inOut'
        }
    })
    .addLabel('start', 0)
    .to(item.preview.DOM.innerElements, {
        yPercent: -101,
        opacity: 0,
    }, 'start')
    .add(() => {
        for (const line of item.preview.multiLines) {
            line.out();
        }
    }, 'start')
    
    .to(item.preview.DOM.backCtrl, {
        opacity: 0
    }, 'start')

    .to(item.preview.DOM.image, {
        y: '101%'
    }, 'start')
    .to(item.preview.DOM.imageInner, {
        y: '-101%'
    }, 'start')
    
    // animate frame element
    .to(frameEl, {
        opacity: 0,
        y: '-100%',
        onComplete: () => {
            body.classList.remove('preview-visible');
            gsap.set(frameEl, {
                opacity: 1,
                y: '0%'
            })
        }
    }, 'start')

    .addLabel('grid', 'start+=0.6')

    .to(overlayRows, {
        //ease: 'expo',
        scaleY: 0,
        onComplete: () => {
            item.preview.DOM.el.classList.remove('preview--current');
            contentEl.classList.remove('content--hidden');
        }
    }, 'grid')
};

Não vamos esquecer os eventListeners:

for (const item of items) {
    // Opens the item preview
    item.DOM.link.addEventListener('click', () => openItem(item));
    // Closes the item preview
    item.preview.DOM.backCtrl.addEventListener('click', () => closeItem(item));
}

Você pode fazer muitas coisas diferentes aqui, tanto para animar o conteúdo quanto para fora. A chave é manter as coisas interessantes ao animar e fazer uma animação de fechamento rápido para que o usuário não precise esperar muito para que a visualização inicial volte 😉 Agora é a sua vez! Explore o código e tente fazer algumas outras animações, timing e easings para dar outra sensação e ver o que funciona e o que não funciona! Espero que você se divirta!

Postado em Blog
Escreva um comentário