【问题标题】:Is there a way to make this custom slider input smoother?有没有办法让这个自定义滑块输入更流畅?
【发布时间】:2022-07-03 19:53:30
【问题描述】:

我正在为只有两个可能答案的问题开发自定义输入,例如:“是”“否”Here's 一个工作示例,here 你可以阅读源代码(它也适用于触摸屏)。

这个想法是使用与“滑动打开”滑块输入类似的原理,但我试图让拇指施加一点阻力。我的实现包括计算拖动运动的速度并让拇指对其做出响应,因此,尽管指针(手指或鼠标)可能接近答案,但如果速度降低,拇指会回到原点。因此,与其说是实际拖动拇指,不如说是快速将指针朝所需答案的方向移动。

问题是,它目前有点不稳定和不稳定。那么,有没有办法让拇指移动更顺畅呢?我的实现中没有任何部分是永久性的,因此请随意尝试和修改任何内容。另外,我无论如何都不是JS方面的专家,所以请不要太过分,好吗?提前致谢。干杯

代码也在这里。

HTML

<!DOCTYPE html>
<html>
<head>
  <title>Yes or No?</title>
</head>
<body>
<canvas id="display"></canvas>
</body>
</html>

JS

const displayCanvas = document.querySelector("#display");
const displayContext = displayCanvas.getContext("2d");

const maxX = displayCanvas.width = 400;
const maxY = displayCanvas.height = 100;

const bgColor = "#000";
const fgColor = "#fff";

const thumbRestX = maxX / 2;

let thumbX = thumbRestX;
let thumbY = maxY / 2;

let yesAnswerX = (maxX / 6) * 5;
let yesAnswerY = maxY / 2;

let noAnswerX = maxX / 6;
let noAnswerY = maxY / 2;

let pointerPrevX = thumbX;
let pointerX = thumbX;

let isDragging = false;
let isAnswered = false;

const setup = () => {
  const startDragging = () => {
    isDragging = true;
  };
  
  const stopDragging = () => {
    isDragging = false;
  };
  
  const monitorPointer = (newX) => {
    pointerPrevX = pointerX;
    pointerX = newX;
  };
  
  displayCanvas
    .addEventListener("mousedown", startDragging);
  displayCanvas
    .addEventListener("mouseup", stopDragging);
  displayCanvas
    .addEventListener("mousemove", (e) => {
      monitorPointer(
        e.clientX - e.target.getBoundingClientRect().left);
    });
  
  displayCanvas
    .addEventListener("touchstart", (e) => {
      e.preventDefault();
      startDragging();
    });
  displayCanvas
    .addEventListener("touchend", stopDragging);
  displayCanvas
    .addEventListener("touchmove", (e) => {
      const touch = e.touches[0];
    
      monitorPointer(
        touch.clientX - e.target.getBoundingClientRect().left);
    });
};

const evaluate = () => {
  if (!isAnswered && isDragging) {
    thumbX = thumbRestX + (pointerX - pointerPrevX - 1) * 2;
    
    if (thumbX >= yesAnswerX) {
      isAnswered = true;
      thumbX = yesAnswerX;
    }
    
    if (thumbX <= noAnswerX) {
      isAnswered = true;
      thumbX = noAnswerX;
    }
  }
};

const render = () => {
  const ctx = displayContext;
  
  ctx.clearRect(0, 0, maxX, maxY);
  
  // Background
  ctx.fillStyle = bgColor;
  ctx.fillRect(0, 0, maxX, maxY);
  
  // Thumb
  
  ctx.fillStyle = fgColor;
  ctx.beginPath();
  ctx.arc(thumbX, thumbY, 20, 0, Math.PI * 2, true);
  ctx.fill();
  
  // Yes answer
  
  ctx.fillStyle = fgColor;
  ctx.font = "50px monospace";
  ctx.textAlign = "center";
  ctx.fillText("YES", yesAnswerX, yesAnswerY);
  
  // No answer
  
  ctx.fillStyle = fgColor;
  ctx.font = "50px monospace";
  ctx.textAlign = "center";
  ctx.fillText("NO", noAnswerX, noAnswerY);

};

function run () {
  const evaluateTimeoutRate = 20;
  let evaluateTimeoutID;
  
  setup();
  
  const evaluateLoop = () => {
    evaluate();
    
    evaluateTimeoutID = 
      setTimeout(evaluateLoop, evaluateTimeoutRate);
  };
  
  evaluateTimeoutID = 
    setTimeout(evaluateLoop, evaluateTimeoutRate);
  
  const renderLoop = () => {
    render();
    
    requestAnimationFrame(renderLoop);
  };
  
  requestAnimationFrame(renderLoop);
}

run();

【问题讨论】:

    标签: javascript canvas mouseevent touch-event


    【解决方案1】:

    问题是,它目前有点不稳定和不稳定。那么,有没有办法让拇指移动更顺畅呢?

    根据我的经验,如果您将 UI 建模为有点像现实世界中的事物,它通常会让人感觉很流畅。

    对于这个控件,一个有趣的方法是实现mass-spring-damper model

    “模型”

    我想象圆形手柄通过弹簧连接到控件的中心。

    为了移动中心盘,我安装了第二个弹簧并开始向左或向右拉动它。一旦磁盘设法超过“否”或“是”标签,我(1)拆下中心弹簧,(2)将中心弹簧连接到新标签,(3)切断我正在拉的绳子(瞬间完成)。

    为了确保当我松开弹簧时控制器不会一直振荡,我引入了一个阻尼器。

    物理学

    控件的中心圆盘有位置(p)、速度(v)和加速度(a)。

    它的位置产生两种力量:

    • 来自连接到控件左侧、中心或右侧的弹簧的力
    • 来自用户拉动的弹簧的力(在交互过程中)

    它的速度产生1个力:

    • 阻尼器的作用力与速度方向相反

    使用F = m * a,我们现在可以推导出磁盘的加速度:

    a = F / m
    

    渲染

    每个渲染循环,我们计算如下:

    • 使用pv 计算弹簧力和阻尼力
    • 使用力计算a
    • 通过加速更新速度 (v += a)
    • 使用速度更新位置 (p += v)

    把它们放在一起

    注意,我有点忽略了您提供的代码。我希望这个答案有用不是因为你可以复制粘贴它,而是因为你可以实现它的一些想法

    const Spring = (anchorPos, k, maxLength = Infinity) => {
      return {
        getForce: p => k * Math.max(Math.min((anchorPos - p), maxLength), -maxLength),
        setAnchorPos: newA => { anchorPos = newA; },
        setSpringConstant: newK => { k = newK; },
        get anchorPos() {
          return anchorPos;
        }
      }
    };
    
    const Slider = el => {
      const CENTER_SPRING_K = 0.1;
      const CENTER_PULL_RATIO = 0.8;
      const DAMPING = 0.15;
      
      // The spring attaching the disc to the current control state
      const centerSpring = Spring(0, CENTER_SPRING_K);
      // The spring attached to the user's pointer
      // Note: to "cut" the spring, we set its constant to 0
      const pullSpring = Spring(0, 0, 100);
      
      const attachPullSpring = () => {
        pullSpring.setAnchorPos(centerSpring.anchorPos);
        pullSpring.setSpringConstant(CENTER_PULL_RATIO * CENTER_SPRING_K);
        
        window.addEventListener("mousemove", onMouseMove);
        window.addEventListener("mouseup", onMouseUp);
      }
      
      const detachPullSpring = () => {
        pullSpring.setSpringConstant(0);
        
        window.removeEventListener("mouseup", onMouseUp);
        window.removeEventListener("mousemove", onMouseMove);
      }
      
      // Handle mouse events
      let mouseStartX = null;
      const onMouseDown = e => {
        mouseStartX = e.clientX;
        attachPullSpring();
      };
      
      const onMouseMove = e => {
        const dx = e.clientX - mouseStartX;
        pullSpring.setAnchorPos(centerSpring.anchorPos + dx);
        e.preventDefault();
      };
      
      const onMouseUp = e => {
        mouseStartX = null;
        detachPullSpring();
      }
      
      el.addEventListener("mousedown", onMouseDown);
      
      // Mass
      const M = 1;
      
      let p = 0;
      let v = 0;
      let a = 0;
      
      const update = () => {
        const springForces = [
          centerSpring.getForce(p),
          pullSpring.getForce(p)
        ];
        
        // One damper is enough for now, and we never update it
        const damperForces = [
          DAMPING * -v
        ];
        
        const fTot = springForces
          .concat(damperForces)
          .reduce((a, b) => a + b, 0);
        
        // f = m * a
        a = fTot / M;
        v += a;
        p += v;
        
        // Check if we've reached a new state
        const offset = p - centerSpring.anchorPos;
        if (Math.abs(offset) > 100) {
          const dir = offset > 0 ? 1 : -1;
          
          // Attach centerSpring to new state
          centerSpring.setAnchorPos(centerSpring.anchorPos + dir * 100);
          
          // Stop pulling
          detachPullSpring();
          mouseStartX = null;
        }
      }
      
      const render = () => {
        el.style.transform = `translateX(${p}px)`;
      }
      
      return { update, render }
    }
    
    
    const slider = Slider(document.querySelector(".Slider"));
    
    const loop = () => {
      slider.update();
      slider.render();
      
      requestAnimationFrame(loop);
    }
    
    loop();
    * {
      position: relative;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: sans-serif;
      font-weight: bold;
      display: flex;
      width: 400px;
    }
    
    .Label {
      touchaction: none;
      flex: 1;
      padding: 10px;
      border: 1px solid red;
      text-align: center;  
      background: #efefef;
    }
    
    .Slider {
      position: absolute;
      width: 20px;
      height: 20px;
      
      top: calc(50% - (20px / 2));
      left: calc(50% - (20px / 2));
      border-radius: 50%;
      background: black;
    }
    <p class="Label">No</p>
    <p class="Label">Yes</p>
    <div class="Slider"></div>

    注意事项:

    • 我尝试平衡系统以使其在值之间翻转足够困难
    • 如果你想让它更难翻转,你可以:
      • 降低拉弦的弹簧常数,或
      • 增加中心弹簧常数,或
      • 增加阻尼常数
    • 我没有使用画布,而是使用了一些常规的 HTML 元素。
    • 您可能需要花一些时间来使控件易于访问。

    【讨论】:

      猜你喜欢
      • 2012-07-10
      • 2014-08-29
      • 2013-11-15
      • 1970-01-01
      • 2015-08-13
      • 1970-01-01
      • 2011-06-28
      • 1970-01-01
      • 2019-11-04
      相关资源
      最近更新 更多