Örnek Olay - 20thingsilearned.com'dan Sayfa Çevirme Efekti

Hakim El Hattab
Hakim El Hattab

Giriş

2010'da F-i.com ve Google Chrome ekibi, 20 Things I Learned about Browsers and the Web (Tarayıcılar ve Web Hakkında Öğrendiğim 20 Şey) adlı HTML5 tabanlı bir eğitici web uygulaması üzerinde işbirliği yaptı (www.20thingsilearned.com). Bu projenin temel fikirlerinden biri, projenin en iyi şekilde kitap bağlamında sunulacağıydı. Kitabın içeriği büyük ölçüde açık web teknolojileriyle ilgili olduğundan, bu teknolojilerin günümüzde neler yapmamıza olanak tanıdığını gösteren bir örnek olarak kitabın kendisini tasarlamanın önemli olduğunu düşündük.

"Tarayıcılar ve Web Hakkında Öğrendiğim 20 Şey" adlı kitabın kapağı ve ana sayfası
"Tarayıcılar ve Web Hakkında Öğrendiğim 20 Şey" adlı kitabın kapak resmi ve ana sayfası (www.20thingsilearned.com)

Gerçek dünyadaki bir kitabın hissini vermenin en iyi yolunun, analog okuma deneyiminin iyi yönlerini simüle ederken gezinme gibi alanlarda dijital dünyanın avantajlarından yararlanmak olduğuna karar verdik. Okuma akışının grafiksel ve etkileşimli olarak ele alınması için çok çaba harcandı. Özellikle kitap sayfalarının nasıl çevrildiği üzerinde duruldu.

Başlarken

Bu eğitimde, tuval öğesini ve bol miktarda JavaScript kullanarak kendi sayfa çevirme efektinizi oluşturma süreci açıklanmaktadır. Değişken bildirimleri ve etkinlik dinleyici aboneliği gibi bazı temel kodlar bu makaledeki snippet'lerde yer almamaktadır. Bu nedenle, çalışan örneğe başvurmayı unutmayın.

Başlamadan önce demoya göz atmanız, ne oluşturmayı hedeflediğimizi anlamanız açısından faydalı olacaktır.

Brüt kar

Tuvalde çizdiğimiz öğelerin arama motorları tarafından dizine eklenemeyeceğini, ziyaretçiler tarafından seçilemeyeceğini veya tarayıcı içi aramalarla bulunamayacağını unutmamak önemlidir. Bu nedenle, üzerinde çalışacağımız içerik doğrudan DOM'a yerleştirilir ve varsa JavaScript tarafından işlenir. Bu işlem için gereken işaretleme minimum düzeydedir:

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

Kitap için bir ana kapsayıcı öğemiz var. Bu öğe, kitabımızın farklı sayfalarını ve sayfaları çevireceğimiz canvas öğesini içeriyor. section öğesinin içinde içerik için bir div sarmalayıcı bulunur. Sayfanın genişliğini, içeriklerinin düzenini etkilemeden değiştirebilmek için bu sarmalayıcıya ihtiyacımız vardır. div, sabit genişliğe sahip ve section, taşmasını gizleyecek şekilde ayarlanmış. Bu durum, section genişliğinin div için yatay maske olarak kullanılmasına neden oluyor.

Kitabı açın.
Kitap öğesine, kâğıt dokusunu ve kahverengi kitap kapağını içeren bir arka plan resmi eklenir.

Mantık

Sayfa çevirme özelliğini etkinleştirmek için gereken kod çok karmaşık olmasa da prosedürel olarak oluşturulmuş birçok grafik içerdiğinden oldukça kapsamlıdır. Öncelikle kod boyunca kullanacağımız sabit değerlerin açıklamasına bakalım.

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;

