aboutsummaryrefslogtreecommitdiff
path: root/site/app/base/spline-edit.js
diff options
context:
space:
mode:
authorRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
committerRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
commit9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 (patch)
tree291bd79ce340e67affa755a8a6b4f6a83cce93ea /site/app/base/spline-edit.js
downloadapps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.gz
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.bz2
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.zip
initial
Diffstat (limited to 'site/app/base/spline-edit.js')
-rw-r--r--site/app/base/spline-edit.js453
1 files changed, 453 insertions, 0 deletions
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<br/>" + (Math.round(val[1] * 1000) / 1000);
+ if (name) {
+ label = name + "<br/>" + 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 = $("<div />");
+ var tb = $("<tbody />");
+ $("<table />").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);
+};