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