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 edukasi berbasis HTML5 yang disebut 20 Things I Learned about Browsers and the Web (www.20thingsilearned.com). Salah satu ide utama di balik proyek ini adalah bahwa proyek ini akan lebih baik disajikan dalam konteks buku. Karena isi buku ini sangat berkaitan dengan teknologi web terbuka, kami merasa penting untuk tetap setia pada hal tersebut dengan menjadikan penampung itu sendiri sebagai contoh dari apa yang dapat dicapai oleh teknologi ini saat 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 dunia nyata adalah dengan menyimulasikan bagian-bagian bagus dari pengalaman membaca analog sambil tetap memanfaatkan manfaat dunia digital di area seperti navigasi. Banyak upaya yang dilakukan untuk menangani alur membaca secara grafis dan interaktif, terutama cara halaman buku dibalik dari satu halaman ke halaman lain.

Memulai

Tutorial ini akan memandu Anda dalam proses pembuatan efek balik halaman Anda sendiri menggunakan elemen kanvas dan banyak JavaScript. Beberapa kode dasar, seperti deklarasi variabel dan langganan pendengar peristiwa, telah dihilangkan dari cuplikan dalam artikel ini, jadi ingatlah untuk merujuk pada contoh yang berfungsi.

Sebelum memulai, sebaiknya lihat demo agar Anda tahu apa yang akan kita bangun.

Markup

Penting untuk selalu 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 hal ini sangat minim:

<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 gunakan untuk menggambar halaman yang berbalik. Di dalam elemen section terdapat wrapper div untuk konten - kita memerlukan ini 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.

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

Logika

Kode yang diperlukan untuk menggerakkan balik halaman tidak terlalu rumit, tetapi cukup ekstensif karena melibatkan banyak grafik yang dibuat secara prosedural. 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 di 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 konstanta yang digunakan di seluruh kode untuk melacak interaksi dan menggambar balik halaman.

Selanjutnya, kita perlu menentukan objek balik untuk setiap halaman, yang akan terus diperbarui saat kita berinteraksi dengan buku untuk mencerminkan status balik 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 disusun dengan benar dengan mengatur indeks z elemen bagian sehingga halaman pertama berada di atas dan halaman terakhir berada di bawah. Properti terpenting dari objek balik adalah nilai progress dan target. Nilai ini digunakan untuk menentukan seberapa jauh halaman harus dilipat saat ini, -1 berarti dilipat sepenuhnya ke kiri, 0 berarti di tengah-tengah buku, dan +1 berarti di tepi paling kanan buku.

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

Setelah menentukan objek balik untuk setiap halaman, kita perlu mulai merekam dan menggunakan input pengguna untuk memperbarui status balik.

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 tombol mouse ditekan di halaman kiri atau kanan sehingga kita tahu ke arah mana kita ingin mulai membalik. Kami juga memastikan bahwa ada halaman lain di arah tersebut karena kita mungkin berada di halaman pertama atau terakhir. Jika opsi balik yang valid tersedia setelah pemeriksaan ini, kami menyetel flag dragging dari objek balik yang sesuai ke true.

Setelah mencapai mouseUpHandler, kita akan memeriksa semua flips dan memeriksa 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 kursor 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). Mengosongkan seluruh kanvas akan sangat memengaruhi performa dan akan jauh lebih efisien jika hanya mengosongkan wilayah yang kita gambar. Agar tutorial ini tetap sesuai topik, kita akan membatasi pembahasannya pada menghapus seluruh kanvas.

Jika flip ditarik, kita akan memperbarui nilai target agar sesuai dengan posisi mouse, tetapi pada skala -1 hingga 1, bukan piksel aktual. Kita juga meningkatkan progress dengan sebagian kecil jarak ke target, yang akan menghasilkan progres flip yang lancar dan beranimasi karena diperbarui di setiap frame.

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

Setelah semua logika diterapkan, kita perlu menggambar representasi grafis balik tergantung pada statusnya saat ini. Sekarang 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 perlu kita gambar agar tampilan di atas layar terlihat realistis. Nilai progress dari balik yang kita gambar memainkan peran besar di sini, karena di situlah kita ingin lipatan halaman muncul. Untuk menambahkan kedalaman pada efek balik halaman, kami membuat kertas meluas di luar tepi atas dan bawah buku. Efek ini mencapai puncaknya saat balik halaman mendekati tulang buku.

Balik
Inilah tampilan batas atas 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 balik halaman dengan bagian atas tulang buku sebagai posisi 0,0. Perhatikan bahwa kita juga perlu save() matriks transformasi saat ini dari kanvas dan restore() ke dalamnya saat kita selesai menggambar.

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

foldGradient adalah warna yang akan kita gunakan untuk mengisi bentuk kertas yang dilipat agar memberikan highlight dan bayangan yang realistis. Kita juga menambahkan garis yang sangat tipis di sekeliling gambar di kertas agar kertas tidak hilang saat diletakkan di atas latar belakang terang.

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

Selesai. Sekarang Anda telah memiliki navigasi balik halaman yang berfungsi penuh.

Demo Balik Halaman

Efek balik halaman adalah tentang mengomunikasikan nuansa interaktif yang tepat, sehingga melihat gambar efek ini tidak akan memberikan gambaran yang akurat.

Langkah Berikutnya

Balik keras
Transisi halaman lembut dalam tutorial ini menjadi lebih efektif jika dipadukan dengan fitur seperti buku lainnya, misalnya sampul keras interaktif.

Ini hanyalah salah satu contoh dari apa yang dapat dicapai dengan memanfaatkan fitur HTML5 seperti elemen kanvas. Sebaiknya Anda melihat pengalaman buku yang lebih baik, yang tekniknya diambil dari buku tersebut di: www.20thingsilearned.com. Di sana, Anda akan melihat cara penerapan balik halaman dalam aplikasi nyata dan seberapa efektifnya jika dipadukan dengan fitur HTML5 lainnya.

Referensi