HTML5 是目前正在討論的新一代 HTML 標(biāo)準(zhǔn),它代表了現(xiàn)在 Web 領(lǐng)域的最新的發(fā)展方向。在 HTML5 標(biāo)準(zhǔn)中,加入了新的多樣的內(nèi)容描述標(biāo)簽,直接支持表單驗(yàn)證,視頻和音頻標(biāo)簽,網(wǎng)頁元素的拖拽,離線存儲(chǔ),工作線程等等。當(dāng)然,其中一個(gè)最令人激動(dòng)的新特性就是新的標(biāo)簽類型 Canvas,開發(fā)人員可以通過該標(biāo)簽,在網(wǎng)頁上直接用腳本進(jìn)行繪圖,產(chǎn)生各種 2D 渲染的效果。所以有人預(yù)言,HTML5 將是 Flash 和 Silverlight 的“殺手”。從 Firefox 1.5 開始就已經(jīng)支持 Canvas,Safari 也是很早就開始支持 Canvas。新的瀏覽器比如 Chrome 也是從一開始就支持。但遺憾的是,到目前為止,IE 一直不支持該標(biāo)準(zhǔn)。
下面內(nèi)容將通過如何用 Canvas 來制作一個(gè)圖片瀏覽器的具體實(shí)例,來說明 Canvas 的各種 API,如何使用這些 API 以及如何應(yīng)用到工程中去。本文將首先介紹如何創(chuàng)建圖片瀏覽器的網(wǎng)頁和 JavaScript 類,介紹整體界面的設(shè)計(jì),然后介紹如何用 Canvas 的 API 來繪制 2D 圖形,然后介紹如何在 Canvas 上加載和繪制圖像,接下來本例會(huì)在圖片瀏覽器中加入其他基于 Canvas 的效果,最后是總結(jié)和展望。
首先我們創(chuàng)建一個(gè)新的 html 文件 thumbnail.html,加入如清單 1 所示的內(nèi)容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <!DOCTYPE HTML> < html > < head > < title >Canvas Based Thumbnail</ title > < style type = "text/css" > body { background: black; color: white; font: 24pt Baskerville, Times, Times New Roman, serif; padding: 0; margin: 0; overflow: hidden; } </ style > <script type="text/javascript" src="thumbnail.js"> </script> </ head > < body > < canvas id = "canvas" ></ canvas > </ body > </ html > |
這里我們可以看到,canvas 是 html 的一個(gè)新的標(biāo)簽,其用法和其他標(biāo)簽一樣,只不過它的高和寬有獨(dú)立的屬性而不是在 css 定義的。如果我們要設(shè)置一個(gè) Canvas 區(qū)域的寬高,必須定義為 <canvas width="100" height="100"> 而不能是 <canvas style="width:100,height:100">。在上面的 html 文件中我們沒有直接定義 Canvas 區(qū)域的大小,而是在 JavaScript 中動(dòng)態(tài)定義,下面將要詳細(xì)說明。
現(xiàn)在我們創(chuàng)建一個(gè)新的 JavaScript 文件 thumbnail.js 來在 Canvas 中繪制圖像,我們?cè)O(shè)計(jì)一個(gè) thumbnail 類,該類可以處理用戶事件,繪制圖形,顯示圖像。然后在 window.onload 事件中加載該類,代碼如清單 2 所示:
1 2 3 4 5 6 7 8 9 | function thumbnail() { this.load = function() } } window.onload = function() { thumb = new thumbnail(); thumb.load(); } |
代碼定義了一個(gè)初始化函數(shù) load,并且聲明了 thumbnail 類,這樣我們就可以在 thumbnail 類中添加代碼,在 Canvas 上繪制各種圖形以及圖像了。
設(shè)計(jì)界面
我們?yōu)檫@個(gè)圖片瀏覽頁面設(shè)計(jì)這樣一種界面,圖片通過縮放占滿全部網(wǎng)頁空間,在中間下方,繪制一個(gè)導(dǎo)航欄,顯示縮略圖,當(dāng)點(diǎn)擊縮略圖時(shí),該圖片顯示到網(wǎng)頁中。同時(shí),如果鼠標(biāo)懸停在某個(gè)縮放圖上,則顯示一個(gè)大一點(diǎn)的預(yù)覽圖用來供用戶預(yù)覽。在導(dǎo)航欄的左右兩邊,添加 2 個(gè)按鈕,用于翻頁顯示上一頁和下一頁的縮略圖。對(duì)導(dǎo)航欄上所有控件的尺寸大小和位置如圖 1 所示的方案。這樣,我們就可以按照該方案在 Canvas 上繪制這些控件了。
首先我們繪制如圖 2 所示的一個(gè)導(dǎo)航欄。在左右兩邊各有一個(gè)按鈕,按鈕上顯示一個(gè)三角形的指示圖形。當(dāng)鼠標(biāo)放到一個(gè)按鈕上時(shí),按鈕的背景色能變成高亮的顏色,顯示當(dāng)前選中的按鈕。
清單 3 顯示了繪制圖 2 所示的導(dǎo)航欄的代碼片段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function thumbnail() { const NAVPANEL_COLOR = 'rgba(100, 100, 100, 0.2)'; // 導(dǎo)航欄背景色 const NAVBUTTON_BACKGROUND = 'rgb(40, 40, 40)'; // 導(dǎo)航欄中 button 的背景色 const NAVBUTTON_COLOR = 'rgb(255, 255, 255)'; //button 的前景色 const NAVBUTTON_HL_COLOR = 'rgb(100, 100, 100)'; //button 高亮?xí)r的前景色 var canvas = document.getElementById('canvas'); // 獲得 canvas 對(duì)象 var context = canvas.getContext('2d'); // 獲得上下文對(duì)象 // 繪制左邊 button function paintLeftButton(navRect, color) { //left button lButtonRect = { x: navRect.x + NAVBUTTON_XOFFSET, y: navRect.y + NAVBUTTON_YOFFSET, width: NAVBUTTON_WIDTH, height: navRect.height - NAVBUTTON_YOFFSET * 2 } context.save(); context.fillStyle = color; context.fillRect(lButtonRect.x, lButtonRect.y, lButtonRect.width, lButtonRect.height); //left arrow context.save(); context.fillStyle = NAVBUTTON_COLOR; context.beginPath(); context.moveTo(lButtonRect.x + NAVBUTTON_ARROW_XOFFSET, lButtonRect.y + lButtonRect.height/2); context.lineTo(lButtonRect.x + lButtonRect.width - NAVBUTTON_ARROW_XOFFSET, lButtonRect.y + NAVBUTTON_ARROW_YOFFSET); context.lineTo(lButtonRect.x + lButtonRect.width - NAVBUTTON_ARROW_XOFFSET, lButtonRect.y + lButtonRect.height - NAVBUTTON_ARROW_YOFFSET); context.lineTo(lButtonRect.x + NAVBUTTON_ARROW_XOFFSET, lButtonRect.y + lButtonRect.height/2); context.closePath(); context.fill(); context.restore(); context.restore(); } |
如上所述,在頁面 html 中我們聲明了 <canvas> 元素。當(dāng)需要在 <canvas> 區(qū)域繪制圖形時(shí),我們需要獲得繪制圖形的上下文對(duì)象,在一個(gè)上下文對(duì)象中,保存了當(dāng)前的初始坐標(biāo)位置,顏色,風(fēng)格等等信息。這里我們通過如清單 4 的語句獲取 Canvas 的 2 維繪圖的上下文對(duì)象。
1 2 | var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); |
獲得上下文對(duì)象之后,我們就可以通過 Canvas 提供的 API 來進(jìn)行繪畫了。在清單 5 中,我們使用了矩形的繪制函數(shù)來繪制整個(gè)導(dǎo)航欄背景和左右兩個(gè)按鈕。所下所示:
1 2 3 | fillRect(x,y,width,height): 繪制一個(gè)填充的矩形 strokeRect(x,y,width,height): 給一個(gè)矩形描邊 clearRect(x,y,width,height): 清除該矩形內(nèi)所有內(nèi)容使之透明 |
例如,我們要繪制一個(gè)簡(jiǎn)單的矩形可以用如清單 6 所示代碼:
1 2 3 4 5 6 | var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); context.fillStyle = 'black'; context.fillRect(0, 0, 50, 50); context.clearRect(0, 0, 20, 20); context.strokeRect(0, 0, 20, 20); |
我們繪制該導(dǎo)航框時(shí),需要在左右兩邊各繪制一個(gè)三角形,對(duì)于除了矩形以外的所有多邊形,必須得通過路徑來繪制,常用的路徑相關(guān)函數(shù)有 :
1 2 3 4 | beginPath(): 開始一段路徑 closePath(): 結(jié)束一段路徑 moveTo(x,y) : 移動(dòng)起始點(diǎn)到某點(diǎn) lineTo(x,y) : 繪制線段到目標(biāo)點(diǎn) |
這樣,我們?cè)诶L制三角形的時(shí)候,只需要確定三個(gè)頂點(diǎn)的坐標(biāo),就可以通過 lineTo 函數(shù)繪制三條線段,但是,我們還需要一個(gè)函數(shù)在該三角形區(qū)域內(nèi)填充顏色,這樣需要用到填充和描邊的函數(shù)和樣式:
1 2 | fillStyle = color : 設(shè)置填充顏色 storkeStyle = color : 設(shè)置描變顏色 |
這里 color 值可以是標(biāo)準(zhǔn)的 CSS 顏色值,還可以通過 rgba 函數(shù)設(shè)置透明度。我們可以如下設(shè)置:
1 2 3 4 | context.fillStyle = "white"; context.strokeStyle = "#FFA500"; context.fillStyle = "rgb(255,165,0)"; context.fillStyle = "rgba(255,165,0,1)"; |
同樣,當(dāng)需要填充顏色樣式或者描邊時(shí),有如下函數(shù):
1 2 | stroke() : 按照當(dāng)前描邊樣式描邊當(dāng)前路徑 fill() : 按照當(dāng)前填充樣式填充路徑所描述的形狀 |
這樣,用上述幾個(gè)函數(shù),我們繪制一個(gè)三角形時(shí),可以用如下語句:
1 2 3 4 5 6 7 8 9 10 | var canvas = document.getElementById('canvas'); var context = canvas.getContext('2d'); context.fillStyle = 'black'; context.beginPath(); context.moveTo(0,0); context.lineTo(10,0); context.lineTo(10,10); context.lineTo(0,0); context.closePath(); context.fill(); |
在清單 3 中,我們還聲明了一些常量來定義導(dǎo)航欄的各種控件的大小,其中長度值都是以像素為單位的。這樣我們繪制了整個(gè)導(dǎo)航欄,但我們現(xiàn)在需要當(dāng)鼠標(biāo)放到按鈕上時(shí),按鈕的前景色能夠高亮,顯示當(dāng)前選中的按鈕。這就需要我們?cè)诖a中響應(yīng)用戶事件,并進(jìn)行不同類型的繪制。
響應(yīng)用戶事件和普通的 DOM 編程類似,如清單 12 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | var lastMousePos; // 當(dāng)前鼠標(biāo)位置 this.load = function() { //event binding canvas.onmousemove = onMouseMove; } function onMouseMove(event) { lastMousePos = {x:event.clientX, y:event.clientY}; paint(); } function pointIsInRect(point, rect) { return (rect.x < point.x && point.x < rect.x + rect.width && rect.y < point.y && point.y < rect.y + rect.height); } function paint() { context.clearRect(0, 0, canvas.width, canvas.height); var paintInfo = {inLeftBtn:false, inRightBtn:false} if (lastMousePos && navRect && lButtonRect && rButtonRect) { if (pointIsInRect(lastMousePos, navRect)) { paintInfo.inLeftBtn = pointIsInRect(lastMousePos, lButtonRect); paintInfo.inRightBtn = pointIsInRect(lastMousePos, rButtonRect); } } paintNavigator(paintInfo); } |
這樣我們就繪制了一個(gè)完整的導(dǎo)航欄,它能夠響應(yīng)鼠標(biāo)移動(dòng)事件,并高亮當(dāng)前選中的按鈕。下面我們需要加載和顯示圖片,這就需要用到 Canvas 的繪制圖像函數(shù)。
加載和顯示圖像的代碼片段如清單 13 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | const PAINT_INTERVAL = 20; // 循環(huán)間隔 const PAINT_SLOW_INTERVAL = 20000; const IDLE_TIME_OUT = 3000; // 空閑超時(shí)時(shí)間 // 定義全部圖片 URL 數(shù)組,在本例中,所有圖片保存在和網(wǎng)頁同目錄中 var imageLocations = [ '2006109173628.jpg', '2007310132939.jpg', '200733094828-1.jpg' ]; // 加載圖片 function loadImages() { var total = imageLocations.length; var imageCounter = 0; var onLoad = function(err, msg) { if (err) { console.log(msg); } imageCounter++; if (imageCounter == total) { loadedImages = true; } } for (var i = 0; i < imageLocations.length; i++) { var img = new Image(); img.onload = function() { onLoad(false); }; img.onerror = function() { onLoad(true, e);}; img.src = imageLocations[i]; images[i] = img; } } // 繪制圖片 function paintImage(index) { if (!loadedImages) return; var image = images[index]; var screen_h = canvas.height; var screen_w = canvas.width; var ratio = getScaleRatio({width:image.width, height:image.height}, {width:screen_w, height:screen_h}); var img_h = image.height * ratio; var img_w = image.width * ratio; context.drawImage(image, (screen_w - img_w)/2, (screen_h - img_h)/2, img_w, img_h); } |
在清單 13 的代碼中,我們更新了主繪制函數(shù) paint,加入了 paintImage 函數(shù),在 paintImage 函數(shù)中,利用 Canvas 的 drawImage 函數(shù),在整個(gè) Canvas 區(qū)域,盡可能大地縮放圖片并顯示在 Canvas 中,其最佳縮放比例如圖 3 所示 :
這里縮放比例是通過本例所定義的函數(shù) getScaleRatio 來獲得的,其詳細(xì)代碼見附件。這樣我們可以在 Canvas 上繪制圖像,繪制圖像的函數(shù)定義如下 :
1 | drawImage(image, x, y) image 為一個(gè)圖像或者 Canvas 對(duì)象,x,y 為圖片所要放至位置的左上角坐標(biāo) |
但該函數(shù)還無法滿足我們的要求,我們需要縮放圖片到一個(gè)最佳大小,這就需要 Canvas 繪制圖片函數(shù)的另外一種形式:
1 | drawImage(image, x, y, width, height) width, height 為圖像在目標(biāo) Canvas 上的大小 |
該函數(shù)將圖片縮放到 width 和 height 所指定的大小并顯示出來。我們通過函數(shù) getScaleRatio 來計(jì)算最佳縮放大小,然后就可以通過如清單 15 所示來繪制最佳大小的圖片。
繪制圖片需要傳入一個(gè) image 對(duì)象,它一般是一個(gè)圖片或者 Canvas 對(duì)象。也就是說你可以從一個(gè) URL 中下載圖片顯示在 Canvas 中,也可以在一個(gè) Canvas 中顯示另外一個(gè) Canvas 中繪制的圖形。通過如清單 16 所示的代碼來加載圖片:
1 2 3 4 5 6 7 | var onLoad = function(err, msg) { if (err) console.log(msg); } var img = new Image(); img.onload = function() { onLoad(false); }; img.onerror = function() { onLoad(true, e);}; img.src = ‘ myImage.png ’ ; // 設(shè)置源路徑 |
在整個(gè)程序中,我們利用了 setInterval 函數(shù)加入了一個(gè)定時(shí)器來觸發(fā)主循環(huán),用于不斷循環(huán)等待全部圖片加載。當(dāng)?shù)却龝r(shí)間超過一個(gè)閥值之后,主循環(huán)進(jìn)入 idle 狀態(tài),該循環(huán)不僅能夠用于等待全部圖片加載,也可以用于繪制動(dòng)畫效果,我們?cè)诤竺鎸?huì)講到如何利用該主循環(huán)來制作動(dòng)態(tài)效果。
繪制縮略圖
下一步需要在導(dǎo)航欄中繪制每個(gè)圖片的縮略圖,該縮略圖必須按照最優(yōu)的大小和間隔排列在導(dǎo)航欄中,同時(shí)縮略圖必須經(jīng)過裁剪,獲得最優(yōu)的顯示區(qū)域。整體效果如圖 4 所示:
實(shí)現(xiàn)代碼片段如清單 17 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | const HL_OFFSET = 3; const THUMBNAIL_LENGTH = NAVPANEL_HEIGHT - NAVBUTTON_YOFFSET*2; // 縮略圖顯示區(qū)域的高度 const MIN_THUMBNAIL_LENGTH = 10; // 最小縮略圖間隔 var currentImage = 0; // 當(dāng)前圖片序號(hào) var firstImageIndex = 0; // 當(dāng)前縮略圖中第一張圖片序號(hào) var thumbNailCount = 0; // 當(dāng)前顯示的縮略圖數(shù) var maxThumbNailCount = 0; // 最大能夠顯示的縮略圖數(shù) // 繪制縮略圖 function paintThumbNails(inThumbIndex) { if (!loadedImages) return; if(inThumbIndex != null) { inThumbIndex -= firstImageIndex; } else { inThumbIndex = -1; } var thumbnail_length = rButtonRect.x - lButtonRect.x - lButtonRect.width; maxThumbNailCount = Math.ceil(thumbnail_length / THUMBNAIL_LENGTH); var offset = (thumbnail_length - THUMBNAIL_LENGTH * maxThumbNailCount) / (maxThumbNailCount + 1); if (offset < MIN_THUMBNAIL_LENGTH ) { maxThumbNailCount = Math .ceil(thumbnail_length/ (THUMBNAIL_LENGTH + MIN_THUMBNAIL_LENGTH)); offset = (thumbnail_length - THUMBNAIL_LENGTH * maxThumbNailCount) / (maxThumbNailCount + 1); } thumbNailCount = maxThumbNailCount > imageCount - firstImageIndex? imageCount - firstImageIndex: maxThumbNailCount; imageRects = new Array(thumbNailCount); for (var i = 0; i < thumbNailCount; i++) { image = images[i+firstImageIndex]; context.save(); var x = lButtonRect.x + lButtonRect.width + (offset+THUMBNAIL_LENGTH)*i; srcRect = getSlicingSrcRect({width:image.width, height:image.height}, {width:THUMBNAIL_LENGTH, height: THUMBNAIL_LENGTH}); imageRects[i] = { image:image, rect: { x:x+offset, y:inThumbIndex == i? navRect.y+NAVBUTTON_YOFFSET-HL_OFFSET: navRect.y+NAVBUTTON_YOFFSET, height: THUMBNAIL_LENGTH, width: THUMBNAIL_LENGTH } } context.translate(x, navRect.y); context.drawImage(image, srcRect.x, srcRect.y, srcRect.width, srcRect.height, offset, imageRects[i].rect.y - navRect.y, THUMBNAIL_LENGTH, THUMBNAIL_LENGTH); context.restore(); } } |
清單 17 的代碼使用了 Canvas 中坐標(biāo)轉(zhuǎn)換的方法來繪制每張縮略圖。轉(zhuǎn)換坐標(biāo)函數(shù)如清單 18 所示:
1 | translate(x, y) x 為橫軸偏移方向大小,y 為縱軸方向偏移大小 |
其原理如圖 5 所示:
Canvas 繪圖的坐標(biāo)系和大部分操作系統(tǒng)繪圖的坐標(biāo)系一致,都是左上角為原點(diǎn),向右為 x 方向,向下為 y 方向。從圖 5 中我們看出,新的坐標(biāo)原點(diǎn)平移到了 (x,y) 位置,后面的 Canvas 繪圖函數(shù)都是以新的原點(diǎn)為基準(zhǔn)繪圖。清單 17 在繪制每張縮略圖時(shí),首先轉(zhuǎn)換原點(diǎn)到縮略圖的左上角,然后在固定的 x 和 y 坐標(biāo)位置顯示圖片,將這個(gè)過程做成一個(gè)循環(huán),就繪制了所有等間距的縮略圖。
將圖片顯示到縮略圖中,我們還需要把圖片縮放到其中較短的一邊能夠和縮略圖的邊長重合,同時(shí)截去超出縮略圖大小的圖片部分,從而達(dá)到最優(yōu)的顯示縮略圖的效果。其示意圖如圖 6 所示。
為了獲得這種最優(yōu)的縮略圖顯示效果,我們需要獲得如下信息:1. 原圖中應(yīng)該截取哪些部分圖片;2 . 縮放多大的比例到目標(biāo)區(qū)域中。本例定義了函數(shù) getSlicingSrcRect 實(shí)現(xiàn)了這個(gè)功能,它返回一個(gè) rect 對(duì)象,包括了應(yīng)該截取原圖的哪些區(qū)域,其詳細(xì)代碼見附件。但還需要一個(gè)函數(shù)來將這個(gè)截取的圖片部分縮放到目標(biāo)區(qū)域中,這就用到了 Canvas 繪制圖像函數(shù) drawImage 的另外一種形式:
1 2 | drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight) sx, sy, sWidth, sHeight 為原圖中的需要截取的區(qū)域, dx, dy, dWidth, dHeight 為目標(biāo)區(qū)域的位置大小 |
該函數(shù)截取原圖片的部分區(qū)域,然后縮放顯示到目標(biāo)區(qū)域中。我們利用這個(gè)函數(shù),就能夠?qū)崿F(xiàn)截取最佳區(qū)域以顯示在縮略圖中的效果。
在繪制縮略圖我們還實(shí)現(xiàn)了一個(gè)小技巧:縮略圖大小是固定的,但之間的間距是動(dòng)態(tài)調(diào)整的,當(dāng)縮略圖之間的間距小于一個(gè)閥值的時(shí)候,我們強(qiáng)制最小間隔不小于閥值,詳細(xì)代碼請(qǐng)看清單 17。
顯示縮略圖以后,我們需要響應(yīng)點(diǎn)擊事件,即能夠點(diǎn)擊縮略圖,顯示所對(duì)應(yīng)的圖片。同時(shí),我們還需要點(diǎn)擊左右兩邊的按鈕,能夠?qū)崿F(xiàn)縮略圖的翻頁。這是通過清單 20 所示的代碼實(shí)現(xiàn)的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // 加入了鼠標(biāo)點(diǎn)擊事件的響應(yīng) this.load = function() { //event binding canvas.onclick = onMouseClick; canvas.onmousemove = onMouseMove; } // 鼠標(biāo)點(diǎn)擊事件處理 function onMouseClick(event) { point = {x: event.clientX, y:event.clientY}; lastMousePos = point; if (pointIsInRect(point, lButtonRect)) { nextPane(true); } else if (pointIsInRect(point, rButtonRect)) { nextPane(false); } else { var selectedIndex = findSelectImageIndex(point); if (selectedIndex != -1) { selectImage(selectedIndex); } } updateIdleTime(); } // 返回所點(diǎn)擊的縮略圖序號(hào),如果沒有點(diǎn)擊縮略圖則返回 -1 function findSelectImageIndex(point) { for(var i = 0; i < imageRects.length ; i++) { if (pointIsInRect(point, imageRects[i].rect)) return i + firstImageIndex; } return -1; } // 將當(dāng)前圖片序號(hào)設(shè)為 index,重畫 function selectImage(index) { currentImage = index ; paint(); } // 將縮略圖翻頁,更新縮略圖中第一張圖片的序號(hào) function nextPane(previous) { if (previous) { firstImageIndex = firstImageIndex - maxThumbNailCount < 0? 0 : firstImageIndex - maxThumbNailCount; } else { firstImageIndex = firstImageIndex + maxThumbNailCount*2 - 1 > imageCount - 1? (imageCount - maxThumbNailCount > 0? imageCount - maxThumbNailCount: 0) : firstImageIndex + maxThumbNailCount; } currentImage = (firstImageIndex <= currentImage && currentImage <= firstImageIndex + maxThumbNailCount)? currentImage : firstImageIndex; paint(); } |
這里我們通過 2 個(gè)變量 firstImageIndex 和 currentImage 來控制縮略圖和當(dāng)前圖片的顯示,并能夠根據(jù)鼠標(biāo)點(diǎn)擊來改變當(dāng)前選中的圖片。
當(dāng)瀏覽器的大小改變的時(shí)候,我們的圖片瀏覽器就會(huì)由于沒能重畫導(dǎo)致部分區(qū)域無法顯示。我們需要根據(jù)瀏覽器當(dāng)前頁面大小來動(dòng)態(tài)定義整個(gè)圖片瀏覽器的大小,從而能夠調(diào)整整個(gè)圖片瀏覽器的最佳大小。代碼如清單 21 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | this.load = function() { //resize resize(); window.onresize = resize; //event binding canvas.onclick = onMouseClick; canvas.onmousemove = onMouseMove; loadImages(); startLoop(); updateIdleTime(); } function resize() { var size = getScreenSize(); canvas.width = size.width; canvas.height = size.height; paint(); } function getScreenSize() { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}; } |
這里代碼響應(yīng)了 window 對(duì)象的 onresize 事件,從而能夠響應(yīng)整個(gè)瀏覽器頁面大小改變的事件,通過 document.documentElement.clientWidth 和 document.documentElement.clientHeight 這兩個(gè) DOM 屬性,我們獲得了當(dāng)前頁面顯示范圍大小,從而能夠動(dòng)態(tài)調(diào)整 Canvas 的大小到最佳位置。
我們還需要實(shí)現(xiàn)這種效果:當(dāng)鼠標(biāo)放置在某個(gè)縮略圖上方時(shí),能夠顯示一個(gè)縮略圖預(yù)覽界面,其效果如圖 7 所示:
實(shí)現(xiàn)代碼如清單 22 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | const ARROW_HEIGHT = 10; // 下方三角形的高度 const BORDER_WRAPPER = 2; // 邊緣白框的厚度 // 繪制預(yù)覽圖 function paintHighLightImage(srcRect, imageRect) { var ratio = imageRect.image.width == srcRect.width? THUMBNAIL_LENGTH/imageRect.image.width:THUMBNAIL_LENGTH/imageRect.image.height; ratio *= 1.5; var destRect = { x:imageRect.rect.x + imageRect.rect.width/2 - imageRect.image.width*ratio/2, y:navRect.y - ARROW_HEIGHT - BORDER_WRAPPER - imageRect.image.height*ratio, width: imageRect.image.width * ratio, height: imageRect.image.height * ratio } var wrapperRect = { x: destRect.x - BORDER_WRAPPER, y: destRect.y - BORDER_WRAPPER, width: destRect.width + BORDER_WRAPPER * 2, height: destRect.height + BORDER_WRAPPER * 2 } var arrowWidth = ARROW_HEIGHT * Math.tan(30/180*Math.PI); context.save(); context.fillStyle = 'white'; context.translate(wrapperRect.x, wrapperRect.y); context.beginPath(); context.moveTo(0, 0); context.lineTo(wrapperRect.width, 0); context.lineTo(wrapperRect.width, wrapperRect.height); context.lineTo(wrapperRect.width/2 + arrowWidth, wrapperRect.height); context.lineTo(wrapperRect.width/2, wrapperRect.height+ARROW_HEIGHT); context.lineTo(wrapperRect.width/2 - arrowWidth, wrapperRect.height); context.lineTo(0, wrapperRect.height); context.lineTo(0, 0); context.closePath(); context.fill(); context.drawImage(imageRect.image, BORDER_WRAPPER, BORDER_WRAPPER, destRect.width, destRect.height); context.restore(); } |
在函數(shù) paintHighLightImage 中大量使用了 Canvas 的路徑繪圖函數(shù)來繪制這個(gè)底部為三角形箭頭,上部為矩形的形狀。感興趣的讀者可以研究這些 Canvas 繪圖函數(shù)的使用。
最后我們?cè)诩尤胍粋€(gè)動(dòng)態(tài)的效果:當(dāng)鼠標(biāo)不再移動(dòng)超過一定時(shí)刻的時(shí)候,導(dǎo)航欄能夠自動(dòng)隱藏。其代碼如清單 23 所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // 加入了自動(dòng)隱藏導(dǎo)航欄的功能 function paint() { context.clearRect(0, 0, canvas.width, canvas.height); paintImage(currentImage); var paintInfo = {inLeftBtn:false, inRightBtn:false, inThumbIndex: null} if (lastMousePos && navRect && lButtonRect && rButtonRect) { if (pointIsInRect(lastMousePos, navRect)) { paintInfo.inLeftBtn = pointIsInRect(lastMousePos, lButtonRect); paintInfo.inRightBtn = pointIsInRect(lastMousePos, rButtonRect); if (!paintInfo.inLeftBtn && !paintInfo.inRightBtn) { var index = findSelectImageIndex(lastMousePos); if (index != -1) { paintInfo.inThumbIndex = index; } } } } if(idleTime && getTime() - idleTime <= IDLE_TIME_OUT) { paintNavigator(paintInfo); } } |
當(dāng)空閑時(shí)間超過閥值時(shí),導(dǎo)航欄能夠自動(dòng)隱藏,這樣瀏覽圖片更加方便。
在合并了上述所有清單代碼之后,我們?cè)跒g覽器上就可以看到如圖 8 所示的效果。
完整的代碼請(qǐng)看附件。運(yùn)行代碼需要 Firefox 1.5, Chrome 1, Safari 3 以上版本的瀏覽器。
本文用圖片瀏覽器的例子來說明 Canvas 的各種函數(shù)的使用。該例子也只是一個(gè)簡(jiǎn)單的 demo,并未涉及更為高級(jí)的 Canvas 使用,例如旋轉(zhuǎn)坐標(biāo)轉(zhuǎn)換,繪制曲線,組合圖形,漸變色彩等等。該例子也可以進(jìn)一步改進(jìn),加入更多動(dòng)態(tài)效果并提高效率。本文就不一一敘述。
從上述例子我們也能看到,Canvas 作為 HTML 5 新的元素,其繪圖功能已經(jīng)很接近操作系統(tǒng)的渲染函數(shù)。Canvas 元素可以進(jìn)行矢量繪圖也可以進(jìn)行位圖的繪制,在不久的將來,Canvas 還能利用 WebGL 技術(shù)支持 3D 繪圖,這為未來的的網(wǎng)頁游戲制作和更為豐富的 Web 用戶體驗(yàn)提供了便利。在最新的 Google Wave 平臺(tái)中,就已經(jīng)大量使用了 Canvas 技術(shù)來渲染用戶界面。我相信,在不久的將來,Canvas 能夠大量被廣大網(wǎng)頁設(shè)計(jì)師和架構(gòu)師所使用,并進(jìn)一步被得到完善和加強(qiáng)。
本文相關(guān)源代碼 (samplecode.zip | 2176)
參看系列文章“Canvas Tutorial”,了解更多 Canvas 繪圖細(xì)節(jié)。
如果對(duì) Canvas 標(biāo)準(zhǔn)感興趣可以參看完整的 Canvas API 標(biāo)準(zhǔn)。
關(guān)于 Cufon 的完整 API 參考,讀者可見 http://wiki.github.com/sorccu/cufon/api。
MDC網(wǎng)站上有豐富的Canvas資料。
本例中的UI設(shè)計(jì)是部分參考了Flickr的界面,但它是用Flash實(shí)現(xiàn)的,而我們用純HTML實(shí)現(xiàn)的。
developerWorks上有一系列文章關(guān)于HTML的未來: 《HTML 的未來》
developerWorks 技術(shù)活動(dòng)和網(wǎng)絡(luò)廣播:隨時(shí)關(guān)注 developerWorks 技術(shù)活動(dòng)和網(wǎng)絡(luò)廣播。
developerWorks Web development 專區(qū):通過專門關(guān)于 Web 技術(shù)的文章和教程,擴(kuò)展您在網(wǎng)站開發(fā)方面的技能。
聯(lián)系客服