zoukankan      html  css  js  c++  java
  • 前端的3D(css3版本)--淘宝造物节3D创景的制作

    其实是依托Css3的功劳,先上一个例子 

    链接: https://pan.baidu.com/s/1cZ-mMI01FHO3u793ZhvF2w 提取码: d3s7
    代码地址:链接: https://pan.baidu.com/s/1sldhljJ 密码: i6qh

    这动画纵有万般变化,也离不开以下几个属性

    • 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结构
    <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>

    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。继续深究
    该课程是由[妙味课堂]提供的,可以从基础开始学习。

  • 相关阅读:
    POI使用详解
    POI导入导出Excel文件(二)
    jsp页面String转JSON
    jQuery的9中构造函数
    元素的BFC特性与自适应布局
    JS replace可以接受回调函数
    基于clip-path的任意元素的碎片拼接动效(源自鑫空间)
    解耦应用逻辑/事件处理程序
    CSS行高line-height的一些深入理解及应用
    常见的三种三栏网页宽度自适应布局 方法
  • 原文地址:https://www.cnblogs.com/QRL909109/p/7041898.html
Copyright © 2011-2022 走看看