Studi Kasus - Efek Balik Halaman dari 20thingsilearned.com

Hakim El Hattab
Hakim El Hattab

Pengantar

Pada tahun 2010, F-i.com dan tim Google Chrome berkolaborasi dalam aplikasi web pendidikan berbasis HTML5 yang disebut 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). Salah satu ide utama di balik project ini adalah bahwa project ini akan lebih baik jika disajikan dalam konteks buku. Karena konten buku ini sangat berkaitan dengan teknologi web terbuka, kami merasa penting untuk tetap setia dengan hal tersebut dengan menjadikan penampung itu sendiri sebagai contoh dari apa yang dapat kita capai saat ini dengan teknologi ini.

Sampul buku dan halaman beranda '20 Things I Learned About Browsers and the Web'
Sampul buku dan halaman beranda "20 Things I Learned About Browsers and the Web" (www.20thingsilearned.com)

Kami memutuskan bahwa cara terbaik untuk mendapatkan nuansa buku di dunia nyata adalah dengan menyimulasikan bagian-bagian baik dari pengalaman membaca analog sekaligus tetap memanfaatkan manfaat dunia digital di area seperti navigasi. Banyak upaya yang dilakukan untuk perlakuan grafis dan interaktif pada alur membaca - terutama cara halaman buku dibalik dari satu halaman ke halaman lainnya.

Memulai

Tutorial ini akan memandu Anda dalam proses pembuatan efek membalik halaman Anda sendiri menggunakan elemen kanvas dan banyak JavaScript. Beberapa kode dasar, seperti deklarasi variabel dan langganan pemroses peristiwa, telah dihapus dari cuplikan dalam artikel ini, jadi jangan lupa untuk mereferensikan contoh yang berfungsi.

Sebelum memulai, sebaiknya lihat demo agar Anda tahu apa yang ingin kita buat.

Markup

Penting untuk diingat bahwa apa yang kita gambar di kanvas tidak dapat diindeks oleh mesin telusur, dipilih oleh pengunjung, atau ditemukan oleh penelusuran dalam browser. Oleh karena itu, konten yang akan kita gunakan ditempatkan langsung di DOM, lalu dimanipulasi oleh JavaScript jika tersedia. Markup yang diperlukan untuk ini minimal:

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

Kita memiliki satu elemen penampung utama untuk buku, yang pada gilirannya berisi berbagai halaman buku dan elemen canvas yang akan kita gambar halaman yang dibalik. Di dalam elemen section, terdapat wrapper div untuk konten - kita memerlukannya agar dapat mengubah lebar halaman tanpa memengaruhi tata letak kontennya. div memiliki lebar tetap dan section disetel untuk menyembunyikan overflow-nya, sehingga lebar section berfungsi sebagai mask horizontal untuk div.

Buka Buku.
Gambar latar belakang yang berisi tekstur kertas dan sampul buku berwarna cokelat ditambahkan ke elemen buku.

Logika

Kode yang diperlukan untuk mendukung pengalihan halaman tidak terlalu rumit, tetapi cukup luas karena melibatkan banyak grafik yang dihasilkan secara terstruktur. Mari kita mulai dengan melihat deskripsi nilai konstanta yang akan kita gunakan di seluruh kode.

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;

CANVAS_PADDING ditambahkan di sekitar kanvas sehingga kita dapat memperluas kertas ke luar buku saat membalik. Perhatikan bahwa beberapa konstanta yang ditentukan di sini juga ditetapkan di CSS, jadi jika Anda ingin mengubah ukuran buku, Anda juga harus memperbarui nilai di sana.

Konstanta.
Nilai konstan yang digunakan di seluruh kode untuk melacak interaksi dan menggambar flip halaman.

Selanjutnya, kita perlu menentukan objek flip untuk setiap halaman. Objek ini akan terus diperbarui saat kita berinteraksi dengan buku untuk mencerminkan status flip saat ini.

// 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
});
}

Pertama, kita perlu memastikan halaman dilapisi dengan benar dengan mengatur z-index elemen bagian sehingga halaman pertama berada di atas dan halaman terakhir berada di bawah. Properti paling penting dari objek flip adalah nilai progress dan target. Ini digunakan untuk menentukan seberapa jauh halaman saat ini harus dilipat, -1 berarti sepenuhnya ke kiri, 0 berarti tepat di tengah buku, dan +1 berarti tepi paling kanan buku.

Progres.
Nilai progres dan target dari flip digunakan untuk menentukan tempat halaman lipat harus digambar pada skala -1 hingga +1.

Setelah memiliki objek flip yang ditentukan untuk setiap halaman, kita perlu mulai mengambil dan menggunakan input pengguna untuk memperbarui status flip.

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;
}
}

Fungsi mouseMoveHandler memperbarui objek mouse sehingga kita selalu bekerja menuju lokasi kursor terbaru.

Di mouseDownHandler, kita mulai dengan memeriksa apakah mouse ditekan di halaman kiri atau kanan sehingga kita tahu arah mana yang ingin kita mulai balik. Kita juga memastikan bahwa halaman lain ada di arah tersebut karena kita mungkin berada di halaman pertama atau terakhir. Jika opsi flip yang valid tersedia setelah pemeriksaan ini, kita menetapkan tanda dragging dari objek flip yang sesuai ke true.

