hi magical HISE friends! here's the code i got so far for this smooth parametric bezier curve in a script panel. i currently don't have a use for it (yet) but wanted to share it because it seems useful, especially with a few custom tweaks
the main features:
- parametric
- adjustable amount of equally spaced points/circles (locked in their x positions)
- the points and line are always centered to your script panel resolution
- the padding between the panel's width and height is easily adjustable (as a ratio rather than pixel values) (the padding border is drawn as the inset rectangle)
- super smooth bezier path, different than "quadraticTo" . based on the code i found here
Smooth Bézier Spline Through Prescribed Points
-
double click to reset values to starting position (.5 in this case)
-
save in preset works for me (each point automatically links to hidden sliders in this example)
hise snippet
HiseSnippet 3684.3oc6aztaabb7niYZrZcaBPe.15BjbLhhhTVJI0Ltw1z1oFN1Vvzw0DBBAKuaI4Ve7tC2GRjwP+ouM8sn.8EoOB8e8msyreb2tGORKo3XfBTAaQt6NyryNesyN6pCSh7XooQINMt1KVFybZ7qZNbYX1rAyn7PmGcemF+9lCmGEkMibHMgNmkkv8H2i8ibVBYPdxILxI8bt2xXZZJy2oQiO3aQDabsq5H94e8M2iFPC8Xkc437xHtG663y4Yk8d3cdLOH3gTe1K3yMfd+67HunvAQAQ4.S9AM65DS8dMcJ6oTDrqzzowG9.edVTxvLZFKEf4dQ9KGNK5zPI7ujmxGGvvF8bFBDR1syfY7.+C0BfTGmFW8vRwwGHEG+1lOg6yK5uTr7whAHkXXJCZbkMwR8t.rTCCV5pRV5SZNzKgGmUNBxO+xlOJLikLgBhZSVQBqyU92eVyAQ.DgYclSeM6gIPiBLb+htcaStY2ts5e8st9V6tqFTpuOvq4zfuMm6ybOBfp2AcOtMo6h8u6CE+fnrdDtIR3UQ35aAJ0zLxIzDvtJjEzibahlFSYYChlGGEBMbugb3aHwBgOLe9.dhW.KEv4K5SH6tKYTTNwiFR7lQCmxHYy3oHbiAizrHBOzKgQSYjnDhOS88rYLMHQSHdRJJmgAITedNR9d6CzWLCOW1E.JhnE3or.lWFOJ74ZzzD3yI857U8MYPp+eIGV2HMlmGjwiCPGIJvsLlOyWuF4ggrD4pTRzGR8.6Ifzc6z6OHnnpGX44y.03bdnbMkx+QllMEjwlY8SnSmxCm9nPe1Bfd6zSPsWDQxR.GKxoy3dyTnP.o3XF.rDKI6Up3dB3KbHnvA.dEJqNPPoTv+DQYLK6TFKjDi5Oxob+rY8I85h.tK7Avgh9VCAGgv0cCDbFiOcVFo2dFDT1mVFZvZlbpfl97S.WvnPxDoTbB7+WATV.hE5irQezZQeTI5JqnKJCnspqvHCtnbhlNVbD.6iB4YbZ.ZeHrgA2sjn.PxGjCPSSRnKM0Fpweob3aSN5XvEDouqvBEsE6Ce70FNjP6s2tE4MWeKzqwh.chySm4ZDl3wgQicugIL+vMHaS3PnB3esP+8UHxQ7i6jxffBmHCfdi1v5NHksIfeNFQvsaGLzE9K3a81Lw8YSnfuon2afHbvlgOkdB6QgGlvfV.7YI4ajgDMbUT8Lkt4twwAKIoABiZP9xoX.kTz8VDqfGxmmOmPC8IyoKDeejRyYpy.3d1jIvjHhTzsa2dx3iOQgejXPKLnKrwXOEFpYQiAn5yCEQ4HTjWG8xBlzUvGsgnMPHkBsOR7hEBP7RVaahq.TH7na4zuSIDsH6RbMByuCoWg8.RVYPTlOrp.BWLI0AAEiw0CnfITR3Jj5kbYTHzTIxmvS.QjLPnDA9DMie6aCBqhUpbRAQ.Yayk4tj8DtnCeM6TCJJgTpoCVJovYZdJgkkmDBB+rYv1zKbkegGpEwFqqVsMkCl1ROTqnvs9lGGvlCtbBN3EyhlC60PClFkvylMWDtHMJ3DL9JjYmOmNMJjFPRWBzcdpgROSfpKsMYbahWahukhF0CzNArvoPT9xt8hkwMPwfGINAxrqbPeiA8qN3B0XhM.hBxQdPKjpMFDZjXG9oPkUi5R3aFKiI4A9kfxZL7g1HS+iuBD+5A4LBCB97VnqKhEvcT7iOGF8HNxqG2ZcSlb1JPvu.gyKwNqvnB+XwOIphxeo.THgsTAn3G6cOTE7Gkphc1wRNu.o3sEyFNAdwxY.5FhBz6390a9uvvXdHjgDjYyDCaZOZfWd.jYcwVXwQ7vrTAWI1YKeLdHk+weSbJEO7TJFVxdPtk4YrARbOTfp6isLmi6o2uqnm8p1CZx+XkIuv3SNTQrkmiN36LCiXmBoBSNggaPmZDiRPQveR8om5yD8LUPpuiMIam4QPviT1TzYVNB8ntGK7.jMGKatmdqGYSMekHa9X7isI6AJgGeTOqYQbZ.gyubRRqwaq2F81nRscOCawwxt12nKuUgJQAkfq3kLX8VIEB25DIBVyTNLtrqurPzTzU2BwSQWekXpUM2F+pkXBNtFbn26t31iI4oXbypAU2fXaUQlXKXL3YQDFw7tpDT4hNG8OWEHk.LoDnDafJDdw8JWoEK5cKjR8qg6q3iuyNbqk.PQwj6plcOoON1sP80RE4rF03.oqH5coyk4BFfOdO4jaZu.PVL48shEBPWp46b.l.BpdAHKDKs5WIXzafwtE7+1.12B4zyVyVs34Jg0BkjJqWRLr2MbFG3W0DOBxraFMifmSG9VRT9zYDFs3jWeVJwig9ilwsDzWVLlCAZ6F2VE3qPh.bzCVfmiKCNDAF4ANBPTJWlJYJKll.TPmzgXaVY3OPbHoDjzQrqdFciABqECwcVzmblYVXKu.3tTgaoleSwvWH3cC1zyLVMddpZigqVLl7nEhiVGhKKQrLlaTTrslQEnQvav4gk4zgJYksUmzLZR1SYmNLerP+n3mivxerr36s5udibkfzXSkZCX3E2SJDLkJcD9g8WAxQUgbzZfbu5n4d0BYczrFHYg9hgQBqEFVtkUADo6xp.VBJnVtuVvWiOkZ+edp8dBRki.9WD4dDJ7ZKDLfZ4HbY2VrjfVE7a4WG0pLBhvoWVNJ7bbGBmEO64QPpogrR69oEZqoclvCBtaPfqr5kocFGP8dskU12xxTlQXMMvR2XjxA12eFqQBHUvkEVXLQaWSSbAX+IY0PJgS1g6Z74T0EfLllx7Ihr6YUKkPp4TTTHCClZ2xJbzeEXGogUwYk.OpdVB4frnLH8CeN3HE5gMIiw3CmvR.tb7RY4sTUCxksvKHWrJvtwBtwEtl50FUJYAEYZqR1SLECUzvZ4rCwE2DQuVao6PUKuVqmsqVfJg4oH58p0KTyGCRK3AKVZ0C9ZcdUrFMnbIETPLqn6ns+JyhriQGV.U0LKsWHlzylwKsTpT0Hw9ak0qXC6eulhDU8D6hpifIDUsHCqTJko5Ro.mDl2pRDkAREfzLTYQtcQcY2l35ZZctCwP0OR0Tq3guZwblSUUArpJWpIuUMo7TQEVJiMj8tpMuaUUSthh7xJsUb3qJEOuxV7vQAfxFcCx1pqe68.pvzRgyaHKtUw7CaLdqR5cVcBrgpPjdPPzjhi3Utu6TLVrLBaQf1krffnSml.di19s5TzBYmJyOCky.OhFfgKIwIrS3QfDvSVmRivZF2OgLQLwV7ZFF1eAIhacylHDgHqMlubRkmantrBUmOolL8pHKsln6uZ1HS6fonTjlH5rbunbHVnaOvcoW8nqb1aqhdArqv8VGQcRRzbicqX9SYWRGdic9pX7WNIp5QuFq1JhCLdvh2lYpEzKWalEmNiCZsTHXyqwPd4YXAHJA8bXuUBJpDdPP.ONk4dTA6ui1SqcISZ1WwsGsmUiisTbUY6z4zf.lnbdbe0hPeMNgXEHrkNxaXJY0arZMW8j0xBSrotkkIQsVa1CXM2hUY0dNttHA1aVMVXNWDPHg4AYMLMvHAJrKyHb8sGxbuAYgZw72sIVo4XFbbfhqSwjL5LzrRkvLShJSaQpZqeCH4MxYwGPnX71.vpHmGTb6Wn6IOTu2c4k8T0Ns6hCdP4kuV0xobVnInP0LVvmIkyL7nj31AZqHoo8yALcORHmaKkosKkIsMVt3E.2YeQQp0WwL4t99j4PvVFVPufw3kOBaKNGR2qHqCHyuLNN5RyDueBhz.ENkYdyNAL2rKimYFzJ7OG4PWBoQVz+rkKbMYD5dARKs0lRYDK+sPrzwOJeb.aP.GN.RkHwClw.QOWlumDtc7P.wq+0vBCulY0E.A6OgIHhaYZdAIWlsB9ImQxOOY7sgrMsCAqEyhapgNNUIuWfAzUKoVju91q7HA9zOkTAkkknLpNTVQroqEJduipX.VYmqygWRGvEVscf5BtTWyIQbUjqR425UWVEAkaSBKFOXrq7tCdNynTIXVCvjmG6qChWTsK89+qRVYMj5KqsEOiPm.BHh3xVyDm9KhPOIB1xaRdBPRHQNOYJxByVvDVGcwlzmU28kT0qQ3Fv72nGy+2U4+QbUp9xW3Fa5qGSYwTQQUTJdPY959WH6H6I82gu2lpVSeeo6v42AdRDlwo7sAfaGZmbGb7F84nW8pjMjgFJ0BE5tjyu1GSHEerFlm4yxZvZ4aDBQyflX9NL9QMtxBoz2GaI7WyagZHlqWgEwoyTkzQlqBXdj.pCrbHkEFrU+hpzGE9znL1yBcas0a15Zac1VjpCMYRsioJIMjBesCiuJxjMgnq7wr0VZ5T.nSiqZ+VD+vy2aQTcBXC.iBwGPzyhYp1OLJvGeig32W8kK5nNsrCLiJND.MS7BF+0pWv3P3jJrDGtO9bJsdIPccDqhxmA5+7abt3To26Dpr26Dpby2ITY+2IT4fZnx2+n6Syn3KNUo3.kYLl9MZyz39rS3dL46O8ZMuOK80fOhSieQw6zD5VN6WW+5TQuUwj+QMkd1NKbZzrANcK0eQ7B.KXi+9e8Nxy0T1SxcFGk.qBYXcy2Prr+g7ez5kESwnh5iF.v+aZdW3zRkc3Xvve342XbUN+DsWiro4SFqhx4hOe8dOOe68dd9t4644a+2yy2A+DluUet2ebymD4ik8v90miOyd0.v1.VO4a7gbDBaGtz7Y3eAdR5c23SR+7xheRyC4YdypmGuRM7HD64mCdT8P9udyGLYBjEUICd0lO7UW1Ws+aY5k2J3THkqDNZK7z74CixS7XvrGBwBSQ6fqfaRJa2UaWLjE5KZ7efeTC1Ca2PMXO8fNyodIQ+fxvC+SE3iD8.7Tn3uLhq07IXaxJaB53Lm6y+AOOaRsBh6cYQ7lWVD2+xh3AWVD+hKKhe4kEwu5siH9GVxcyyhlKcabbdxgOPr4biFOHjBVfBqUm+KfEn2uX
the code
Content.makeFrontInterface(600, 300);
//Content.addVisualGuide([0, 150], 0x4AFFFFFF);
//Content.addVisualGuide([300, 0], 0x4AFFFFFF);
const var Panel1 = Content.getComponent("Panel1");
var numCircles = 6; // You can change this number to increase or decrease the number of circles
var Cradius = 14; // Radius of the circles
var selectionRadius = Cradius * 1.8; // You can adjust the multiplier as needed
var innerCircleRadiusFactor = 0.19; // Factor to determine the size of the inner circles
var draggingIndex = -1; // To track which circle is being dragged
const var MainPaddingX = 15; // spacing between panel width; 10 = 1/10 of width
const var MainPaddingY = 10; // spacing between panel height 12 = 1/10 of height
var PaddingX = MainPaddingX; // division factor for X padding
var PaddingY = MainPaddingY; // division factor for Y padding
var CPaddingX = MainPaddingX; // division factor for Circles X padding
var CPaddingY = MainPaddingY; // division factor for Circles Y padding
// Initialize the control values array
const var controlValues = [];
for (var i = 0; i < numCircles; i++) {
controlValues.push(Content.addKnob("controlValue_" + i, 0, 0));
controlValues[i].set("visible", false);
controlValues[i].setRange(0.0, 1.0, 0.01);
controlValues[i].set("defaultValue", 0.5);
controlValues[i].set("saveInPreset", true);
controlValues[i].setValue(0.5);
}
// Apply slight variations to the minimum and maximum Y values
const var minOffset = 0.0001; // Minimum offset
const var maxOffset = 0.0002; // Maximum offset
function applyYVariations(value, index) {
var variation = minOffset + (index * (maxOffset - minOffset) / (numCircles - 1));
var adjustedMin = variation;
var adjustedMax = 1 - variation;
// Apply variation only to the first circle
if (index === 0) {
value += minOffset / 2; // Skew the first value slightly
}
return Math.max(Math.min(value, adjustedMax), adjustedMin);
}
// Function to implement the Thomas algorithm for solving tridiagonal systems
function thomas(a, b, c, d) {
var n = a.length;
var cp = []; // c prime
var dp = []; // d prime
var x = []; // solution
for (var i = 0; i < n - 1; i++) {
if (i === 0) {
cp.push(c[i] / b[i]);
dp.push(d[i] / b[i]);
} else {
cp.push(c[i] / (b[i] - a[i] * cp[i - 1]));
dp.push((d[i] - a[i] * dp[i - 1]) / (b[i] - a[i] * cp[i - 1]));
}
}
x.push((d[i] - a[i] * dp[i - 1]) / (b[i] - a[i] * cp[i - 1])); // i === n - 1
for (i = n - 2; i >= 0; i--) {
x[i] = dp[i] - cp[i] * x[i + 1];
}
return x;
}
// Spline function to calculate control points for the cubic Bézier curve
function computeControlPoints(K) {
var p1 = [];
var p2 = [];
var n = K.length - 1;
// Right-hand side vectors
var a = [], b = [], c = [], r = [];
// Left-most segment
a[0] = 0;
b[0] = 2;
c[0] = 1;
r[0] = K[0] + 2 * K[1];
// Internal segments
for (var i = 1; i < n - 1; i++) {
a[i] = 1;
b[i] = 4;
c[i] = 1;
r[i] = 4 * K[i] + 2 * K[i + 1];
}
// Right-most segment
a[n - 1] = 2;
b[n - 1] = 7;
c[n - 1] = 0;
r[n - 1] = 8 * K[n - 1] + K[n];
// Solve Ax=b using Thomas algorithm
for (var i = 1; i < n; i++) {
var m = a[i] / b[i - 1];
b[i] = b[i] - m * c[i - 1];
r[i] = r[i] - m * r[i - 1];
}
p1[n - 1] = r[n - 1] / b[n - 1];
for (var i = n - 2; i >= 0; --i) {
p1[i] = (r[i] - c[i] * p1[i + 1]) / b[i];
}
// Compute p2 values
for (var i = 0; i < n - 1; i++) {
p2[i] = 2 * K[i + 1] - p1[i + 1];
}
p2[n - 1] = 0.5 * (K[n] + p1[n - 1]);
return { p1: p1, p2: p2 };
}
// Function to create a smooth path with cubic Bézier curves that pass through each circle's center
function createSmoothPath(p, points) {
// Extract X and Y positions separately
var xPoints = points.map(function(p) { return p.x; });
var yPoints = points.map(function(p) { return p.y; });
// Calculate control points for x and y
var controlPointsX = computeControlPoints(xPoints);
var controlPointsY = computeControlPoints(yPoints);
// Loop through each segment and draw the path
p.startNewSubPath(xPoints[0], yPoints[0]);
for (var i = 0; i < points.length - 1; i++) {
var cp1X = controlPointsX.p1[i];
var cp1Y = controlPointsY.p1[i];
var cp2X = controlPointsX.p2[i];
var cp2Y = controlPointsY.p2[i];
var endPointX = xPoints[i + 1];
var endPointY = yPoints[i + 1];
// Draw the cubic Bézier curve for this segment
p.cubicTo([cp1X, cp1Y], [cp2X, cp2Y], endPointX, endPointY);
}
}
Panel1.setPaintRoutine(function(g) {
g.fillAll(Colours.black);
// Get the panel size
var panelWidth = this.getWidth();
var panelHeight = this.getHeight();
// Calculate padding based on the division factors
var paddingX = panelWidth / CPaddingX;
var paddingY = panelHeight / CPaddingY;
// Calculate the total distance to be covered by the spacing (excluding the radii and padding at the ends)
var totalSpacing = panelWidth - (2 * paddingX) - (2 * Cradius);
// Calculate the spacing between the centers of the circles
var Cspacing = totalSpacing / (numCircles - 1);
// Array to store Y positions for circles
var circleYPositions = [];
// Calculate the Y positions of the circles based on control values with variations
for (var i = 0; i < numCircles; i++) {
var adjustedValue = applyYVariations(controlValues[i].getValue(), i);
var CcenterY = paddingY + Cradius + ((panelHeight - 2 * paddingY - 2 * Cradius) * adjustedValue);
circleYPositions.push(CcenterY);
}
// Array to store the circle positions (X and Y)
var circlePositions = [];
for (var i = 0; i < numCircles; i++) {
var CcenterX = paddingX + Cradius + (i * Cspacing);
var CcenterY = circleYPositions[i];
circlePositions.push({ x: CcenterX, y: CcenterY });
}
// Set the color for the path
g.setColour(Colours.yellowgreen);
// Create a new path and clear any previous content
var p = Content.createPath();
p.clear();
// Create the smoothed path using cubic Bézier curves
createSmoothPath(p, circlePositions);
// Draw the path
g.drawPath(p, p.getBounds(1), 1);
// Draw the circles, centered with padding from the panel edges
for (var i = 0; i < numCircles; i++) {
// Get the circle position from the array
var CcenterX = circlePositions[i].x;
var CcenterY = circlePositions[i].y;
// Draw the white stroked outline
g.setColour(Colours.yellowgreen);
g.drawEllipse([CcenterX - Cradius, CcenterY - Cradius, Cradius * 2, Cradius * 2], 1);
// Draw the smaller solid white circle inside
var innerCradius = Cradius * innerCircleRadiusFactor;
g.fillEllipse([CcenterX - innerCradius, CcenterY - innerCradius, innerCradius * 2, innerCradius * 2]);
}
// Calculate the bounds for the rectangle
var rectX = paddingX;
var rectY = paddingY; // Start the rectangle from the top padding
var rectWidth = panelWidth - 2 * paddingX;
var rectHeight = panelHeight - 2 * paddingY; // The rectangle spans the full height within the Y padding
g.setColour(0x5EFFFFFF);
// Draw the rectangle around the circles' bounded Y space
g.drawRect([rectX, rectY, rectWidth, rectHeight], 0.4);
});
// Add mouse callback to move circles vertically
Panel1.setMouseCallback(function(event) {
var panelWidth = Panel1.getWidth();
var panelHeight = Panel1.getHeight();
var paddingX = panelWidth / CPaddingX;
var paddingY = panelHeight / CPaddingY;
var Cspacing = (panelWidth - (2 * paddingX) - (2 * Cradius)) / (numCircles - 1);
if (event.doubleClick) {
// Check if the double-click is within the detection area of any circle
for (var i = 0; i < numCircles; i++) {
var CcenterX = paddingX + Cradius + (i * Cspacing);
var CcenterY = paddingY + Cradius + ((panelHeight - 2 * paddingY - 2 * Cradius) * controlValues[i].getValue());
if (Math.abs(event.x - CcenterX) <= selectionRadius && Math.abs(event.y - CcenterY) <= selectionRadius) {
// Reset the Y control value of the selected circle to the default (0.5)
controlValues[i].setValue(0.5);
Panel1.repaint(); // Redraw the panel to update the circle's position
return; // Exit after resetting to avoid further actions in this callback
}
}
}
if (event.clicked) {
// Check if the click is within the detection area of any circle
for (var i = 0; i < numCircles; i++) {
var CcenterX = paddingX + Cradius + (i * Cspacing);
var CcenterY = paddingY + Cradius + ((panelHeight - 2 * paddingY - 2 * Cradius) * controlValues[i].getValue());
if (Math.abs(event.x - CcenterX) <= selectionRadius && Math.abs(event.y - CcenterY) <= selectionRadius) {
draggingIndex = i; // Start dragging this circle
break;
}
}
}
if (draggingIndex != -1) {
// Update the Y control value of the selected circle to follow the mouse
var newValue = Math.max(Math.min((event.y - paddingY - Cradius) / (panelHeight - 2 * paddingY - 2 * Cradius), 1), 0);
controlValues[draggingIndex].setValue(newValue);
Panel1.repaint(); // Redraw the panel to update the circle's position
}
if (event.mouseUp) {
draggingIndex = -1; // Stop dragging when the mouse is released
}
});
i have been messing about with more interactive / animated hise panels, and collaborating with chatGPT , so please let me know if this code works for you and if have any suggestions to make this code even better or simpler please share it !