【问题标题】:How to animate a progress bar with negatives using Element.animate()如何使用 Element.animate() 为带有负数的进度条设置动画
【发布时间】:2020-11-02 02:48:32
【问题描述】:

我正在尝试使用 HTML/CSS/JavaScript 模拟以下小部件: https://gyazo.com/76bee875d35b571bd08edbe73ead12cb

我的设置方式如下:

  • 我有一个条形图,其背景色具有从红色到绿色的渐变,是静态的。
  • 然后我有两个眼罩,它们应该代表负空间,以产生彩色条正在动画的错觉(实际上,眼罩只是滑开)

我这样做是因为我认为它可能更容易,而不是尝试在两个方向上设置动画,但现在我不太确定,哈哈。我试图保留的一项要求是动画仅处理 transformopacity 以利用浏览器可以执行的优化(如此处所述:https://hacks.mozilla.org/2016/08/animating-like-you-just-dont-care-with-element-animate/

这个例子有几个按钮来帮助测试各种东西。 “随机正面”效果很好,正是我想要的。我还没有完全了解负面因素,因为我不确定如何解决从正面到负面的转换问题,反之亦然。

理想情况下,当从正面变为负面时,右眼帘将在中间完成,而左眼帘将拾取动画并在需要去的地方结束。

例如,如果值最初设置为 40%,然后设置为 -30%,则右眼帘应为 transform: translateX(40%) -> transform: translateX(0%) 设置动画,然后左眼帘应从 transform: translateX(0%) 设置动画-> transform: translateX(-30%) 暴露红色。

此外,缓动应该是无缝的。

我不确定设置是否可能(特别是保持缓动无缝,因为我认为缓动将是每个元素,并且不能“延续”到另一个元素?)

寻找有关如何挽救此问题以产生预期结果的指导,或者是否有更好的方法来处理此问题。

注意: 我使用 jquery 只是为了轻松处理点击事件等,但这最终会出现在不支持 jquery 的应用程序中。

这是我目前的尝试: https://codepen.io/blitzmann/pen/vYLrqEW

let currentPercentageState = 0;

function animate(percentage) {
  var animation = [{
      transform: `translateX(${currentPercentageState}%)`,
      easing: "ease-out"
    },
    {
      transform: `translateX(${percentage}%)`
    }
  ];

  var timing = {
    fill: "forwards",
    duration: 1000
  };

  $(".blind.right")[0].animate(animation, timing);

  // save the new value so that the next iteration has a proper from keyframe
  currentPercentageState = percentage;
}

$(document).ready(function() {
  $(".apply").click(function() {
    animate($("#amount").val());
  });

  $(".reset").click(function() {
    animate(0);

  });

  $(".random").click(function() {
    var val = (Math.random() * 2 - 1) * 100;
    $("#amount").val(val);
    animate(val);

  });

  $(".randomPos").click(function() {
    var val = Math.random() * 100;
    $("#amount").val(val);
    animate(val);

  });

  $(".randomNeg").click(function() {
    var val = Math.random() * -100;
    $("#amount").val(val);
    animate(val);
  });

  $(".toggleBlinds").click(function() {
    $(".blind").toggle();
  });

  $(".toggleLeft").click(function() {
    $(".blind.left").toggle();
  });

  $(".toggleRight").click(function() {
    $(".blind.right").toggle();
  });
});

$(document).ready(function() {});
.wrapper {
  margin: 10px;
  height: 10px;
  width: 800px;
  background: linear-gradient(to right, red 50%, green 50%);
  border: 1px solid black;
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
}

.blind {
  height: 100%;
  position: absolute;
  top: 0;
  background-color: rgb(51, 51, 51);
  min-width: 50%;
}

.blind.right {
  left: 50%;
  border-left: 1px solid white;
  transform-origin: left top;
}

.blind.left {
  border-right: 1px solid white;
  transform-origin: left top;
}
<div class="wrapper">
  <div class='blind right'></div>
  <div class='blind left'></div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>

<input id="amount" type="number" placeholder="Enter percentage..." value='40' />
<button class="apply">Apply</button>
<button class="random">Random</button>
<button class="randomPos">Random Positive</button>
<button class="randomNeg">Random Negative</button>
<button class="toggleBlinds">Toggle Blinds</button>
<button class="toggleLeft">Toggle L Blind</button>
<button class="toggleRight">Toggle R Blind</button>

<button class="reset" href="#">Reset</button>

【问题讨论】:

  • 您对使用 css 过渡属性有任何顾虑吗?因为可以利用 transition-delay 属性解决正/负盲板之间的无缝动画。

标签: javascript html css animation dom


【解决方案1】:

我已经修改了您的代码。看看代码。

let currentPercentageState = 0;

function animate(percentage) {

  var animation = [{
      transform: `translateX(${currentPercentageState}%)`,
      easing: "ease-out"
    },
    {
      transform: `translateX(${percentage}%)`
    }
  ];

  var timing = {
    fill: "forwards",
    duration: 1000
  };

  if (percentage < 0) {
    $(".blind.right")[0].animate(
      [{
          transform: `translateX(0%)`,
          easing: "ease-out"
        },
        {
          transform: `translateX(0%)`
        }
      ], timing);
    $(".blind.left")[0].animate(animation, timing);

  } else {
    $(".blind.left")[0].animate(
      [{
          transform: `translateX(0%)`,
          easing: "ease-out"
        },
        {
          transform: `translateX(0%)`
        }
      ], timing);
    $(".blind.right")[0].animate(animation, timing);
  }


  // save the new value so that the next iteration has a proper from keyframe
  //currentPercentageState = percentage;
}

$(document).ready(function() {
  $(".apply").click(function() {
    animate($("#amount").val());
  });

  $(".reset").click(function() {
    animate(0);

  });

  $(".random").click(function() {
    var val = (Math.random() * 2 - 1) * 100;
    $("#amount").val(val);
    animate(val);

  });

  $(".randomPos").click(function() {
    var val = Math.random() * 100;
    $("#amount").val(val);
    animate(val);

  });

  $(".randomNeg").click(function() {
    var val = Math.random() * -100;
    $("#amount").val(val);
    animate(val);
  });

  $(".toggleBlinds").click(function() {
    $(".blind").toggle();
  });

  $(".toggleLeft").click(function() {
    $(".blind.left").toggle();
  });

  $(".toggleRight").click(function() {
    $(".blind.right").toggle();
  });
});

$(document).ready(function() {});
.wrapper {
  margin: 10px;
  height: 10px;
  width: 800px;
  background: linear-gradient(to right, red 50%, green 50%);
  border: 1px solid black;
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
}

.blind {
  height: 100%;
  position: absolute;
  top: 0;
  background-color: rgb(51, 51, 51);
  min-width: 50%;
}

.blind.right {
  left: 50%;
  border-left: 1px solid white;
  transform-origin: left top;
}

.blind.left {
  border-right: 1px solid white;
  transform-origin: left top;
}
<div class="wrapper">
  <div class='blind right'></div>
  <div class='blind left'></div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>

<input id="amount" type="number" placeholder="Enter percentage..." value='40' />
<button class="apply">Apply</button>
<button class="random">Random</button>
<button class="randomPos">Random Positive</button>
<button class="randomNeg">Random Negative</button>
<button class="toggleBlinds">Toggle Blinds</button>
<button class="toggleLeft">Toggle L Blind</button>
<button class="toggleRight">Toggle R Blind</button>

<button class="reset" href="#">Reset</button>

【讨论】:

  • 这是在每次设置新值时重置事物的状态。这很容易,无需太多修改(根据值是否为负来修改)。但是,问题更多的是与从以前的值平滑地动画到新值有关。请参阅 Gyazo 链接了解我要模拟的内容。基本上,如果它是 40%,并且它被设置为 -30%,我不希望栏只是立即弹出并从 0 开始然后转到 -30%,而是我需要它从 40% 开始并逐渐向 -30% 移动(在同一缓动期间,无缝)。希望能澄清事情!
  • 哦,误会了:)让我再试一次
  • 我添加了否定或不检查,因为您的代码只是为.blind.right 设置动画。这就是为什么要为.blind.left 设置动画,我必须添加该检查:)
  • 是的,我独自离开了.blind.left,因为我真的不知道如何使用.blind.right 正确地为它设置动画。自己制作动画非常容易,或者像您通过重置设置的值之间的状态所做的那样。但是,至少对我来说,如果有意义的话,通过“拾取”另一个动画的动画来制作动画要困难得多。到了那个地步,找不到办法,所以我就不管了:)
