var SplineEdit = function (elTarget, colour, duration, constraints, name) { var self = this; var targetName = "#" + elTarget.attr("id"); var svg; var path; var line = d3.line(); var yStep; var selected; var dragged; var yScale = [constraints[0], constraints[1]]; var region = [0, 1]; this.changed = false; Object.defineProperty(this, "width", { get: function() { return parseFloat(elTarget.width()); }, set: function(x) {} }); Object.defineProperty(this, "height", { get: function() { return parseFloat(elTarget.height()); }, set: function(x) {} }); if (constraints[3] > 0.0001) { yStep = self.height / ((constraints[1] - constraints[0]) / constraints[3]); } var dfaultPos = ((constraints[2] - constraints[0]) / (constraints[1] - constraints[0])); var rawPoints = [[0, dfaultPos], [1, dfaultPos]]; var pointRange = [0, 0]; var points = [...rawPoints]; var circlePoints = []; this.getRawPoints = function() { return rawPoints; }; this.setRawPoints = function(p, noredraw) { rawPoints = p; if (!noredraw) self.redraw(); }; Object.defineProperty(this, "displayPoints", { get: function() { var output = []; var x; for (let p of points) { output.push([p[0] * self.width, (1 - p[1]) * self.height]); } return output; }, set: function(x) {} }); this.resize = function(ratio, noredraw) { for (var i in points) { if (i != 0 && i != rawPoints.length - 1) { rawPoints[i][0] = rawPoints[i][0] * ratio; } } if (!noredraw) self.redraw(); }; function regionToAbsolute(xVal) { return (xVal - region[0]) / (region[1] - region[0]); } function absoluteToRegion(xVal) { return (xVal * (region[1] - region[0])) + region[0]; } function interpolateRange(scaled, start, end) { if (!start) start = region[0]; if (!end) end = region[1]; var scaling = 1 - (end - start); var output = []; var lastIndex; var firstIndex = 0; var lastIndex; var yVal; var xVal; var allPoints = rawPoints; if (start == 0 && end == 1) { return {points: allPoints, indexes: [0, allPoints.length - 1]}; } for (var i in allPoints) { var x = allPoints[i][0]; if (x < start) { firstIndex = parseInt(i); } } for (var i = parseInt(allPoints.length - 1); i > firstIndex; i--) { var x = allPoints[i][0]; if (x >= end) { lastIndex = i; } } for (var i = firstIndex; i <= lastIndex; i++) { var v = allPoints[i]; xVal = (i == lastIndex) ? end : v[0]; if (i == firstIndex && v[0] != start) { var next = allPoints[parseInt(i + 1)]; //yVal = v[1] + (start - v[0]) * (next[1] - v[1]) / (next[0] - v[0]); yVal = v[1] + (v[0] - start) * (next[1] - v[1]) / (next[0] - v[0]); } else if (i == lastIndex && v[0] != end) { var last = allPoints[parseInt(i - 1)]; yVal = last[1] + (end - last[0]) * (v[1] - last[1]) / (v[0] - last[0]); } else { yVal = v[1]; } if (scaled) xVal = regionToAbsolute(xVal); output.push([xVal, yVal]); } return {points: output, indexes: [firstIndex, lastIndex]}; } function getIndex(point) { for (var i = 0; i < points.length; i ++) { if (points[i][0] == point[0] && points[i][1] == point[1]) { return i; } } } function getDuration() { if (typeof(duration) == "function") { return duration(); } else { return duration; } } var redrawing = false; this.redraw = function() { if (redrawing) return; redrawing = true; build(); redraw(); redrawing = false; }; function setRangePoints() { /* if (points.length == rawPoints.length) { //rawPoints.length = 0; for (var i in points) { //rawPoints[i] = [regionToAbsolute(points[i][0]), points[i][1]]; } }*/ var res = interpolateRange(true); pointRange = res.indexes; points.length = 0; circlePoints.length = 0; for (let i in res.points) { points.push(res.points[i]); if ((region[0] == 0 && region[1] == 1) || ((i != 0 || region[0] == 0) && (i != res.points.length - 1 || region[1] == 1))) { circlePoints.push(res.points[i]); } } } this.setRange = function(start, end) { if (start != null) region[0] = start; if (end != null) region[1] = end; self.redraw(); }; this.setYScale = function(min, max) { // TODO unused if (!min && !max) { yScale = [constraints[0], constraints[1]]; } }; this.setDuration = function(val) { duration = val; }; this.setConstraints = function(val) { constraints = val; } function calculateValue(xy, asDuration) { var x = xy[0]; if (asDuration) { x *= getDuration(); } var y = ((constraints[1] - constraints[0]) * xy[1]) + constraints[0]; return [x, y]; } this.getData = function() { var output = []; var val; var allPoints = rawPoints; for (var p of allPoints) { val = calculateValue(p, true); output.push([Math.round(val[0] * 1000) / 1000, Math.round(val[1] * 1000) / 1000]); } return output; }; this.getLinsegData = function(start, end, nullOnEmpty) { if (nullOnEmpty && rawPoints.length == 2) return; var duration = getDuration(); //(end - start) * getDuration(); // IF we are having dynamic area view var rounding = 10000; var output = []; var lastTime = 0; var time; var lastIndex; var firstIndex; var lastIndex; var allPoints = rawPoints; for (var i in allPoints) { var x = allPoints[i][0]; if (x <= start) { firstIndex = parseInt(i); } } for (var i = parseInt(allPoints.length - 1); i > firstIndex; i--) { var x = allPoints[i][0]; if (x >= end) { lastIndex = i; } } for (var i = firstIndex; i <= lastIndex; i++) { var v = calculateValue(allPoints[i], false); if (i != firstIndex) { time = (((i == lastIndex) ? end : v[0]) - start) * duration; output.push((Math.round((time - lastTime) * rounding)) / rounding); lastTime = time; } if (i == firstIndex && v[0] != start) { var next = calculateValue(allPoints[parseInt(i + 1)], false); //var interpVal = v[1] + (v[0] - start) * (next[1] - v[1]) / (next[0] - v[0]); var interpVal = v[1] + (start - v[0]) * (next[1] - v[1]) / (next[0] - v[0]); output.push(Math.round(interpVal * rounding) / rounding); } else if (i == lastIndex && v[0] != end) { var last = calculateValue(allPoints[parseInt(i - 1)], false); var interpVal = last[1] + (end - last[0]) * (v[1] - last[1]) / (v[0] - last[0]); //var interpVal = last[1] + (last[0] - end) * (v[1] - last[1]) / (v[0] - last[0]); output.push(Math.round(interpVal * rounding) / rounding); } else { output.push(Math.round(v[1] * rounding) / rounding); } } return output.join(","); } function getLabel(d) { var val = calculateValue([absoluteToRegion(d[0]), d[1]], true); var label = (Math.round(val[0] * 100) / 100) + "s
" + (Math.round(val[1] * 1000) / 1000); if (name) { label = name + "
" + label; } return label; } function redraw() { setRangePoints(); path.datum(self.displayPoints); svg.select("path").attr("d", line); const circle = svg.selectAll("g").data(circlePoints, function(point){ return [point[0] * self.width, (1 - point[1]) * self.height]; }); circle.enter().append("g").call( g => g.append("circle").attr("r", 0).attr("stroke", colour).attr("stroke-width", 1).attr("r", 7) ).merge(circle).attr("transform", function(d){ return "translate(" + (d[0] * self.width) + "," + ((1 - d[1]) * self.height) + ")"; }).select("circle:last-child").attr("fill", function(d) { if (selected && d[0] == selected[0] && d[1] == selected[1]) { return "#000000"; } else { return colour; } }).on("mouseover", function(event, d) { twirl.tooltip.show(event, getLabel(d), colour); }).on("mouseout", function(event) { twirl.tooltip.hide(); }).on("dblclick", function(event, d) { return; // TODO: not working properly particularly when zoomed if (!window.twirl) return; var index = getIndex(d); var duration = getDuration(); console.log(d, regionToAbsolute(d[0])); var minTime = ((points[index - 1]) ? absoluteToRegion(points[index - 1] + 0.00001) : 0) * duration; var maxTime = ((points[index + 1]) ? absoluteToRegion(points[index + 1] - 0.00001) : 1) * duration; var el = $("
"); var tb = $(""); $("").append(tb).appendTo(el); var tpTime = new twirl.transform.Parameter({ host: null, definition: { name: "Time", min: minTime, max: maxTime, dfault: d[0] } }); var tpValue = new twirl.transform.Parameter({ host: null, definition: { name: name, min: constraints[0], max: constraints[1], dfault: d[1] } }); tb.append(tpTime.getElementRow(true)).append(tpValue.getElementRow(true)); twirl.prompt.show(el, function(){ d[0] = regionToAbsolute(tpTime.getValue()) d[1] = tpValue.getValue(); self.changed = true; }); //d[0] = 0.4; d[1] = 0.4; }); circle.exit().remove(); } function build() { line.curve(d3.curveLinear); if (path) { path.remove(); delete path; } if (svg) { svg.remove(); delete svg; } svg = d3.select(targetName).append("svg").attr("width", self.width).attr("height", self.height); svg.append("rect").attr("width", self.width).attr("height", self.height).attr("fill", "none"); path = svg.append("path").attr("fill", "none").attr("stroke", colour).attr("line-width", "3px"); //.call(redraw); // .datum(points) svg.call( d3.drag().subject(function(event){ let pos = event.sourceEvent.target.__data__; var index; var y; if (!pos) { var index; for (var i = 0; i < rawPoints.length; i++) { if (event.x > regionToAbsolute(rawPoints[i][0]) * self.width && rawPoints[i + 1] && event.x < regionToAbsolute(rawPoints[i + 1][0]) * self.width) { index = i + 1; if (yStep) { y = Math.round(event.y / yStep) * yStep; } else { y = event.y; } //pos = [absoluteToRegion(event.x / self.width), 1 - (y / self.height)]; pos = [absoluteToRegion(event.x / self.width), 1 - (y / self.height)]; break; } } if (index) { var tmp = rawPoints.slice(0, index); tmp.push(pos); var newPoints = tmp.concat(rawPoints.slice(index)); rawPoints.length = 0; Array.prototype.push.apply(rawPoints, newPoints); redraw(); } } else if (pos[0] == 0) { index = 0; } else if (pos[0] == 1) { index = rawPoints.length - 1; } else { var p0; var p1; pos[0] = absoluteToRegion(pos[0]); for (var p = 0; p < rawPoints.length; p++) { p1 = (rawPoints[p + 1]) ? rawPoints[p + 1][0] : 1; if (pos[0] != rawPoints[p][0]) { p0 = rawPoints[p][0]; } else if (rawPoints[p + 1] && pos[0] == rawPoints[p + 1][0]) { continue; } if (pos[0] > p0 && pos[0] < p1) { index = p; break; } } } return {pos: pos, index: index}; }).on("start", function(event) { selected = event.subject.pos; redraw(); }).on("drag", function(event) { if (!event.subject) return; if (!event.subject.hasOwnProperty("index")) return; self.changed = true; var val; var pos; if (event.subject.index != 0 && event.subject.index != rawPoints.length - 1) { if (rawPoints[event.subject.index - 1] && event.x < regionToAbsolute(rawPoints[event.subject.index - 1][0]) * self.width) { pos = rawPoints[event.subject.index - 1][0] + 0.00001; } else if (rawPoints[event.subject.index - 1] && event.x > regionToAbsolute(rawPoints[event.subject.index + 1][0]) * self.width) { pos = rawPoints[event.subject.index + 1][0] - 0.00001; } else { pos = absoluteToRegion(event.x / self.width); } pos = Math.max(0, Math.min(1, pos)); event.subject.pos[0] = pos; } if (yStep) { val = Math.round(event.y / yStep) * yStep; } else { val = event.y; } val = 1 - (val / self.height); event.subject.pos[1] = Math.max(0, Math.min(1, val)); rawPoints[event.subject.index][0] = event.subject.pos[0]; rawPoints[event.subject.index][1] = event.subject.pos[1]; redraw(); }) ); svg.node().focus(); } d3.select(window).on("keydown", function(event){ if (!selected) return; switch (event.key) { case "Backspace": case "Delete": { event.preventDefault(); var i = rawPoints.indexOf(selected); if (i != 0 && i != points.length - 1) { rawPoints.splice(i, 1); selected = rawPoints.length ? rawPoints[i > 0 ? i - 1 : 0] : null; self.changed = true; redraw(); } } } }); build() self.setRange(0, 1); };