功能說明:
通過鼠標移動,實時繪制出3d旋轉的線條。
兼容IE 5 6 7 8 9 10 firefox chrome
效果預覽:
請按着鼠標左鍵,在畫板上拖動繪制
實現原理:
在上一篇文章《javascript橢圓旋轉相冊》中,通過圖片繞橢圓軌跡移動實現視覺上的3d旋轉,之前在某個網站中看到過這種效果,於是我想到其實我們也可以用同樣原理,通過把點繞不同橢圓軌跡的運動實現任意線的3d旋轉。當按下鼠標左鍵在畫板上繪制時,每個點根據初始位置,確定其橢圓軌跡大小,以相同的速率改變每個點的位置,從而實現整條曲線的3d旋轉。
代碼分析:
init:function(id,options){//初始化函數
options=options||{};
this.container=document.getElementById(id||'container');
this.centerLeft=this.container.clientWidth/2;//原點的left值
this.centerTop=this.container.clientHeight/2; //原點的top值
this.maxA=options.maxA||300;//旋轉橢圓軌跡的橫半軸長
this.maxB=options.maxB||1;//旋轉橢圓軌跡的豎半軸長
this.ballMargin=options.ballMargin||20;//繪畫時小球與小球間間的距離
this.arr=[];//保存畫板上所有小球的數組
this._id;//計時器Id
this.containerPos=getContainerPos(this.container);//容器位置
bindHandler.call(this);
this.run();
}
先看初始化需要哪些值,要使所有點實現橢圓旋轉,首先確定橢圓圓心的left和top值,這里橢圓圓心取容器的中點位置。之后確定的是橢圓軌跡的橫半軸和豎半軸長,還有繪制時小球與小球之間的距離(ballMargin),距離越大,每個小球之間間隙越寬。另外我們需要獲取容器的位置,為獲取鼠標在容器內的位置打下基礎。
var getContainerPos=function(container){//獲取容器位置
var left=0;
var top=0;
while(container.offsetParent){
left+=container.offsetLeft;
top+=container.offsetTop;
container=container.offsetParent;
}
return [left,top];
}
該方法在很多時候都會需要用到,它循環獲取有定位的父對象,累計出容器在頁面的位置,返回結果。注意offsetParent是有定位的父對象,而不是單純的父對象,沒有的話就為window。該方法作為私有方法存在。
var bindHandler = function() {//綁定容器的事件處理程序
var self = this;
this.container.onmousedown = function(eve) {
add = true;
}
this.container.onmousemove = function(eve) {
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); //取消瀏覽器圖片選擇
if (add === true) {
eve = eve || window.event;
var pageX = eve.pageX || eve.clientX + document.documentElement.scrollLeft - document.documentElement.clientLeft;
var pageY = eve.pageY || eve.clientY + document.documentElement.scrollTop - document.documentElement.clientTop;
var mouseX = pageX - self.containerPos[0] - self.centerLeft;
var mouseY = -(pageY - self.containerPos[1]) + self.centerTop;
if (Math.abs(mouseX - preX) > self.ballMargin || Math.abs(mouseY - preY) > self.ballMargin) {
self.addElem(mouseX, mouseY, 'ball/ball2.gif');
preX = mouseX;
preY = mouseY;
}
}
}
this.container.onmouseup = function(eve) {
add = false;
preX = 0;
preY = 0;
}
};
之后要實現鼠標拖動繪制,必須為鼠標按下,鼠標移動,鼠標松開都綁定事件處理程序。主要講講mousemove里的處理,首先由於在鼠標拖動時瀏覽器會默認選擇圖片(使圖片上面有一層藍色),所以我們通過removeAllRanges或empty方法取消選擇,使我們的圖片顯示正常。之后,如果鼠標是按下狀態(add句柄為true),則再判斷上一次繪制小球的位置,如果位置大於我們之前設定的小球間隔,則繪制小球,並保存本次繪制小球的位置,為下次繪制所用。這樣就使小球之間保持適當間隔而不至於太密。
addElem:function(initX,initY,src){//添加小球
var newElem=new Image();
newElem.src=src;
var self=this;
self.container.appendChild(newElem);
if(newElem.complete){;
imgLoad.call(this,newElem,initX,initY)();
}
else{
newElem.onload=imgLoad.call(this,newElem,initX,initY);
}
},
繪制小球的時候,會調用addElem方法,該方法生成小球對象,添加到文檔。注意在IE8及以下版本,由於瀏覽器的緩存原因,只設置圖片的onload的話,會不能執行onload處理程序(參考這里:http://apps.hi.baidu.com/share/detail/21688582),解決方法是通過complete方法先判斷是否已再緩存中,如果是立刻執行處理程序,如果不在再綁定onload處理程序。
var imgLoad=function(newElem,initX,initY){//圖片加載完成事件處理程序
var self=this;
return function(){
initX<0?newElem.angle=Math.PI:newElem.angle=0;//小球被添加的位置,如果小球被添加在負區域,初始旋轉角度為pai,否則為0
newElem._x=newElem._initX=initX;
newElem._y=newElem._initY=initY;
newElem._preSin=0;
newElem._a=Math.abs(initX); //該小球旋轉軌跡的a值
newElem._b=self.maxB; //該小球旋轉軌跡的b值
//根據小球初始位置和上一個小球的位置,設置小球的層級
var preElem=self.arr[self.arr.length-1];
if(preElem){
var preZin=parseInt(preElem.style.zIndex);
var preX=preElem._initX;
preX>=initX?newElem.style.zIndex=preZin+1:newElem.style.zIndex=preZin-1;
}
else{
newElem.style.zIndex=zIn;
}
newElem._initWidth=newElem.clientWidth;
newElem._initHeight=newElem.clientHeight;
self.arr.push(newElem);
}
};
現在看看圖片加載完成的事件處理程序,每個小球根據其初始位置設置旋轉的橫軸長和初始角度,橫軸長為小球x值的絕對值,豎軸長則全部一樣,這里建議豎軸長設置為較小值,使小球運動軌跡的上下浮動不至於過大,保證視覺上的合理性。另外需要重點注意的是,每個小球添加時的zIndex值和該小球初始的x坐標值以及上一次添加的小球的x坐標值有關。如果添加的小球x值比上次小球添加時的x值小,則證明該小球更接近原點,因此視覺上應該更靠前,zIndex比上個小球的zIndex增加1,同理,若比上次小球x值大,則小球的zIndex值減少1.如果是第一個小球,則取默認zIndex(代碼中的zIn)。
run:(function(){
var updatePos=function(elem,angle,a,b,centerLeft,centerTop){//update每次小球位置 參數:小球對象,增加的角度,小球軌跡a值,小球軌跡b值
elem.angle+=angle;
(elem.angle>2*Math.PI)&&(elem.angle-=2*Math.PI);//使小球角度介於0-pai之間,方便計算
elem._x=a*Math.cos(elem.angle); //根據角度計算x值
elem._y=b*Math.sin(elem.angle)+elem._initY;//根據角度計算y值
elem.style.left=elem._x+centerLeft-elem._initWidth/2+'px';
elem.style.top=-elem._y+centerTop-elem._initHeight/2+'px';
//與小球上次的sin值比較,如果與本次sin值互為正負,則小球zIndex值取反(這樣的目的是每次經過x軸時使小球的層級取反)
if(Math.sin(elem.angle)*elem._preSin<0){
elem.style.zIndex*=-1;
}
elem._preSin=Math.sin(elem.angle); //記錄小球每次的sin值,用於下次小球層級計算
}
return function(){
var self=this;
this._id=window.setTimeout(function(){
for(var i=0,len=self.arr.length;i<len;i++){//循環遍歷更新所有小球的位置
updatePos(self.arr[i],Math.PI/100,self.arr[i]._a,self.arr[i]._b,self.centerLeft,self.centerTop);
}
self._id=window.setTimeout(arguments.callee,50);
},50);
};
當一張張圖片被添加之后,我們就要通過定時器,不斷改變小球位置,使它們以不同的橢圓軌跡,以相同的角速度旋轉。關於橢圓軌跡的計算問題,上一篇文章《橢圓旋轉相冊》中描述過,現在再簡短描述一次。橢圓的標准方程為:
(
),由於需要處理的是旋轉,所以我們希望把對x,y的處理轉換成對旋轉角度的處理,因此x,y坐標可以表示為:x=a*cosα , y=b*sinα 。所以我們每次增加角速度α,就可以映射到xy直角坐標系中,實現旋轉。還需要注意的是由於小球每次經過x軸,小球的層級就會相反(意思是原來小球A在B前面,經過x軸后,B在A前面),於是我們需要記錄小球上一次的位置的sin值,並和本次的sin值比較,如果相乘小於0,則表示小球正在經過x軸,此時就取反小球的層級zIndex。run方法調用后,每次遍歷數組,更新小球位置。
var r3d=new rotate3dDraw();
最后是調用方法,不傳值的話都取默認值。
所有源代碼:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>無標題文檔</title>
<style type="text/css" rel="stylesheet">
#container{width:600px; height:400px; background:black; position:relative; z-index:0; overflow:hidden;}
#container img{ position:absolute; left:-9999px; top:-9999px;}
</style>
</head>
<body>
<div id="container">
</div>
</body>
<script>
var rotate3dDraw = (function() {
var r3Draw = function(id, options) {//options:橢圓軌跡的最大a和b
this.init(id, options);
}
r3Draw.prototype = (function() {
var zIn = 999; //默認初始的zIndex值
var add = false; //是否開始添加小球
var preX = 0; //上一個小球的X值
var preY = 0; //上一個小球的Y值
var getContainerPos = function(container) {//獲取容器位置
var left = 0;
var top = 0;
while (container.offsetParent) {
left += container.offsetLeft;
top += container.offsetTop;
container = container.offsetParent;
}
return [left, top];
}
var bindHandler = function() {//綁定容器的事件處理程序
var self = this;
this.container.onmousedown = function(eve) {
add = true;
}
this.container.onmousemove = function(eve) {
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); //取消瀏覽器圖片選擇
if (add === true) {
eve = eve || window.event;
var pageX = eve.pageX || eve.clientX + document.documentElement.scrollLeft - document.documentElement.clientLeft;
var pageY = eve.pageY || eve.clientY + document.documentElement.scrollTop - document.documentElement.clientTop;
var mouseX = pageX - self.containerPos[0] - self.centerLeft;
var mouseY = -(pageY - self.containerPos[1]) + self.centerTop;
if (Math.abs(mouseX - preX) > self.ballMargin || Math.abs(mouseY - preY) > self.ballMargin) {
self.addElem(mouseX, mouseY, 'ball/ball2.gif');
preX = mouseX;
preY = mouseY;
}
}
}
this.container.onmouseup = function(eve) {
add = false;
preX = 0;
preY = 0;
}
};
var imgLoad = function(newElem, initX, initY) {//圖片加載完成事件處理程序
var self = this;
return function() {
initX < 0 ? newElem.angle = Math.PI : newElem.angle = 0; //小球被添加的位置,如果小球被添加在負區域,初始旋轉角度為pai,否則為0
newElem._x = newElem._initX = initX;
newElem._y = newElem._initY = initY;
newElem._preSin = 0;
newElem._a = Math.abs(initX); //該小球旋轉軌跡的a值
newElem._b = self.maxB; //該小球旋轉軌跡的b值
//根據小球初始位置和上一個小球的位置,設置小球的層級
var preElem = self.arr[self.arr.length - 1];
if (preElem) {
var preZin = parseInt(preElem.style.zIndex);
var preX = preElem._initX;
preX >= initX ? newElem.style.zIndex = preZin + 1 : newElem.style.zIndex = preZin - 1;
}
else {
newElem.style.zIndex = zIn;
}
newElem._initWidth = newElem.clientWidth;
newElem._initHeight = newElem.clientHeight;
self.arr.push(newElem);
}
};
return {
init: function(id, options) {//初始化函數
options = options || {};
this.container = document.getElementById(id || 'container');
this.centerLeft = this.container.clientWidth / 2; //原點的left值
this.centerTop = this.container.clientHeight / 2; //原點的top值
this.maxA = options.maxA || 300; //旋轉橢圓軌跡的橫半軸長
this.maxB = options.maxB || 1; //旋轉橢圓軌跡的豎半軸長
this.ballMargin = options.ballMargin || 20; //繪畫時小球與小球間間的距離
this.arr = []; //保存畫板上所有小球的數組
this._id; //計時器Id
this.containerPos = getContainerPos(this.container); //容器位置
bindHandler.call(this);
this.run();
},
addElem: function(initX, initY, src) {//添加小球
var newElem = new Image();
newElem.src = src;
var self = this;
self.container.appendChild(newElem);
if (newElem.complete) {
;
imgLoad.call(this, newElem, initX, initY)();
}
else {
newElem.onload = imgLoad.call(this, newElem, initX, initY);
}
},
run: (function() {
var updatePos = function(elem, angle, a, b, centerLeft, centerTop) {//update每次小球位置 參數:小球對象,增加的角度,小球軌跡a值,小球軌跡b值
elem.angle += angle;
(elem.angle > 2 * Math.PI) && (elem.angle -= 2 * Math.PI); //使小球角度介於0-pai之間,方便計算
elem._x = a * Math.cos(elem.angle); //根據角度計算x值
elem._y = b * Math.sin(elem.angle) + elem._initY; //根據角度計算y值
elem.style.left = elem._x + centerLeft - elem._initWidth / 2 + 'px';
elem.style.top = -elem._y + centerTop - elem._initHeight / 2 + 'px';
//與小球上次的sin值比較,如果與本次sin值互為正負,則小球zIndex值取反(這樣的目的是每次經過x軸時使小球的層級取反)
if (Math.sin(elem.angle) * elem._preSin < 0) {
elem.style.zIndex *= -1;
}
elem._preSin = Math.sin(elem.angle); //記錄小球每次的sin值,用於下次小球層級計算
}
return function() {
var self = this;
this._id = window.setTimeout(function() {
for (var i = 0, len = self.arr.length; i < len; i++) {//循環遍歷更新所有小球的位置
updatePos(self.arr[i], Math.PI / 100, self.arr[i]._a, self.arr[i]._b, self.centerLeft, self.centerTop);
}
self._id = window.setTimeout(arguments.callee, 50);
}, 50);
};
})()
}
})();
return r3Draw;
})();
var r3d=new rotate3dDraw();
</script>
</html>
歡迎轉載,請標明出處:http://www.cnblogs.com/Cson/archive/2012/01/28/2330505.html
