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 sebaiknya disajikan dalam konteks buku. Karena isi buku ini sangat berkaitan dengan teknologi web terbuka, kami merasa penting untuk tetap jujur terhadap hal tersebut dengan menjadikan container itu sendiri sebagai contoh yang memungkinkan kami capai saat ini.

Sampul buku dan halaman beranda '20 Hal yang Saya Pelajari Tentang Browser dan Web'
Sampul buku dan halaman beranda "20 Things I Learned About Browsers and the Web" (www.20thingsilearned.com)

Kami memutuskan bahwa cara terbaik untuk membuat buku terasa seperti di dunia nyata adalah dengan menyimulasikan bagian yang baik dari pengalaman membaca analog sambil tetap memanfaatkan manfaat dunia digital di berbagai bidang seperti navigasi. Banyak upaya dilakukan untuk memberikan penanganan grafis dan interaktif pada alur membaca - terutama bagaimana halaman buku dibalik dari satu halaman ke halaman lainnya.

Memulai

Tutorial ini akan membawa Anda melalui proses pembuatan efek balik halaman sendiri menggunakan elemen kanvas dan berbagai JavaScript. Beberapa kode dasar, seperti deklarasi variabel dan langganan pemroses peristiwa, tidak disertakan dalam cuplikan dalam artikel ini, jadi ingatlah untuk mereferensikan contoh yang berfungsi.

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

Markup

Penting untuk diingat bahwa apa yang kita gambar di kanvas tidak dapat diindeks oleh mesin telusur, yang dipilih oleh pengunjung, atau ditemukan oleh penelusuran dalam browser. Karena alasan itu, konten yang akan kita kerjakan ditempatkan langsung di DOM, lalu dimanipulasi oleh JavaScript jika tersedia. Markup yang diperlukan untuk konversi 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 berisi halaman buku yang berbeda dan elemen canvas yang akan menggambar pembalikan halaman. 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 tambahannya, sehingga lebar section berfungsi sebagai mask horizontal untuk div.

Buka Buku.
Gambar latar yang berisi tekstur kertas dan jaket buku cokelat ditambahkan ke elemen buku.

Logika

Kode yang diperlukan untuk mendukung pembalikan halaman tidak terlalu kompleks, tetapi cukup luas karena melibatkan banyak grafis yang dihasilkan secara prosedural. Mari kita mulai dengan melihat deskripsi nilai konstan 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 membuat kertas memanjang ke luar buku saat dibalik. Perhatikan bahwa beberapa konstanta yang ditentukan di sini juga ditetapkan dalam CSS, jadi jika ingin mengubah ukuran buku, Anda juga perlu mengubah nilainya di sana.

Konstanta.
Nilai konstanta yang digunakan di seluruh kode untuk melacak interaksi dan menggambar pembalikan 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 bahwa halaman tersebut telah dilapisi dengan benar dengan mengatur indeks z dari elemen bagian sehingga halaman pertama berada di atas dan halaman terakhir berada di bawah. Properti yang paling penting dari objek balik adalah nilai progress dan target. Parameter ini digunakan untuk menentukan seberapa jauh halaman saat ini harus dilipat, -1 berarti ke kiri, 0 berarti tengah halaman buku, dan +1 berarti tepi paling kanan buku.

Kemajuan.
Progres dan nilai target balik digunakan untuk menentukan tempat halaman lipat harus digambar pada skala -1 hingga +1.

Setelah memiliki objek balik yang ditentukan untuk setiap halaman, kita perlu mulai merekam 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 berusaha menuju lokasi kursor terbaru.

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

Setelah mencapai mouseUpHandler, kita akan melalui semua flips dan memeriksa apakah ada yang ditandai sebagai dragging dan sekarang seharusnya dirilis. Saat flip dirilis, 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 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 mengupdate dan menggambar status saat ini dari semua balik yang 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 dengan menggunakan metode clearRect(x,y,w,h). Membersihkan seluruh kanvas akan menghabiskan biaya performa yang besar dan akan jauh lebih efisien untuk membersihkan hanya wilayah yang kita gambar. Agar tutorial ini tetap tetap sesuai topik, kita akan membiarkan seluruh kanvas dibersihkan.

Jika balik ditarik, kita akan mengupdate nilai target-nya agar sesuai dengan posisi mouse, tetapi pada skala -1 sampai 1, bukan piksel yang sebenarnya. Kita juga menambahkan progress dengan sepersekian jarak ke target, ini akan menghasilkan perkembangan balik yang lancar dan animasi karena diperbarui di setiap frame.

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

Setelah semua logika berada, kita perlu menggambar representasi grafis sebuah membalik, bergantung pada statusnya saat ini. Saatnya untuk 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 lipatan secara realistis. Nilai progress dari balik yang kita gambar memainkan peran besar di sini, karena di situlah kita ingin lipatan halaman muncul. Untuk menambah kedalaman efek membalik halaman, kita membuat kertas memanjang ke luar tepi atas dan bawah buku, efek ini berada pada puncaknya saat membalik mendekati belakang buku.

Balik
Seperti inilah tampilan lipatan halaman saat halaman dibalik atau ditarik.

Sekarang setelah semua nilai sudah 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) API kanvas digunakan untuk mengimbangi sistem koordinasi sehingga kita dapat menggambar flip halaman dengan bagian atas tulang belakang yang bertindak sebagai posisi 0,0. Perhatikan bahwa kita juga perlu melakukan save() matriks transformasi kanvas saat ini dan restore() ke matriks tersebut saat kita selesai menggambar.

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

foldGradient adalah elemen yang akan kita isi dengan bentuk kertas lipat untuk memberikan sorotan dan bayangan yang realistis. Kita juga menambahkan garis yang sangat tipis di sekitar gambar kertas sehingga kertas tidak hilang saat diberi latar belakang terang.

Yang tersisa sekarang adalah menggambar bentuk kertas lipat menggunakan properti yang telah kita tentukan di atas. Sisi kiri dan kanan kertas digambar sebagai garis lurus, sedangkan sisi atas dan bawah dilengkungkan untuk menimbulkan rasa kertas lipatan yang tertekuk. Kekuatan lekukan kertas ini ditentukan oleh nilai verticalOutdent.

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

Demo Membalik Halaman

Efek {i>page flip<i} adalah tentang mengomunikasikan nuansa interaktif yang tepat sehingga melihat gambarnya tidak benar-benar tepat.

Langkah Berikutnya

Balik keras
Flip halaman lembut dalam tutorial ini akan menjadi lebih efektif jika dipasangkan dengan fitur mirip buku lainnya seperti hardcover interaktif.

Ini hanya salah satu contoh hal yang dapat dicapai dengan memanfaatkan fitur HTML5 seperti elemen kanvas. Sebaiknya lihat pengalaman buku yang lebih baik dari teknik ini sebagai kutipan di: www.20thingsilearned.com. Di sana, Anda akan melihat bagaimana membalik halaman dapat diterapkan dalam aplikasi nyata dan seberapa kuatnya saat dipasangkan dengan fitur HTML5 lainnya.

Referensi