Introdução
Em 2010, a F-i.com e a equipe do Google Chrome colaboraram em um aplicativo 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 é muito relacionado a tecnologias da Web aberta, pensamos que era importante permanecer fiel a isso, tornando o próprio contêiner um exemplo do que essas tecnologias nos permitem realizar hoje.
Decidimos que a melhor maneira de ter a sensação de um livro real é simular as partes boas da experiência de leitura analógica e ainda aproveitar os benefícios do mundo digital em áreas como a navegação. O tratamento gráfico e interativo do fluxo de leitura foi muito esforço para ser dedicado, especialmente na forma como as páginas dos livros viram de uma página para outra.
Como começar
Confira neste tutorial o processo de criação do seu próprio efeito de virada de página usando o elemento canvas e bastante JavaScript. Alguns códigos rudimentares, como declarações de variáveis e assinatura do listener de eventos, foram deixados de fora dos snippets deste artigo, portanto, lembre-se de consultar o exemplo funcional.
Antes de começar, confira a demonstração para conhecer o que pretendemos 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 isso, o conteúdo com o qual trabalharemos é 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 viradas. Dentro do elemento section
, há um
wrapper div
para o conteúdo. Precisamos dele para mudar a largura
da página sem afetar o layout do conteúdo. A div
tem uma
largura fixa e a section
está definida para ocultar o estouro. Isso faz com que
a largura da section
atue como uma máscara horizontal para a div
.
Lógica
O código necessário para fazer a virada de página não é muito complexo, mas é bastante extenso, porque envolve muitos gráficos gerados processualmente. Para começar, vamos analisar 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 possa se estender para fora do livro ao virar. Observe que algumas das constantes definidas aqui também são definidas no CSS. Portanto, se você quiser alterar o tamanho do livro, também será necessário atualizar os valores.
Em seguida, precisamos definir um objeto de virada para cada página. Ele é atualizado 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 organizar 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 virada são os valores progress
e target
.
Eles são usados para determinar até onde a página deve ser dobrada no momento, -1 significa totalmente para a esquerda, 0 significa o centro morto do livro e +1 significa a borda direita dele.
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 && 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 estejamos sempre trabalhando na localização mais recente do cursor.
Em mouseDownHandler
, começamos verificando se o mouse foi pressionado
na página esquerda ou direita para saber para qual
direção queremos 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 válida de virada estiver disponível após essas verificações,
definiremos a flag dragging
do objeto de virada correspondente como true
.
Ao alcançar o mouseUpHandler
, analisamos todos os flips
e verificamos se algum deles foi sinalizado como dragging
e agora precisa ser
lançado. Quando um flip é liberado, definimos o valor de destino para corresponder
ao lado em que ele vai 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á pronta, veremos como
renderizar o papel dobrável no elemento de tela. A maior parte disso acontece
dentro da função render()
, que é chamada 60 vezes
por segundo para atualizar e renderizar 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 a flips
, redefinimos a
tela usando o método clearRect(x,y,w,h)
. A limpeza de toda a tela
tem um alto custo de desempenho, e seria muito mais eficiente
limpar apenas as regiões em que estamos desenhando. Para manter este tutorial no assunto, deixaremos de limpar toda a tela.
Se um flip estiver sendo arrastado, atualizaremos o valor target
para corresponder à
posição do mouse, mas em uma escala de -1 a 1, em vez de pixels reais.
Também incrementamos a progress
em uma fração da distância até
a target
. Isso resulta em uma progressão suave e animada
da virada, já que ela é atualizada a cada frame.
Como estamos passando por todo o flips
em cada frame, precisamos garantir
que apenas os que estejam ativos sejam redesenhados. 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á pronta, precisamos desenhar a representação gráfica de um flip, dependendo do estado atual. É 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 pelo cálculo de várias variáveis visuais
necessárias para desenhar a dobra de maneira realista. O
valor progress
do flip que estamos desenhando tem um papel importante nesse caso, já que
é nesse local que 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 das margens superior e inferior do livro. Esse efeito está em seu auge quando uma virada está próxima à lombada do livro.
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 & 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 deslocar o
sistema de coordenadas para que possamos desenhar a virada de página com a parte superior da
lombada atuando como posição 0,0. Também precisamos usar save()
na
matriz de transformação atual da tela e usar restore()
nela
quando terminarmos o desenho.
A foldGradient
é usada para preencher a forma do papel dobrado
para oferecer realces e sombras realistas. Também adicionamos uma linha muito fina
ao redor do desenho do papel para que ele não desapareça quando for colocado
em fundos claros.
Agora só resta 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 dobrar. A intensidade da dobra do 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 tem a ver com comunicar a sensação interativa correta, portanto, olhar imagens não faz jus ao processo.
Próximas etapas
Este é apenas um exemplo do que pode ser feito utilizando recursos HTML5, como o elemento canvas. Recomendo que você confira a experiência de livros mais refinada, da qual essa técnica é um trecho em: www.20thingsilearned.com (link em inglês). Lá, você verá como a virada de página pode ser aplicada em um aplicativo real e como ela é eficiente quando combinada com outros recursos HTML5.
Referências
- Especificação da API Canvas