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.
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.
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.
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.
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 && page - 1 >= 0) {
// We are on the left side, drag the previous page
flips[page - 1].dragging = true;
}
else if (mouse.x > 0 && 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.
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 & 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.
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
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
- Canvas API spesifikasyonu