【CSON原創】javascript實現3D塗鴉效果


功能說明:

通過鼠標移動,實時繪制出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



 



 



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM