diff --git a/webserver.py b/webserver.py index 991ced9..28338da 100644 --- a/webserver.py +++ b/webserver.py @@ -257,7 +257,7 @@ if __name__ == "__main__": logger.addHandler( logFileHandler ) - logger.info("Start server") + logger.info(f"Start server: http://localhost:{args.port}") server = Server(args, logger) server.start() diff --git a/www/annotate.html b/www/annotate.html new file mode 100644 index 0000000..8f6fdbc --- /dev/null +++ b/www/annotate.html @@ -0,0 +1,119 @@ + + + + + + Annotate a line animation + + + + + +
+
+ + + + + + + \ No newline at end of file diff --git a/www/annotate.js b/www/annotate.js new file mode 100644 index 0000000..04eff02 --- /dev/null +++ b/www/annotate.js @@ -0,0 +1,446 @@ +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]; + } + + +} \ No newline at end of file