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

Hakim El Hattab
Hakim El Hattab

Giriş

2010 yılında F-i.com ve Google Chrome ekibi, Tarayıcı ve Web Hakkında Öğrendiğim 20 Şey (www.20thingsilearned.com) adlı HTML5 tabanlı bir eğitim web uygulaması üzerinde birlikte çalıştı. Bu projenin temel fikirlerinden biri, en iyi şekilde bir kitap bağlamında sunulacağıydı. Kitabın içeriği büyük oranda açık web teknolojileri hakkında olduğundan, kapsayıcının kendisini bu teknolojilerin günümüzde neler başarmamıza olanak tanıdığının bir örneği haline getirerek bu konuya sadık kalmanın önemli olduğunu düşündük.

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

Gerçek bir kitap hissine ulaşmanın en iyi yolunun, gezinme gibi alanlarda dijital dünyanın avantajlarından yararlanırken analog okuma deneyiminin iyi yanlarını taklit etmek olduğuna karar verdik. Okuma akışının grafiksel ve etkileşimli olarak işlenmesi (özellikle de kitapların sayfalarının bir sayfadan diğerine nasıl çevrildiği) için çok çaba sarf ettik.

Başlarken

Bu eğitimde, tuval öğesini ve bol miktarda JavaScript'i kullanarak kendi sayfa çevirme efektinizi oluşturma işleminde size yol gösterilmektedir. Değişken bildirimleri ve etkinlik dinleyici aboneliği gibi temel kodların bazıları bu makaledeki snippet'lerden çıkarılmıştır. Bu nedenle, çalışan örneğe başvurmayı unutmayın.

Başlamadan önce, ne tür bir uygulama oluşturmayı amaçladığımızı anlamak için demoya göz atmanız önerilir.

Brüt kar

Tuvalde çizdiğimiz resimlerin arama motorları tarafından dizine eklenemeyeceğini, ziyaretçiler tarafından seçilemeyeceğini veya tarayıcı içi aramalarla bulunamayacağını her zaman unutmayın. Bu nedenle, üzerinde çalışacağımız içerik doğrudan DOM'a yerleştirilir ve ardından JavaScript tarafından (varsa) değiştirilir. Bunun 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ın farklı sayfalarını ve sayfaları çevirirken çizeceğimiz canvas öğesini içerir. section öğesinin içinde, içeriğin div sarmalayıcısı bulunur. İçeriğin düzenini etkilemeden sayfanın genişliğini değiştirebilmek için buna ihtiyacımız vardır. div sabit genişliğe sahiptir ve section, taşmasını gizleyecek şekilde ayarlanmıştır. Bu, section genişliğinin div için yatay bir maske görevi görmesine neden olur.

Açık Kitap&#39;ı açın.
Kitap öğesine kağıt dokusu ve kahverengi kitap kılıfı içeren bir arka plan resmi eklenir.

Mantık

Sayfa çevirme özelliğini desteklemek için gereken kod çok karmaşık değildir ancak birçok işlemsel olarak oluşturulmuş grafik içerdiğinden oldukça kapsamlıdır. Kod boyunca kullanacağımız sabit değerlerin açıklamasına göz atarak başlayalı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;

Sayfalar çevrilirken kağıdın kitabın dışına taşması için CANVAS_PADDING kanvasın etrafına eklenir. Burada tanımlanan bazı sabitlerin CSS'de de ayarlandığını unutmayın. Bu nedenle, kitabın boyutunu değiştirmek istiyorsanız buradaki değerleri de güncellemeniz gerekir.

Sabitler.
Etkileşim 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, kitapla etkileşim kurarken çevirmenin mevcut durumunu yansıtacak şekilde 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 ilk sayfanın üstte, son sayfanın altta olacak şekilde düzenleyerek sayfaların doğru şekilde katmanlandırıldığından emin olmamız gerekir. Döndürme nesnelerinin en önemli özellikleri progress ve target değerleridir. Bu değerler, sayfanın şu anda ne kadar katlanacağını belirlemek için kullanılır. -1, sayfanın en sola, 0 kitabın tam ortasına ve +1 kitabın en sağ kenarına katlanacağı anlamına gelir.

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

Her sayfa için tanımlanmış bir çevirme nesnesi bulunduğundan, çevirmenin durumunu güncellemek için kullanıcı girişini yakalamaya ve kullanmaya başlamamız gerekir.

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, her zaman en son imleç konumuna doğru çalışabilmemiz için mouse nesnesini günceller.

mouseDownHandler işlevinde, hangi yönde sayfa çevirmeye başlayacağımızı bilmek için farenin sol veya sağ sayfaya basılı olup olmadığını kontrol ederek başlarız. Ayrıca, ilk veya son sayfada olabileceğimiz için bu yönde başka bir sayfanın bulunduğundan emin oluruz. Bu kontrollerden sonra geçerli bir çevirme seçeneği varsa ilgili çevirme nesnesinin dragging işaretini true olarak ayarlarız.

mouseUpHandler'e ulaştığımızda tüm flips'leri inceleyip bunlardan herhangi birinin dragging olarak işaretlenip işaretlenmediğini ve artık yayınlanıp yayınlanamayacağını kontrol ederiz. Bir kart döndürüldüğünde, hedef değerini mevcut fare konumuna bağlı olarak döndürülmesi gereken tarafla eşleşecek şekilde ayarlarız. Sayfa numarası da bu gezinmeyi yansıtacak şekilde güncellenir.

Oluşturma

Mantığımızın çoğunu tamamladığımıza göre, katlama kağıdını kanvas öğesinde nasıl oluşturacağımızı inceleyeceğiz. Bunların çoğu, tüm etkin çevirmelerin mevcut durumunu güncellemek ve çizmek için saniyede 60 kez çağrılan render() işlevi içinde 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 öğesini oluşturmaya başlamadan önce clearRect(x,y,w,h) yöntemini kullanarak kanvası sıfırlarız. Tüm kanvası temizlemek büyük bir performans maliyeti getirir. Yalnızca çizim yaptığımız bölgeleri temizlemek çok daha verimli olur. Bu eğitimde konuyu saptırmamak için tüm kanvası temizlemeyi bırakacağız.

Bir kart sürükleniyorsa target değerini fare konumuyla eşleşecek şekilde güncelleriz ancak gerçek piksel yerine -1 ile 1 arasında bir ölçekte. Ayrıca progress değerini, target'a olan mesafenin bir kesri kadar artırırız. Bu işlem, her karede güncellendiği için çevirmenin 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. Bir sayfa çevirme işlemi, kitabın 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.

Tüm mantık yerine oturduktan sonra, bir çevirmenin mevcut durumuna bağlı olarak grafiksel temsilini çizmemiz gerekir. drawFlip(flip) işlevinin ilk bölümüne bakalım.

// 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ü, katlamayı gerçekçi bir şekilde çizmek için ihtiyaç duyduğumuz bir dizi görsel değişkeni hesaplayarak başlar. Sayfanın katlanmasını istediğimiz yer burası olduğundan, çizdiğimiz çevirmenin progress değeri burada önemli bir rol oynar. Sayfa çevirme efektinin derinliğini artırmak için kağıdın kitabın üst ve alt kenarlarının dışına taşmasını sağlıyoruz. Bu efekt, sayfa çevirme işlemi kitabın omurgasına yakın olduğunda en üst düzeye çıkar.

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

Tüm değerler hazır olduğuna göre geriye kağıt çizmek kalıyor.

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

Tuval API'sinin translate(x,y) yöntemi, sayfa çevirmemizi 0,0 konumu olarak omurganın üst kısmıyla çizebilmemiz için koordinat sisteminin ofsetlenmesi amacıyla kullanılır. Ayrıca, çizim işlemini tamamladığımızda kanvasın mevcut dönüşüm matrisini save() ve restore() olarak ayarlamamız gerektiğini unutmayın.

Çeviri
Bu, sayfa çevirme işlemini başlattığımız noktadır. Orijinal 0,0 noktası resmin sol üst kısmındadır ancak translate(x,y) aracılığıyla bunu değiştirerek çizim mantığını basitleştiririz.

foldGradient, katlanmış kağıdın şeklini gerçekçi vurgu ve gölgelerle doldurmak için kullanacağımız değerdir. Ayrıca, kağıt çizimin etrafına çok ince bir çizgi ekleyerek kağıdın açık renkli arka planlara yerleştirildiğinde kaybolmasını önleriz.

Geriye kalan tek şey, yukarıda tanımladığımız özellikleri kullanarak katlanmış kağıdın şeklini çizmektir. Kağıdımızın sol ve sağ tarafları düz çizgiler olarak çizilir. Üst ve alt taraflar ise kağıdın katlanmış hissini vermek için kavisli çizilir. Bu kağıt kıvrımının gücü, verticalOutdent değerine göre belirlenir.

İşte bu kadar. Artık tamamen işlevsel bir sayfa çevirme gezinme menüsüne sahipsiniz.

Sayfa Çevirme Demosu

Sayfa çevirme efektinin amacı, doğru etkileşimli hissi iletmek olduğundan, bu efektin resimlerine bakmak tam olarak doğru bir fikir vermez.

Sonraki Adımlar

Sert çevirme
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, kanvas öğesi gibi HTML5 özelliklerinden yararlanarak neler yapılabileceğini gösteren yalnızca bir örnektir. Bu tekniğin bir alıntısı olduğu daha rafine kitap deneyimine göz atmanızı öneririz: www.20thingsilearned.com. Burada, sayfa çevirme özelliğinin gerçek bir uygulamada nasıl uygulanabileceğini ve diğer HTML5 özellikleriyle birlikte kullanıldığında ne kadar güçlü hale geldiğini görebilirsiniz.

Referanslar