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

Hakim El Hattab
Hakim El Hattab

Introdução

Em 2010, a F-i.com e a equipe do Google Chrome colaboraram em um web app 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 é muito sobre tecnologias da Web aberta, achamos importante manter essa ideia e fazer do próprio contêiner um exemplo do que essas tecnologias permitem realizar 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 do mundo real é simular as boas partes da experiência de leitura analógica, aproveitando os benefícios do mundo digital em áreas como a navegação. Muito esforço foi dedicado ao tratamento gráfico e interativo do fluxo de leitura, especialmente como as páginas dos livros mudam 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 omitida dos snippets neste artigo. Portanto, consulte o exemplo funcional.

Antes de começar, é bom conferir a demonstração para saber o que vamos criar.

Marcação

É 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 isso, o conteúdo com que vamos trabalhar é colocado diretamente no DOM e manipulado pelo 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 que estão sendo viradas. Dentro do elemento section, há um wrapper div para o conteúdo. Precisamos disso para mudar a largura da página sem afetar o layout do conteúdo. O div tem uma largura fixa, e o section está definido para ocultar o estouro. Isso faz com que a largura do section funcione como uma máscara horizontal para o div.

Abrir livro.
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 troca de página não é muito complexo, mas é bastante extenso, já que envolve muitos gráficos gerados de forma procedural. 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 o papel se estenda para fora do livro ao virar as páginas. Algumas das constantes definidas aqui também são definidas em CSS. Portanto, se você quiser mudar o tamanho do livro, também precisará atualizar os valores lá.

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 flip para cada página. Eles serão atualizados constantemente à medida que interagimos com o livro para refletir o status atual do flip.

// 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-índices 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. São usados para determinar até que ponto a página deve ser dobrada. -1 significa totalmente à esquerda, 0 significa o centro do livro e +1 significa a borda mais à direita do livro.

Progresso.
Os valores de progresso e destino das inversões 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 inversão definido para cada página, precisamos começar a capturar e usar a entrada dos usuários para atualizar o estado da inversão.

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 posição mais recente do cursor.

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

Quando atingimos o mouseUpHandler, analisamos todos os flips e verificamos se algum deles foi sinalizado como dragging e precisa ser lançado. Quando um flip é liberado, definimos o valor de destino dele para corresponder ao lado para que ele deve virar, 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á no lugar, vamos mostrar como renderizar o papel dobrado no elemento de 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 inversões 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, redefina o canvas usando o método clearRect(x,y,w,h). Limpar toda a tela tem um custo de desempenho muito alto. 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 um flip estiver sendo arrastado, vamos atualizar o valor target dele para corresponder à posição do mouse, mas em uma escala de -1 a 1, em vez de pixels reais. Também incrementamos o progress por uma fração da distância até o target. Isso resulta em uma progressão suave e animada da inversão, já que ela é atualizada em todos os frames.

Como estamos analisando todos os flips em cada frame, precisamos garantir que apenas os ativos sejam renderizados novamente. Se uma virada não estiver muito perto da borda do livro (dentro de 0,3% de BOOK_WIDTH) ou se for sinalizada como dragging, ela será considerada ativa.

Agora que toda a lógica está no lugar, precisamos desenhar a representação gráfica de uma inversão dependendo do estado atual dela. É hora de analisar 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 várias variáveis visuais necessárias 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 dar profundidade ao efeito de virada de página, fazemos com que o papel se estenda para fora das bordas superior e inferior do livro. Esse efeito atinge o pico quando uma virada está perto da lombada do livro.

Virar
Esta é a aparência da dobra da página quando ela está sendo virada ou arrastada.

Agora que todos os valores estão preparados, só falta sortear 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 Canvas é usado para compensar o sistema de coordenadas para que possamos desenhar a virada de página com a parte de cima da lombada atuando como a posição 0,0. Também precisamos save() a matriz de transformação atual da tela e restore() a ela quando terminarmos de desenhar.

Traduzir
Este é o ponto de onde tiramos a animação de virada de página. O ponto 0,0 original está no canto superior esquerdo da imagem, mas, ao mudar isso usando translate(x,y), simplificamos a lógica de desenho.

O foldGradient é o que vamos usar para preencher o formato do papel dobrado e dar a ele realces e sombras realistas. Também adicionamos uma linha muito fina ao redor do desenho para que o papel não desapareça quando colocado em fundos claros.

Agora, basta 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 os lados superior e inferior são curvos para dar a sensação de um papel dobrado. A força dessa dobra é 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 tem tudo a ver com a comunicação da sensação interativa certa, então olhar imagens dele não faz justiça.

Próximas etapas

Virada brusca
A transição suave de página neste tutorial fica ainda mais poderosa quando combinada com outros recursos semelhantes a livros, como uma capa dura interativa.

Este é apenas um exemplo do que pode ser feito usando recursos do HTML5, como o elemento canvas. Recomendo que você confira a experiência de livro mais refinada, de onde essa técnica é um trecho, em: www.20thingsilearned.com. Lá, você vai ver como as viradas de página podem ser aplicadas em um aplicativo real e como elas se tornam poderosas quando combinadas com outros recursos do HTML5.

Referências

  • Especificação da API Canvas