【解决方案2】:

您需要分两步制作动画。第一步是将先前的状态重置为初始状态(应设置为 0),在第二步中,您需要运行另一个动画,该动画实际上会将其移动到目标状态。 为了实现这一点,您可以这样做,

let currentPercentageState = 0;
const animationTiming = 300;

function animate(percentage) {
  let defaultTranformVal = [{
    transform: `translateX(${currentPercentageState}%)`,
    easing: "ease-out"
  }, {transform: `translateX(0%)`}];
  var animation = [{
      transform: `translateX(0%)`,
      easing: "ease-out"
    },{
      transform: `translateX(${percentage}%)`,
      easing: "ease-out"
    }];
  var timing = {
    fill: "forwards",
    duration: animationTiming
  };
  if (percentage < 0) {
    if(currentPercentageState > 0) {
      $(".blind.right")[0].animate(defaultTranformVal, timing); 
      setTimeout(() => {
        $(".blind.left")[0].animate(animation, timing);
      }, animationTiming); 
    } else {
      $(".blind.left")[0].animate(animation, timing);
    }
  }
  if(percentage > 0) {
   if(currentPercentageState < 0) {
    $(".blind.left")[0].animate(defaultTranformVal, timing);
     setTimeout(() => {
       $(".blind.right")[0].animate(animation, timing);
     }, animationTiming);
   } else {
     $(".blind.right")[0].animate(animation, timing);
   }
  }

  // save the new value so that the next iteration has a proper from keyframe
  currentPercentageState = percentage;
}

