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