From 9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 13 Apr 2025 18:48:02 +0100 Subject: initial --- site/app/base/spline-edit.js | 453 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) create mode 100644 site/app/base/spline-edit.js (limited to 'site/app/base/spline-edit.js') diff --git a/site/app/base/spline-edit.js b/site/app/base/spline-edit.js new file mode 100644 index 0000000..06faa7e --- /dev/null +++ b/site/app/base/spline-edit.js @@ -0,0 +1,453 @@ +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); +}; -- cgit v1.2.3