Setelah mencapai mouseUpHandler, kita akan memeriksa semua flips dan melihat apakah ada yang ditandai sebagai dragging dan sekarang harus dirilis. Saat flip dilepaskan, kita menetapkan nilai targetnya agar cocok dengan sisi yang harus dibalik, bergantung pada posisi mouse saat ini. Nomor halaman juga diperbarui untuk mencerminkan navigasi ini.

Rendering

Setelah sebagian besar logika kita diterapkan, kita akan membahas cara merender kertas lipat ke elemen kanvas. Sebagian besar hal ini terjadi di dalam fungsi render(), yang dipanggil 60 kali per detik untuk memperbarui dan menggambar status saat ini dari semua flip aktif.

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 );
}

}
}

Sebelum mulai merender flips, kita mereset kanvas menggunakan metode clearRect(x,y,w,h). Menghapus seluruh kanvas akan menimbulkan beban performa yang besar dan akan jauh lebih efisien jika hanya menghapus area yang kita gambar. Agar tutorial ini tetap sesuai dengan topik, kita akan membiarkannya dengan menghapus seluruh kanvas.

Jika flip sedang ditarik, kita akan memperbarui nilai target-nya agar cocok dengan posisi mouse, tetapi pada skala -1 hingga 1, bukan piksel sebenarnya. Kita juga menambahkan progress dengan sebagian kecil jarak ke target, hal ini akan menghasilkan progres pengalihan yang halus dan animasi karena diperbarui di setiap frame.

Karena kita akan membahas semua flips di setiap frame, kita perlu memastikan bahwa kita hanya menggambar ulang yang aktif. Jika tidak sangat dekat dengan tepi buku (dalam 0,3% dari BOOK_WIDTH), atau jika diberi tanda sebagai dragging, halaman dianggap aktif.

Setelah semua logika diterapkan, kita perlu menggambar representasi grafis flip bergantung pada statusnya saat ini. Saatnya melihat bagian pertama fungsi 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';

Bagian kode ini dimulai dengan menghitung sejumlah variabel visual yang kita perlukan untuk menggambar lipatan secara realistis. Nilai progress dari flip yang kita gambar memainkan peran besar di sini, karena di sinilah kita ingin lipatan halaman muncul. Untuk menambahkan kedalaman pada efek pembalikan halaman, kita membuat kertas memanjang di luar tepi atas dan bawah buku, efek ini berada di puncaknya saat pembalikan dekat dengan tulang punggung buku.

Balik
Tampilan lipatan halaman saat halaman dibalik atau ditarik.

Setelah semua nilai disiapkan, yang tersisa hanyalah menggambar kertas.

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();

Metode translate(x,y) canvas API digunakan untuk mengimbangi sistem koordinat sehingga kita dapat menggambar flip halaman dengan bagian atas spine yang berfungsi sebagai posisi 0,0. Perhatikan bahwa kita juga perlu save() matriks transformasi kanvas saat ini dan restore() ke kanvas tersebut saat kita selesai menggambar.

Terjemahan
Ini adalah titik tempat kita menggambar flip halaman. Titik 0,0 asli berada di kiri atas gambar, tetapi dengan mengubahnya, melalui translate(x,y), kita menyederhanakan logika gambar.

foldGradient adalah yang akan kita isi dengan bentuk kertas yang dilipat untuk memberinya sorotan dan bayangan yang realistis. Kita juga menambahkan garis yang sangat tipis di sekitar gambar kertas sehingga kertas tidak menghilang saat ditempatkan di latar belakang yang terang.

Yang tersisa sekarang adalah menggambar bentuk kertas yang dilipat menggunakan properti yang kita tentukan di atas. Sisi kiri dan kanan kertas kita digambar sebagai garis lurus dan sisi atas dan bawah melengkung untuk memberikan kesan lipatan kertas yang terlipat. Kekuatan tekukan kertas ini ditentukan oleh nilai verticalOutdent.

Selesai. Sekarang Anda memiliki navigasi flip halaman yang berfungsi penuh.

Demo Pengalihan Halaman

Efek membalik halaman adalah tentang menyampaikan nuansa interaktivitas yang tepat sehingga melihat gambarnya tidak sepenuhnya adil.

Langkah Berikutnya

Hard-flip
Pengalihan halaman soft dalam tutorial ini menjadi lebih efektif saat disambungkan dengan fitur seperti buku lainnya, seperti sampul keras interaktif.

Ini hanyalah salah satu contoh yang dapat dicapai dengan memanfaatkan fitur HTML5 seperti elemen kanvas. Sebaiknya lihat pengalaman buku yang lebih halus yang merupakan kutipan dari teknik ini di: www.20thingsilearned.com. Di sana, Anda akan melihat cara flip halaman dapat diterapkan dalam aplikasi nyata dan betapa andal teknik ini jika dipadukan dengan fitur HTML5 lainnya.

Referensi