当前位置: 首页 > Web前端 > HTML5

vue3版3D老虎机实现思路

时间:2023-04-05 11:12:43 HTML5

.bandit{display:flex;}生成数据因为老虎机每一列的数据一般都是一致的,所以我们需要有一个默认的初始化数据,这里我们简化成一个0-9的数组,有动态生成的方法很多,从长到短的实现方法有:方法一newArray(10).join(',').split(',').map((item,idx)=>idx)方法二(Array.from({length:10})).map((item,idx)=>idx)方法三Array.from({length:10},(item,idx)=>idx)方法四[...newArray(10).keys()]元素布局我们需要应用3D属性进行布局,实现3D视觉和无缝循环效果分别需要经过:绝对定位X轴旋转Z轴偏移转移视角的隐藏线sight绝对定位实现图形效果ul{position:relative;宽度:100px;height:160px;}li{position:absolute;顶部:0;左:0;宽度:100%;高度:100%;框大小:边框框;边框:1px实心#3e3e3e;background-color:rgba(233,155,67,0.1);}X轴旋转我们有十个数字,周围是正十边形,所以每个数字的旋转角度也很容易计算,实现第二个效果graphics//边数$num:10;li{@for$idxfrom1through$num{&:nth-child(#{$idx}){transform:rotateX(-#{($idx-1)*360/$num}deg);}}}Z轴偏移首先,这是一个3D维度属性。我们都知道基本轴分为多少个,偏移量值多少。这是一个比较复杂的涉及初中数学公式的问题。已知边长为160,每个三角形的内角为360/10=36,其中r就是我们要求的公理,即Z轴的偏移值,根据数学公式r=直角三角形对边内角/Math.tan(直角三角形内角/180*Math.PI)即r=(160/2)/Math.tan((36/2)/180*Math.PI)≈246所以最终样式//边数$num:10;li{@for$idxfrom1through$num{&:nth-child(#{$idx}){变换:rotateX(-#{($idx-1)*360/$num}deg)translateZ(246px);}}}以上三步虽然已经调布局实现了,但是没有用。我们的屏幕是平面的,透视可以决定我们是用2D透视还是3D透视看界面。让我们了解这些关键属性的作用。perspective透视属性定义了3D元素和视图之间的距离。像素计此属性允许您更改查看3D元素的视图。当为元素定义透视属性时,其子元素获得透视效果,而不是元素本身。注意:perspective属性只影响3D变换元素。perspective-originperspective-origin属性定义3D元素所基于的X轴和Y轴。此属性允许您更改3D元素的底部位置。当为元素定义了perspective-origin属性时,它的子元素获得透视效果,而不是元素本身。注意:此属性必须与perspective属性一起使用,并且只影响3D转换元素。transform-style指定嵌套元素应如何在3D空间中呈现。如果你接触过设计或建筑等专业,你可能看过这些图片。一般来说,改变视角就是改变视线的角度和距离。所以我们稍微调整一下属性,然后复制多个叠加看看效果。主容器{perspective:3000px;//3dperspective-origin:50%50%;//观察角度,50%50%表示从中间观察ul{margin:04px;变换样式:保留3d;}}我们可以发现它在视觉上是3D的并且是居中的,但是需要处理它们堆叠优先级的问题。我们用逆向计算的方法来处理-(当前索引-平均值)ul{@for$idxfrom1through$num{&:nth-child(#{$idx}){z-index:-(#{($idx-3)});}}}隐藏视线。我们只需要保持视线,将元素隐藏在容器外即可。需要注意的是,以上属性会影响容器的实际大小,需要预留一个内边距。主容器{overflow:hidden;宽度:600px;高度:160px;padding:20px60px;}不预留marginMargin动画效果我们已经知道整圈数字是一个正十边形,每个数字的旋转角度也是已知的,所以只需要一个循环就可以完成。我们给动画一些固定的旋转圈来达到更好的效果。另外需要设置默认的动画属性和延时效果,顺序为ul{animation-duration:2s;动画填充模式:转发;动画计时功能:缓入缓出;@for$idx从1到$num{&:nth-child(#{$idx}){animation-delay:#{($idx-1)*0.2}s;}@keyframesnum#{($idx-1)}{到{变换:rotateX(calc(5*360deg+360deg/#{$num}*#{($idx-1)}));}}}}RollUp组件实现为了保证通用性,我们选择将样式留给外部实现并通过slot方法引入。该组件只负责三件事:动态扩展槽切换动画动画完成回调动态扩展槽.bandit{display:flex;}是需要根据传入值复制多列槽,横向布局切换动画需要监听传入的结果索引,设置该列播放动画。这里需要注意两个地方,因为我们无法直接获取槽的实例,所以通过获取容器实例,修改其下方子元素的样式。banditDom.value?.children是HTMLCollection类型,所以不能直接使用,需要转成数组动画完成回调因为animationend事件也可以被自身以外的子元素触发,所以我们需要等待所有动画在触发事件之前结束使用示例看一下效果,会发现有两个问题,如果更新索引和现在一样,则不会执行动画,更新动画时,会在开始修复动画失败之前重置为初始值,解决方法是将当前类名移除,然后重新赋值。为了避免两个操作被合并吞噬,我们需要延迟执行assignmentstep.这里有一个option.setTimeout:在指定的毫秒数后调用函数或计算表达式.推迟到下一个DOM更新周期之后分别尝试这两种方法,打印它们的执行顺序)?.forEach((item,idx)=>{console.log(1)item.style.animationName=''//或setTimeoutnextTick(()=>{item.style.animationName=`num${ary[idx]}`console.log(2)})})},{immediate:true})onBeforeUpdate(()=>{console.log(3)})onUpdated(()=>{console.log(4)})执行顺序完全一样1*6342*6,但是为什么只有setTimeout才能解决问题呢?可以发现,我们可以顺利的重新执行动画,成功的将问题1转化为问题2。从概念上我们知道了setTimeout和nextTick的区别,我们来区分一下前者是宏任务,后者是微任务。即使执行顺序相同,也不代表执行时序相同。我们添加了一个新的参考来进行比较。setTimeoutconstbanditDom=ref(null)watch(()=>props.result,(ary)=>{banditDom.value?.children&&Array.from(banditDom.value?.children)?.forEach((item,idx)=>{item.style.animationName=''Promise.resolve().then(()=>console.log('microtask'))setTimeout(()=>console.log('macrotask'))setTimeout(()=>{item.style.animationName=`num${ary[idx]}`console.log(2)})})},{immediate:true})microtask*6macrotask2macrotask2macrotask2macrotask2macrotask2macrotask2nextTickconstbanditDom=ref(null)watch(()=>props.result,(ary)=>{banditDom.value?.children&&Array.from(banditDom.value?.children)?.forEach((item,idx)=>{item.style.animationName=''Promise.resolve().then(()=>console.log('microtask'))setTimeout(()=>console.log('macrotask'))nextTick(()=>{item.style.animationName=`num${ary[idx]}`console.log('businesslogic')})})},{immediate:true})Microtask*6businesslogic*6macrotask*6所以原因很可能是中间的变化被合并执行更多的原因可以查看https://github.com/vuejs/vue/...启动动画前重置问题首先我们要知道为什么会出现这个问题,从我们的初始状态->动画->保留上次动作->清除动画属性->返回到初始状态->赋值类->动画从流程可以看出,如果要解决问题,只需要在清除之前记录动画属性即可,因为我们最没有办法获取动画下一帧的样式,所以我们只能使用一些技巧来实现它。我们现在知道可以使用的条件:lastresultedgenumbercomponentmodification//thelastresultconstlastResult=ref({})//<==========animationevent==========constbanditDom=ref(null)watch(()=>props.result,(ary,oAry)=>{lastResult.value=oArybanditDom.value?.children&&Array.from(banditDom.value?.children)?.forEach((item,idx)=>{item.setAttribute('class','num'+lastResult.value[idx])item.style.animationName=''setTimeout(()=>{item.style.animationName=`num${ary[idx]}`})})},{immediate:true})父组件修改添加最后一帧Styleul{@for$idxfrom1through$num{&.num#{($idx-1)}{变换:rotateX(calc(-5*360deg+360deg/#{$num}*#{($idx-1)}));}@keyframesnum#{($idx-1)}{到{变换:rotateX(calc(5*360deg+360deg/#{$num}*#{($idx-1)}));}}}}扩展优化有时候我们需要进入页面定位到最后的结果,但是这时候需求可以跳过动画直接完成。这时候我们需要添加新的属性来判断constprops=defineProps({//行数col:{type:[Number],default:4,},isDelazy:{type:[Boolean],default:true,},result:{type:[Array],},})因为直接省略了过程,因此,作业需要分为两个步骤。同时跳过动画也不会违反end事件,所以需要单独处理。constbanditDom=ref(null)//动画结束需要定位最后一帧样式。Array.from(banditDom.value?.children)?.forEach((item,idx)=>{item.setAttribute('class','num'+props.result[idx])})}watch(()=>props.result,(ary,oAry)=>{if(!banditDom.value?.children)return//支持动画和直接定位if(props.isDelazy){lastResult.value=oAryArray.from(banditDom.value?.children)?.forEach((item,idx)=>{item.style.animationName=''setTimeout(()=>{item.style.animationName=`num${ary[idx]}`})})}else{//不触发动画,直接定位到最后一帧样式lastResult.value=arysetEndding()}},{immediate:true})同时我们把设置最后一帧的操作放在后面动画结束letcount=0constanimationend=()=>{count++if(count===props.col){count=0setEndding()emit('onComplete')}}最终效果