Estudo de caso - Efeito Page Flip do 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Introdução

Em 2010, o F-i.com e a equipe do Google Chrome colaboraram em um app da Web educacional baseado em HTML5 chamado 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). Uma das principais ideias por trás desse projeto era que ele seria melhor apresentado no contexto de um livro. Como o conteúdo do livro é sobre tecnologias da Web abertas, achamos importante manter essa fidelidade, fazendo com que o contêiner seja um exemplo do que essas tecnologias nos permitem alcançar hoje.

Capa do livro e página inicial de "20 Things I Learned About Browsers and the Web"
Capa do livro e página inicial de "20 Things I Learned About Browsers and the Web" (www.20thingsilearned.com)

Decidimos que a melhor maneira de alcançar a sensação de um livro real é simular as partes boas da experiência de leitura analógica aproveitando os benefícios do mundo digital em áreas como a navegação. Muito esforço foi feito no tratamento gráfico e interativo do fluxo de leitura, especialmente como as páginas dos livros viram de uma para outra.

Primeiros passos

Neste tutorial, você vai aprender a criar seu próprio efeito de virada de página usando o elemento canvas e muito JavaScript. Parte do código rudimentar, como declarações de variáveis e assinatura de listener de eventos, foi deixada de fora dos snippets neste artigo. Portanto, lembre-se de fazer referência ao exemplo funcional.

Antes de começar, é recomendável confira a demonstração para saber o que vamos criar.

Marcação

É sempre importante lembrar que o que desenhamos na tela não pode ser indexado por mecanismos de pesquisa, selecionado por um visitante ou encontrado por pesquisas no navegador. Por esse motivo, o conteúdo com que vamos trabalhar é colocado diretamente no DOM e manipulado por JavaScript, se disponível. A marcação necessária para isso é mínima:

<div id='book'>
<canvas id='pageflip-canvas'></canvas>
<div id='pages'>
<section>
    <div> <!-- Any type of contents here --> </div>
</section>
<!-- More <section>s here -->
</div>
</div>

Temos um elemento de contêiner principal para o livro, que por sua vez contém as diferentes páginas do livro e o elemento canvas em que vamos desenhar as páginas. Dentro do elemento section, há um wrapper div para o conteúdo. Precisamos disso para poder mudar a largura da página sem afetar o layout do conteúdo. O div tem uma largura fixa, e o section é definido para ocultar o overflow. Isso resulta na largura do section atuando como uma máscara horizontal para o div.

Livro aberto.
Uma imagem de plano de fundo com a textura de papel e a capa marrom do livro é adicionada ao elemento do livro.

Lógica

O código necessário para ativar a virada de página não é muito complexo, mas é bastante extenso, já que envolve muitos gráficos gerados proceduralmente. Vamos começar analisando a descrição dos valores constantes que vamos usar em todo o código.

var BOOK_WIDTH = 830;
var BOOK_HEIGHT = 260;
var PAGE_WIDTH = 400;
var PAGE_HEIGHT = 250;
var PAGE_Y = ( BOOK_HEIGHT - PAGE_HEIGHT ) / 2;
var CANVAS_PADDING = 60;

O CANVAS_PADDING é adicionado ao redor da tela para que possamos ter o papel estendido para fora do livro ao virar as páginas. Algumas das constantes definidas aqui também são definidas no CSS. Portanto, se você quiser mudar o tamanho do livro, também será necessário atualizar os valores.

Constantes.
Os valores constantes usados em todo o código para rastrear a interação e desenhar a virada de página.

Em seguida, precisamos definir um objeto de virada para cada página. Eles serão atualizados constantemente à medida que interagimos com o livro para refletir o status atual da virada.

// Create a reference to the book container element
var book = document.getElementById( 'book' );

// Grab a list of all section elements (pages) within the book
var pages = book.getElementsByTagName( 'section' );

for( var i = 0, len = pages.length; i < len; i++ ) {
pages[i].style.zIndex = len - i;

flips.push( {
progress: 1,
target: 1,
page: pages[i],
dragging: false
});
}

Primeiro, precisamos garantir que as páginas estejam em camadas corretamente, organizando os z-indexes dos elementos da seção para que a primeira página fique na parte de cima e a última na parte de baixo. As propriedades mais importantes dos objetos de inversão são os valores progress e target. Eles são usados para determinar até onde a página precisa ser dobrada. -1 significa totalmente para a esquerda, 0 significa o centro do livro e +1 significa a borda mais à direita do livro.

Progresso.
O progresso e os valores de destino das viradas são usados para determinar onde a página de dobra deve ser desenhada em uma escala de -1 a +1.

Agora que temos um objeto de virada definido para cada página, precisamos começar a capturar e usar a entrada do usuário para atualizar o estado da virada.

function mouseMoveHandler( event ) {
// Offset mouse position so that the top of the book spine is 0,0
mouse.x = event.clientX - book.offsetLeft - ( BOOK_WIDTH / 2 );
mouse.y = event.clientY - book.offsetTop;
}

