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.
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.
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á.
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.
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 && page - 1 >= 0) {
// We are on the left side, drag the previous page
flips[page - 1].dragging = true;
}
else if (mouse.x > 0 && 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.
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 & 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.
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
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