资讯专栏INFORMATION COLUMN

前端的3D(css3版本)--淘宝造物节3D创景的制作

MarvinZhang / 1815人阅读

摘要:同时需要注意横竖屏会把陀螺仪的改变开始倾斜时,记录开始的陀螺仪位置,主体层的位置。检测陀螺仪转动时间与插件的兼容角度倾斜进行缓冲动画以上便是主要代码,最好自己运行调试下,运用好动画函数,理解每一个步骤。前端实现还有更牛的。

前端的3D(css3版本),其实是依托Css3的功劳,先上一个例子 http://antario.act.qq.com/
代码地址:链接:https://pan.baidu.com/s/1XYI-... 密码: thw9
这动画纵有万般变化,也离不开以下几个属性

transform (元素2D 3D转换)

   translate,3d,X,Y,Z (移动距离)
   scale,3d,X,Y,Z (缩放比例)
   rotate,3d,X,Y,Z (旋转角度)
   skew,X,Y (倾斜角度)
   

transform-origin (允许被转换元素位置)

   left center right length % 
   

transform-style (被嵌套元素在3D空间中显示)

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

perspective (3D元素透视效果 俗称"景深")

   number
   

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);
}

以上只是基础知识,为下面的教程铺垫

正文开始:

1、安踏图标转动,来回变化,消失
2、碎片,云朵不规则圆柱转动
3、主体,浮层 圆柱形滚动入场
4、移动事件,陀螺仪,横竖屏事件

// 整体Html结构

已加载 0%

1、安踏图标转动,来回变化,消失

分析: 安踏图标有三个 分别为 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 
      })
    }
  })
2、碎片,云朵不规则圆柱转动

分析:将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);
      }
    }
  })
3、主体,浮层 圆柱形滚动入场


这里的图片是由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)
  }
4、移动事件,陀螺仪,横竖屏事件


移动事件需要监听三个事件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。继续深究
该课程是由妙味课堂提供的,可以从基础开始学习。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83505.html

相关文章

  • CSS 3D Panorama(全景) - 淘宝造物技术剖析

    摘要:淘宝造物节的活动页就是全景的一个很赞的页面,它将全景图分割成等份,相邻的元素构成的夹角,相邻两侧面相对于棱柱中心所构成的夹角。 本文转自凹凸实验室:https://aotu.io/notes/2016/08... showImg(https://segmentfault.com/img/remote/1460000011381045); 前言 3D 全景并不是什么新鲜事物了,但以前...

    LiuRhoRamen 评论0 收藏0
  • H5打造3d场景不完全攻略(二): Amazing CSS3D

    摘要:实现方法可参考这篇文章纯打造的模型渲染器实现全景。天空盒子相信很多打造过或有了解过全景的同行们都知道这个概念。首先将创建好的六个面切割出来,以命名标记位置。柱形柱形全景也不算复杂。 前言 对的,本文就是着重介绍如何使用CSS3中的3D变换打造出H5中的3D效果。灵感来源于造物节团队的3d引擎,因为使用方法比较复杂,也没有开源的API文档,于是想自己另外造个轮子,便开始了相关内容的学习和...

    fancyLuo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<