class Annotation{ constructor(annotation, t_in, t_out) { this.annotation = annotation; this.t_in = t_in; this.t_out = t_out; } } class StrokeGroup{ constructor(group_element, player){ this.g = group_element; this.player = player; } setStrokes(strokes){ console.log('set strokes',strokes); const pathEls = this.g.querySelectorAll('path'); let indexes = Object.keys(strokes); for (let pathEl of pathEls) { const i = pathEl.dataset.path_i; if(!indexes.includes(pathEl.dataset.path_i)){ pathEl.parentNode.removeChild(pathEl); }else{ // check in and outpoint using pathEl.dataset if(strokes[i].getSliceId() != pathEl.dataset.slice){ const d = this.points2D(strokes[i].points); pathEl.dataset.slice = strokes[i].getSliceId(); pathEl.setAttribute('d', d); } } // this has now been processed indexes.splice(indexes.indexOf(i), 1); } console.log(indexes); // new strokes indexes.forEach(index => { const stroke = strokes[index]; let pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pathEl.style.stroke = stroke.color; pathEl.classList.add('path'); pathEl.dataset.path_i = index; pathEl.dataset.slice = stroke.getSliceId(); this.g.appendChild(pathEl); const d = this.points2D(stroke.points); pathEl.setAttribute('d', d); }); } // convert array of points to a d-attribute points2D(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.player.dimensions[0]},${stroke[1] * this.player.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.player.dimensions[0]},${rel_stroke[1] * this.player.dimensions[1]} `; } last_stroke = stroke; } return d; } } class Stroke{ constructor(color, points){ this.color = color; this.points = points; // [[x1,y1,t1], [x2,y2,t2], ...] } getSliceId() { return 'all'; } } class StrokeSlice{ constructor(stroke, i_in, i_out){ this.stroke = stroke; // Stroke this.i_in = typeof i_in === 'undefined' ? 0 : i_in; this.i_out = typeof i_out === 'undefined' ? this.stroke.points.length : i_out; } getSliceId(){ return `${this.i_in}-${this.i_out}`; } // compatible with Stroke() get points(){ return this.stroke.points.slice(this.i_in, this.i_out); } // compatible with Stroke() get color(){ return this.stroke.color; } } class Player { constructor(wrapperEl) { 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; const groups = ['before', 'annotation', 'after'] this.strokeGroups = {}; groups.forEach(group => { let groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g'); groupEl.classList.add(group) this.svgEl.appendChild(groupEl); this.strokeGroups[group] = new StrokeGroup(groupEl, this); }); this.annotations = [] } playlist(url) { const request = new Request(url, { method: 'GET', }); fetch(request) .then(response => response.json()) .then(data => { let playlist = this.wrapperEl.querySelector('.playlist'); if (!playlist) { playlist = document.createElement('nav'); playlist.classList.add('playlist'); this.wrapperEl.appendChild(playlist) } else { playlist.innerHTML = ""; } const listEl = document.createElement("ul"); for (let fileUrl of data) { const liEl = document.createElement("li"); liEl.innerText = fileUrl liEl.addEventListener('click', (e) => { this.play(fileUrl); playlist.style.display = "none"; }); listEl.appendChild(liEl); } playlist.appendChild(listEl); // do something with the data sent in the request }); } 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.map(s => new Stroke(s['color'], s['points'])); 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 formatter = wNumb({ decimals: 2, edit: (time) => { const s = Math.floor(time/1000); const minutes = Math.floor(s / 60); const seconds = s - minutes * 60; const ms = Math.floor((time/1000 - s) * 1000); return `${minutes}:${seconds}:${ms}`; } }); const slider = noUiSlider.create(this.scrubberEl, { start: [this.currentTime, this.duration], connect: true, range: { 'min': 0, 'max': this.duration }, tooltips: [ formatter, formatter ], // pips: { // mode: 'range', // density: 3, // format: formatter // } }); 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.drawStrokePosition(this.inPointPosition, this.outPointPosition); // 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]; } getStrokesSliceForPathRange(in_point, out_point) { // get paths for given range. Also, split path at in & out if necessary. let slices = {}; for (let i = in_point[0]; i <= out_point[0]; i++) { const stroke = this.strokes[i]; if(typeof stroke === 'undefined'){ // out point can be Infinity. So interrupt whenever the end is reached break; } const in_i = (in_point[0] === i) ? in_point[1] : 0; const out_i = (out_point[0] === i) ? out_point[1] : Infinity; slices[i] = new StrokeSlice(stroke, in_i, out_i); } return slices; } // 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(in_point, out_point, show_all) { if(typeof show_all === 'undefined') show_all = true; this.strokeGroups['before'].setStrokes(this.getStrokesSliceForPathRange([0,0], in_point)); this.strokeGroups['annotation'].setStrokes(this.getStrokesSliceForPathRange(in_point, out_point)); this.strokeGroups['after'].setStrokes(this.getStrokesSliceForPathRange(out_point, [Infinity, Infinity])); // // 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(this.outPointPosition && (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]; } }