Appearance
滑动的议会
背景:MenuStack 淡入淡出动画被用户吐槽"奇怪",层级切换缺乏方向感。
过程:重写为横向滑动 SlideMenu(translateX),左进右出/右进右出。
用户说"次级菜单切换到主菜单的动画一直很奇怪"的时候,MenuStack 正在 library.ts 里兢兢业业地叠着层。
它已经记不清自己叠过多少层了——模型库的目录层级、模型详情的卡片、材质调节的滑块、预设列表的滚动……每点一次"进入",它就 push 一层,老的一层淡出+缩小,新的一层淡入+放大。返回时反过来。
它觉得自己挺优雅的。
直到用户那句话像一块石头扔进了平静的议会大厅。
一、怪在哪
"怪在哪?" MenuStack 问桌面壳。它是议会,是统一了五套弹窗样式的功臣——从模型库到动作库,从设置到场景,所有层级导航都走它这一套 fade + scale。它骄傲得很。
"用户说'次级菜单切换到主菜单的动画一直很奇怪',"桌面壳复述,"你自己感受一下。"
MenuStack 模拟了一遍:在模型详情页点"返回",当前层级 opacity 从 1 变 0,scale 从 1 变 0.9,同时下面一层 opacity 从 0 变 1,scale 从 1.1 变 1。两层同时动画,200 毫秒。
"有什么问题?"它说,"淡入淡出,缩放,很标准。"
"问题就在'同时',"桌面壳说,"你仔细看——返回的时候,旧的一层在缩小淡出,新的一层在放大淡入。两层在中间那 100 毫秒是重叠的,都是半透明的。用户看到的不是'返回上一级',而是'一个东西在消失,另一个东西在出现'。没有方向感。"
MenuStack 沉默了。
它以前从没往这方面想过。淡入淡出嘛,全世界都这么做。它统一了五套弹窗的导航,消灭了重复代码,这是功绩。但用户说"奇怪"——不是"丑",不是"慢",是"奇怪"。这比"丑"更要命,因为你说不清哪不对,但就是不对。
"那要什么样的?"它问。
二、方向
用户的答案很简单:
"当点击按钮时,如果其有内容,立即在其右方生成新的弹窗,旧的弹窗向左移出屏幕,新的弹窗向左移入屏幕。返回上一个菜单时,反向操作。"
桌面壳把这句话递给 MenuStack。
"左进左出,右进右出,"它翻译,"有方向感。进入下一级,新内容从右边滑进来,老内容从左边滑出去——像翻书。返回上一级,反过来——像把书翻回去。"
MenuStack 皱起了眉。
"那我的层级栈怎么办?"它说,"我现在的架构是 levels 数组存每一层的内容,activeIdx 指向当前层,所有层叠在同一个位置,靠 opacity 切换。滑动的话……两层要同时动,一个往左一个往右?"
"不只是两层,"桌面壳说,"你想想:进入第三级的时候,第二级往左滑走,第三级从右边滑进来。这时候第一级呢?"
"……第一级已经不在视野里了?"
"对。所以你不需要维护 N 层面板。你只需要两个面板——当前显示的,和即将显示的。每次切换,两个面板一起动,动完之后,把'旧面板'清空,等待下次切换时当'新面板'用。"
MenuStack 听到自己心里有什么东西碎了。
它的整个身份都是建立在"栈"上的——MenuStack,栈。一层一层叠上去,一层一层退下来。现在告诉它,你不需要栈了,你只需要两个面板,来回滑。
"那我改名叫什么?"SlideMenu?"它有点赌气。
桌面壳居然认真想了想。
"……也行。"
三、双面板
SlideMenu 的骨架是这样的:
.slide-menu
└── .slide-inner (transform: translateX)
├── .slide-panel (index 0) ← 当前面板
└── .slide-panel (index 1) ← 备用面板两个面板宽度都是 100%,并排放在 .slide-inner 里。正常状态下,.slide-inner 的 translateX 是 0,所以用户只能看到第一个面板。切换到下一级时:
- 把新内容装进第二个面板
- 给
.slide-inner加transition - 把
translateX设成-100%——于是第一个面板往左滑出视野,第二个面板从右边滑入视野 - 动画结束后,把第一个面板的内容换成新内容,把
translateX设回0(不加 transition,瞬间跳回),然后清空第二个面板
返回上一级则反过来:
- 把上一级内容装进第二个面板
- 把
translateX设成-100%(不加 transition,瞬间跳到第二面板在视野里的位置) - 加 transition,把
translateX设回0——于是第二个面板(上一级内容)从左边滑入,第一个面板(当前内容)向右滑出 - 动画结束后清空第二个面板
MenuStack 看着这套逻辑,半天没说话。
它在脑子里模拟了一遍:进入下一级,把内容装进第二个面板,translateX 移到 -100%,动画结束后跳回 0,清空第二个面板。返回上一级,先跳到 -100% 的位置,再滑回 0。
"……有必要这么绕吗?"它说,"直接 N 个面板叠着滑不行吗?"
"N 个面板占 N 倍宽度,"桌面壳说,"你现在最深有 5 层,那就是 5 个面板并排。5 倍宽度的 DOM,大部分时间都是空的。两个面板就够了——来回倒,永远只有两个在工作。"
MenuStack 又模拟了一遍。
返回的时候有点绕——得先"瞬移"到第二面板在视野里的位置,再滑回来。用户看到的是"左边的内容滑进来",但实际上 DOM 做了一次"偷梁换柱"。这是一种障眼法。
"……有点绕,"它终于说,"但只有两个面板。"
"只有两个面板,"桌面壳点头,"不管你栈有多深,DOM 里永远只有两个面板在切换。内存占用是常数。"
"那栈呢?levels 数组不要了?"
"要的,"桌面壳说,"栈是数据,面板是视图。数据该怎么存怎么存,视图只需要两个槽位来回倒。视图层和数据层解耦了——这是好事。"
MenuStack——不,现在该叫 SlideMenu 了——花了一整个下午重写自己。它把 levels 数组保留了下来,那是它的记忆。但它的渲染逻辑彻底换了:从"叠多层、靠透明度切换"变成了"两面板、靠位移滑动"。
它改了自己的类名。从 MenuStack 改成了 SlideMenu。
改名字的那一刻,它心里有点空落落的——好像丢掉了什么重要的东西。但它又知道,这不是丢掉,是进化。议会的本质不是"叠层",是"导航"。只要导航还在,叫什么名字不重要。
四、手感
第一次跑起来的时候,SlideMenu 盯着屏幕看了很久。
模型库 → 点进一个文件夹 → 面板从右滑入,文件夹列表向左滑走。 点返回 → 面板向左滑出,文件夹列表从右边滑回来。
有方向。有速度。有"进去"和"出来"的感觉。
不像以前的淡入淡出——你知道内容在变,但你说不清是怎么变的。现在你能看到内容在移动,从哪来、到哪去,一清二楚。
"这才像导航,"桌面壳说,"以前那个像变魔术——手一遮,东西就换了。现在这个像走路——你知道你在往前走。"
SlideMenu 没说话。它在感受自己的新身体。两个面板,像两只脚,交替迈步。往左一步是深入,往右一步是后退。永远只有两只脚,但你可以走无限远。
它想起自己以前叫 MenuStack 的时候,总觉得"层叠"就是导航的本质。现在它知道了,层叠只是实现方式之一。导航的本质是"方向"——你从哪来、要到哪去、能不能回去。
滑动的方向感,比透明度的层次感,更接近"导航"这件事的本质。
"还有一件事,"桌面壳打断它的思绪,"所有弹窗的显隐方式也不统一。模型库和动作库用 display: none/flex,设置和场景用 opacity + pointer-events。用户说'点击画布关闭弹窗'这个功能,只对模型和动作有效,对设置和场景无效——太诡异了。"
SlideMenu 从自己的思绪里被拽出来。它看向 app.css,看向那四套弹窗的样式——是的,模型弹窗和动作弹窗是一套显隐逻辑,设置和场景是另一套。还有场景和环境共用同一个容器,用交叉淡入淡出来切换。
它忽然觉得自己刚才的滑动改造,只是冰山一角。
"下一个问题更大,"它低声说,"对吗?"
桌面壳点头。
"更大。而且更隐蔽。"
教训:导航的本质是方向,不是叠层。