在这里,您将看到我们有两个转换。第一个 defaultTranformVal 会将 currentPercentageState 移动到零,然后另一个将从 0 移动到百分比。

您需要在这里处理几个条件。第一个是如果你第一次运行它(意味着没有currentPercentageState),你不需要运行defaultTranformVal。如果你有 currentPercentageState 那么你需要运行 defaultTranformVal 然后运行第二个动画。

注意:-您还需要清除超时以防止内存泄漏。这可以通过存储 setTimout 返回值来处理,然后在下次运行时借助 clearTimeout 清除前一个。

这是更新后的 codepen 示例:- https://codepen.io/gauravsoni119/pen/yLeZBmb?editors=0011

【讨论】:

  • 您好,谢谢您的解释!不过,该示例存在一些问题 - 多次将其设置为“Random Negative”会显示一个奇怪的定位错误。多次点击“随机正数”,每次去值前都会重置为0。这些都是我可以通过仔细评估条件来弄清楚的。
  • 但我看到的最大问题是,由于动画分为两个步骤,每个步骤都有相同的动画持续时间。缓动功能不是无缝的,我认为这是最大的???我有。我觉得我需要创建自己的缓动函数,该函数根据缓动的百分比来计算,然后不是动画,而是简单地更改变换属性。 ¯_(ツ)_/¯
  • 我修复了您在第一条评论中提到的问题。但我不确定你关于如何处理平滑度的第二条评论。让我尝试其他可能对这种情况有所帮助的方法。
  • 你可以试试去掉两个div的白边吗?我认为这也增加了对平滑度的一些影响(视觉上)。
  • 从正面到负面仍然会产生两个具有相同持续时间的动画。如果我将持续时间设置为 1 秒,那么右百叶窗在 1 秒内向 0 前进,然后左百叶窗在一秒内达到它的值,有效地使整个事情变成 2 秒。最重要的是,缓动并非无懈可击 - 每个柱都有自己的缓动被应用。我正在寻找一种解决方案,在给定 1 秒动画的情况下,右百叶窗可能需要(例如).3 秒并且基本上使用缓动曲线的前 30%,而第二个百叶窗在 0.7 秒内使用其余的动画曲线。
【解决方案3】:

编辑:我确实设法解决了这个问题!

let easing = "cubic-bezier(0.5, 1, 0.89, 1)";
let duration = 1000;
let easeReversal = y => 1 - Math.sqrt((y-1)/-1)

https://codepen.io/blitzmann/pen/WNrBWpG

我给了它我自己的三次贝塞尔函数,我知道它的反转。下面的帖子和我的解释是基于使用不容易可逆的 sin() 的缓动函数。不仅如此,ease-out 的内置缓动函数与我参考的 sin() 函数不匹配(我不太确定内置函数基于什么)。但我意识到我可以给它我自己的功能,我知道反转,并且繁荣,就像一个魅力!

这对我来说是一次非常丰富的经历,我很高兴我有一个可行的解决方案。我仍然认为我会尝试其他一些我必须看看从长远来看效果更好的想法。


历史帖子:

因此,经过几个晚上的努力,我得出的结论是,这要么不可能按照我的想法进行,要么如果有可能,那么解决方案就是这样人为地认为这可能不值得,我最好开发一个新的解决方案(我已经想到了一个或两个我想尝试的东西)。

请参阅这个 jsfiddle 了解我的最终“解决方案”和验尸

https://jsfiddle.net/blitzmann/zc80p1n4/

