最终实现效果如视频所示。
需求描述
若干个星球如图定时公转,支持手势左右滑动(本次demo懒得做)
应用于手机H5、微信公众号、小程序端
前端动效技术分析
- css3 transition
-
- 性能优秀,只关心关键帧,有且只有两个关键帧
- css3 animation
-
- 性能一般,只关心关键帧,存在多个关键帧
- js - canvas
-
- 性能优秀,一切都可控制,但是需要代码逻辑控制所有帧
- media gif | video
-
- 性能优秀,需要预加载,基本上不支持交互
需求分析
- 存在交互需求,淘汰media方案
- 有比较明确的动画路线,而且并不复杂,淘汰js - canvas方案
- 遵循如果transition能做,就不用animation的原则,采用transition来实现
实现方案分析
- 需求动效为一个3D效果(近大远小,透视),transition可以使用transition-3d来模拟实现,也可使用scale只模拟做出近大远小的效果。DEMO中采用transition-3d实现所有动效。
- 动效为星球旋转,首先考虑的就是rotate属性来实现,但是实际上实现过程中存在太多问题,不在本次DEMO之中,有兴趣的可自行尝试。既然决定使用transition-3d实现,完全可以直接在立体坐标系中通过三轴定位每个星球,然后使用transition动画切换位置,即可实现需求。有一个弊端:星球公转走的是点到点的直线,而不是围绕公转轨道弧线移动。
- 涉及到交互效果,所以我们使用className来控制每个星球的位置,通过切换className来触发旋转动效。
具体实现方案
- 先在XY平面上平均分布若干个球,组成环形。
- step1 将XY平面沿Z轴做一个初始旋转
@rotateZ - step2 在上一步基础上,将YZ平面沿X轴做一个旋转
@rotateX - step3 在上一步基础上,再次将XY平面沿Z轴做一个初始旋转
@rotateZ2
变量定义
// 星星个数
@starLength: 6;
// 景深
@perspective: 1000px;
// 初始角
@rotateZ: 12;
@rotateX: 65;
@rotateZ2: -10;
// 圆半径
@rSize: 200;
// 星球图大小
@imgSize: 250px;
// 选中放大倍率
@scaleSize: 1.2;
// 动画速度
@duration: 0.5s;
- 将所有的球,绝对定位到同一个点,我们定位到视窗正中心
- 这里最外层使用flex布局,每个球使用绝对定位,即可保证每个球重叠并且中心点在视窗正中心
.star-box{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.star{
height: @imgSize;
width: @imgSize;
position: absolute;
}
- step1 将所有的球,在视窗平面,以中心点为正圆圆心,使用transition3D分散到圆边上排列,并将所有的球分散到XY平面,并将XY平面沿Z轴做一个初始旋转
@rotateZ,分别计算出每个球的XYZ(Z坐标为0)。
@rotateZItem: (@rotateZ + ((@i - 1) * 360 / @starLength));
@transformX: sin(@rotateZItem * 1deg) * @rSize;
@transformY: cos(@rotateZItem * 1deg) * @rSize;
@transformZ: 0;
- step2 在上一步基础上,将YZ平面沿X轴做一个旋转
@rotateX,分别计算出所有球的YZ(X坐标不会变)。
@step1Y: cos(@rotateZItem * 1deg) * @rSize;
@transformY: cos(@rotateX * 1deg) * @step1Y;
@transformZ: sin(@rotateX * 1deg) * @step1Y;
- step3 在上一步基础上,再次将XY平面沿Z轴做一个初始旋转
@rotateZ2,我们取任意一个点在XY平面做计算:
x = sin(m + n) * r
= (sin(m) * cos(n) + sin(n) * cos(m)) * r
= ((a/r) * cos(n) + (b/r) * sin(n)) * r
= a * con(n) + b * sin(n)
y = cos(m + n) * r
= (cos(m) * cos(n) - sin(n) * sin(m)) * r
= ((b/r) * cos(n) - (a/r) * sin(n)) * r
= b * con(n) - a * sin(n)
@step1X: sin(@rotateZItem * 1deg) * @rSize;
@step2Y: cos(@rotateX * 1deg) * @step1Y;
@transformY: @step2Y * cos(@rotateZ2 * 1deg) + @step1X * sin(@rotateZ2 * 1deg);
@transformX: @step1X * cos(@rotateZ2 * 1deg) - @step2Y * sin(@rotateZ2 * 1deg);
- 至此所有球点位都计算出来了,剩下的就是分配classname,通过state动态修改classname,使用transition补全关键帧。就做完了,具体可看demo代码。
transform: translate3D( @transformX * 1px, @transformY * 1px, @transformZ * 1px );
核心less代码:
.star-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.star {
height: @imgSize;
width: @imgSize;
position: absolute;
&-content {
height: 100%;
width: 100%;
transition: all @duration;
.bgContain();
background-size: 0;
background-image: url('./assets/img/star-shadow.png');
}
&-position-01 {
scale: @scaleSize;
&.star-content {
background-size: 100%;
&::before {
opacity: 1;
transition: all @duration;
transition-delay: @duration;
}
}
}
.loopStar(@i) when (@i <=@starLength) {
// 初始角度 加上Z轴初始角度 rotateZ
@rotateZItem: (@rotateZ + ((@i - 1) * 360 / @starLength));
@transformX: sin(@rotateZItem * 1deg) * @rSize;
@transformY: cos(@rotateZItem * 1deg) * @rSize;
@transformZ: 0;
// X轴旋转 rotateX transformX不会变
@step1Y: cos(@rotateZItem * 1deg) * @rSize;
@transformZ: sin(@rotateX * 1deg) * @step1Y;
@transformY: cos(@rotateX * 1deg) * @step1Y;
// Z轴旋转 rotateZ2
@step1X: sin(@rotateZItem * 1deg) * @rSize;
@step2Y: cos(@rotateX * 1deg) * @step1Y;
@transformY: @step2Y * cos(@rotateZ2 * 1deg)+@step1X * sin(@rotateZ2 * 1deg);
@transformX: @step1X * cos(@rotateZ2 * 1deg) - @step2Y * sin(@rotateZ2 * 1deg);
&-0@{i} {
&::after {
content: "";
display: block;
width: 100%;
height: 100%;
position: absolute;
background-image: url('./assets/img/star0@{i}.png');
.bgContain();
}
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
position: absolute;
bottom: 50%;
left: -5%;
background-image: url('./assets/img/light.png');
background-size: 100% 100%;
transform: rotate(4deg);
opacity: 0;
}
}
&-position-0@{i} {
transform: translate3D(@transformX * 1px, @transformY * 1px, @transformZ * 1px);
}
.loopStar(@i+1);
}
.loopStar(1);
}
function Index() {
// 选中 第一个
const [selectIndex, setSelectIndex] = useState(0)
// 定时滚动
const clearInterval = useInterval(() => {
setSelectIndex(flatIndex(selectIndex + 1))
}, intervalTime)
useEffect(() => {
return () => {
clearInterval()
}
}, [])
return (
<div className={styles.page}>
<div className='star-box'>
{Array.from(new Array(starLength)).map((_, index) => {
const imgClass = `star-0${index + 1}`
const positionIndex = flatIndex(selectIndex - index + starLength)
const positionClass = `star-position-0${positionIndex + 1}`
return (
<div key={index} className={'star'}>
<div className={ `star-content ${imgClass} ${positionClass}`}></div>
</div>
)
})}
</div>
</div>
)
}