使用Chrome瀏覽器的同學應該會經常看到一些網站的搜索框右邊有一個麥克風的標志,點擊圖標后即可連接麥克風進行語音識別搜索,那么這是如何實現的呢?
其實非常簡單,這是html5下一個新的屬性,通過調用google的語音服務api,用于支持網頁內的語音識別輸入功能。
只需要在輸入框的input上添加一個x-webkit-speech 屬性就可以了。
如本博客的搜索框:
<input type="text" name="edtSearch" x-webkit-speech id="edtSearch" value="Search...." onfocus="if (this.value == 'Search....') {this.value = '';}" onblur="if (this.value == '') {this.value = 'Search....';}" />
另外它還可以添加以下屬性:
語言種類: lang="zh-CN"
發聲改變時觸發相關語音識別:onwebkitspeechchange="foo()"
讓語音輸入的內容更加精確盡量接近搜索內容:x-webkit-grammar="bUIltin:search"
通過平時的使用感覺如果發音清晰的話,識別率還算不錯。這是google應用于Chrome瀏覽器上的一項實驗性功能,以上屬性同樣也對webkit內核瀏覽器生效。
相關W3C標準參考文檔:http://lists.w3.org/Archives/Public/public-xg-htmlspeech/2011Feb/att-0020/api-draft.html
早在公元2011年6月3日傍晚,人人網推出了一個很裝B且完全無視IE瀏覽器的功能——拖拽上床。哦,Sorry, 是拖拽上傳。本文將重點介紹實現拖拽上傳的幾個HTML5技術:Drag&Drop、FileReader API和FormData。
關于這個拖拽上傳,其實國外有很多網站已經有這樣的應用,最早推出拖拽上傳應用的是Gmail,它支持標準瀏覽器下拖拽本地文件到瀏覽器中作為郵件的附件發送。人人網的這個拖拽上傳也是同理,可以讓使用標準瀏覽器的用戶通過簡單的拖拽行為,將本地文件夾中的照片直接上傳到人人網,用戶體驗能得到提升的同時,也希望借此機會推廣一下標準瀏覽器,淘汰IE。人人網當時也向廣大用戶推出升級瀏覽器活動,并喊出口號:”工欲善其計算機,必先利其瀏覽器”。本次拖拽上傳的宣傳口號:你敢”脫”,我就敢上傳…
言歸正題,首先看看效果,大家如果有人人網帳號的話可以在首頁試一試拖拽上傳功能,下面是演示視頻:
拖拽上傳應用主要使用了以下HTML5技術:
關于Drag&Drop拖拽事件,之前我寫過一篇專門介紹的文章《給力的 Google HTML5 訓練營(HTML5 Drag&Drop 拖拽、FileReader實例教程)》,那篇文章詳細講解了Drag & Drap事件的原理和代碼實例,這里的拖拽上傳實現原理基本上是一樣的,大家有興趣或不太了解的話可以先看看那篇文章,我在這里就不再過多啰嗦了~下面直接出拖拽上傳簡要代碼實例:
//拖進
oDragWrap.addEventListener(’dragenter’, function(e) {
e.preventDefault();
}, false);
//拖離
oDragWrap.addEventListener(’dragleave’, function(e) {
dragleaveHandler(e);
}, false);
//拖來拖去 , 一定要注意dragover事件一定要清除默認事件
//不然會無法觸發后面的drop事件
oDragWrap.addEventListener(’dragover’, function(e) {
e.preventDefault();
}, false);
//扔
oDragWrap.addEventListener(’drop’, function(e) {
dropHandler(e);
}, false);
var dropHandler = function(e) {
//將本地圖片拖拽到頁面中后要進行的處理都在這
}
在之前那篇文章中我也有介紹過關于File API中的FileReader接口,作為 File API 的一部分,FileReader 專門用于讀取文件,根據 W3C 的定義,FileReader 接口 “提供一些讀取文件的方法與一個包含讀取結果的事件模型”。關于FileReader的詳細介紹和代碼實例大家可以先去看看那篇文章。
今天我著重介紹一下File API中的FileList接口,它主要通過兩個途徑獲取本地文件列表,一是<input type=”file”>的表單形式,另一種則是e.dataTransfer.files拖拽事件傳遞的文件信息。很顯然,我們這里會用到后者。
使用files方法將會獲取到拖拽文件的數組形勢的數據,每個文件占用一個數組的索引,如果該索引不存在文件數據,將返回null值。可以通過length屬性獲取文件數量.
拖拽上傳需要注意的是需要判斷兩個條件,1:拖拽的是文件不是頁面中的元素; 2:拖拽的是圖片而不是其它文件,可以通過file.type屬性獲取文件的類型
下面讓我們來看看如何結合之前的拖拽事件來實現拖拽圖片并在頁面中進行預覽:
//獲取文件列表
var fileList = e.dataTransfer.files;
//檢測是否是拖拽文件到頁面的操作
if (fileList.length == 0) {return;};
//檢測文件是不是圖片
if (fileList[0].type.indexOf(’image’) === -1) {return;}
//實例化file reader對象
var reader = new FileReader();
var img = document.createElement(’img’);
reader.onload = function(e) {
img.src = this.result;
oDragWrap.appendChild(img);
}
reader.readAsDataURL(fileList[0]);
}
這里有一個簡單的拖拽圖片預覽的Demo
這時你如果用FireBug等類似調試工具查看DOM的話,會看到<img>標簽的src屬性是一個超長的文件二進制數據,所以如果DOM有很多這類圖片,那就要當心瀏覽器性能了,因為這些數據極大地擴充的頁面的代碼量,而每次頁面的reflow都會對瀏覽器形成很大的負擔,So,如果這些圖片還在DOM中,那就盡量不要做動畫或任何重繪操作,如果真的要做就盡量讓圖片脫離文檔流,讓其絕對定位比較靠譜。
補充:可以使用window.URL.createObjectURL(file)來獲取文件的URL(Chrome下用window.webkitURL.createObjectURL(file)),這種方式獲取的URL要比上面說的readAsDataURL簡短很多。而且可以省去使用FileReader。這里感謝BinBinLiao的留言建議:) 下面是使用readAsDataURL與createObjectURL生成的代碼對比:
優化后的代碼:(紅色為優化的代碼)
var fileList = e.dataTransfer.files; //獲取文件列表
var img = document.createElement(’img’);
//檢測是否是拖拽文件到頁面的操作
if (fileList.length == 0) {return;};
//檢測文件是不是圖片
if (fileList[0].type.indexOf(’image’) === -1) {return;}
reader.onload = function(e) {
img.src = this.result;
oDragWrap.appendChild(img);
}
reader.readAsDataURL(fileList[0]);
}
}
需要注意的是,window.URL.createObjectURL是有生命周期的,也就意味著你每用此方法獲取URL,其生命周期都會和DOM一樣,它會單獨占用內存,所以當刪除圖片或不再需要它是,記得用window.URL.revokeObjectURL(file)來釋放其內存。當然,如果你沒有釋放,刷新頁面也是可以釋放的。
既然已經獲取到了拖拽到web頁面中圖片的數據,下一步就是將其發送到服務器端了。
話說HTML5時代之前,AJAX傳輸文件二進制流數據是不可能完成的事情,而現在我們完全可以通過file.getAsBinary獲取文件的二進制數據流,進而將其當做XHR的data數據傳送到后端,8過由于Chrome不支持file的getAsBinary方法,FF3.6+支持此方法。所以Chrome就要另尋它法了,這時我們發現XMLHttpRequest Level 2中的FormData接口完美解決了這個問題,它可以很快捷的模擬Form表單數據并通過AJAX發送至后端,FormData的支持情況是FF5及以上支持,Chrome12及以上支持。
file.getAsBinary獲取文件流很簡單,但是要想上傳數據,就要模擬一下表單的數據格式了,首先看看模擬表單的js代碼, FormData模擬表單數據時更是簡潔,不用麻煩的去拼字符串,而是直接將數據append到formdata對象中即可:
xhr.open(”post”, url, true);
xhr.setRequestHeader(’Content-Type’, ‘multipart/form-data; boundary=’ + boundary);
if (window.FormData) {
//Chrome12+
var formData = new FormData();
formData.append(’file’, file);
formData.append(’hostid’, userId);
formData.append(’requestToken’, t);
data = formData;
} else if (file.getAsBinary) {
//FireFox 3.6+
data = “–” +
boundary +
crlf +
”Content-Disposition: form-data; ” +
”name=\”" +
’file’ +
”\”; ” +
”filename=\”" +
unescape(encodeURIComponent(file.name)) +
”\”" +
crlf +
”Content-Type: image/jpeg” +
crlf +
crlf +
file.getAsBinary() +
crlf +
”–” +
boundary +
crlf +
”Content-Disposition: form-data; ” +
”name=\”hostid\”" +
crlf +
crlf +
userId +
crlf +
”–” +
boundary +
crlf +
”Content-Disposition: form-data; ” +
”name=\”requestToken\”" +
crlf +
crlf +
t +
crlf +
”–” +
boundary +
’–’;
}
xhr.send(data);
首先表單數據headers頭信息需要以下兩項:
發送時的post數據類似這樣:
ÿØÿà?JFIF?…這里是文件二進制流…~iúoî5P%-vãîHü 4QHgÿÙ
————————-1323611763556
Content-Disposition: form-data; name=”hostid”
229421603
—————————–1323612996486
Content-Disposition: form-data; name=”requestToken”
369009193
————————-1323611763556–
好了,現在文件上傳成功后你就可以按照平常AJAX的操作來進行后續處理了。
最后,再來總結一下拖拽上傳的技術要點:
OK,拖拽上傳就講到這里,歡迎大家一起探討。
HTML5提供了一組API用來獲取用戶的地理位置,如果瀏覽器支持且設備具有定位功能,就能夠直接使用這組API來獲取當前位置信息。
該API是navigator對象的一個屬性 – Geolocation。目前除了ie內核瀏覽器外,其他瀏覽器的最新版本基本都支持Geolocation。同時,移動設備IOS 3.0+ 和 Android 2.0+ 系統也支持它,現在很多移動設備的應用加入了地理定位的元素。
那么我們接下來看如何使用Geolocation API:
一、檢查瀏覽器是否支持Geolocation
var hasGeolocation = !!(navigator.geolocation);
if(hasGeolocation){
alert(“瀏覽器支持hasGeolocation”);
}
二、navigator.geolocation 的方法:
* navigator.geolocation有三個方法,分別是getCurrentPosition()、watchPosition()和clearWatch()
getCurrentPosition()方法
* getCurrentPosition()方法檢索用戶的當前位置,但只檢索一次。當該方法被腳本調用時,方法以異步的方式來嘗試獲取宿主設備的當前位置。
* 該方法最多可以有三個參數:
geolocationSuccess:帶回當前位置的回調(callback)(必需的)
geolocationError:有錯誤發生時使用的回調(可選的)
geolocationOptions:地理位置選項(可選的)
調用如下所示:
navigator.geolocation.getCurrentPosition(geolocationSuccess, geolocationError, geolocationOptions);
watchPosition()方法
* watchPosition()方法定期輪詢用戶的位置,查看用戶的位置是否發生改變。其最多可帶三個參數。
調用如下所示:
var watchPositionHandler = navigator.geolocation.watchPosition(geolocationSuccess, geolocationError, geolocationOptions);
clearWatch()方法
* clearWatch()方法終止正在進行的watchPosition(),該方法只能帶一個參數。在調用時,其找到之前已經開始了的watchID參數并立即停止它。
調用如下所示:
navigator.geolocation.clearWatch(watchID);
三、navigator.geolocation返回一個Position對象:
* Position對象有兩個屬性:timestamp和coords。timestamp屬性表示地理位置數據的創建時間,coords屬性又包含七個屬性:
coords.latitude:估計緯度
coords.longitude:估計經度
coords.altitude:估計高度
coords.accuracy:所提供的以米為單位的經度和緯度估計的精確度
coords.altitudeAccuracy:所提供的以米為單位的高度估計的精確度
coords.heading: 宿主設備當前移動的角度方向,相對于正北方向順時針計算
coords.speed:以米每秒為單位的設備的當前對地速度
* 注意altitude, altitudeAccuracy, heading, speed不一定被瀏覽器支持,所以大家最好看一下官方規范的描述,多一些了解。
四、注意事項
* Geolocation App是不能直接訪問設備的,只能通過請求瀏覽器來訪問設備;
* Geolocation涉及到用戶隱私,在獲取 Geolocation 的時候,需要先征求用戶的意思。
* Geolocation目前沒有比較好的前端兼容解決方案,但是在移動設備 iOS 和 Android上,我們可以大膽嘗試使用。
不得不佩服下谷歌Chrome團隊,利用HTML5和CSS3實現了一本相當漂亮的在線電子書:《關于瀏覽器和互聯網20件事》。
訪問地址:http://www.20thingsilearned.com
話說這本電子書已經出來很久了,不過今天來看依然覺得相當的贊。我們無需刷新頁面,就可以來回切換電子書頁面,這正是OPOA(One Page One Application)的完美體現。
現在正在學習關于history API這方面的東西,所以特別感興趣的是他們如何使用window.history.pushState()和window.history.replaceState()來做頁面之間的不刷新切換。
今天查閱了一些資料,基本上對history API有了一個基本了解。
首先要說的就是history是個全局,即window.history。看到這個變量名你一定很熟悉,因為經常可以看到用window.history.back()或者window.history.go(-1)來返回上一頁的JavaScript代碼。
所以history并不是什么新東西,在HTML4的時代,我們可以使用它的這幾個屬性和方法:
length:歷史堆棧中的記錄數。
back():返回上一頁。
forward():前進到下一頁。
go([delta]):delta是個數字,如果不寫或為0,則刷新本頁;如果為正數,則前進到相應數目的頁面;若為負數,則后退到相應數目的頁面。
現在,HTML5為其又添加了以下2個方法:
pushState(data, title [, url]):往歷史堆棧的頂部添加一條記錄。data為一個對象或null,它會在觸發window的popstate事件(window.onpopstate)時,作為參數的state屬性傳遞過去;title為頁面的標題,但當前所有瀏覽器都忽略這個參數;url為頁面的URL,不寫則為當前頁。
replaceState(data, title [, url]):更改當前頁面的歷史記錄。參數同上。這種更改并不會去訪問該URL。不過目前只有Safari 5.0+、Chrome 8.0+、Firefox 4.0+和iOS 4.2.1+支持。如果想兼容老瀏覽器的話,可以試試History.js,而且它還修正了一些bug。
當然,在移動平臺上,我們可以大膽嘗試html5的history API。ios3.0+ 和Android2.0+ 平臺的內置瀏覽器對history都比較完美了,利用它我們可以web app更趨向與native app。
下面,推薦幾篇文章:
.Manipulating the browser history
.Session history and navigation
.Manipulating History for Fun & Profit
今天看到 twitter 上有人在分享 Min.us 這個網站。說是只要把圖片進去,就可以分享。是的,它確實是這樣。體驗了一翻,無論從界面,還操作的方便性上說,這體驗還真是很贊的。
不過,我更在意的是,他用了什么樣的技術。想著今晚回來瞄下代碼。這下,汗的事來了。這頁面看起來不像前端寫的啊。亂七八糟的。真是看不下去了。不過,為了更好的用戶體驗,看吧。從頁面的 HTML5 標簽猜測到可能用到 HTML5 的某些 API,再查一下 JS 發現了這兩行:
function isValidBrowser() { var browser = BrowserDetect.browser; var version = BrowserDetect.version; var OS = BrowserDetect.OS; //alert(browser+','+version+','+OS); return ( (browser == 'Chrome' && version >= 6) || (browser == 'Firefox' && version >= 3.6) || (browser == 'Safari' && version >= 5.0 && OS == 'Mac') || ("FileReader" in self && "ondrag" in document) ); }
先來個預覽吧,你可以先玩玩,再接著往下面看:HTML5 文件拖放上傳
好吧,確定是使用HTML5。我們來看一下這種技術是怎么實現的吧。嗯,再回看上面的代碼,還有我提了這么多次,其實,它就用到兩個技術:
這里有兩篇比較詳細的文章,看(等看完我講的,你再去看吧):
現在 1:10 am 了,同學。明天還要上班。而具體講來,還是需要很多時間滴。我們就簡單一點。說些應該注意的吧。具體的實現,你看我 DEMO 的代碼吧,和上面的文章吧。
一、 瀏覽器檢測 & 支持
支持的瀏覽器并不多,我測試的是 Filefox 和 Chrome, winSafari 5.0.2 沒通過。IE8 以下就更不用說了,當然,Opera 也沒有支持。
if (window.FileReader) { // 做事的 } else { alert('你的瀏覽器不支持啊,同學') }
從 Minus 的代碼中可以看出,支持的有:
其他的,就是浮云了。不過,這已經足夠了,因為我們可以用 hack 來實現。我們這里,就講可以用的吧。不可以的,就用原來項目中使用的方法吧。
二、注意事項
默認情況下,你向 Firefox 拖進一張圖片,他可能會重定向于新頁面。或者拖過一個鏈接,可能會讓頁面重定向。我想,你一定不會讓你的用戶一邊上傳,一邊又打開一個本地文件頁面,然后讓他再跑回來上傳的頁面。那就,阻止瀏覽器默認行為是非常有必要的。
你可能發現了,在 DEMO 當我們把東西拖進容器的時候,容器的邊框樣式變成虛線。因為我這里綁定了一個事件,當文件進入目標是,添加一個 dragenter 的 CLASSNAME。這里需要注意的事,減少事件的解法,你可以在事件 ondragenter 的時候,添加 CLASSNAME ,而 ondragleave 的時候,刪除 CLASSNAME。但綁定在 dragover 上,他就像 mouseover ,在拖動的過程中不斷解發,這對于瀏覽器的負擔就很大了。可能會導致一些亂七八糟的,類似于崩潰之類的事。
你可能需要用 e.dataTransfer.setData 和 e.dataTransfer.getData 來決定是否上傳用戶拖放的文件。
檢測狀態,以便下一步操作,而 result 則可以實現本地預覽的功能,讓用戶知道自己要上傳的東西是什么。
FileReader.readAsBinaryString(fileBlob)
- result
屬性會包含一個文件的二進制的格式FileReader.readAsText(fileBlob, opt_encoding)
- result
屬性將會包含一個文件的文本格式,默認解碼參數是 “utf-8”。FileReader.readAsDataURL(file)
- result
將會包含一個文件的 DataURL 格式(圖片通常用這種方式)其實我還沒有見到國內用上這個技術的網站。剛才瞄了一下 QQ 微博。也沒有。對于這些新技術。于我們前端開來,其實挑戰并不是會不會使用,而是,如何去使用。
比如,在支付寶,下次需要改版用戶上傳身份證認證的時候,或許我會去推動這樣的改進。又比如,如果是我在做 QQ 微博,或者任何一個微博,我將會提供一個讓用戶拖讓一張圖片就能上傳的功能。
HTML5 能用到的地方太多了。就慢你沒想法。因為別人其實并不清楚,連最清楚的你,前端,都不去推動,還能有誰去推動呢?
好吧。在接下來的項目中,你,或許也可以試試。
1.Let's Crate
Let's Crate,采用了較新的手藝,拖曳就能快速上傳文件(但需要瀏覽器的撐持,建議使用谷歌瀏覽器)的一款國外網盤,簡單易用,它不需要注冊,打開頁面后,直接把要上傳的文件拖曳到頁面中心的木箱里,完成后,會生成文件分享鏈接。還有它會提醒你注冊一個帳戶。若是不想注冊,那么復制分享鏈接,但只能使用30 分鐘,相當于的用于姑且分享的,建議注冊,注冊后可以看到你今朝成立的文件夾數、上傳文件總數以及可使用的空間,今朝免費帳戶可以使用的容量為 200MB。
網址:
http://www.letscrate.com/
2.Ge.tt
Ge.tt,一個設計精練清雅文件存儲分享網盤,上傳速度很快。它最年夜的特點在于上傳存儲分享極其精練便捷,就跟它精練的域名一樣,Ge.tt頁面只有一個精明的上傳文件的按鈕。
它為你供給無限量的收集空間,免費用戶撐持保留文件30天,撐持批量上傳,可是是經由過程一路選擇當地多個文件上傳,不需要你一個一個添加文件,一次最多可以上傳30個,上傳完成后系統會供給超短分享外鏈地址,也供給一鍵分享到Facebook和Twitter,當你的鼠標滑過此鏈接時可復制該鏈接與你的伴侶分享。
此外,您還可以經由過程注冊獲得額外功能,如對下載次數的實時統計、更長久的文件保留時刻。
網址:http://www.ge.tt/
3..Min.us
Min.us,帶給你全新上傳體驗在線圖片存儲,瀏覽,分享和打點工具。頁面很是精練,您只需把圖片拖拽到Min.us的頁面里,圖片就能自動上傳。若是想成批上傳圖片,只需用鼠標拔取多張一塊拖拽到Min.us即可。要考試考試此項功能,必需你的瀏覽器是Firefox 3.6+, Chrome, Safari, IE9瀏覽器。否則,會呈現指定文件上傳的上傳頁面。圖片上傳后Min.us會自動完成而且生成相冊鏈接,便利您分享給伴侶,有作為編纂用(Edit)鏈接,有用來分享的(Viewer)鏈接。在每個圖片正下方有一個鏈接(Link to image)便利你將上傳后的圖片貼到任何網站和博客中。
今朝Min.us撐持上傳的圖片名目有:JPEG, JPG, GIF, PNG, APNG,BMP。撐持單張7MB以下的圖片上傳,每個相冊圖片不跨越50張,上傳的圖片是永遠保留的。
網址:http://min.us/
4.PicsEngine
PicsEngine,一款在線圖片打點存儲軟件,近似與桌面版的picasa,你也可以將它算作專注于圖片存儲的網盤。PicsEngine打點照片很是便利輕易,供給使用標簽來智能打點相冊。它的整體界面設計很是酷的,供給了無限的存儲空間,而且可以從任何處所來進行訪謁。
PicsEngine有一個強年夜的很新的功能,就是你可以直接從桌面拖動照片到瀏覽器進行上傳,當然體驗這一功能你需要安裝較高版本的網頁瀏覽器
導語:前年圣誕節上,西班牙程序員Roman Cortes帶來了用純javascript腳本編寫的神奇3D圣誕樹,令人印象深刻。2月14日情人節就要來臨了,還是Roman Cortes,這次他又帶來了用javascript腳本編寫的紅色玫瑰花。用代碼做出的玫瑰花,這才是牛逼程序員送給女友的最好情人節禮物呢!(提示:在不同瀏覽器下觀看效果、速度會有很大的不同)
圖片是由代碼生成,用戶可以刷新該頁面,重復觀看這朵玫瑰的呈現過程。
3D玫瑰花的實現代碼如下:
with(m=Math)C=cos,S=sin,P=pow,R=random;c.width=c.height=f=500;h=-250;function p(a,b,c){if(c>60)return[S(a*7)*(13+5/(.2+P(b*4,4)))-S(b)*50,b*f+50,625+C(a*7)*(13+5/(.2+P(b*4,4)))+b*400,a*1-b/2,a];A=a*2-1;B=b*2-1;if(A*A+B*B<1){if(c>37){n=(j=c&1)?6:4;o=.5/(a+.01)+C(b*125)*3-a*300;w=b*h;return[o*C(n)+w*S(n)+j*610-390,o*S(n)-w*C(n)+550-j*350,1180+C(B+A)*99-j*300,.4-a*.1+P(1-B*B,-h*6)*.15-a*b*.4+C(a+b)/5+P(C((o*(a+1)+(B>0?w:-w))/25),30)*.1*(1-B*B),o/1e3+.7-o*w*3e-6]}if(c>32){c=c*1.16-.15;o=a*45-20;w=b*b*h;z=o*S(c)+w*C(c)+620;return[o*C(c)-w*S(c),28+C(B*.5)*99-b*b*b*60-z/2-h,z,(b*b*.3+P((1-(A*A)),7)*.15+.3)*b,b*.7]}o=A*(2-b)*(80-c*2);w=99-C(A)*120-C(b)*(-h-c*4.9)+C(P(1-b,7))*50+c*2;z=o*S(c)+w*C(c)+700;return[o*C(c)-w*S(c),B*99-C(P(b, 7))*50-c/3-z/1.35+450,z,(1-b/1.2)*.9+a*.1, P((1-b),20)/4+.05]}}setInterval('for(i=0;i<1e4;i++)if(s=p(R(),R(),i%46/.74)){z=s[2];x=~~(s[0]*f/z-h);y=~~(s[1]*f/z-h);if(!m[q=y*f+x]|m[q]>z)m[q]=z,a.fillStyle="rgb("+~(s[3]*h)+","+~(s[4]*h)+","+~(s[3]*s[3]*-80)+")",a.fillRect(x,y,1,1)}',0) |
當然,感興趣的人可以了解下面的實現過程與相關理論:
這朵三維代碼玫瑰的呈現效果采用了蒙特卡羅方法,創造者對蒙特卡羅方法非常推崇,他表示在功能優化和采樣方面,蒙特卡羅方法是“令人難以置信的強大工具”。關于蒙特卡羅方法可以參考:Monte Carlo method 。
具體操作:
外觀采樣呈現效果繪制
我用了多個不同的形狀圖來組成這朵代碼玫瑰。共使用了31個形狀:24個花瓣,4個萼片,2個葉子和1根花莖,其中每一個形狀圖都用代碼進行描繪。
首先,來定義一個采樣范圍:
function surface(a, b) { // I'm using a and b as parameters ranging from 0 to 1. return { x: a*50, y: b*50 }; // this surface will be a square of 50x50 units of size } |
然后,編寫形狀描繪代碼:
var canvas = document.body.appendChild(document.createElement("canvas")), context = canvas.getContext("2d"), a, b, position; // Now I'm going to sample the surface at .1 intervals for a and b parameters: for (a = 0; a < 1; a += .1) { for (b = 0; b < 1; b += .1) { position = surface(a, b); context.fillRect(position.x, position.y, 1, 1); } } |
這時,看到的效果是這樣的:
現在,嘗試一下更密集的采樣間隔:
正如現在所看到的,因為采樣間隔越來越密集,點越來越接近,到最高密度時,相鄰點之間的距離小于一個像素,肉眼就看不到間隔(見0.01)。為了不造成太大的視覺差,再進一步縮小采樣間隔,此時,繪制區已經填滿(比較結果為0.01和0.001)。
接下來,我用這個公式來繪制一個圓形:(X-X0)^ 2 +(Y-Y0)^ 2 <半徑^ 2,其中(X0,Y0)為圓心:
function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { // inside the circle return { x: x, y: y }; } else { // outside the circle return null; } } |
為了防止溢出,還要加上一個采樣條件:
if (position = surface(a, b)) { context.fillRect(position.x, position.y, 1, 1); } |
結果如下:
有不同的方法來定義一個圓,其中一些并不需要拒絕采樣。我并無一定要使用哪一種來定義圓圈的意思,所以下面用另一種方法來定義一個圓:
function surface(a, b) { // Circle using polar coordinates var angle = a * Math.PI * 2, radius = 50, x0 = 50, y0 = 50; return { x: Math.cos(angle) * radius * b + x0, y: Math.sin(angle) * radius * b + y0 }; } |
如圖:
(此方法相比前一個方法需要密集采樣以進行填充。)
好了,現在讓圓變形,以使它看起來更像是一個花瓣:
function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { return { x: x, y: y * (1 + b) / 2 // deformation }; } else { return null; } } |
結果:
這看起來已經很像一個玫瑰花瓣的形狀了。在這里也可以試試通過修改一些函數數值,將會出現很多有趣的形狀。
接下來應該給它添加色彩了:
function surface(a, b) { var x = a * 100, y = b * 100, radius = 50, x0 = 50, y0 = 50; if ((x - x0) * (x - x0) + (y - y0) * (y - y0) < radius * radius) { return { x: x, y: y * (1 + b) / 2, r: 100 + Math.floor((1 - b) * 155), // this will add a gradient g: 50, b: 50 }; } else { return null; } } for (a = 0; a < 1; a += .01) { for (b = 0; b < 1; b += .001) { if (point = surface(a, b)) { context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(point.x, point.y, 1, 1); } } } |
結果:
一片帶色的花瓣就出現了。
3D曲面和透視投影
定義三維表面很簡單,比如,來定義一個管狀物體:
function surface(a, b) { var angle = a * Math.PI * 2, radius = 100, length = 400; return { x: Math.cos(angle) * radius, y: Math.sin(angle) * radius, z: b * length - length / 2, // by subtracting length/2 I have centered the tube at (0, 0, 0) r: 0, g: Math.floor(b * 255), b: 0 }; } |
接著添加投影透視圖,首先需要我們定義一個攝像頭:
如上圖,將攝像頭放置在(0,0,Z)位置,畫布在X / Y平面。投影到畫布上的采樣點為:
var pX, pY, // projected on canvas x and y coordinates perspective = 350, halfHeight = canvas.height / 2, halfWidth = canvas.width / 2, cameraZ = -700; for (a = 0; a < 1; a += .001) { for (b = 0; b < 1; b += .01) { if (point = surface(a, b)) { pX = (point.x * perspective) / (point.z - cameraZ) + halfWidth; pY = (point.y * perspective) / (point.z - cameraZ) + halfHeight; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } |
效果為:
z-buffer
z-buffer在計算機圖形學中是一個相當普遍的技術,在為物件進行著色時,執行“隱藏面消除”工作,使隱藏物件背后的部分就不會被顯示出來。
上圖是用z-buffer技術處理后的玫瑰。(可以看到已經具有立體感了)
代碼如下:
var zBuffer = [], zBufferIndex; for (a = 0; a < 1; a += .001) { for (b = 0; b < 1; b += .01) { if (point = surface(a, b)) { pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth); pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight); zBufferIndex = pY * canvas.width + pX; if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) { zBuffer[zBufferIndex] = point.z; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } } |
旋轉
你可以使用任何矢量旋轉的方法。在代碼玫瑰的創建中,我使用的是歐拉旋轉。現在將之前編寫的管狀物進行旋轉,實現繞Y軸旋轉:
function surface(a, b) { var angle = a * Math.PI * 2, radius = 100, length = 400, x = Math.cos(angle) * radius, y = Math.sin(angle) * radius, z = b * length - length / 2, yAxisRotationAngle = -.4, // in radians! rotatedX = x * Math.cos(yAxisRotationAngle) + z * Math.sin(yAxisRotationAngle), rotatedZ = x * -Math.sin(yAxisRotationAngle) + z * Math.cos(yAxisRotationAngle); return { x: rotatedX, y: y, z: rotatedZ, r: 0, g: Math.floor(b * 255), b: 0 }; } |
效果:
蒙特卡羅方法
關于采樣時間,間隔過大過小都會引起極差的視覺感受,所以,需要設置合理的采樣間隔,這里使用蒙特卡羅方法。
var i; window.setInterval(function () { for (i = 0; i < 10000; i++) { if (point = surface(Math.random(), Math.random())) { pX = Math.floor((point.x * perspective) / (point.z - cameraZ) + halfWidth); pY = Math.floor((point.y * perspective) / (point.z - cameraZ) + halfHeight); zBufferIndex = pY * canvas.width + pX; if ((typeof zBuffer[zBufferIndex] === "undefined") || (point.z < zBuffer[zBufferIndex])) { zBuffer[zBufferIndex] = point.z; context.fillStyle = "rgb(" + point.r + "," + point.g + "," + point.b + ")"; context.fillRect(pX, pY, 1, 1); } } } }, 0); |
設置a和b為隨機參數,用足夠的采樣完成表面填充。我每次繪制10000點,然后靜待屏幕完成更新。
另外需要注意的是,如果隨機數發生錯誤時,表面填充效果會出錯。有些瀏覽器中,Math.random的執行是線性的,這就有可能導致表面填充效果出錯。這時,就得使用類似Mersenne Twister(一種隨機數算法)這樣的東西去進行高質量的PRNG采樣,從而避免錯誤的發生。
完成
為了使玫瑰的每個部分在同一時間完成并呈現,還需要添加一個功能,為每部分設置一個參數以返回值來進行同步。并用一個分段函數代表玫瑰的各個部分。比如在花瓣部分,我用旋轉和變形來創建它們。
雖然表面采樣方法是創建三維圖形非常著名的、最古老的方法之一,但這種把蒙特卡羅、z-buffer加入到表面采樣中的方法并不常見。對于現實生活場景的制作,這也許算不上很有創意,但它簡易的代碼實現和很小的體積仍令人滿意。
希望這篇文章能激發計算機圖形學愛好者來嘗試不同的呈現方法,并從中獲得樂趣。(Roman Cortes)
英文原址:romancortes.com