let currentPercentageState = 0;
let easing = "linear";
let duration = 1000;

function animate(percentage) {
  percentage = parseFloat(percentage);

  // determine if we've crossed the 0 threshold, which would force us to do something else here
  let threshold = currentPercentageState / percentage < 0;
  console.log("Crosses 0: " + threshold);

  if (!threshold && percentage != 0) {
    // determine which blind we're animating
    let blind = percentage < 0 ? "left" : "right";

    $(`.blind.${blind}`)[0].animate(
      [
        {
          transform: `translateX(${currentPercentageState}%)`,
          easing: easing
        },
        {
          transform: `translateX(${percentage}%)`
        }
      ],
      {
        fill: "forwards",
        duration: duration
      }
    );
  } else {
    // this happens when we cross the 0 boundry
    // we'll have to create two animations - one for moving the currently offset blind back to 0, and then another to move the second blind
    let firstBlind = percentage < 0 ? "right" : "left";
    let secondBlind = percentage < 0 ? "left" : "right";
    
    // get total travel distance
    let delta = currentPercentageState - percentage;
    
    // find the percentage of that travel that the first blind is responsible for
    let firstTravel  = currentPercentageState / delta;
    let secondTravel = 1 - firstTravel;

    console.log("delta; total values to travel: ", delta);
    console.log(
      "firstTravel; percentage of the total travel that should be done by the first blind: ",
      firstTravel
    );
    console.log(
      "secondTravel; percentage of the total travel that should be done by the second blind: ",
      secondTravel
    );
    
    // animate the first blind.
    $(`.blind.${firstBlind}`)[0].animate(
      [
        {
          transform: `translateX(${currentPercentageState}%)`,
          easing: easing
        },
        {
          // we go towards the target value instead of 0 since we'll cut the animation short
          transform: `translateX(${percentage}%)`
        }
      ],
      {
        fill: "forwards",
        duration: duration,
        // cut the animation short, this should run the animation to this x value of the easing function
        iterations: firstTravel
      }
    );

    // animate the second blind
    $(`.blind.${secondBlind}`)[0].animate(
      [
        {
          transform: `translateX(${currentPercentageState}%)`,
          easing: easing
        },
        {
          transform: `translateX(${percentage}%)`
        }
      ],
      {
        fill: "forwards",
        duration: duration,
        // start the iteration where the first should have left off. This should put up where the easing function left off
        iterationStart: firstTravel,
        // we only need to carry this aniamtion the rest of the way
        iterations: 1-firstTravel,
        // delay this animation until the first "meets" it
        delay: duration * firstTravel
      }
    );
  }
  // save the new value so that the next iteration has a proper from keyframe
  currentPercentageState = percentage;
}

// the following are just binding set ups for the buttons

$(document).ready(function () {
  $(".apply").click(function () {
    animate($("#amount").val());
  });

  $(".reset").click(function () {
    animate(0);
  });

  $(".random").click(function () {
    var val = (Math.random() * 2 - 1) * 100;
    $("#amount").val(val);
    animate(val);
  });

  $(".randomPos").click(function () {
    var val = Math.random() * 100;
    $("#amount").val(val);
    animate(val);
  });

  $(".randomNeg").click(function () {
    var val = Math.random() * -100;
    $("#amount").val(val);
    animate(val);
  });

  $(".flipSign").click(function () {
    animate(currentPercentageState * -1);
  });

  $(".toggleBlinds").click(function () {
    $(".blind").toggle();
  });

  $(".toggleLeft").click(function () {
    $(".blind.left").toggle();
  });

  $(".toggleRight").click(function () {
    $(".blind.right").toggle();
  });
});

animate(50);
//setTimeout(()=>animate(-100), 1050)

$(function () {
  // Build "dynamic" rulers by adding items
  $(".ruler[data-items]").each(function () {
    var ruler = $(this).empty(),
      len = Number(ruler.attr("data-items")) || 0,
      item = $(document.createElement("li")),
      i;

    for (i = -11; i < len - 11; i++) {
      ruler.append(item.clone().text(i + 1));
    }
  });
  // Change the spacing programatically
  function changeRulerSpacing(spacing) {
    $(".ruler")
      .css("padding-right", spacing)
      .find("li")
      .css("padding-left", spacing);
  }

  changeRulerSpacing("30px");
});
.wrapper {
  margin: 10px auto 2px;
  height: 10px;
  width: 600px;
  background: linear-gradient(to right, red 50%, green 50%);
  border: 1px solid black;
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
}

