HISE Logo Forum
    • Categories
    • Register
    • Login

    Smooth Parametric Bezier Curve in a Script Panel

    Scheduled Pinned Locked Moved Presets / Scripts / Ideas
    3 Posts 3 Posters 368 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • S
      sinewavekid
      last edited by

      smooth bezier spline

      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:

      1. 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)
      1. super smooth bezier path, different than "quadraticTo" . based on the code i found here

      Smooth Bézier Spline Through Prescribed Points

      1. double click to reset values to starting position (.5 in this case)

      2. 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 ! ☮

      d.healeyD 1 Reply Last reply Reply Quote 11
      • d.healeyD
        d.healey @sinewavekid
        last edited by

        @sinewavekid The code needs a bit of work to correct ChatGPT's output but the end result looks very nice!

        Libre Wave - Freedom respecting instruments and effects
        My Patreon - HISE tutorials
        YouTube Channel - Public HISE tutorials

        1 Reply Last reply Reply Quote 0
        • M
          Mighty23
          last edited by

          WOW! this looks amazing.

          Free Party, Free Tekno & Free Software too

          1 Reply Last reply Reply Quote 0
          • d.healeyD d.healey referenced this topic on
          • First post
            Last post

          16

          Online

          1.7k

          Users

          11.8k

          Topics

          102.6k

          Posts