chronodiagram/www/play.js
2021-12-21 14:31:02 +01:00

302 lines
No EOL
10 KiB
JavaScript

class Player {
constructor(wrapperEl, fileurl) {
this.wrapperEl = wrapperEl;
this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
this.wrapperEl.appendChild(this.svgEl);
this.scrubberElOld = document.createElement('input');
this.scrubberElOld.type = "range";
this.scrubberElOld.min = 0;
this.scrubberElOld.step = 0.01;
this.wrapperEl.appendChild(this.scrubberElOld);
this.scrubberEl = document.createElement('div');
this.scrubberEl.classList.add('scrubber')
this.wrapperEl.appendChild(this.scrubberEl);
this.scrubberElOld.addEventListener("input", (ev) => {
this.scrubTo(ev.target.value);
})
this.inPointPosition = null;
this.outPointPosition = null;
this.currentTime = 0;
this.isPlaying = false;
this.play(fileurl);
}
play(file) {
const request = new Request(file, {
method: 'GET',
});
fetch(request)
.then(response => response.json())
.then(data => {
this.playStrokes(data)
// do something with the data sent in the request
});
}
playStrokes(drawing) {
this.strokes = drawing.shape;
this.currentPathI = null;
this.currentPointI = null;
this.dimensions = drawing.dimensions;
this.svgEl.setAttribute('viewBox', `0 0 ${this.dimensions[0]} ${this.dimensions[1]}`)
this.startTime = window.performance.now() - this.strokes[0].points[0][3];
this.playStrokePosition(0, 1);
this.duration = this.getDuration();
this.scrubberElOld.max = this.duration;
this.playTimout = null;
const slider = noUiSlider.create(this.scrubberEl, {
start: [this.currentTime, this.duration],
connect: true,
range: {
'min': 0,
'max': this.duration
}
});
slider.on("slide", (values, handle) => {
this.isPlaying = false;
// console.log(values, handle);
// both in and out need to have a value
this.inPointPosition = this.findPositionForTime(values[0]);
this.outPointPosition = this.findPositionForTime(values[1]);
if (handle === 0) {
// in point
if (
this.currentPathI < this.inPointPosition[0] ||
this.currentPointI < this.inPointPosition[1]) {
this.drawStrokePosition(
// this.inPointPosition[0],
// this.inPointPosition[1],
// always draw at out position, as to see the whole shape of the range
this.outPointPosition[0],
this.outPointPosition[1],
);
}
}
if (handle === 1) {
// out point
// this.outPointPosition = this.findPositionForTime(values[1]);
this.drawStrokePosition(
this.outPointPosition[0],
this.outPointPosition[1],
);
}
// this.inPointPosition = values;
// this.outPointPosition = vaalues[0];
// this.scrubTo()
// this.scrubTo(ev.target.value);
});
}
getDuration() {
const points = this.strokes[this.strokes.length - 1].points;
return points[points.length - 1][3];
}
getStrokesForPathRange(in_point, out_point) {
// get paths for given range. Also, split path at in & out if necessary.
strokes = {};
for (let i = in_point[0]; i <= out_point[0]; i++) {
const path = this.strokes[i];
const in_i = (in_point[0] === i) ? in_point[1] : 0;
const out_i = (out_point[0] === i) ? out_point[1] : Math.inf;
const points = path.points.slice(in_i, out_i);
strokes[i] = points; // preserve indexes
}
return strokes;
}
// TODO: when drawing, have a group active & inactive.
// active is getPathRange(currentIn, currentOut)
// inactive is what comes before and after.
// then, playing the video is just running pathRanghe(0, playhead)
drawStrokePosition(path_i, point_i, show_all) {
if(typeof show_all === 'undefined')
show_all = false;
// check if anything is placed that is in the future from the current playhead
if (this.currentPathI !== null && this.currentPoint !== null) {
if (this.currentPathI > path_i) {
console.log('remove', path_i, ' -> ', this.currentPathI)
// remove everything that comes later
for (let i = path_i + 1; i <= this.currentPathI; i++) {
// console.log('remove', i);
const pathEl = this.svgEl.querySelector(`.path${i}`);
if (pathEl) {
pathEl.parentNode.removeChild(pathEl);
}
}
}
}
// an inpoint is set, so we're annotating
// make everything coming before translucent
if (this.inPointPosition !== null) {
const [inPath_i, inPoint_i] = this.inPointPosition;
// returns a static NodeList
const currentBeforeEls = this.svgEl.querySelectorAll(`.before_in`);
for (let currentBeforeEl of currentBeforeEls) {
currentBeforeEl.classList.remove('before_in');
}
for (let index = 0; index < inPath_i; index++) {
const pathEl = this.svgEl.querySelector(`.path${index}`);
if (pathEl) {
pathEl.classList.add('before_in');
}
}
}
this.currentPathI = path_i;
this.currentPointI = point_i;
const path = this.strokes[path_i];
// console.log(path);
let pathEl = this.svgEl.querySelector(`.path${path_i}`);
if (!pathEl) {
pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathEl.style.stroke = path.color;
pathEl.classList.add('path' + path_i)
this.svgEl.appendChild(pathEl)
}
const stroke = path.points.slice(0, point_i);
const d = this.strokes2D(stroke);
pathEl.setAttribute('d', d);
this.scrubberElOld.value = path.points[point_i][3];
this.currentTime = path.points[point_i][3];
}
getNextPosition(path_i, point_i) {
const path = this.strokes[path_i];
let next_path, next_point;
if (path.points.length > point_i + 1) {
next_path = path_i;
next_point = point_i + 1;
// setTimeout(() => this.playStroke(next_path, next_point), dt);
} else if (this.strokes.length > path_i + 1) {
next_path = path_i + 1;
next_point = 1;
// use starttime instead of diff, to prevent floating
} else {
return [null, null];
}
// when an outpoint is set, stop playing there
if(next_path > this.outPointPosition[0] || next_point > this.outPointPosition[1]){
return [null, null];
}
return [next_path, next_point];
}
playStrokePosition(path_i, point_i, allow_interrupt) {
if(allow_interrupt) {
if(!this.isPlaying) {
console.log('not playing because of interrupt');
return;
}
} else{
this.isPlaying = true;
}
this.drawStrokePosition(path_i, point_i);
const [next_path, next_point] = this.getNextPosition(path_i, point_i);
if (next_path === null) {
console.log('done playing');
return;
}
const t = this.strokes[next_path].points[next_point][3];// - path.points[point_i][3];
const dt = t - (window.performance.now() - this.startTime);
this.playTimout = setTimeout(() => this.playStrokePosition(next_path, next_point, true), dt);
}
playUntil(path_i) {
// for scrubber
}
scrubTo(ms) {
const [path_i, point_i] = this.findPositionForTime(ms);
// console.log(path_i, point_i);
clearTimeout(this.playTimout);
this.playStrokePosition(path_i, point_i);
// this.playHead = ms;
}
findPositionForTime(ms) {
ms = Math.min(Math.max(ms, 0), this.duration);
console.log('scrub to', ms)
let path_i = 0;
let point_i = 0;
this.strokes.every((stroke, index) => {
const startAt = stroke.points[0][3];
const endAt = stroke.points[stroke.points.length - 1][3];
if (startAt > ms) {
return false; // too far
}
if (endAt > ms) {
// we're getting close. Find the right point_i
path_i = index;
stroke.points.every((point, pi) => {
if (point[3] > ms) {
// too far
return false;
}
point_i = pi;
return true;
});
return false;
} else {
// in case nothings comes after, we store the last best option thus far
path_i = index;
point_i = stroke.points.length - 1;
return true;
}
});
return [path_i, point_i];
}
strokes2D(strokes) {
// strokes to a d attribute for a path
let d = "";
let last_stroke = undefined;
let cmd = "";
for (let stroke of strokes) {
if (!last_stroke) {
d += `M${stroke[0] * this.dimensions[0]},${stroke[1] * this.dimensions[1]} `;
cmd = 'M';
} else {
if (last_stroke[2] == 1) {
d += " m";
cmd = 'm';
} else if (cmd != 'l') {
d += ' l ';
cmd = 'l';
}
let rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]];
d += `${rel_stroke[0] * this.dimensions[0]},${rel_stroke[1] * this.dimensions[1]} `;
}
last_stroke = stroke;
}
return d;
}
}