.blind {
  height: 100%;
  position: absolute;
  top: 0;
  background-color: rgb(51, 51, 51);
  min-width: 50%;
}

.blind.right {
  left: 50%;
  border-left: 1px solid white;
  transform-origin: left top;  
}

.blind.left {
  border-right: 1px solid white;
  transform-origin: left top;
}

#buttons {
  text-align: center;
}

/* Ruler crap */

.ruler-container {
  text-align: center;
}
.ruler, .ruler li {
    margin: 0;
    padding: 0;
    list-style: none;
    display: inline-block;
}
/* IE6-7 Fix */
.ruler, .ruler li {
    *display: inline;
}
.ruler {
  display:inline-block;
    margin: 0 auto;https://jsfiddle.net/user/login/
    background: lightYellow;
    box-shadow: 0 -1px 1em hsl(60, 60%, 84%) inset;
    border-radius: 2px;
    border: 1px solid #ccc;
    color: #ccc;
    height: 3em;
    padding-right: 1cm;
    white-space: nowrap;
  margin-left: 1px;
}
.ruler li {
    padding-left: 1cm;
    width: 2em;
    margin: .64em -1em -.64em;
    text-align: center;
    position: relative;
    text-shadow: 1px 1px hsl(60, 60%, 84%);
}
.ruler li:before {
    content: '';
    position: absolute;
    border-left: 1px solid #ccc;
    height: .64em;
    top: -.64em;
    right: 1em;
}
<div class="wrapper">
  <div class='blind right'></div>
  <div class='blind left'></div>
</div>

<div class="ruler-container">
  <ul class="ruler" data-items="21"></ul>
</div>

<div id="buttons">
  <input id="amount" type="number" placeholder="Enter percentage..." value='-80' />
  <button class="apply">Apply</button>
  <button class="random">Random</button>
  <button class="randomPos">Random Positive</button>
  <button class="randomNeg">Random Negative</button>
  <button class="flipSign">Flip Sign</button>
  <button class="toggleBlinds">Toggle Blinds</button>
  <button class="toggleLeft">Toggle L Blind</button>
  <button class="toggleRight">Toggle R Blind</button>

  <button class="reset" href="#">Reset</button>

</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js" type="text/javascript"></script>

<hr />
<p><strong>A note</strong> on the attempt made here:</p>
<p>
  I was trying to animate a percentage bar that has both positive and negative values. But I set a challenge as well: I wanted to achieve this via animations utilizing only the compositor - which means animating opacity or transform <strong>only</strong> (no color, width, height, position, etc). The ideas presented here were based on the concept of blinds. I have a static element with a background gradient of red to green, then I have two elements that "blind" the user to the background. These blinds, being a simple element, simply slide into and out of place.
</p>
<p>The problem that I ran into was timing the two animations correctly when they switched signage. It's currently working (very well) for linear animation, but as soon as you introduce an easing function it gets wonky. The reason for this is due to the value that I'm using to set the first animation length (iteration, not duration), as well as the second animations start to pick up where the first left off. The value that I was using is the percentage of the total travel distance that each of the blinds will have to do.</p>
<p>So, for example, if you have a value of 50, and go to -80, that's a total travel distance of 130. The first blind travels <code>50 / 130 = ~0.3846</code> of the total distance, and the second blind will travel <code>1 - ~0.3846 = ~0.6154</code> of the total distance.</p>
<p>But, these are not the correct values for the <em>duration</em> of the animation. Instead, these are the percentages of the easing values (the y-axis). To get the duration for these, I would have to find the x value (given the known y value). eg, for an ease-out animation for a value going from 50 to -80, the animation crosses our 0 at ~0.03846, and we would have to solve for x given <code>0.03846 = sin((x * PI) / 2)</code>.</p>
<p>With the help of Wolfram Alpha, I was able to find a few test values this got me much closer to the actual animation, but the blinds always stopped slightly off the mark. I eventually chalked this up to one of two reasons: the fact that the valuess are always going to be approximate and the browser is never going to be 100% accurate, or / and 2) the browser is using a slightly different easing function than I was using for reference. Regardless, being so constrained by the fact that this "animation" relies on two different aniamtions lining up perfectly, I decided to leave this version be and go in a different direction.</p>
<p>
If anyone finds an actual solution to this, please post an answer here: https://stackoverflow.com/questions/62866844/how-to-animate-a-progress-bar-with-negatives-using-element-animate
</p>

感谢那些尝试这个公认的棘手问题的人

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-30
    • 1970-01-01
    • 2014-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-03
    • 2015-03-23
    相关资源
    最近更新 更多