不跳动地改变CSS动画持续时间

15

我有一个简单的旋转轮动画。我正在尝试使用滑块(输入范围)控制旋转轮的速度。我已经成功实现了这一点,但每次更改动画时,动画都会重新开始(跳跃)。寻找解决方案以平稳增加速度。当用户增加滑块的值时,轮子将以增加的速度旋转。

在下面的代码中,#loading是旋转轮。

$(document).on('input', '#slider', function() {
  var speed = $(this).val();
  $('#speed').html(speed);
  $("#loading").css("animation-duration", 50 / speed + "s");
});
#loading {
  position: absolute;
  width:100px; height:100px; background-color:black;
  left: 0;
  right: 0;
  margin: auto;
  transform-origin: 50% 50%;
  animation: rotateRight infinite linear;
  animation-duration: 0;
}

@keyframes rotateRight {
  100% {
    transform: rotate(360deg);
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div id="loading"></div>
<input type="range" min="0" max="100" value="0" class="slider" id="slider">
<p>Speed: <span id="speed"></span></p>


我只看到滑块和速度指示器。 - Nandu Kalidindi
@NanduKalidindi,你现在应该能看到它了。 - kig
考虑将“input”事件更改为“change”,这样它就会等待用户决定他想要的速度。 - Mosh Feu
1
你应该考虑使用像GreenSock这样的框架。 - psdpainter
以下的解决方案中有没有解决您的问题? - Brett DeWoody
4个回答

7

经典问题
(跳跃..现在还没有)

jQuery版本

var lasttime = 0,  lastduration = 0,  angle = 0;
$(document).on('input', '#slider', function(event) {
  var speed = $(this).val();
  $('#speed').html(speed);
  
  var el = $("#loading");
  var duration = (speed > 0) ? 50 / speed : 0;
  var currenttime = event.originalEvent.timeStamp / 1000;
  var difftime = currenttime - lasttime;
  el.removeClass("enable_rotate").show();
  
  if (!lastduration && duration)
      el.css("transform", "");
  else
      angle += (difftime % lastduration) / lastduration;
      
  if (duration){     
    el.css("animation-duration", duration + "s")
    .css("animation-delay", -duration * angle + "s")    
    .addClass("enable_rotate");
    }
  else
    el.css("transform", "rotate(" + 360 * angle + "deg)");
  
  angle -= angle | 0; //use fractional part only
  lasttime = currenttime;
  lastduration = duration;
});
.anime_object {
  width:100px; height:100px; background-color:black;
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
}

.enable_rotate {
  animation: rotateRight infinite linear;
}

@keyframes rotateRight {
  100% {
    transform: rotate(360deg);
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<img id="loading" class="anime_object">
<input type="range" min="0" max="100" value="0" id="slider">
<p>Speed: <span id="speed"></span></p>

工作草案

保存当前动画变量
http://www.w3.org/TR/css-animations-1/#interface-animationevent-attributes

  1. SO
  2. css-tricks.com

0

我不太确定没有使用CSS动画是否可能实现这一点。问题在于,每当您更改速度时,动画都无法归一化。它总是从动画的0%到100%进行动画处理。每次调整animation-duration时,您都将使用动画中当前位置重新插值。

换句话说,如果您从animation-duration:25更改为50,在t = 12,那么动画就已经完成了一半(180度);现在只完成了四分之一(90度)。但是,您无法控制t,那是浏览器的控制。如果可以的话,您希望将t设置为保持在插值的原位置,在此示例中为t = 25,以便保持动画的完成百分比相同,但是拉伸剩余时间。

我稍微修改了你的脚本,以更好地展示我的描述。它将在速度0和5之间每秒增加0.25的速度。你可以看到问题是浏览器控制的t变量是个问题。

你可以用JavaScript重写它来控制t,但我认为你必须放弃CSS动画。

为了更直接地谈论这个由浏览器控制的t变量,看一下CSS-Tricks上的这篇文章:Myth Busting: CSS Animations vs. JavaScript

有些浏览器允许您暂停/恢复CSS关键帧动画,但仅限于此。您无法寻找动画中的特定位置,也无法平滑地部分反向或更改时间比例或在某些位置添加回调或将其绑定到丰富的播放事件集。如下面的演示所示,JavaScript提供了很好的控制。

这是你的问题,你想要能够更改动画的持续时间,但又要在动画中正确地寻找位置。

$(function() {
  var speed = parseInt($("#slider").val(), 10);
  $("#speed").html(speed);
  $("#loading").css("animation-duration", 50 / speed + "s");

  var forward = true;
  setInterval(function() {
    speed += (forward ? 0.25 : -0.25);
    if (speed >= 5) {
      forward = false;
    } else if (speed <= 0) {
      forward = true;
    }
    $("#loading").css("animation-duration", 50 / speed + "s");
    $("#slider").val(speed);
    $("#speed").html(speed);
  }, 1000);
});
#loading {
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  transform-origin: 50% 50%;
  animation: rotateRight infinite linear;
  animation-duration: 0;
}

@keyframes rotateRight {
  100% {
    transform: rotate(360deg);
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<img id="loading" src="//placehold.it/100">
<input type="range" min="0" max="100" value="0" step=".25" class="slider" id="slider" value="0">
<p>Speed: <span id="speed"></span></p>


0

TL;DR

不行(根据我的测试)

  • 首先,让我们将 css 中的动画声明移除并移到 Javascript 中,这样动画循环就不会开始(即使你看不到它在运行)。

你是否注意到,即使你将滑块从初始位置移动,方框似乎也会从随机位置开始?这是因为动画循环实际上已经在运行。

  • 现在,你可以通过你的动画在任何给定时间获得应用于你的方框的当前变换值,使用getComputedStyle(loadingElement).getPropertyValue('transform');,这将返回一个矩阵,一开始看起来并没有太多意义,但我们可以从该矩阵计算出旋转的角度:

(使用此处解释的某些数学链接

Math.round(Math.atan2(b, a) * (180/Math.PI));

现在我们已经有了这个值,我们必须将其归一化,以便角度只有正值,然后我们可以将此角度应用为transform: rotate(Xdeg)的基础值

到目前为止还不错,您可以在代码片段中看到它正在工作,但是即使您这样做,并增加/减少速度值,动画循环已经以设置的时间比例运行,您无法重置此循环。

我的答案到目前为止是让其他更深入了解动画循环的人可以构建,并可能提出一个可行的代码。

如果您仍在阅读此内容,则可能认为,好吧,只需使用loadingElement.style.removeProperty('animation')删除动画,然后再次分配它,尝试过了,不起作用。那么使用setInterval(...,0)重新启动动画,使其在下一个循环中运行呢?也不会起作用。

$(document).on('input', '#slider', function() {
 var speed = $(this).val();
 var loadingElement = document.querySelector("#loading")
 $('#speed').html(speed);
 //get the current status of the animation applied to the element (this is a matrix)
 var currentCss = getComputedStyle(loadingElement).getPropertyValue('transform'); 

 if (currentCss !== 'none'){
  //parse each value we need from the matrix (there is a total of 6)
  var values = currentCss.split('(')[1];
  values = values.split(')')[0];
  values = values.split(',');

  var a = values[0];
  var b = values[1];
  var c = values[2];
  var d = values[3];

  //here we make the actual calculation
  var angle = Math.round(Math.atan2(b, a) * (180/Math.PI)); 

  //normalize to positive values
  angle = angle < 0 ? angle + 360 : angle;

  loadingElement.style.removeProperty('animation'); //remove the property for testing purposes
  $("#loading").css('transform', 'rotate(' + angle + 'deg)');
 }
 else{ //else for testing purposes, this will change the speed of the animation only on the first slider input change
  $("#loading").css('animation', 'rotateRight infinite linear'); //see how the animation now actually starts from the initial location
  $("#loading").css("animation-duration", 50 / speed + "s");
 }

});
#loading {
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  transform-origin: 50% 50%;
  /*I removed the initialization of the animation here*/
}

@keyframes rotateRight {
  100% {
    transform: rotate(360deg);
  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<img id="loading" src="https://placehold.it/100">
<input type="range" min="0" max="100" value="0" class="slider" id="slider">
<p>Speed: <span id="speed"></span></p>


0

可以的!

我们将采取以下方法:

  1. 在输入更改时,获取新的速度值
  2. 获取元素的当前 transform 值,该值以 matrix() 形式返回
  3. matrix() 转换为以度为单位的 rotate
  4. 删除现有动画
  5. 基于当前速度和旋转值创建新动画
  6. 将新动画应用于元素

我们需要克服的主要问题是基于当前旋转值创建新的动画关键帧 - 我们需要创建一个新的动画,其起始值等于当前旋转值,结束值等于当前旋转值 + 360。

换句话说,如果我们的元素旋转了 90deg 并且速度发生了变化,我们需要创建一个新的 @keyframes

@keyframes updatedKeyframes {
  0% {
    transform: rotate(90deg);
  },
  100% {
    transform: rotate(450deg); // 90 + 360 
  }
}

为了创建动态动画,我使用jQuery.keyframes插件来创建动态的@keyframes
虽然这个解决方案可行,但我认为它的性能不是很好,因为jQuery.keyframes插件的工作方式。对于每个新的动画关键帧,插件都会附加一个内联<style>。这可能导致定义数十个、数百个甚至数千个keyframes。在下面的示例中,我使用speed变量来创建@keyframe名称,因此它将创建多达100个唯一的@keyframes样式。我们可以在这里进行一些优化,但这超出了这个解决方案的范围。
以下是工作示例:

$(document).on('input', '#slider', function() {
  var speed = $(this).val();
  $('#speed').html(speed);
  var transform = $("#loading").css('transform');

  var angle = getRotationDegrees(transform);
  
  $("#loading").css({
    "animation": "none"
  });
  
  $.keyframe.define([{
    name: `rotateRight_${speed}`,
    "0%": {
      "transform": "rotate(" + angle + "deg)"
    },
    "100%": {
      'transform': "rotate(" + (angle + 360) + "deg)"
    }
  }]);
  
  if (speed === "0") {
    $("#loading").css("transform", "rotate(" + angle + "deg)");
  } else {
    $("#loading").playKeyframe({
      name: `rotateRight_${speed}`,
      duration: 50 / speed + "s",
      timingFunction: "linear",
      iterationCount: "infinite"
    });
  }

});

function getRotationDegrees(matrix) {
  if (matrix !== 'none') {
    var values = matrix.split('(')[1].split(')')[0].split(',');
    var a = values[0];
    var b = values[1];
    var angle = Math.round(Math.atan2(b, a) * (180 / Math.PI));
  } else {
    var angle = 0;
  }
  return angle;
}
#loading {
  position: absolute;
  left: 0;
  right: 0;
  margin: auto;
  transform-origin: 50% 50%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://rawgit.com/jQueryKeyframes/jQuery.Keyframes/master/jquery.keyframes.js"></script>
<img id="loading" src="http://via.placeholder.com/100x100">
<input type="range" min="0" max="100" value="0" class="slider" id="slider">
<p>Speed: <span id="speed"></span></p>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接