简体   繁体   English

平衡输入滑块会产生不一致的结果

[英]Balancing input sliders gives inconsistent results

I have a couple of range input sliders which are supposed to balance themselves to always sum up to 100¹, regardless of the amount of sliders.我有几个范围输入滑块,它们应该平衡自己,总和为 100¹,无论滑块的数量如何。 For this example I'll show the minimum of two, but the problem occurs with any number of them.对于这个例子,我将展示最少两个,但问题出现在任意数量的情况下。 I am using Svelte but this probably applies to vanilla JS too.我正在使用 Svelte,但这可能也适用于香草 JS。

¹: The full requirements: ¹:全部要求:

  • They must sum up to 100.它们的总和必须为 100。
  • When a slider is changed, the others must retain their proportions as they are balanced.当一个 slider 改变时,其他的必须保持它们的比例,因为它们是平衡的。 Namely: it's not sufficient to do 100 - newValue and split the result amongst the remaining sliders.即:仅执行 100 - newValue 并将结果拆分到剩余的滑块中是不够的。
  • The behaviour must hold for any number of sliders.该行为必须适用于任意数量的滑块。
<script lang="ts">
  const sliders = [
    { id: 1, percentage: 100 },
    { id: 2, percentage: 100 },
    // { id: 3, percentage: 100 },
    // { id: 4, percentage: 100 },
    // { id: 5, percentage: 100 },
  ];

  const handleInput = (event: InputEvent) => {
    const element = (event.target as HTMLInputElement);
    const newValue = Number(element.value);
    const sliderId = Number(element.dataset["slider"]);
    sliders.find((slider) => slider.id == poolId).percentage = newValue;

    balanceSliders();
  }

  const balanceSliders = () => {
    const total = sliders
      .map((slider) => slider.percentage)
      .reduce((prev, next) => prev + next, 0);
    const normaliser = 100 / total;

    sliders.forEach((slider) => {
      slider.percentage = slider.percentage * normaliser;
    });
  };

  balanceSliders();
</script>