function mouseDownHandler( event ) {
// Make sure the mouse pointer is inside of the book
if (Math.abs(mouse.x) < PAGE_WIDTH) {
if (mouse.x < 0 &amp;&amp; page - 1 >= 0) {
    // We are on the left side, drag the previous page
    flips[page - 1].dragging = true;
}
else if (mouse.x > 0 &amp;&amp; page + 1 < flips.length) {
    // We are on the right side, drag the current page
    flips[page].dragging = true;
}
}

// Prevents the text selection
event.preventDefault();
}

function mouseUpHandler( event ) {
for( var i = 0; i < flips.length; i++ ) {
// If this flip was being dragged, animate to its destination
if( flips[i].dragging ) {
    // Figure out which page we should navigate to
    if( mouse.x < 0 ) {
    flips[i].target = -1;
    page = Math.min( page + 1, flips.length );
    }
    else {
    flips[i].target = 1;
    page = Math.max( page - 1, 0 );
    }
}

flips[i].dragging = false;
}
}

A função mouseMoveHandler atualiza o objeto mouse para que sempre trabalhemos com a localização mais recente do cursor.

Em mouseDownHandler, começamos verificando se o mouse foi pressionado na página esquerda ou direita para sabermos em qual direção queremos começar a virar. Também verificamos se há outra página nessa direção, já que podemos estar na primeira ou na última página. Se uma opção de virada válida estiver disponível após essas verificações, vamos definir a flag dragging do objeto de virada correspondente como true.

Quando chegamos ao mouseUpHandler, analisamos todas as flips e verificamos se alguma delas foi sinalizada como dragging e precisa ser liberada. Quando uma inversão é lançada, definimos o valor de destino para corresponder ao lado para o qual ela precisa ser invertida, dependendo da posição atual do mouse. O número da página também é atualizado para refletir essa navegação.

Renderização

Agora que a maior parte da nossa lógica está em vigor, vamos mostrar como renderizar o papel dobrável no elemento da tela. A maior parte disso acontece dentro da função render(), que é chamada 60 vezes por segundo para atualizar e desenhar o estado atual de todas as viradas ativas.

function render() {
// Reset all pixels in the canvas
context.clearRect( 0, 0, canvas.width, canvas.height );

for( var i = 0, len = flips.length; i < len; i++ ) {
var flip = flips[i];

if( flip.dragging ) {
    flip.target = Math.max( Math.min( mouse.x / PAGE_WIDTH, 1 ), -1 );
}

// Ease progress towards the target value
flip.progress += ( flip.target - flip.progress ) * 0.2;

// If the flip is being dragged or is somewhere in the middle
// of the book, render it
if( flip.dragging || Math.abs( flip.progress ) < 0.997 ) {
    drawFlip( flip );
}

}
}

Antes de começar a renderizar o flips, redefinimos a tela usando o método clearRect(x,y,w,h). Limpar toda a tela tem um custo de desempenho muito alto, e seria muito mais eficiente limpar apenas as regiões em que estamos desenhando. Para manter este tutorial no assunto, vamos deixar a tela inteira limpa.

Se uma inversão estiver sendo arrastada, vamos atualizar o valor target dela para corresponder à posição do mouse, mas em uma escala de -1 a 1 em vez de pixels reais. Também incrementamos o progress em uma fração da distância para o target. Isso resulta em uma progressão suave e animada do flip, já que ele é atualizado em todos os frames.

Como estamos analisando todos os flips em cada frame, precisamos nos certificar de que estamos redesenhando apenas os que estão ativos. Se uma virada não estiver muito próxima da borda do livro (dentro de 0,3% de BOOK_WIDTH) ou se ela estiver sinalizada como dragging, ela será considerada ativa.

Agora que toda a lógica está em vigor, precisamos desenhar a representação gráfica de uma inversão dependendo do estado atual dela. É hora de conferir a primeira parte da função drawFlip(flip).

// Determines the strength of the fold/bend on a 0-1 range
var strength = 1 - Math.abs( flip.progress );

// Width of the folded paper
var foldWidth = ( PAGE_WIDTH * 0.5 ) * ( 1 - flip.progress );

// X position of the folded paper
var foldX = PAGE_WIDTH * flip.progress + foldWidth;

// How far outside of the book the paper is bent due to perspective
var verticalOutdent = 20 * strength;

// The maximum widths of the three shadows used
var paperShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(1 - flip.progress, 0.5), 0);
var rightShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);
var leftShadowWidth = (PAGE_WIDTH*0.5) * Math.max(Math.min(strength, 0.5), 0);

// Mask the page by setting its width to match the foldX
flip.page.style.width = Math.max(foldX, 0) + 'px';

Esta seção do código começa calculando um número de variáveis visuais que precisamos para desenhar a dobra de maneira realista. O valor progress da inversão que estamos desenhando tem um papel importante aqui, já que é onde queremos que a dobra da página apareça. Para adicionar profundidade ao efeito de virada de página, fazemos com que o papel se estenda para fora dos bordas superior e inferior do livro. Esse efeito está no pico quando uma virada está próxima à lombada do livro.

Virar
A dobra da página fica assim quando ela é virada ou arrastada.

Agora que todos os valores estão preparados, só falta desenhar o papel!

context.save();
context.translate( CANVAS_PADDING + ( BOOK_WIDTH / 2 ), PAGE_Y + CANVAS_PADDING );

// Draw a sharp shadow on the left side of the page
context.strokeStyle = `rgba(0,0,0,`+(0.05 * strength)+`)`;
context.lineWidth = 30 * strength;
context.beginPath();
context.moveTo(foldX - foldWidth, -verticalOutdent * 0.5);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT + (verticalOutdent * 0.5));
context.stroke();

// Right side drop shadow
var rightShadowGradient = context.createLinearGradient(foldX, 0,
            foldX + rightShadowWidth, 0);
rightShadowGradient.addColorStop(0, `rgba(0,0,0,`+(strength*0.2)+`)`);
rightShadowGradient.addColorStop(0.8, `rgba(0,0,0,0.0)`);

context.fillStyle = rightShadowGradient;
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX + rightShadowWidth, 0);
context.lineTo(foldX + rightShadowWidth, PAGE_HEIGHT);
context.lineTo(foldX, PAGE_HEIGHT);
context.fill();

// Left side drop shadow
var leftShadowGradient = context.createLinearGradient(
foldX - foldWidth - leftShadowWidth, 0, foldX - foldWidth, 0);
leftShadowGradient.addColorStop(0, `rgba(0,0,0,0.0)`);
leftShadowGradient.addColorStop(1, `rgba(0,0,0,`+(strength*0.15)+`)`);

context.fillStyle = leftShadowGradient;
context.beginPath();
context.moveTo(foldX - foldWidth - leftShadowWidth, 0);
context.lineTo(foldX - foldWidth, 0);
context.lineTo(foldX - foldWidth, PAGE_HEIGHT);
context.lineTo(foldX - foldWidth - leftShadowWidth, PAGE_HEIGHT);
context.fill();

// Gradient applied to the folded paper (highlights &amp; shadows)
var foldGradient = context.createLinearGradient(
foldX - paperShadowWidth, 0, foldX, 0);
foldGradient.addColorStop(0.35, `#fafafa`);
foldGradient.addColorStop(0.73, `#eeeeee`);
foldGradient.addColorStop(0.9, `#fafafa`);
foldGradient.addColorStop(1.0, `#e2e2e2`);

context.fillStyle = foldGradient;
context.strokeStyle = `rgba(0,0,0,0.06)`;
context.lineWidth = 0.5;

// Draw the folded piece of paper
context.beginPath();
context.moveTo(foldX, 0);
context.lineTo(foldX, PAGE_HEIGHT);
context.quadraticCurveTo(foldX, PAGE_HEIGHT + (verticalOutdent * 2),
                        foldX - foldWidth, PAGE_HEIGHT + verticalOutdent);
context.lineTo(foldX - foldWidth, -verticalOutdent);
context.quadraticCurveTo(foldX, -verticalOutdent * 2, foldX, 0);

context.fill();
context.stroke();

context.restore();

O método translate(x,y) da API de tela é usado para compensar o sistema de coordenadas para que possamos desenhar a virada de página com a parte de cima da coluna vertebral atuando como a posição 0,0. Também precisamos save() a matriz de transformação atual da tela e restore() para ela quando terminarmos de desenhar.

Traduzir
Esse é o ponto em que desenhamos a virada da página. O ponto original 0,0 está no canto superior esquerdo da imagem, mas, ao mudar isso, por meio de translate(x,y), simplificamos a lógica de exibição.

O foldGradient é o que vamos usar para preencher a forma do papel dobrado e dar destaques e sombras realistas. Também adicionamos uma linha muito fina ao redor do desenho no papel para que ele não desapareça quando colocado em segundo plano claro.

Tudo o que resta agora é desenhar a forma do papel dobrado usando as propriedades definidas acima. Os lados esquerdo e direito do papel são desenhados como linhas retas, e as partes de cima e de baixo são curvas para transmitir a sensação de um papel dobrado. A resistência dessa dobra de papel é determinada pelo valor verticalOutdent.

Pronto! Agora você tem uma navegação de virada de página totalmente funcional.

Demonstração de virada de página

O efeito de virada de página é sobre comunicar a sensação interativa certa, então olhar imagens dele não faz justiça.

Próximas etapas

Virada forçada
O giro de página suave neste tutorial fica ainda mais poderoso quando combinado com outros recursos semelhantes a livros, como uma capa rígida interativa.

Esse é apenas um exemplo do que pode ser feito usando recursos do HTML5, como o elemento canvas. Recomendamos que você confira a experiência de livro mais refinada de que esta técnica é um trecho em: www.20thingsilearned.com. Nele, você vai ver como os flips de página podem ser aplicados em um aplicativo real e como ele se torna mais poderoso quando combinado com outros recursos do HTML5.

Referências

  • Especificação da API Canvas