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.
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
.
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.
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.
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
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.
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 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.
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
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