Çevirme sırasında kağıdın kitabın dışına çıkabilmesi için tuvalin etrafına CANVAS_PADDING eklenir. Burada tanımlanan bazı sabitlerin CSS'de de ayarlandığını unutmayın. Bu nedenle, kitabın boyutunu değiştirmek istiyorsanız oradaki değerleri de güncellemeniz gerekir.

Sabitler.
Etkileşimi izlemek ve sayfa çevirme işlemini çizmek için kod boyunca kullanılan sabit değerler.

Ardından, her sayfa için bir çevirme nesnesi tanımlamamız gerekir. Bu nesneler, kitabın mevcut durumunu yansıtmak için kitapla etkileşimde bulundukça sürekli olarak güncellenir.

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

Öncelikle, bölüm öğelerinin z-endekslerini düzenleyerek sayfaların doğru şekilde katmanlandığından emin olmamız gerekir. Böylece ilk sayfa en üstte, son sayfa ise en altta yer alır. Çevirme nesnelerinin en önemli özellikleri progress ve target değerleridir. Bu değerler, sayfanın şu anda ne kadar katlanması gerektiğini belirlemek için kullanılır. -1, tamamen sola katlanması, 0, kitabın tam ortası ve +1, kitabın en sağ kenarı anlamına gelir.

İlerleme
Çevirmelerin ilerleme ve hedef değerleri, katlama sayfasının -1 ile +1 ölçeğinde nerede çizileceğini belirlemek için kullanılır.

Artık her sayfa için tanımlanmış bir çevirme nesnemiz olduğuna göre, çevirmenin durumunu güncellemek için kullanıcı girişini yakalamaya ve kullanmaya başlamamız gerekiyor.

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

mouseMoveHandler işlevi, mouse nesnesini güncelleyerek her zaman en son imleç konumuna doğru çalışmamızı sağlar.

mouseDownHandler içinde, hangi yöne doğru çevirmeye başlamak istediğimizi bilmek için farenin sol veya sağ sayfada basılıp basılmadığını kontrol ederek başlarız. Ayrıca, ilk veya son sayfada olabileceğimiz için o yönde başka bir sayfanın bulunduğundan da emin oluruz. Bu kontrollerden sonra geçerli bir çevirme seçeneği varsa ilgili çevirme nesnesinin dragging işaretini true olarak ayarlarız.

mouseUpHandler sınırına ulaştığımızda tüm flips öğelerini inceleriz ve bunlardan herhangi birinin dragging olarak işaretlenip işaretlenmediğini ve artık yayınlanması gerekip gerekmediğini kontrol ederiz. Bir çevirme yayınlandığında, hedef değeri mevcut fare konumuna bağlı olarak çevrilmesi gereken tarafla eşleşecek şekilde ayarlarız. Sayfa numarası da bu gezinmeyi yansıtacak şekilde güncellenir.

Oluşturma

Mantığımızın büyük bir kısmı artık yerinde olduğuna göre, katlanmış kağıdın tuval öğesine nasıl yerleştirileceğini inceleyeceğiz. Bu işlemlerin çoğu, etkin tüm çevirmelerin mevcut durumunu güncellemek ve çizmek için saniyede 60 kez çağrılan render() işlevinde gerçekleşir.

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

}
}

flips oluşturmaya başlamadan önce clearRect(x,y,w,h) yöntemini kullanarak tuvali sıfırlarız. Tüm tuvali temizlemek büyük bir performans maliyetine neden olur. Bu nedenle, yalnızca çizim yaptığımız bölgeleri temizlemek çok daha verimli olacaktır. Bu eğitimin konusunu korumak için tüm tuvali temizleme işlemini ele alacağız.

Bir çevirme işlemi sürükleniyorsa target değeri, fare konumuna uyacak şekilde güncellenir ancak gerçek pikseller yerine -1 ile 1 arasında bir ölçek kullanılır. Ayrıca, progress değerini target mesafesinin bir kısmı kadar artırırız. Bu, her karede güncellendiği için çevirme işleminin sorunsuz ve animasyonlu bir şekilde ilerlemesini sağlar.

