So I've a feature I'm trying to develop in js which is showing the path of a player on top of static map. I've already a sequence of timestamp and x,y, and I'm trying to find a library which can take that sequence and render it as a video player like where I can seek and the canvas will draw the navigation accordingly.
The closes lib I found is this but it's still far from my needs. https://embiem.github.io/react-canvas-draw/
Thanks in advance for your help
Ignore the design - concept demonstration.
Using the canvas you can create a path using the way-points. Then use ctx.setLineDash to create a single long dash. Then you use ctx.lineDashOffset to move over time.
As there are many items to cover this answer is by example. One assumes you are familiar with JS and DOM basics.
It uses performance.now()
to get a time stamp (converted to seconds). The duration in example is fixed to 10 seconds.
If syncing to video you should get the time and duration from the media element.
If the media time stamp (media.currentTime
) does not change between animation frames the renderer will stall. To continue play call travelLine.render()
as needed. If you have problems determining when the media has updated use the alternative renderer animLoopNoStall
(see comments on how)
const P2 = (x, y) => ({x, y});
const points = [
P2(1, 4), P2(10, 10), P2(12, 10), P2(13, 15),
P2(13, 20), P2(10, 20), P2(9, 12), P2(7, 19),
P2(6, 21), P2(1, 22), P2(1, 24), P2(21, 24),
P2(23, 1), P2(13, 3), P2(13, 8), P2(12, 8)
];
const listener = (qe, name, call, opt = {}) => ((qe = qryEl(qe)).addEventListener(name, call, opt), qe);
const qryEl = qryEl => typeof qryEl === "string" ? query(qryEl) : qryEl;
const query = (qStr, el = document) => el.querySelector(qStr);
function initUI() {
rangeEl.min = 0;
rangeEl.max = travelLine.totalTime;
rangeEl.step = 1 / 60;
rangeEl.value = 0;
listener(rangeEl, "input", sliderUpdate);
listener(startEl, "click", toStart);
listener(endEl, "click", toEnd);
listener(playEl, "click", playPause);
}
function sliderUpdate() { travelLine.seek(rangeEl.value); }
function toStart() { travelLine.seek(0) }
function toEnd() { travelLine.seek(travelLine.totalTime) }
function playPause() {
if (travelLine.playing) { travelLine.pause() }
else { travelLine.play() }
}
// Note pathW, pathH is the size of the points source.
// These are requiered so that points can be scaled to
// fit the canvas.
// NOTE!! The path MUST be called again if the canvas is resized
const path = (pathW, pathH, renderCanvas, ...points) => {
const xScale = renderCanvas.width / pathW;
const yScale = renderCanvas.height / pathH;
const path = new Path2D;
var i = 0, len = 0, p;
while (i < points.length) {
const p2 = points[i++];
const x = p2.x * xScale;
const y = p2.y * yScale;
path.lineTo(x, y);
if (p) { len += Math.hypot(x - p.x, y - p.y); }
p = P2(x, y);
}
path.pathLen = len;
return path;
}
const ctx = canvas.getContext("2d");
const travelLine = {
path: path(24, 25, canvas, ...points),
amount: 0, // unit time
width: 8,
color: "green",
startTime: undefined, // seconds
totalTime: 10, // seconds
playing: false,
update: false,
renderer: animLoop,
render() {
if (!this.renderer.framePending) {
this.renderer.framePending = true;
requestAnimationFrame(this.renderer);
}
},
pause() {
this.playing = false;
this.update = true;
this.render();
},
play() {
if (this.amount === 1.0) { this.amount = 0.0 }
this.playing = true;
this.update = true;
this.startTime = performance.now() * 0.001 - this.amount * this.totalTime;
this.render();
},
seek(time) {
this.pause();
this.amount = time / this.totalTime;
this.render();
},
get time() { return this.amount * this.totalTime },
set time(time) {
if (this.startTime === undefined) { this.startTime = time }
const t = time - this.startTime;
const amount = Math.max(0, Math.min(1, t / this.totalTime));
if (this.amount !== amount) {
this.amount = amount;
this.update = true;
}
if (this.amount === 1.0) { this.pause() }
}
}
function drawTravelLine({amount, path, width, color}) {
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.setLineDash([path.pathLen]);
ctx.lineDashOffset = path.pathLen - amount * path.pathLen;
ctx.beginPath();
ctx.stroke(path);
}
function animLoop(time) {
time = performance.now() * 0.001; // This is the same as passed.
// If syncing from media you should
// get the time from the media element here.
if (travelLine.playing) { travelLine.time = time; }
if (travelLine.update) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawTravelLine(travelLine);
travelLine.update = false;
rangeEl.value = travelLine.time;
playEl.textContent = travelLine.playing ? "||" : ">";
requestAnimationFrame(travelLine.renderer);
travelLine.renderer.framePending = true;
} else {
travelLine.renderer.framePending = false;
}
}
// If you have problems with render stalling use the following renderer function
// by setting travelLine.renderer = animLoopNoStall;
function animLoopNoStall(time) {
time = performance.now() * 0.001;
if (travelLine.playing) { travelLine.time = time; }
if (travelLine.update) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawTravelLine(travelLine);
travelLine.update = false;
rangeEl.value = travelLine.time;
playEl.textContent = travelLine.playing ? "||" : ">";
}
requestAnimationFrame(travelLine.renderer);
travelLine.renderer.framePending = true;
}
initUI();
travelLine.play();
canvas { border : 2px solid black; }
* { font-family: arial black; }
<canvas id="canvas" width="256" height="160"></canvas><br>
<button id="startEl"><<</button>
<input id="rangeEl" type="range">
<button id="playEl">></button>
<button id="endEl">>></button>