Smooth Parametric Bezier Curve in a Script Panel
-
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 !
-
@sinewavekid The code needs a bit of work to correct ChatGPT's output but the end result looks very nice!
-
WOW! this looks amazing.
-