Her karede tüm flips öğelerini incelediğimiz için yalnızca etkin olanları yeniden çizdiğimizden emin olmamız gerekir. Çevirme işlemi kitap kenarına çok yakın değilse (BOOK_WIDTH değerinin% 0,3'ü içinde) veya dragging olarak işaretlenmişse etkin olarak kabul edilir.

Artık tüm mantık yerinde olduğuna göre, mevcut durumuna bağlı olarak bir çevirmenin grafiksel gösterimini çizmemiz gerekiyor. Şimdi drawFlip(flip) işlevinin ilk bölümüne bakma zamanı.

// 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';

Kodun bu bölümü, katmanı gerçekçi bir şekilde çizmek için gereken bir dizi görsel değişkeni hesaplayarak başlar. Sayfa katlamasının görünmesini istediğimiz yer burası olduğundan, çizdiğimiz katlamanın progress değeri bu noktada büyük bir rol oynar. Sayfa çevirme efektine derinlik katmak için kağıdı kitabın üst ve alt kenarlarının dışına uzatırız. Bu efekt, çevirme işlemi kitabın sırtına yakın olduğunda en iyi şekilde görünür.

Çevir
Sayfa çevrilirken veya sürüklenirken sayfa katlaması bu şekilde görünür.

Tüm değerler hazırlandığına göre artık sadece kağıdı çizmek kaldı.

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

Sayfa çevirme animasyonumuzu, sırtın üst kısmı 0,0 konumu olarak kullanılacak şekilde çizebilmemiz için tuval API'sinin translate(x,y) yöntemi, koordinat sistemini kaydırmak üzere kullanılır. Çizim bittiğinde tuvalin mevcut dönüşüm matrisini save() ve restore() yapmamız gerektiğini unutmayın.

Çeviri
Sayfa çevirme işleminin yapıldığı nokta. Orijinal 0,0 noktası resmin sol üst kısmındadır ancak bunu translate(x,y) ile değiştirerek çizim mantığını basitleştiririz.

foldGradient, katlanmış kağıdın şeklini doldurarak gerçekçi vurgular ve gölgeler oluşturmak için kullanacağımız renktir. Ayrıca, kağıt çiziminin etrafına çok ince bir çizgi ekliyoruz. Böylece, kağıt açık renkli arka planlara yerleştirildiğinde kaybolmuyor.

Şimdi tek yapmanız gereken, yukarıda tanımladığımız özellikleri kullanarak katlanmış kağıdın şeklini çizmek. Kağıdımızın sol ve sağ tarafları düz çizgilerle çizilmiş, üst ve alt tarafları ise katlanmış kağıt hissini vermek için eğri şekilde tasarlanmıştır. Bu kağıt bükülmesinin gücü, verticalOutdent değeriyle belirlenir.

İşte bu kadar. Artık tamamen işlevsel bir sayfa çevirme navigasyonunuz var.

Sayfa Çevirme Demosu

Sayfa çevirme efekti, doğru etkileşimli hissi iletmekle ilgilidir. Bu nedenle, efektin resimlerine bakmak tam olarak hakkını vermez.

Sonraki Adımlar

Hard-flip
Bu eğitimdeki yumuşak sayfa çevirme özelliği, etkileşimli sert kapak gibi kitap benzeri diğer özelliklerle birlikte kullanıldığında daha da güçlü hale gelir.

Bu, tuval öğesi gibi HTML5 özelliklerinden yararlanarak yapılabileceklerin yalnızca bir örneğidir. Bu tekniğin bir alıntısı olan daha ayrıntılı kitap deneyimine www.20thingsilearned.com adresinden göz atmanızı öneririm. Burada, sayfa çevirme efektlerinin gerçek bir uygulamada nasıl kullanılabileceğini ve diğer HTML5 özellikleriyle birlikte kullanıldığında ne kadar etkili olduğunu göreceksiniz.

Referanslar