前端的3D(css3版本)--淘寶造物節3D創景的製做

實際上是依託Css3的功勞,先上一個例子 css

連接: https://pan.baidu.com/s/1cZ-mMI01FHO3u793ZhvF2w 提取碼: d3s7
代碼地址:連接: https://pan.baidu.com/s/1sldhljJ 密碼: i6qh前端

這動畫縱有萬般變化,也離不開如下幾個屬性css3

  • transform (元素2D 3D轉換)

    translate,3d,X,Y,Z (移動距離)
    scale,3d,X,Y,Z (縮放比例)
    rotate,3d,X,Y,Z (旋轉角度)
    skew,X,Y (傾斜角度)css3動畫

  • transform-origin (容許被轉換元素位置)

    left center right length % app

  • transform-style (被嵌套元素在3D空間中顯示)

    flat (2d) presever-3d (3d)框架

  • perspective (3D元素透視效果 俗稱"景深")

    numberdom

  • perspective-origin (設置3D基數位置 x,y)

    top center right length %函數

  • backface-visibility (元素不面對屏幕是否可見)

    visible hidden學習



這裏寫一個變化的例子,幫助理解動畫

以上例子只是單一的變化 若是多個變化一塊兒執行 遵照 「慢寫的先執行
好比:
原始圖片

 

"translateX(150px) rotateY(180deg)": 先旋轉再移動

"rotateY(180deg) translateX(150px)": 先移動再旋轉

爲何二者只是先後順序不一樣 結果倒是相反的呢?
這就涉及到了 中心點的問題 transform-origin
transform-origin 變換原點 center center;

關鍵字: top bottom center left right;
具體的長度單位(em,rem,px...)

會受到原點影響的變換有:rotate、skew、scale
translate不受影響

第一個是先根據中心原點旋轉180度 再向右移動150pxbr
第二個向右移動150px 中心點未改變 再旋轉180deg

還有一點須要注意:

在js中沒有辦法 經過計算後樣式 獲取到 transform中的相關操做,只能獲取到矩陣

getComputedStyle(XX)['transform'] 獲得的是 matrix3d(...)

關於 transform的全部操做,經過封裝cssTransform來進行操做,
在 cssTransform 中來記錄 對transform的每一步操做,至關於對象賦值。獲取的時候,就獲取 cssTransform中的記錄

function css(element, attr , val){
    // 經過判斷 概括transform 屬性 直接跳到cssTramsform 剩下的直接常規方法處理
    if(attr == "rotate" || attr == "rotateX" 
    || attr == "rotateY" ||attr == "rotateZ" 
    || attr == "scale" || attr == "scaleX"
    || attr == "scaleY" || attr == "skewX"
    || attr == "skewY" || attr == "translateX"
    || attr == "translateY" || attr == "translateZ" ){
        return cssTransform(element, attr, val);
    }
    if(arguments.length == 2){
        var val = getComputedStyle(element)[attr];
        if(attr=='opacity'){
            val = Math.round(val*100);
        }
        return parseFloat(val);
    } 
    if(attr == "opacity") {
        element.style.opacity= val/100;
    } else {
        element.style[attr]= val + "px";    
    }
}
function cssTransform(el, attr, val) {
    if(!el.transform){
        el.transform = {}
    }
    // 若是val爲空 爲獲取值
    if(typeof val == "undefined"){
        if(typeof el.transform[attr] == "undefined"){
            switch(attr) {
                case "scale":
                case "scaleX":
                case "scaleY":
                    el.transform[attr] = 100;
                    break;
                default:
                    el.transform[attr] = 0;    
            }
        }
        return el.transform[attr];
    } else {
        // 設置值 原理就是對象的賦值
        var transformVal = "";
        el.transform[attr] = Number(val);
        for(var s in el.transform){
            switch(s){
                case "rotate":
                case "rotateX":
                case "rotateY":
                case "rotateZ":
                case "skewX":
                case "skewY":
                    transformVal += " "+s+"("+el.transform[s]+"deg)";
                    break;
                case "translateX":
                case "translateY":
                case "translateZ":
                    transformVal += " "+s+"("+el.transform[s]+"px)";
                    break;
                case "scale":
                case "scaleX":
                case "scaleY":
                    transformVal += " "+s+"("+el.transform[s]/100+")";
                    break;
            }
        }
        el.style.WebkitTransform = el.style.transform = transformVal;
    }
}

 

