Ваша задача показалась мне интересной и я написал... (даже не знаю, как это обозвать) "нечто", что может послужить вам либо основой, либо подтолкнуть вас к решению задачи.
Тоже самое, что и ниже, только на
codesandbox.
<!DOCTYPE html>
<html>
<head>
<title>Vanila js. Line on canvas with Marker</title>
<meta charset="UTF-8" />
<style>
#canvas {
background: #eee;
}
</style>
</head>
<body>
<div>
<lable>
<span>Completeness</span>
<div>
<span>0%</span>
<input
type="range"
name="completeness"
min="0"
max="100"
step="1"
value="0"
/>
<span>100%</span>
</div>
</lable>
</div>
<div id="canvas-container">
<div>
<div style="margin-top: 3px; color: gray;">
Try to draw a line on the canvas below
</div>
<canvas id="canvas"></canvas>
</div>
<button type="button" id="set-new-route">Set a new route</button>
</div>
<script>
(() => {
const getElement = (selector) => {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element with selector [${selector}] not found`);
}
return element;
};
const getRoutePoints = (canvas, externalListeners = {}) => {
return new Promise((resolve) => {
const canvasRect = canvas.getBoundingClientRect();
const getPoint = (e) => ({
x: e.clientX - canvasRect.left,
y: e.clientY - canvasRect.top
});
let pointsBuffer = [];
let shouldRecordPoints = false;
const done = () => {
for (const eventName of Object.keys(listeners)) {
canvas.removeEventListener(eventName, listeners[eventName]);
}
resolve(pointsBuffer);
};
const listeners = {
mousedown(e) {
if (shouldRecordPoints) {
return;
}
pointsBuffer = [];
shouldRecordPoints = true;
if (externalListeners.onmousedown) {
externalListeners.onmousedown(getPoint(e));
}
},
mousemove(e) {
if (!shouldRecordPoints) {
return;
}
pointsBuffer.push(getPoint(e));
if (externalListeners.onmousemove) {
externalListeners.onmousemove(getPoint(e));
}
},
mouseup(e) {
if (!shouldRecordPoints) {
return;
}
shouldRecordPoints = false;
if (externalListeners.onmouseup) {
externalListeners.onmouseup(getPoint(e));
}
done();
}
};
for (const eventName of Object.keys(listeners)) {
canvas.addEventListener(eventName, listeners[eventName]);
}
});
};
const listenCompletenesChanges = (() => {
let listener = null;
const node = getElement('[name="completeness"]');
node.addEventListener("input", function () {
try {
if (listener) {
listener(+this.value);
}
} catch (_) {}
});
return (callable) => {
try {
(listener = callable)(+node.value);
} catch (_) {}
};
})();
const canvas = getElement("#canvas");
canvas.width = window.innerWidth;
const ctx = canvas.getContext("2d");
ctx.lineWidth = 3;
ctx.setLineDash([15, 5]);
const setNewRouteButton = getElement("#set-new-route");
setNewRouteButton.addEventListener("click", async function () {
listenCompletenesChanges(() => {}); // remove last listener
setNewRouteButton.disabled = true;
const ctx = canvas.getContext("2d");
// clear canvas
ctx.beginPath();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.stroke();
// await route points
const points = await getRoutePoints(canvas, {
onmousedown({ x, y }) {
ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.stroke();
},
onmousemove({ x, y }) {
ctx.lineTo(x, y);
ctx.stroke();
}
}).then((points) => {
setNewRouteButton.disabled = false;
return points;
});
if (!points) {
return;
}
const cache = document.createElement("canvas");
cache.width = canvas.width;
cache.height = canvas.height;
cache.getContext("2d").drawImage(canvas, 0, 0);
listenCompletenesChanges((completeness) => {
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(cache, 0, 0);
const lastCompletedPointIndex = Math.max(
0,
Math.min(
points.length,
Math.round((points.length / 100) * completeness)
) - 1
);
const cursorSize = 10;
const halfOfCursorSize = cursorSize / 2;
const { x, y } = points[lastCompletedPointIndex];
ctx.save();
ctx.strokeStyle = "green";
ctx.setLineDash([]);
ctx.roundRect(
x - halfOfCursorSize,
y - halfOfCursorSize,
cursorSize,
cursorSize,
halfOfCursorSize
);
ctx.stroke();
ctx.restore();
});
});
setNewRouteButton.dispatchEvent(new Event("click"));
})();
</script>
</body>
</html>