I'm developing a custom input for questions with only two possible answers, eg.: "Yes" or "No". Here's a working example and here you can read the source code (it also work in touch screens).
The idea is to use a similar principle to the "Slide to open" slider input, but I'm trying to make the thumb put a bit of resistance. My implementation consists of calculating the speed of the dragging movement and make the thumb respond to it, so, though the pointer (finger or mouse) might be close to the answer, if the speed decreases, the thumb gets back to origin. So, it's not so much about actually dragging the thumb but about moving the pointer quickly in the direction of the desired answer.
The problem is, it's currently a bit wonky and shaky. So, is there a way to make the thumb move smoother? No part of my implementation is permanent, so feel free to experiment and modify anything. Also, I'm no expert in JS by any means, so don't be too hasrh, please? Thanks in advance. Cheers
The code is here too.
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();