加下來介紹核心庫:m.Tween.js運動函數
使用以下:

MTween({
    el: div, // 目標元素
    target: { // 指望最後變化的值
        scale: 200,
        translateX: 200,
        translateY: 200,
        rotate: 360
    },
    time: 1000, // 動畫執行時間
    type: "backOut", // 動畫特效 貝塞爾曲線
    callBack: function(){ // 動畫執行結束的回調
        console.log("動畫執行完了");
    },
    callIn: function(){ // 動畫執行過程的回調
        console.log("動畫執行中");
    }
})

 


實現的代碼也很簡單

function MTween(init){
    var t = 0;
    var b = {};
    var c = {};
    var d = init.time / 20;
    for(var s in init.target){ 
        b[s] = css(init.el, s); 
        c[s] = init.target[s] - b[s];
    }
    clearInterval(init.el.timer); 
    init.el.timer = setInterval(
        function(){
            t++;
            if(t>d){
                clearInterval(init.el.timer);
                init.callBack&&init.callBack.call(init.el);
            } else {
                init.callIn&&init.callIn.call(init.el);
                for(var s in b){
                    var val = (Tween[init.type](t,b[s],c[s],d)).toFixed(2);
                    css(init.el, s, val);
                }
            }
        },20);
}

 

以上只是基礎知識,爲下面的教程鋪墊

正文開始:

一、安踏圖標轉動,來回變化,消失

二、碎片,雲朵不規則圓柱轉動

三、主體,浮層 圓柱形滾動入場

四、移動事件,陀螺儀,橫豎屏事件

// 總體Html結構
<div id="pageBg"></div>
<div id="view">
    <div id="logo1">
        <div class="logoImg">
            <img src="load/logo.png">
        </div>
        <p class="logoText">已加載 0%</p>
    </div>
    <div id="main">
        <div id="tZ">
            <div id="panoBg"></div>
            <div id="cloud"></div>
            <div id="pano"></div>
        </div>
    </div>
</div>

 

一、安踏圖標轉動,來回變化,消失

 

分析: 安踏圖標有三個 分別爲 logo1 logo2 logo3 (logo2 logo3 爲動態生成,並提早賦值屬性,加上360度旋轉動畫)
logo1 使用css3動畫animation 360度轉動 1s後透明度爲0 並刪除
logo2 由 translateZ : -1000 通過300ms 變爲0 向前移動;接着通過800ms 變爲-1000 向後移動
logo3 在logo2 刪除後出現 由遠到近 再接着消失

其實代碼很簡單 就是用下面的模型代碼實現

MTween({
    el: logo1,
    target: {
      opacity: 0 // 將要最終變化的值
    },
    time: 1000,
    type: 'easeOut',
    callBack: function() { // 運動結束的執行動做
      view.removeChild(logo1)
      css(logo2, 'opacity', 100) // 顯示logo2
      // 接下來作logo2動做 以此類推
      MTween({
        el: logo2,
        target: {
          translateZ: 0
        },
        time: 300,
        type: 'easeBoth',
        callBack: anmt2 
      })
    }
  })

二、碎片,雲朵不規則圓柱轉動

分析:將9張碎片圖片乘3 而後設置隨機的 rotateY rotateX translateZ translateY 變成一個隨機圓柱排布,而後在碎片的主層加上 rotateY 旋轉動畫,再用動畫控制translateZ 向後移動
祥雲入場: 利用 sin cos R 計算translateX translateZ,而後在雲層主層加上 rotateY 旋轉動畫,再用動畫控制translateZ 向後移動

碎片代碼

//基礎框架版本 排成一圈
for (var i = 0; i < 27; i++) {
    var R = 10 + Math.round(Math.random()*240);
    var deg =  Math.round(Math.random()*360)
    css(span, 'rotateY', deg)
    css(span, 'translateZ', R)
}
// 添加上下分佈
css(logo4, "translateZ", -2000)
css(logo4, "scale", 0)
for (var i = 0; i < 27; i++) {
    var xR = 20 + Math.round(Math.random() * 240) // 圓柱碎片的X半徑
    var xDeg = Math.round(Math.random() * 360)
    var yR = 10 + Math.round(Math.random() * 240) // 圓柱碎片的Y半徑
    var yDeg = Math.round(Math.random() * 360)
    css(span, "rotateY", xDeg);
    css(span, "translateZ", xR);
    css(span, "rotateX", yDeg);
    css(span, "translateY", yR)
}
// 從遠到近的移動
MTween({
    el: logo4,
    target: {
      translateZ: 0,
      scale: 100
    },
    time: 500,
    type: "easeOutStrong",
    callBack: function() {
      setTimeout(function() { //從近到遠
        MTween({
          el: logo4,
          target: {
            translateZ: -1000,
            scale: 20
          },
          ...
          })

祥雲代碼
這裏須要每一片雲朵都面對咱們本身

 


這裏知道每個R deg,便能求得x, z
x = Math.sin(deg * Math.PI / 180) * R
z = Math.cos(deg * Math.PI / 180) * R

  var span = document.createElement("span");
    span.style.backgroundImage = 'url(' + imgData.cloud[i % 3] + ')';
    var R = 200 + (Math.random() * 150) // 設置隨機半徑
    var deg = (360 / 9) * i // 圓柱各個角度
    var x = Math.sin(deg * Math.PI / 180) * R // sin求得X
    var z = Math.cos(deg * Math.PI / 180) * R // cos求得Z
    var y = (Math.random() - .5) * 200 // 上下分佈
    css(span, "translateX", x)
    css(span, "translateZ", z)
    css(span, "translateY", y)
    ...
    // 設置動畫
    MTween({
    el: cloud,
    target: {
      rotateY: 540
    },
    time: 3500,
    type: "easeIn",
    callIn: function() { // 這裏須要用到運動過程的回調 將祥雲外層的角度賦予內層祥雲的每一個角度
      var deg = -css(cloud, "rotateY");
      for (var i = 0; i < cloud.children.length; i++) {
        css(cloud.children[i], "rotateY", deg);
      }
    }
  })

三、主體,浮層 圓柱形滾動入場


這裏的圖片是由20張分割好的寬129px的圖片組成

 


每張圖片的角度deg爲360/20,這樣就能獲得中心點距離每張圖片的距離,利用數學的tan公式 R = (width / 2) / Math.tan((deg/ 2 )* Math.PI / 180)

 

var panoBg = document.querySelector('#panoBg')
var width = 129 // 一張圖片寬度
var deg = 360 / imgData.bg.length // 圓柱圖片角度
var R = parseInt((width / 2) / Math.tan((deg/ 2 )* Math.PI / 180) - 1) // tan@ = 對邊(R) / 臨邊(W/2)
var startDeg = 180; // 開始角度
for (var i = 0; i < imgData.bg.length; i++) {
  var span = document.createElement("span");
  css(span, 'rotateY', startDeg)
  css(span, 'translateZ', -R)
  span.style.backgroundImage = "url(" + imgData.bg[i] + ")";
  panoBg.appendChild(span);
  startDeg -= deg // 每張圖片角度遞減
}

設置主體從遠到近 相似畫軸顯示出來,在span初始化時候都設置display="none",而後設置定時器依次打開

var timer = setInterval(function() {
  panoBg.children[num].style.display = "block";
  num++
  if (num >= panoBg.children.length) {
    clearInterval(timer)
  }
}, 3600 / 2 / 20)

設置漂浮層
漂浮層相對簡單一些,動態建立漂浮層,設置初始translateX translateZ,遍歷對應的浮層,設置上面求得的半徑距離,角度

  var pano = document.querySelector('#pano'); // 浮層容器
  var deg = 18; // 差值角度
  var R = 406; // 上圖計算的R
  var nub = 0; // 計數
  var startDeg = 180; // 初始角度 
  css(pano, "rotateX", 0);
  css(pano, "rotateY", -180);
  css(pano, "scale", 0);
  var pano1 = document.createElement("div");
  pano1.className = "pano";
  css(pano1, "translateX", 1.564);
  css(pano1, "translateZ", -9.877);
  for (var i = 0; i < 2; i++) {
    var span = document.createElement("span");
    span.style.cssText = "height:344px;margin-top:-172px;";
    span.style.background = "url(" + imgData["pano"][nub] + ")";
    css(span, "translateY", -163); // 設定固定的值
    css(span, "rotateY", startDeg); // 角度逐級遞減
    css(span, "translateZ", -R);
    nub++;
    startDeg -= deg;
    pano1.appendChild(span)
  }
  var pano2 = document.createElement("div");
  pano2.className = "pano";
  css(pano2, "translateX", 20.225);
  css(pano2, "translateZ", -14.695);
  for (var i = 0; i < 3; i++) {
    var span = document.createElement("span");
    span.style.cssText = "height:326px;margin-top:-163px;";
    span.style.background = "url(" + imgData["pano"][nub] + ")";
    css(span, "translateY", 278);
    css(span, "rotateY", startDeg);
    css(span, "translateZ", -R);
    nub++;
    startDeg -= deg;
    pano2.appendChild(span)
  }

四、移動事件,陀螺儀,橫豎屏事件


移動事件須要監聽三個事件touchstart touchmove touchend
初始化 按下的點startPoint, 主層角度panoBgDeg, 移動一度變化多少px的係數scale,主層深度startZ,最後角度lastDeg,最後差距lastDis

手指按下 touchstart

 document.addEventListener('touchstart', function(e) {
    startPoint.x = e.changedTouches[0].pageX //手指初始位置
    startPoint.y = e.changedTouches[0].pageY //
    panoBgDeg.x = css(panoBg, 'rotateY') //主體容器左右移動 rotateY即是X軸
    panoBgDeg.y = css(panoBg, 'rotateX')
  })

手指移動 touchmove

document.addEventListener('touchmove', function(e) {
    var nowDeg = {}
    var nowDeg2 = {} // 懸浮層也須要移動
    var nowPoint = {}
    nowPoint.x = e.changedTouches[0].pageX; //變化的位置
    nowPoint.y = e.changedTouches[0].pageY;
    var dis = {}
    dis.x = nowPoint.x - startPoint.x // 移動的距離X
    dis.y = nowPoint.y - startPoint.y
    var disDeg = {}
    disDeg.x = -(dis.x / scale.x) // 距離轉度數 
    disDeg.y = dis.y / scale.y
    nowDeg.y = panoBgDeg.y + disDeg.y // 開始角度 + 移動角度
    nowDeg.x = panoBgDeg.x + disDeg.x
    nowDeg2.x = panoBgDeg.x + (disDeg.x) * 0.95 // 浮層的稍微偏動
    nowDeg2.y = panoBgDeg.y + (disDeg.y) * 0.95
    if (nowDeg.y > 45) {
      nowDeg.y = 45
    } else if (nowDeg.y < -45) {
      nowDeg.y = -45
    }

    if (nowDeg2.y > 45) {
      nowDeg2.y = 45
    } else if (nowDeg2.y < -45) {
      nowDeg2.y = -45
    }
    lastDis.x = nowDeg.x - lastDeg.x //進行差距計算
    lastDeg.x = nowDeg.x
    lastDis.y = nowDeg.y - lastDeg.y
    lastDeg.y = nowDeg.y
    css(panoBg, "rotateX", nowDeg.y); // 進行主體角度賦值
    css(panoBg, "rotateY", nowDeg.x);
    css(pano, "rotateX", nowDeg2.y); // 懸浮層角度
    css(pano, "rotateY", nowDeg2.x);
    var disZ = Math.max(Math.abs(dis.x), Math.abs(dis.y))
    if (disZ > 300) {
      disZ = 300
    }
    css(tZ, 'translateZ', startZ - disZ) // 控制拖拉遠近距離
  })

手指擡起 touchend

document.addEventListener('touchend', function(e) {
    var nowDeg = {
      x: css(panoBg, "rotateY"), // 獲取結束角度
      y: css(panoBg, "rotateX")
    };
    var disDeg = {
      x: lastDis.x * 10, // 
      y: lastDis.y * 10
    }
    MTween({
      el: tZ,
      target: {
        translateZ: startZ // 移動後回來 變近
      },
      time: 700,
      type: "easeOut"
    })
    MTween({
      el: panoBg,
      target: {
        rotateY: nowDeg.x + disDeg.x // 主體緩衝
      },
      time: 800,
      type: "easeOut"
    })
    MTween({
      el: pano,
      target: {
        rotateY: nowDeg.x + disDeg.x // 懸浮層緩衝
      },
      time: 800,
      type: "easeOut",
      callBack: function() {
        window.isTouch = false
        window.isStart = false
      }
    })
  })
}


設置景深隨不一樣屏幕適配進行調整

function setPerc() {
  resteview()
  window.onresize = resteview

  function resteview() {
    var view = document.querySelector('#view') // 最外層
    var main = document.querySelector('#main')
    var deg = 52.5
    var height = document.documentElement.clientHeight;
    var R = Math.round(Math.tan(deg / 180 * Math.PI) * height * .5);
    view.style.WebkitPerspective = view.style.perspective = R + "px"; // 設置景深
    css(main, 'translateZ', R)
  }
}

陀螺儀 橫豎屏事件

陀螺儀基礎

 window.addEventListener('deviceorientation', function(e) {
    e.beta // 左右
    e.gamma // 上下
})

橫豎屏基礎

 window.addEventListener('orientationchange', function(e) {
      window.orientation // 0 90 -90 180 表明四個方向
})

這裏須要解決觸摸事件的衝突,須要定義一個全局的isTouch判斷,遇到觸摸就終止陀螺儀事件引發的變化。
同時須要注意橫豎屏會把陀螺儀的beta gamma 改變

 dir = window.orientation
 switch (dir) {
      case 0:
        x = e.beta;
        y = e.gamma;
        break;
      case 90:
        x = e.gamma;
        y = e.beta;
        break;
      case -90:
        x = -e.gamma;
        y = -e.beta;
        break;
      case 180:
        x = -e.beta;
        y = -e.gamma;
        break;
    }

開始傾斜時,記錄開始的陀螺儀位置,主體層的位置。
移動時候和觸摸同樣進行距離差值計算,並進行相加賦予主體層的變化。而後進行遠近動畫,主體移動動畫,懸浮層動畫。

 var nowTime = Date.now()
      // 檢測陀螺儀 轉動時間 與插件的20ms 兼容
    if (nowTime - lastTime < 30) {
      return
    }
    lastTime = nowTime
      // 角度傾斜
    if (!isStart) {
      //start
      isStart = true;
      start.x = x
      start.y = y
      startEl.x = css(pano, 'rotateX')
      startEl.y = css(pano, 'rotateY')
    } else {
      // move
      now.x = x
      now.y = y

      var dis = {}
      dis.x = now.x - start.x
      dis.y = now.y - start.y

      var deg = {}
      deg.x = startEl.x + dis.x
      deg.y = startEl.y + dis.y

      if (deg.x > 45) {
        deg.x = 45;
      } else if (deg.x < -45) {
        deg.x = -45;
      }

      var disXZ = Math.abs(Math.round((deg.x - css(pano, 'rotateX')) * scale))
      var disYZ = Math.abs(Math.round((deg.y - css(pano, "rotateY")) * scale))

      var disZ = Math.max(disXZ, disYZ)
      if (disZ > 300) {
        disZ = 300
      }
      MTween({
        el: tZ,
        target: {
          translateZ: startZ - disZ
        },
        time: 300,
        type: 'easeOut',
        callBack: function(){
          MTween({
            el:tZ,
            target:{
              translateZ: startZ // 進行緩衝動畫
            },
            time: 400,
            type: "easeOut"
          })
        }
      })

      MTween({
        el: pano,
        target: {
          rotateX: deg.x,
          rotateY: deg.y
        },
        time: 800,
        type: 'easeOut'
      })

      MTween({
        el: panoBg,
        target: {
          rotateX: deg.x,
          rotateY: deg.y
        },
        time: 800,
        type: 'easeOut'
      })

以上即是主要代碼,最好本身運行調試下,運用好動畫函數,理解每個步驟。
前端實現3D VR 還有更牛的Three.js, A-Frame。繼續深究
該課程是由[妙味課堂]提供的,能夠從基礎開始學習。

相關文章
相關標籤/搜索