动态旋转与平滑恢复
需求:使用CSS+JavaScript实现以下Web效果:
- 三个DOM元素:一张图片,两个按钮
- 点击开始按钮,使图片开始不停旋转(rotate0 -> rotate360)
- 点击停止按钮,图片停止旋转,并
平滑恢复到初始状态
小结:
- 抽象了函数 getMatrixFromDegree() 和 getDegreeFromMatrix() 用于矩阵和角度间的转换
- 需要加深理解 transform 和 matrix 的关系原理
实现:
<html>
<body>
<div id="target">G</div>
<button id="play">PLAY</button>
<button id="pause">PAUSE</button>
<style>
body {
background: #333;
}
button {
200px;
height: 50px;
font-size: 30px;
}
#target {
200px;
height: 200px;
font-size: 170px;
line-height: 170px;
text-align: center;
border-radius: 50%;
color: #fff;
background: lightgreen;
}
#target.play {
animation-name: rotateTarget;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-duration: 5s;
}
@keyframes rotateTarget {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
由于 transform matrix 控制的角度从 180 迈向 180+ 会发生一个未知的回转现象
故不采用 matrix 进行角度旋转的控制
0% { transform: matrix( 1, 0, 0, 1, 0, 0); }
25% { transform: matrix( 0, 1,-1, 0, 0, 0); }
50% { transform: matrix(-1, 0, 0,-1, 0, 0); }
75% { transform: matrix( 0,-1, 1, 0, 0, 0); }
100% { transform: matrix( 1, 0, 0, 1, 0, 0); }
*/
</style>
<script>
const getMatrixFromDegree = (degreeValue) => {
// transform中Matrix的学习可参考:
// https://www.zhangxinxu.com/wordpress/2012/06/css3-transform-matrix-矩阵/
const cosVal = Math.cos( this.value * Math.PI / 180 ).toFixed(6);
const sinVal = Math.sin( this.value * Math.PI / 180 ).toFixed(6);
const transform = `matrix(${cosVal},${sinVal},${-1 * sinVal},${cosVal},0,0)`;
return transform;
}
const getDegreeFromMatrix = (matrixString) => {
// degree的精确度参考:https://developer.mozilla.org/en-US/docs/Web/CSS/angle
const reg = matrixString.match(/matrix((.*?), (.*?),/);
const scaleX = +reg[1];
const skewY = +reg[2];
const degree = +(Math.acos(scaleX) * 180 / Math.PI).toFixed(2);
if (skewY > 0) {
return degree;
} else {
return 360 - degree;
}
}
const target = document.getElementById('target');
const play = document.getElementById('play');
const pause = document.getElementById('pause');
play.onclick = () => {
target.className = 'play';
}
pause.onclick = () => {
const smoothTime = 0.5; // 平滑恢复一周所用时间
const currentTransform = window.getComputedStyle(target).getPropertyValue('transform');
const currentDegree = getDegreeFromMatrix(currentTransform);
const percent = +(currentDegree / 360).toFixed(2);
const runTime = smoothTime * (1 - percent);
const getHalfKeyFrame = () => {
if (currentDegree < 180) {
const halfPercent = (((0.5 - percent) / (1 - percent)) * 100).toFixed(2);
return `${halfPercent}% { transform: rotate(180deg); }`;
} else {
return '';
}
};
const runkeyframes = `
#target.pause {
animation-name: resetTargetSmooth;
animation-timing-function: linear;
animation-duration: ${runTime}s;
will-change: auto;
}
@keyframes resetTargetSmooth {
0% { transform: rotate(${currentDegree}deg); }
${getHalfKeyFrame()}
100% { transform: rotate(360deg); }
}`;
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = runkeyframes;
document.body.appendChild(style);
target.onanimationend = (e) => { // 动画结束时刻清理
if (e.animationName === 'resetTargetSmooth') {
document.body.removeChild(style)
}
};
const animationFunc = function () {
target.className = 'pause';
}
requestAnimationFrame(animationFunc);
}
</script>
</body>
</html>