Giriş
2010 yılında F-i.com ve Google Chrome ekibi, "Tarayıcılar ve Web Hakkında Öğrendiğim 20 Şey" adlı HTML5 tabanlı bir eğitim web uygulaması (www.20thingsilearned.com) üzerinde ortak çalışma yürüttüler. Bu projenin temel fikirlerinden biri, sunumun en iyi kitap bağlamında sunulmasıydı. Kitabın içeriği açık web teknolojileriyle ilgili olduğundan, kapsayıcının kendisini bu teknolojilerin bugün başarmamıza olanak tanıdığı şeylere örnek yaparak buna sadık kalmanın önemli olduğunu düşündük.
Gerçek dünya kitabı hissi yaratmanın en iyi yolunun, navigasyon gibi alanlarda dijital dünyanın avantajlarından yararlanmaya devam ederken analog okuma deneyiminin iyi yanlarını simüle etmek olduğuna karar verdik. Okuma akışının grafik ve etkileşimli olarak işlenmesi, özellikle de kitap sayfalarının bir sayfadan diğerine nasıl dönüştüğü üzerine çok çalışıldı.
Başlarken
Bu eğitim, tuval öğesini ve bol miktarda JavaScript'i kullanarak kendi sayfa çevirme efektinizi oluşturma sürecini adım adım gösterir. Değişken bildirimleri ve etkinlik işleyici aboneliği gibi bazı temel kod, bu makaledeki snippet'lerin dışında bırakılmıştır. Dolayısıyla, çalışan örneğe referans vermeyi unutmayın.
Başlamadan önce demoya göz atıp ne geliştirmeye çalıştığımızı öğrenmenizi öneririz.
Markup
Tuvale çizdiğimiz şeylerin arama motorları tarafından dizine eklenemeyeceğini, bir ziyaretçi tarafından seçilemeyeceğini veya tarayıcıda yapılan aramalarla bulunamayacağını her zaman unutmamak gerekir. Bu nedenle, çalışacağımız içerik doğrudan DOM'nin içine yerleştirilir ve varsa JavaScript ile 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 vardır. Bu öğe de aynı şekilde kitabımızın farklı sayfalarını ve çevrilir sayfaları çizeceğimiz canvas
öğesini içerir. section
öğesinin içinde içerik için bir div
sarmalayıcı bulunur. Bu sarmalayıcı, içeriğin düzenini etkilemeden sayfanın genişliğini değiştirebilmemiz için gereklidir. div
sabit bir genişliğe sahiptir ve section
, taşmalarını gizleyecek şekilde ayarlanmıştır. Bu durum, section
genişliğinin div
için yatay maske görevi görür.
Mantık
Sayfa çevirmeyi desteklemek için gereken kod çok karmaşık değildir, ancak prosedürel olarak oluşturulan birçok grafik içerdiğinden oldukça kapsamlıdır. Kodda kullanacağımız sabit değerlerin açıklamasını inceleyerek 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;
CANVAS_PADDING
, çevirirken kağıdın kitabın dışına taşabilmesi için tuvalin etrafına eklenir. Burada tanımlanan bazı sabit değerlerin CSS'de de ayarlandığını unutmayın. Bu nedenle, kitabın boyutunu değiştirmek isterseniz buradaki değerleri de güncellemeniz gerekir.
Daha sonra, her sayfa için bir çevirme nesnesi tanımlamamız gerekir; bunlar, kitapla etkileşimimiz sürecin mevcut durumunu yansıtacak şekilde 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 sayfa üstte ve son sayfa en altta olacak şekilde düzenleyerek sayfaların doğru bir şekilde katmanlandığından emin olmamız gerekir. Döndürme nesnelerinin en önemli özellikleri progress
ve target
değerleridir.
Bunlar, sayfanın şu anda ne kadar katlanacağını belirlemek için kullanılır; -1 tamamen sola, 0 kitabın ölü merkezi, +1 ise kitabın en sağ kenarı anlamına gelir.
Artık her sayfa için tanımlanmış bir çevirme nesnesi olduğuna göre, ç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ışmamız için mouse
nesnesini günceller.
mouseDownHandler
'de farenin sol tarafta mı yoksa sağ sayfada mı basıldığını kontrol ederek başlarız. Böylece, hangi yöne doğru dönmek istediğimizi biliriz. Ayrıca, ilk veya son sayfada olabileceğimiz için bu yönde başka bir sayfanın mevcut olmasını da sağlarız. Bu kontrollerden sonra geçerli bir çevirme seçeneği kullanılabilirse karşılık gelen çevirme nesnesinin dragging
işaretini true
olarak ayarlarız.
mouseUpHandler
kitlesine ulaştıktan sonra tüm flips
öğelerini inceleyip bunlardan herhangi birinin dragging
olarak işaretlenip işaretlenmediğini ve artık serbest bırakılmış olup olmadığını kontrol ederiz. Bir çevirme serbest kaldığında, geçerli fare konumuna bağlı olarak
hedef değerini, çevirmesi gereken tarafla eşleşecek şekilde ayarlarız.
Sayfa numarası da bu gezinme tarzını yansıtacak şekilde güncellenir.
Oluşturma
Artık mantığımızın büyük bir kısmı yerinde olduğuna göre, katlanan kağıdın tuval öğesine nasıl oluşturulacağını göreceğiz. Bu işlemlerin çoğu, tüm etkin çevirmelerin güncel durumunu güncellemek ve mevcut durumunu çizmek için saniyede 60 kez çağrılan render()
işlevinin 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ırladık. Tuvalin tamamının temizlenmesi yüksek performans maliyetine neden olur ve yalnızca faydalandığımız bölgeleri temizlemek çok daha verimli olur. Bu eğiticiyi konudan sapmaması için tüm zemini temizlemeye devam edeceğiz.
Bir kapak sürükleniyorsa target
değerini fare konumuyla eşleşecek şekilde güncelleriz ancak gerçek pikseller yerine -1 ila 1 ölçeğinde çalışırız.
Ayrıca progress
değerini target
öğesine olan mesafenin bir kısmına kadar artırırız. Bu, her karede güncellendiğinden çevirmenin sorunsuz ve animasyonlu ilerlemesini sağlar.
Her karede flips
öğesinin tamamını incelediğimiz için yalnızca etkin olanları tekrar çizdiğimizden emin olmamız gerekiyor. Kapak, kitap kenarına çok yakın değilse (BOOK_WIDTH
alanının% 0, 3'ü kadar mesafede) veya dragging
olarak işaretlenmişse etkin olarak kabul edilir.
Artık tüm mantığın uygulandığına göre, bir çevirmenin mevcut durumuna bağlı
grafik temsilini çizmemiz gerekir. Şimdi 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ü, katı gerçekçi bir şekilde çizmemiz için gereken bir dizi görsel değişkeni hesaplayarak başlar. Çizdiğimiz çevirmenin progress
değeri, sayfa katlama sayfasının görünmesini istediğimiz için burada büyük bir rol oynar. Sayfa çevirme efektine derinlik eklemek için kağıdın kitabın
üst ve alt kenarlarının dışına uzanmasını sağlarız. Bu efekt, kitap sırtına yakın olduğunda
en üst düzeye ulaşır.
Tüm değerler hazır olduğuna göre geriye kalan tek şey kağıda çizim yapmaktır.
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, koordinat sistemini dengelemek için kullanılır. Böylece, sayfa çevirmemizi, sırtın üst kısmı 0,0 konumu olarak çalışacak şekilde çizebiliriz. Çizimi tamamladığımızda zeminin mevcut dönüşüm matrisine save()
ve restore()
buna da eklememiz gerektiğini unutmayın.
foldGradient
, katlanmış kağıdın şeklini gerçekçi vurgular ve gölgeler
sağlamak için dolduracağımız öğedir. Ayrıca, kağıt çizimin etrafına çok ince bir çizgi de ekleriz.
Böylece, kağıt açık renkli arka planlar üzerine yerleştirildiğinde ortaya çıkmaz.
Şimdi geriye kalan tek şey yukarıda tanımladığımız özellikleri kullanarak katlanmış kağıdın
şeklini çizmektir. Kağıdımın sol ve sağ kenarları düz çizgiler olarak çizilir. Üst ve alt kenarları katlanan bir kağıdın bükülmüş hissini verecek şekilde eğimlidir. Bu kağıt kıvrımının gücü verticalOutdent
değeriyle belirlenir.
İşte bu kadar. Artık tam işlevli bir sayfa çevirmeye sahipsiniz.
Sayfa Çevirme Demosu
Sayfa çevirme efekti, doğru etkileşimli hissi iletmekle ilgilidir. Bu nedenle bu efektin resimlerine bakmak tam olarak adaletli olmaz.
Sonraki adımlar
Bu, tuval öğesi gibi HTML5 özellikleri kullanılarak neler yapılabileceğine dair yalnızca bir örnektir. Bu tekniğin alıntı niteliğinde olduğu daha ayrıntılı kitap deneyimine göz atmanızı öneririz: www.20thingsilearned.com. Burada, sayfa çevirmelerinin 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öreceksiniz.
Referanslar
- Canvas API spesifikasyonu