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; } }