<form>
  {#each sliders as {id, percentage}}
    <div>
      <input type="range" min="0" max="100" step="0.01" data-slider={id} bind:value={percentage} on:input={handleInput} />
      <span>{percentage.toFixed(2)}</span>
    </div>
  {/each}
</form>

This could probably be done better, I'm open to advice.这可能会做得更好,我愿意接受建议。 But the main issue is that when I bring a slider close to 100, the sum adds up to more than 100. Moreover, if I move the slider slowly, I get something around:但主要问题是,当我将 slider 带到接近 100 时,总和会超过 100。此外,如果我慢慢移动 slider,我会得到一些东西:
滑块显示 98.18 和 5.88

If I move it fast, the error is much bigger:如果我快速移动它,错误会更大:
滑块显示 100 和 25.85

Which is obviously unacceptable for my purposes.对于我的目的而言,这显然是不可接受的。

The fact that the speed influences the error suggests to me that the issue lies somewhere in the Svelte/event handling part rather than the formula itself.速度影响错误的事实向我表明,问题出在 Svelte/事件处理部分的某个地方,而不是公式本身。

I could have the sliders balance themselves only after the user stopped changing them, but for presentation purposes I'd like them to keep balancing themselves as input is being given, too.只有在用户停止更改滑块后,我才能让滑块自行平衡,但出于演示目的,我希望它们在输入时也能保持平衡。

Any ideas?有任何想法吗?

Here is the solution, you can make it dynamic and optimize it according to the requirement.这是解决方案,您可以使其动态化并根据需要进行优化。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>

  <body>
    <div id="app"></div>
    <div class="slidecontainer">
      <p>Slider 1</p>
      <span class="slider1Val">50%</span>
      <input
        class="slider1"
        type="range"
        min="1"
        max="100"
        value="50"
        class="slider"
        id="myRange"
      />
      <p>Slider 2</p>
      <span class="slider2Val">50%</span>
      <input
        class="slider2"
        type="range"
        min="1"
        max="100"
        value="50"
        class="slider"
        id="myRange"
      />
    </div>
    <script src="src/index.js"></script>
  </body>
</html>

CSS CSS

body {
  font-family: sans-serif;
}

.slidecontainer {
  display: flex;
  flex-direction: column;
}

Javscript Javascript

import "./styles.css";

const slider1 = document.querySelector(".slider1");
const slider2 = document.querySelector(".slider2");

const balanceSliders = (e) => {
  if (e.target.classList.contains("slider1")) {
    const slider1Val = document.querySelector(".slider1").value;
    slider2.value = 100 - slider1Val;
    document.querySelector(".slider1Val").innerHTML = slider1Val + "%";
    document.querySelector(".slider2Val").innerHTML = slider2.value + "%";
  } else {
    const slider2Val = document.querySelector(".slider2").value;
    slider1.value = 100 - slider2Val;
    document.querySelector(".slider2Val").innerHTML = slider2Val + "%";
    document.querySelector(".slider1Val").innerHTML = slider1.value + "%";
  }
};
slider1.addEventListener("input", (e) => balanceSliders(e));
slider2.addEventListener("input", (e) => balanceSliders(e));

Demo code sandbox 演示代码沙箱

I would remove the value binding and handle all changes inside handleInput with a slightly different logic.我将删除值绑定并使用稍微不同的逻辑处理handleInput中的所有更改。

Assuming the start values are 50-30-20 and the first slider is moved to 60, that's a difference of 10. This difference than has to be subtracted from the other sliders, taking into account their current proportions.假设起始值为 50-30-20 并且第一个 slider 移动到 60,则差值为 10。考虑到它们当前的比例,必须从其他滑块中减去此差。 I would calculate this proportion in relation to the sum of their percentages.我会根据他们的百分比总和来计算这个比例。
start 50 - 30 - 20开始50 - 30 - 20
30 + 20 = 50
30: 30/50 = 0.630 - 0.6 * 10 30: 30/50 = 0.630 - 0.6 * 10
20: 20/50 = 0.420 - 0.4 * 10 20: 20/50 = 0.420 - 0.4 * 10
end 60 - 24 - 16结束60 - 24 - 16

This works for all cases except when one slider is at 100 and the sum of the others is 0. In that case their share is 1/ the amout of sliders the difference should be split onto.这适用于所有情况,除了当一个 slider 为 100 并且其他的总和为 0 时。在这种情况下,它们的份额是 1/滑块的数量,差异应该被分割到。
REPL REPL

<script>
    let sliders = [
        { id: 1, percentage: 100 },
        { id: 2, percentage: 100 },
        { id: 3, percentage: 100 },
//      { id: 4, percentage: 100 },
//      { id: 5, percentage: 100 },
    ];

    sliders = sliders.map(slider => {
        slider.percentage = 100/sliders.length
        return slider
    })

    const handleInput = (event) => {
        const changedSlider = event.target;
        const sliderId = Number(changedSlider.dataset["slider"]);

        const slider = sliders.find((slider) => slider.id == sliderId)
        const currentValue = slider.percentage

        const newValue = Number(changedSlider.value);
        const difference = newValue - currentValue

        slider.percentage = newValue;

        const otherSliders = sliders.filter(slider => slider.id !== sliderId)
        const otherSlidersPercentageSum = otherSliders.reduce((sum, slider) => sum += slider.percentage, 0)

        otherSliders.forEach(slider => {
            const share = otherSlidersPercentageSum === 0 ? 1 / otherSliders.length : slider.percentage / otherSlidersPercentageSum
            slider.percentage = slider.percentage - difference * share
            return slider
        })

        sliders = sliders       
    }

</script>

<form>
    {#each sliders as {id, percentage}}
    <div>
        <input type="range" min="0" max="100" step="0.01" data-slider={id} value={percentage} on:input={handleInput} />
        <span>{Math.max(percentage.toFixed(2), 0)}</span>
<!--        Math.max to prevent -0 -->
    </div>
    {/each}
</form>

Comparing this to your way将此与您的方式进行比较

start 50 - 30 - 20开始50 - 30 - 20
change 60 - 30 - 20改变60 - 30 - 20
new sum 110新总和110
normaliser 100 / 110 (~0.9)标准化100 / 110 (~0.9)
end 54,54 - 27,27 - 18,18结束54,54 - 27,27 - 18,18

While this sums up to 100, the value which was intentionally changed is directly modified again.虽然这总和为 100,但故意更改的值被再次直接修改。 After removing the bind:value from your example and adding sliders=sliders at the end of balanceSliders so that Svelte knows of the changes, this can be noticed when dragging one slider against 100 REPL从示例中删除bind:value并在balanceSliders末尾添加sliders=sliders以便 Svelte 知道更改后,在将 slider 拖动到 100 REPL时可以注意到这一点

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM