Circular XY pad
-
I’m working on creating a circular XY pad and I’ve run into some difficulties.
I’ve already taken the code from this post: https://forum.hise.audio/topic/9665/detect-if-two-paths-intersect/6. However, I’m unsure how to map the mouse pointer’s position within the circle to these knob values:
- Upper Left of Circle: x=0, y=100
- Upper Right of Circle: x=100, y=100
- Lower Left of Circle: x=0, y=0
- Lower Right of Circle: x=100, y=0
The only example of this behavior I can find is in blepfx's destruqtor plugin. Heres a demonstration:
https://www.youtube.com/watch?v=MqpRXpoaBHU.This is my preliminary code which does not have the desired behavior. In the corners of the circle the values cannot reach 0 or 100:
HiseSnippet 1904.3oc4X8zbaabEGTRHwD0IsdlbNyN5RAsgjHHojsGlL5+JkShb3Xk3ZOpbbAAVRti.wxAXokPq0wdteExmfbJe.7L4bldnG6o7MnW5jisu2t.DKjnjUzDmKURPj69du88a+su8s6Cci49zjDdrQEquJcB0nxcMOJMRLZ2QdrHiN6YT42XteXHaRB84uvXmzIdIIz.iJUV7yPEpTcIC4O+6M2wKzKxmVzkgwy3Le5WvFyDE81cqOmEFdfW.8qXi0zt0Vc74Q6xC4SAvrnYciId9m3Mj9DOTsELM9CdIiLpbeyG0p0iBFz2Mf1Ln458WuEs4Ca9n9ObiMZNXi5TuVq2pu2Ca7XiJu29ALAO9HgmflXTYoc3AoGMheZjxAOikv5GRwFtFGAdV08A7v.bJh8Zr6HVXP2bVJw.FktEb1hJN6iLOjEvl0eA286jBHEVnSfUVnL7VrD7b0gWcM3MGHUQCRKofz8LOxOlMQTHQsV1IRPiG3AqS5PQoqwBKeWyc4fFQhUG6cB8fXnwLKr2ndcGB7uZssrVaMxIQ8edpErnkHTem7oji6AxFviI1LnU81DF4SHMfOdvCpY8WspJ0a0ISSFYm6ngTwt7wS3QPC6kkJrL4ADVMvMJ8Ol0a0DTqHQLObWuvv9PngMORJMqaP6ykvpqWDMLCVuxKlLIJTBs46OozkAiuVwy28RgEtG78gc1qCIqmPZbRFLFyCRPxwppbuE5i8GLf5CNX2Q73oItKWy4JE1X4ZV8TTtziVVxOtQXBMZ2XJDfQd9KHS7BbHft7SoADOnanUT.weZLDfnQYmk10SLRiy7kCA1oMLnEJlMV2PsU94JUDw5YoRbAHULpDf1F6Env0cV2oQy53CvIJftpWPPVNJakl4ybkCUCl765pdrq65N3SCX3p2qVavnKB1L2lYLrzrCeZTPhs6p0qIcw1Zr4EQcF6jCcoaf+jO8rz3NcToYT1r3P9zDJYDrRExhFVr3K6e1R+foQ9BFOxl9JfZka1XCHpVqFD6Mj75WSTs7CY9mPCpYUETRpkNV.7Kf7WI1Gqz9LG0mo8pAVHMopXDKY0.Og2prjNQIr.JLAEwSo.hKI9Ln+rgocYIoyjj1tJH5b3gFlPuVWLvCzPNP3CvMCXP7qXDkvvrTITIEPlvglj9TwoTZDwmhxH7AkB8kQ9ikTaV7OLh3pVoQ5S0Cww0+NZRsOVaw53F8VqgidGMgN54bIVzQRT0lMKJSV5d+XLD+RjVIMbkZbNx5Rs7gvjgz.X6DL1macNFBwhf3FJIO.gTNuosed1NGHnMDPFF5j+60EdXqRNWuGxKOCM0t18gP6ZO.ByWw891YJ3dIEVoQi50TgSpE6ppf54GQU8hZf7z0471Wz.j1d6.RZFFFlGGdsXqHTTgue4CFqFSGVZnZ7tNbbtL87hHuH4NmXRjIsloodboEdPMlXauXuSwb.wohRI15BwWhmxmB8RKxqMDBLgQDSq6E5OMDOSCosD3JTTIsqnteeh7nNdBSR+VxM03DQei1JZ42kTUakdokzKsrdMU5IwP.hczo4mXAvE2lYUcn5fY7dr1xgBVlrWlIniUctr7hMCwbxmV9TKGRSb+ZogWdZRLPH4QJy7yZqUdLzVqcbqcIbpN9eV30aCkMxg4.315xSnUV5b7YNoNkHOmRTTO7hX3zPtD+T5DbwThfI3EyfrO3bBy7LmDS52dY9IlB49dgxkyYhyVSamKL8xBSkzAdMLLiALy2VHhY8mJn1vUZsOaEX6+ZMj2rUok6bzxFRRrRpRO45zrTPI4YSJORyx1TH+RiQAPiUTEdSnyaaoQJOgKneYjsJu74VjKJZvf4Jq3ZnyULV8U70Ynczzw8owkNU.TDpvnbYKlWcYK5UU4qtymlh7nNQLwWNgFcU0ZYjcQQnDmExPEnpPVfyGlUfyQgP93XCFTGycLkLdcCIhwRK+9+i8e5e72+tMMt4V6VX8O9W9nu8mdwOrowW2YOHJBKzJCP.HmPiELb9WYO5qfxbUkcU0bOZxIB9DvSyJi.psV40OHunLbmfzouuoLj23L8xlS0abJKPLpninsFQYCGIz6QtyO+hf.h9slvsRIEczeXdY020rUiG25wa.UGutQwl8qTPiKHwPaNszaecPaV8hszlUhs9YMPtW0.8lS2x3xk9BEcyCvyGJWIN99JxD.w4kJ+EOEENWWjp+9L9Eq77aJDumYWlvez7w3ByAi31h2AXL6kZ7AlpxPK.3RlG772QuAiEz7+6q7+cLUk+Jcc0rFtk7+SA2lQG+Yba9eTauxa93+1+ZyCnz.bOPVea0r4+by8ngdkWnW7FOKbudRTcmkgG5AGa.grlOY53ifcQ9TfCifs7X1xJKf40TsqisQPbDMJP13+B+jIzEaWISnatPiaHQ03pHpuAyn8+CD0uF9XrmeL+kY2mAiiuirGXdGIeqkUMODaSzNTIeq6XV.6k99kGpKYXiaqgMusF151Z352VC231Z3CusF9n2tg36sc6oB9XUlXCiC6tu7R.UprejGDkK2WY7+3BnViJ
My first thought is to calculate the distance from the point to the left side of the circle. I got this somewhat working however the values get wonky when you're outside the square
The main changes were made in the onpnlXyControl(), and I'm only doing the x values for now
HiseSnippet 1858.3oc4X07aaajEmx1rMhaa2VfdtXfurjancDojbRfp2nD+QWgVmJDmM0AFFETjijFXJNpjihLwVea6o9uzdn+Ozi8Ohs.85Br66MCk3P+U7ZztWpfosl22yu2ad7MteJOjlkwSMpY8x7oTiZum4g4Ihw6LNfkXzaWiZefYBc9dwwroYzidswyxmFjkQiLpUa0OCkoV80Lje94m7rf3fjPZIICiWwYgzufMgIJo1u6myhi2OHh9R1DMoa0sWHOYGdLeFDOqZ1vXZP3oAinOO.EaESi+ZP1XiZ+YyG0p0ihFNvKh1LpY6AsaQa9vlOZvC2ZqlC2pAMnU6VCBdn+iMp8N6EwD7zCEABZlQs0dFOJ+vw74IJG7JVFaPLEW3YbH3YE484wQ3VDoZryXVbT+E.UlAXk9kv1pJX6iMOfEwVRuD99PICRoF5.XsUpFdqVI77zCuFZg2UDR0zBo0TgzGYdXXJapnjCFO+AydIBZ5v.HOoGJJYMV4eXYtCGjHQr4jfSo6mBKVpg8VMZ3Rfe4zwxxBRVYBxaBRImlL3n7FjsIKTcDUrCexTdBrvdcE60AktfJd2rJdqK8C74AOfzOHgFqYfoIwGkes5K4hpeir2LColHR4w6DDGO.J2r4IRlEjAK.99fd61iTPIlllUDFS3QYPDbrUc4YFzG6MbHMDbvNi4oyxfMf60xzecGqS5H2aROZYI+ysJlPk1IkBUMjidMYZPjKAjkOmFQB.xvpjHR3rTHqqAYmk2OPLVCyBkl.IZWI4TXqaozJ+bsBhw5Y4x3BhTw3JAzSQp.D11ssqeyF3CfIp.cyfnnhFO1JIWryUNTYL420E8XOu1t3iOXtFm3zoi0kB1B2VnLjZdFeVRTls2lMbjt3oZn4Ei5BzYQnKcC7i74DKMrSOpzTpXWb.eVFkLFxTwrjQkIeI8ko9gyRBELdhM8M.z5X82spyFRTq1LJMXD4a+VhZUXLK7TZjiUcPHoT5wBD+BnoTl8wJoOyU827Sb.MjpTWLlksYTfHXSVVujLVDE1fhzYTHhqv9LfdgY5TkS9RN4cpCrNGdnwYzazECC.IjFBe.rYHCpeEioDF15IiJg.xTNrjLfJlSoIjPJxivGVozWV4OQBsE0+fEwrVEKssdINl+6ow09Xsj0w9m7.eWcBMABm3dITzUBTNK2EUAKcueLVheIPqhDdRINGQcoTgPYxHZDbbBr84VmWzWDPpcSCli.dZtnRUTeHWKdAeFPkVVDMBJf.KfmgBhCmEiMPP.KCdIDUB3JP6OkI6qvyXR3xRhf39PeWsg1gIIL0QIWdE4xqJWSkbxXHBiczoKZO.gKbV.pUFo5BhSBXKMEjhrWmInSTDW2AA5Q3Af4UaQ3RZhfSEyKO5lB.xhZDM+naAsrrqmiLJUB7Brs8wsa3B+30R9.4aeTDc+nZEurt6ssI7WrKFBiCI6Vpzz83ybycqfstUPPnm1xR.VBtSHKxvjpupvNbw67bgVWwP8I1.IlGFDKSmKYWjS6rfY9kYlqf0HVl.mxiH3jX5PAAOCuPskL2lb.dxJ6aRE11xuNkO21CGev2YikDr88arQtyFELbjoNryk8YNjOcahmeCGB12nHj2Er+y4oSfuucoytOw9LzDNa32nCd9XoI9K2rEfF1P040aGq5x4PfCrXp7UH.BlszDNOvWMKT8RrJkNEO5gu467hoWvyoRCU7RD42wWcfu9eH7lLaFrpQGBi7oDe3O2+9x7jTtMmNKar8MLkz5PbybT0hpvkcxUODgja4PDmqUz7btf9kI1R2ZctE4hrFN7J4UNSzUxFmuO8lTzNY1jAzT8hSTPXF1pCFad8CFqO2dnBkzDjmzKgI9xozjqaZdiBnEFhdkhnBDUHGg9CJFg9vXnFO0fASJeOS0zrFxHFu7hf+Ke++7e+udhwsWauRsk27o6Wz03u0aW3TFNJeQ.AA4TZpfg6+Z6ReCbQJ0f80M2klcpfOE7zxhA3BbJu99KF6GmWV5z20T1Rv3L8KlkquXNKRLtjPR2wT1nwBcJxdiKpkfH5OZBiHQJILXzhKt8dls7ebqGuEb+q1Fkc6tVF9Wfig1dZs2ddPaW85tZ6JQ2+mLj20YneXdWiKe4J3Zc7H78mUuqGdi3BFPcdkKXgSX.y6Hx0uw7uZW.71FhejYelHb7UGiqbEwHdr32fXr3Zyuuo5NQkA3Zl6ezuQ2QdEM++tJ+eOS0cwjttdwBuJ9+EfaKfitMa9iO4qzNq7Cex28SOYeJMBOCnKztz3fpI5Uu06BuaFDUyzMBdEZJCJYMe9rIGBmhBo.Fl.G4wtk0VA6qoV2.WiAwgzjH4h+C7ofoGttVASuELMtk.k+u6Ap+e3iIAgo7uNT8xErN9dRJv9NQ9+Eqt4A3ZxEeoB7YBKh80ggUM0kTz+tpXy6phstqJ19tp3V2UEe3cUwG81UD+OC9zYB9DUmXCiC5umbHfZ01KI.pxkmqL9uTOXTqC
Other ideas, fixes?
-
@mmprod not distance from the centre and angle?
-
@Lindon how would I use that?
-
@mmprod I had some fun working on your Circular XY pad (for a few hours
). The solution to your problem is that you need to define a transformation from Knobspace (x,y) to Circlespace (angle, distance) and back. The pixel coordinates of the XY cursor can't be transformed into the knob values in a linear way so I used some trigonometry to make it work.
HiseSnippet 2356.3oc4Y80TiibDWFv2sVIWt6pJOe0D+RjwFijMdWtkkk+SBUB6Qsr2dPQn1LHM1dJj03SRFrtb7X9NkGy2gT4o7ddNeC1z8LRVi.afPkKuDphBOS+ueS28zc6giBEtrnHQnQIy2kLjYT5mW93jf396zmxCLNXWiR+rx646yGFwN4TisSFRihXdFkJM+uAYnTkELj+7u1XapOMvkkukgw6EbW1umOfGmu6Qa963996S8XuiOPi6U17.WQvNBewH.LyW11XH08RZO1anHayU132Ri5aTZwxdNq3zcUW5yW0YkVttsV8Es95W3xnc6xdtSmWrxpcWoK00tkQoOYOOdrH73XZLKxnzBaK7RNtu35.kAdOOhegOCW3XbLXY016K78viHtqwN849dGk4khL.sbTtOadkO6WV9PtGex949tuPRfjKgtCrzbEg27EfmiN7r0f2TfTIMHsfBReY4icC4CiyonhkGDDyBA2Cq.TT7ZL2+7yKui.3HHt4.5kr8CgESjvpiscCRaa6ZqYZt7xjKCt3jDSHnEEq9LYcxYmulYWQHwhCKrWivIuhzB9S850L+SlUjrcF+bfXlc5wh2QLXnH.VXUUxPURcBGLxD1aFgLEDGJ72g56eAjXXIBjTS2F39FyTrbEMjbxoe3sas6Ae6wfkbZYq.7NgLvSCzHCodMHz.Oh6nPv2nI33jinw80vmqTHbSKvH5LtEPAOxcZzoQq113uvoWoflTOuzqMVJNqsFAfv3DBEEaHviNdU3X1lURVWom43zoA9aKvv1mWaM77oThT22V0ofMUQfSeawn.uHKmlxvYN6CC7O4zVyL.oHWEjQ8ooGZTzxiMn2mGviIw8YfeumOS5883fQiEjHw.VbedPuIZ0iFSapXDxiZ1YsJEnHEDhr1XjEU9aYCg6bJ8OjFv7Ih.hqx9l7.ed.izcTfaLG1+V3yxM630.7.9iXxTUegK0mLF8YYjU1drJPR3QDaD7.Hx3N4tbmH4NoH2lUFH7hNyVlXuUbbH+hQwLK3103kcv6WjTNbtKGIJNLytcnTx6QbakIMXx.QLIZTHiv6RRDiHWSAuiBADHGIGKJs3nokjbs3wFxfrDzYhd1KCDWP.fw.qmeNCU9dLO8FUn9PwnHFoODhAGeOsLEIgI4IYADK1Ufdjdc.spUM8Bo8H+3ORTqb84tWx7pYVAXpBlmFRu9DvcqHOdoIW3WKm9o.8I6ujhSU7nKbIBBJQ7dAni.q0AKoignzPAlGMZHnlTMklrcHdsd.OvB7NMjKh99vXKDGKJAScoMwOeZsZYvXRNrztpUfUbgaePYUhKj3b40bva0MTL.xpPLzDjk2Up4e0510vSbkL8HMLjXEfjOcYjmZfkkL+J6MHsaBcG670sddmWZKAwML+HVAUfB9ZahNuK2h7RxR5qkhp7AvMynhWHk+EOPHI73PUWbHWvhulwBHsO5.Pk3U7kvOUTKo9S7O2mNrWRlehxt7xvU0HgOq4vPLSCOr0q1nZcYPtNo5RultN10PBL4wVK10Zha6hHK4GbEQVJVmDmPFaeGFifvsFiYZS920WG8g.Fseobikk6hmH5UBNVb6JtGbbRH+.KTjJc6ToaOUoa+.R2ROMTtQCoXpdKYNR7lZfHb.0m+CLUQVbvrLWIQUESU.VVhBaIoKAMBqZ.UqulADnjnueDxAz6RVZsPvbbFjx8oKJQ1hM6.AjNp7daxqVGRte05pPZt3IYhm6omk3ISD+1oCjpiWuZccLUuZRgcRH0VqhV1rKTapGyyRt6Ml2f0SyqRcDVN6shQwPWi7hT8fBT.2XeVpu6HebTBzGFAivwjNcUy0ecjbBCQDW1rwTldgtIc.tjVa4yZct79FxWRA9RJxW6b9nE3SceTQIr.E4kLSErgJpWKwY1PHvIDaLZVompKNN5skTVnmuUUdLafZypxz+dXM4qKNUCLOnrWDndPERsisBUsY6l1N1iLBBu8HghXIETUxOyrVBzvYSpQ2XxmNu1cPsZrsrI1dPL2RBZjutv22PNHkRzF41CKYe2PQCHKIusws7+MJvbiBTQPKSkj8.OX2CHoSX3yBiRGwBaqiCMZVQ90rPTuG7EWbAruSeQ3nHmp0ZLShvgx7bUO1KXcw7NXHNP4vEbOzCiNhFjg9LJzQINLABFhKQBzXxr5BWaJyGoOZ8CMdzjwP5kM.AF6xGHZx.F5zMqnONGplwKJmlqv9n3Io6aVQwUKaYigzgvBw6wXQObuIyznjSmyjYvYFPehsogtziyZQe2NzIKOV1ad7zZLOoublbIOldx2TzA8zaHmcvKzVQNPy3Eg5mf6KQ9cJdvlJ4pYq6q8opp9Z57u880WdB+SYreUKP5XYKvsjs.2tl5zCkjciGkZ.Dfp.lKSFFsxB70TEWJ1FoJJibLh6XSXDiF+gXfj5bVWtpdpgQckJgdekaL0tP8FQL6aBrjWc.JjaSpa2oRKu9wTIiufR38InUvnAWvB0u3hLZTZghO8P4Y+zC5uLhq5qDpwnH3.3q08Mv2RXVuWhQ52izvnzbonBXMV9HE+hzGo3XeXVmPCtmQomUVVuv1PhX82Wx3wKsSAo+i+ku5uugw2dvtP3DerjT.AfbHKLlim+R6xth6xTOcRkx6xhtLVLDrzju4K3xdXLOdBd2bzlI4f+caZ7ehhbloh7JnnJJE8YYO0CNbVpdT4iZ5w305540adM2Kte9FAa1mw60OVeGnAg35r1DfS5yKukuOIei7dsfTehgwG+HJmVGX41ez3ip286NOQ0WT9PgGNGUwWLCeWwTBPtbgmoBeJp.XtpD87h+q8LZOVH9kkOhG61e5XbtofQL0+m.Ll93ieVY0LB4.bgx6exOQuz3bZ1+SU1+YkUylHMckzENEr+aAypGx9Nsju+5W8m+GarOi4gIUo6sY61+sM1k4SKFnm+QeJbtemnZ19dPejPNbEo7aFM3XHg0kA9v.3RDVQrzbXsK0ZabMBhiYAdxEeD9IknCttTJQmLhFORGUqY4njUs9+AG0+Krw.pan3CouGHlG+L4Nv4NP9eWnR4Cw0Dma21wvX.2i+AW2hp5NB15oJX6mpfq7TErySUvm+TE7EOUAW8gED++qr0nXw.UkXCiCOZOYi9Rk1KfBY4x6UF+a+In0rA
Content.makeFrontInterface(500, 300); // knbXy const knbXy = []; for (i = 0; i < 2; i++) { knbXy[i] = Content.getComponent("knbXy" + i); knbXy[i].setControlCallback(onknbXyControl); } const var XY_RADIUS = 120; // Create XY pad, and cursor const var xyPath = Content.createPath(); const var xyArea = [5,5,230,230]; xyPath.addEllipse(xyArea); // xy area path const var cursor = Content.createPath(); cursor.addEllipse([115,115,20,20]);// Cursor path const var cursorArea = cursor.getBounds(1.0); const var pnlXY2 = Content.getComponent("pnlXY2"); pnlXY2.setControlCallback(onpnlXY2Control); // init the angle and dist to something; pnlXY2.data.angle = 0.5; pnlXY2.data.dist = 100; // Repaint the panel on control inline function onpnlXY2Control(component, value) { local x = component.data.x; // x is 0 to 100 local y = component.data.y; // y is 0 to 100 mods[0].setAttribute(0, x/100); mods[1].setAttribute(0, y/100); knbXy[0].setValue(x/100); //not sure if you want 0 to 1 or 0 to 100 knbXy[1].setValue(y/100); //depends on the knob mode component.repaint(); }; // Mouse handling pnlXY2.setMouseCallback(function(event) { if (event.drag || event.clicked) { var rawX = event.x-XY_RADIUS; var rawY = XY_RADIUS-event.y; // flip y sign to make y axis point up var dist = Math.min(100,Math.sqrt(rawX*rawX + rawY*rawY)); var angle = 0; // angle is counter clockwise from x axis. if(rawX!=0){ angle = Math.atan(rawY/rawX) + (rawX<0? 3.14159265:0); }else{ angle = rawY>0 ? 3.14159265/2 : -3.14159265/2; } this.data.angle = angle; //this is a value between 3PI/2 and -PI/2 this.data.dist = dist; //this is a value between 0-100 //Console.print(rawX+","+rawY + "->a=" + angle); var dist2 = Math.abs(Math.cos(angle)); var dist3 = Math.abs(Math.sin(angle)); dist2 = dist2==0 ? 1000: dist/dist2; //avoid divide by zero dist3 = dist3==0 ? 1000: dist/dist3; //avoid divide by zero dist2 = Math.min(dist2,dist3); // this is the normalized distance // data.x and data.y are normalized as if it were a square xy panel this.data.x = Math.cos(angle)*dist2*.5 + 50; // 0 <= x <=100 this.data.y = Math.sin(angle)*dist2*.5 + 50; // 0 <= y <=100 //Console.print( "x="+this.data.x +"y="+this.data.y ); this.changed(); } }); pnlXY2.setPaintRoutine(function(g) { // Calculate and store the cursor's XY position var x = this.data.x - cursorArea[2]/2; var y = this.data.y - cursorArea[3]/2; var a = this.data.angle; var r = this.data.dist; // draw the xy area outline g.setColour(this.get("itemColour")); g.drawEllipse(xyArea, 3); // set the location of the pad using rotation g.rotate(-a, [XY_RADIUS,XY_RADIUS]); // draw the XY pad cursor g.setColour(this.get("itemColour2")); g.fillPath(cursor,[XY_RADIUS + r - cursorArea[2]/2, XY_RADIUS-cursorArea[3]/2,cursorArea[2],cursorArea[3]]); }); // MIDI Controllers const mods = [ Synth.getEffect("Chorus1"), Synth.getEffect("Chorus2") ]; // before understanding this, please try looking at pnlXY2.setMouseCallback() inline function onknbXyControl(component, value) { local x = knbXy[0].getValue(); local y = knbXy[1].getValue(); pnlXY2.data.x = x*100; pnlXY2.data.y = y*100; x = x*200-100; // x range is -100 to 100 y = y*200-100; // y range is -100 to 100 local angle = 0; // angle is counter clockwise from x axis. if(x!=0){ angle = Math.atan(y/x) + (x<0? 3.14159265:0); }else{ angle = y>0 ? 3.14159265/2 : -3.14159265/2; } pnlXY2.data.angle = angle; //this is a value between 3PI/2 and -PI/2 local dist2 = Math.sqrt(x*x + y*y);// this is the normalized distance local distA = Math.abs(Math.sin(angle))*dist2; local distB = Math.abs(Math.cos(angle))*dist2; pnlXY2.data.dist = Math.max(distA,distB); //the actual distance from center (0 to 100) //Console.print("dist=" + pnlXY2.data.dist + ",\t" +distA +",\t"+distB); pnlXY2.changed(); }
Feel free to ask for more details about how it works. I know the comments are not completely understandable.
-
@hujackus thanks!! Once I get access to my computer again I’ll test this. Conceptually, how is the distance and angle used to calculate the x and y knob values?
-
@mmprod said in Circular XY pad:
Conceptually, how is the distance and angle used to calculate the x and y knob values?
It's the only transformation that I could think of that I knew how to code. Here's a picture comparing a square XY pad to the circular one I made.
Basically, I calculate the angle the XY cursor pad makes with the center of the circle relative to the x-axis. Then I calculate the distance between that point from the center. The tricky part is that I multiply this distance by a scaling factor so that every point on the boundary of the circle corresponds to a point on the boundary on the square. Doing it this way allows a one-to-one mapping from XY pad to the knobs and vise versa. The angle relative to the center is also preserved with this transformation.
-
@hujackus said in Circular XY pad:
It's the only transformation that I could think of that I knew how to code.
Is there any interest in trying a different transformation? There are others out there that have different pros and cons.
Some equations on stack overflow
Schwarz-Christoffel transformation
squircular.blogspot.com<- I think this is the transformation I wrote from scratch. Not sure.
Square/Disc mappings Includes an interactive demo! -
@hujackus Just tested it, and it works perfectly! Thanks so much. I really appreciate the extra resources too—I still need to wrap my head around the math.
-
@mmprod said in Circular XY pad:
Just tested it, and it works perfectly! Thanks so much. I really appreciate the extra resources too—I still need to wrap my head around the math.
I've spent the day coding a more polished version of the XY pad for each of the different transformations. I downloaded the destruqtor plugin and determined that it uses the an elliptic transformation, so I'm excited to see how that looks.
-
An Assortment of Square and Circular XY Pads!
Ok here it is. I put five XY pads and some fun knobs into one snippet for you all to enjoy. I hope you find the code interesting. It should be possible to cut out the parts you need and put them in your projects. I decided not to use broadcasters to keep things simple, but I did have some difficulty linking the X and Y knobs to processors in the module tree because of this. Let me know what you all think.
// An Assortment of Square and Circular XY Pads // By Jack Huppert // // Helpfull Reading // Analytical Methods for Squaring the Disc by Chamberlain Fong // https://arxiv.org/pdf/1509.06344 // Elliptification of Rectangular Imagery By Chamberlain Fong // https://arxiv.org/pdf/1709.07875 // Square/Disc mappings by Marc B. Reynolds // https://marc-b-reynolds.github.io/math/2017/01/08/SquareDisc.html Content.makeFrontInterface(810, 250); // init X and Y knobs const knbX = Content.getComponent("knbX"); const knbY = Content.getComponent("knbY"); knbX.setControlCallback(onknbXYControl); knbY.setControlCallback(onknbXYControl); // button to enable updating all XY pads when one XY pad is changed const btnLink = Content.getComponent("btnLink"); reg curRadius = 8; const XY_MARGIN = 5; const XY_INSET = 5; // init XY pads const var pnlXY = []; for(i=0; i<5; i++){ pnlXY[i] = Content.getComponent("pnlXY"+i); pnlXY[i].setControlCallback(onpnlXYControl); pnlXY[i].setMouseCallback(pnlXYMouseCallback); pnlXY[i].setPaintRoutine(paintpnlXY); pnlXY[i].data.shape = i; pnlXY[i].data.u = 0; pnlXY[i].data.v = 0; pnlXY[i].data.x = 0; pnlXY[i].data.y = 0; } //just for fun knob to change the cursor size inline function onknbCursorSizeControl(component, value) { curRadius = value; for(i=0; i<5; i++){ pnlXY[i].repaint(); } }; Content.getComponent("knbCursorSize").setControlCallback(onknbCursorSizeControl); //just for fun knob to change the aspect ratio inline function onknbAspectRatioControl(component, value) { local h = Math.range(150*value,50,150); local w = Math.range(150*(1/value),50,150); for(i=0; i<5; i++){ pnlXY[i].set("height",h); pnlXY[i].set("width",w); pnlXY[i].repaint(); } }; Content.getComponent("knbAspectRatio").setControlCallback(onknbAspectRatioControl); // Update knobs and repaint the panel on control inline function onpnlXYControl(component, value) { local x = component.data.x; local y = component.data.y; knbX.setValue(x); knbY.setValue(y); }; // Mouse handling inline function pnlXYMouseCallback(event) { if (!event.drag && !event.clicked){ return; } // get the mouse position in the right coordnate system local b = this.getLocalBounds(0); local m = XY_MARGIN+XY_INSET+curRadius; this.data.u = (event.x-m)/(b[2]-2*m); this.data.v = (b[3]-event.y-m)/(b[3]-2*m); // clamp u and v to the bounds if(this.data.shape<=0||this.data.shape>4){//bounds = square this.data.u = Math.range(this.data.u,0,1); this.data.v = Math.range(this.data.v,0,1); // square needs no further calculations so just return this.data.x = this.data.u; this.data.y = this.data.v; if(btnLink.getValue()){ // squareToCircle(this.data.x,this.data.y); }else{ this.changed(); } return; }// else bounds = circle local uN = this.data.u-.5; //uN is -.5 to .5 if within the circle local vN = this.data.v-.5; //vN is -.5 to .5 if within the circle local dist = Math.sqrt(uN*uN + vN*vN); if(dist>.5){ this.data.u = uN *.5 / dist + .5; this.data.v = vN *.5 / dist + .5; dist=.5; }//dist is now 0 to .5 // now convert (u,v) --> (x,y) depending on the transformation if(this.data.shape==1){ // radial stretching (2017 Marc B. Reynolds) //[x,y] = sqrt(u^2 +v^2)/max(|u|,|v|) * [u,v] //we can cheat and use uN, vN, and dist again from above. this.data.x = (uN * dist/Math.max(Math.abs(uN),Math.abs(vN)+.00000001))+.5; this.data.y = (vN * dist/Math.max(Math.abs(uN),Math.abs(vN)+.00000001))+.5; }else if(this.data.shape==2){ // elliptic grid (2005 Philip Nowell) //x = .5*(sqrt(|2+u^2-v^2+sqrt(8)u|)-sqrt(|2+u^2-v^2-sqrt(8)u|)) //y = .5*(sqrt(|2-u^2+v^2+sqrt(8)v|)-sqrt(|2-u^2+v^2-sqrt(8)v|)) uN = this.data.u*2-1; //uN is now -1 to 1 vN = this.data.v*2-1; //vN is now -1 to 1 local SQRT8 = Math.sqrt(8); local temp1 = Math.abs(2 + (uN+SQRT8)*uN - vN*vN); local temp2 = Math.abs(2 + (uN-SQRT8)*uN - vN*vN); this.data.x = .25*(Math.sqrt(temp1) - Math.sqrt(temp2))+.5; temp1 = Math.abs(2 - uN*uN + (vN+SQRT8)*vN); temp2 = Math.abs(2 - uN*uN + (vN-SQRT8)*vN); this.data.y = .25*(Math.sqrt(temp1) - Math.sqrt(temp2))+.5; }else if(this.data.shape==3){ //FG-Squircle (2014 Chamberlain Fong) //x = sgn(uv)/(v*sqrt(2)*sqrt(u^2+v^2-sqrt((u^2+v^2)(u^2+v^2-4u^2v^2))) //y = sgn(uv)/(u*sqrt(2)*sqrt(u^2+v^2-sqrt((u^2+v^2)(u^2+v^2-4u^2v^2))) uN = this.data.u*2-1; //uN is now -1 to 1 vN = this.data.v*2-1; //vN is now -1 to 1 local temp1 = uN*uN + vN*vN; local temp2 = Math.sqrt(temp1*(temp1 - 4*uN*uN*vN*vN)); temp2 = Math.sign(uN*vN) * Math.sqrt(temp1 - temp2)/Math.sqrt(2); this.data.x = vN==0 ? this.data.u : .5*temp2/vN + .5; this.data.y = uN==0 ? this.data.v : .5*temp2/uN + .5; }else if(this.data.shape==4){ //Concentric (1997 Peter Shirley and Kenneth Chiu) //x = u^2>v^2 ? sgn(u)sqrt(u^2+v^2) : 4/PI*sqrt(u^2+v^2)atan(u/|v|) //y = u^2>v^2 ? 4/PI*sqrt(u^2+v^2)atan(v/|u|) : sgn(v)sqrt(u^2+v^2) uN = this.data.u*2-1; //uN is now -1 to 1 vN = this.data.v*2-1; //vN is now -1 to 1 local temp = Math.sqrt(uN*uN + vN*vN); if(uN*uN>vN*vN){ this.data.x = .5*temp*Math.sign(uN) + .5; this.data.y = 2/Math.PI*temp*Math.atan(vN/(Math.abs(uN)+.00000001)) + .5; }else{ this.data.x = 2/Math.PI*temp*Math.atan(uN/(Math.abs(vN)+.00000001)) + .5; this.data.y = .5*temp*Math.sign(vN) + .5; } } if(btnLink.getValue()){ squareToCircle(this.data.x,this.data.y); }else{ this.changed(); } return; } // on paint function for all XY pads inline function paintpnlXY(g) { g.fillAll(this.get("bgColour")); // Calculate and store the cursor's XY position local b = this.getLocalBounds(XY_MARGIN); local u = this.data.u * (b[2]-2*(curRadius+XY_INSET)) + XY_MARGIN + XY_INSET; local v = (1-this.data.v) * (b[3]-2*(curRadius+XY_INSET)) + XY_MARGIN + XY_INSET; // draw the xy area outline g.setColour(this.get("itemColour")); local bs = this.get("borderSize"); if(this.data.shape==0){ g.drawRect(b, bs); }else{ b = this.getLocalBounds(XY_MARGIN+bs/2); g.drawEllipse(b, bs); } // draw the XY pad cursor g.setColour(this.get("itemColour2")); g.fillEllipse([u,v,curRadius*2,curRadius*2]); } // update all the XY pads when the knobs are changed inline function onknbXYControl(component, value) { local x = knbX.getValue(); local y = knbY.getValue(); squareToCircle(x,y); } // helper function to convert (x,y) --> (u,v) for all XY Panels inline function squareToCircle(x,y){ //square pnlXY[0].data.u = x; pnlXY[0].data.v = y; //radial stretching inverse (2017 Marc B. Reynolds) local xN = (x*2-1); //xN is -1 to 1 local yN = (y*2-1); //yN is -1 to 1 local temp = Math.max(Math.abs(xN),Math.abs(yN)); local dist = Math.sqrt(xN*xN + yN*yN +.00000001); //dist is (0,sqrt(2)] pnlXY[1].data.u = .5*xN*temp/dist +.5; pnlXY[1].data.v = .5*yN*temp/dist +.5; //elliptic grid inverse (2005 Philip Nowell) //u = x*sqrt(1-.5*y^2) //v = y*sqrt(1-.5*x^2) //use xN and yN from above pnlXY[2].data.u = .5*(xN * Math.sqrt(1-yN*yN*.5))+.5; pnlXY[2].data.v = .5*(yN * Math.sqrt(1-xN*xN*.5))+.5; //FG-Squircle (2014 Chamberlain Fong) //u = x*sqrt(x^2+y^2-x^2*y^2)/sqrt(x^2+y^2) //v = y*sqrt(x^2+y^2-x^2*y^2)/sqrt(x^2+y^2) //use xN, yN, and dist from above temp = Math.sqrt(xN*xN + yN*yN - xN*xN*yN*yN)/dist; pnlXY[3].data.u = .5*xN*temp + .5; pnlXY[3].data.v = .5*yN*temp + .5; //Concentric (1997 Peter Shirley and Kenneth Chiu) //u = |x|>=|y| ? x*cos(PI/4*y/x) : y*sin(PI/4*x/y) //v = |x|>=|y| ? x*sin(PI/4*y/x) : y*cos(PI/4*x/y) //use xN and yN from above temp = Math.PI/4; if(Math.abs(xN)>=Math.abs(yN)){ pnlXY[4].data.u = .5*xN*Math.cos(temp*yN/xN) + .5; pnlXY[4].data.v = .5*xN*Math.sin(temp*yN/xN) + .5; }else{ pnlXY[4].data.u = .5*yN*Math.sin(temp*xN/yN) + .5; pnlXY[4].data.v = .5*yN*Math.cos(temp*xN/yN) + .5; } //set x and y as well for(i=0; i<5; i++){ pnlXY[i].data.x = x; pnlXY[i].data.y = y; pnlXY[i].changed(); } }
HiseSnippet 3686.3oc2ZszbabbDdgjfKQ32U4bMdBqTN6h2.DTjxzTlRTR1LVDFVT1EYoh10hcG.rQK1EdeQrQjw9fSk+F4d9Cji9OPNlS4RtjS4R9G3z8L6iYwtfuRbpJ1UhM2Y5tmt+5tmo6Ay.GaMpqqsiToJOKbFUpzqU9fPKuI6NQ0vRZuGhCrqgiluopygGI8fvYpttTcoRkt4GgTTZkaIw9m+0G9.USUKMZ5PRRegsgF8IFSM7RGcvNehgo4iU0oOyXp.081YOMaqcsMs8As4lkaKMSU6Epio8UQxtQYoOV0chTopk06zqynM0Tuylc50USq6laz8tanQUGMhdmNquQuMG0ajpV6tRkdkGoa3Y6bfmpG0Upzsdfsd3ASrOwhu.eggqwPSJ9QGoCfUlO7isM0QSDGUZ2IFl5ChgIWIPJCRAsaxAs2o79F5FIimBduEaBRJGh.XoajU8tYF0qin50VP8JPkJInR2hqRuc4CzbLl4kNCpOuZ48r7nN.7PynJbZktwe5cK2pE49Vj6ir3MkZ4QrGQN3q8UcnDUKcRbv.4viHCT0cq.z+fPxuF7UjO1e1LpiGLDN5GSMmMx2zj7Tpptg03JLIqZF5YnoZR1m5MwV2kLx1gKefDh2DJ4gFtZjggD.EmNj5XBXI4w1b9WYhm2L22uUKUm4FAMscF2Zl9nVcVu8ca19Nq0qGRziLMAiwXDrLdF1VnA7Tplmp0Xlhu2THnxID05qxRrAtDar4FqiDwAjVLUcp5rYft6h579pNZjGzDVuPKHJxMi.mBS1XXCmn4ZN1vah+vlF1vLdSZ0scmMZ0tSq1a1hKdT5Mm3M0jToxt1feyxq4T0WPerC7QheTdyNsqS5tdakspfKmgkgG4Plu5HxKrrG5VAxqb8f+d3gjsIwRZL0aW6oyrsfOjWEmbUPBIjdz4Q5QHoHKMcwor7brM2U0zbHDEHaagybTzvbBO5RQHp9C887.elmMgZoBQ+D+Y5faDhM.1vftYPPG4jITvuZQiFfX3Rzl.NXpdjELzy5IFVuXoFQz7ncTwgNln467THJ02E3XyXX3vi9p8u+S+n85CCttvf60+fG8L9XBPNW0VIhr.HRalk4gHN97i2pBDlKarc6sHFev5v+pVMkWVYEFAO233kplLBVslAnlIDWLTxlMEJyP8919tzDZYyjYnEoe.jO38Tae.1oxyvOXylgLvon1zch5LJn7F4lwGFsctQCJbz4ENZHezyPD923CHJtQwHeKVLMFevc3rsL.uGrcEw032RqXXYBpMRoFO6Gix1kQvAv7QPjrVLHWGbUl9TkJf+PLJfMJnUE53R0UGJCfjQv4rJmsUkklzjpCqprzzgbJJOs3h..U2YvNbDGbCuhQf6yn3oHAmKDXZiaNOA.f8gMkZ5fKgLr+ZUFI0Wuc8NrsZhH7j7DJ2oEWbBDeAfHfFxqNgZLdh2p0mfLrvbmXn6MY05mjcpKO1KX8mC3mGiPzmkh+43tPT99or8ViVaF5OS0hZBvLQiyUAd.wzyyC3wbgjoiROR.6v7SFBSFuQ7WfxRdtBeniRGJDF5rXKgk4SfPGcS7P4EU076NHSCfkioiFiHx+B1mM0cTGSdu2iD8olog1Kn5L2pC0y2wh4SprBrhf2fASSYq7LaWC1JAm4hi5fdcvrrczsPH1Mz0iNckXadHXydSLbQe5SvQdfsuktqrPL3TfjjspqEu+bsjjYfPlDR1XhaQMm2XpRK4gOu6wM5VcpRFxvcpfoV63FbZCincsDZYlllo5zYDeVDQ.lShVzPlFhnkbp.YaU9Aa29zSWXr60S4ksZw4AVTW1Q+.JlUkExwDlnNjfwRIxp3ERbPBwfdyWEhEkBKpkM39c.M2g.vIVcG5ebIt1D19Nb+YlUYdrWgqGYUgvLSFfSBPQzItnejGUpn7RBTo3JqrBWadlMVaoonNOutfTY59YTSWJFjwWunC8k4ykI1CrRjVRBxpwjdkjHK+9YMhFMgslZ0BFFpl.9.cmv+Fh4OAJTKJZMVHbQDjUDAQhH3JHBcC.gibYtesimre+pfJTCjc0f9JaQXwQHU2q45.hkKx.HtJrFs3RpFrd4CHBJjF7isg+bEFZwlx.iFNgzlq3QA43HvNaAPo8DY+5AJjFMtGQdd8PEhNcF0BqtG29CsNOHtyE1teJKHpnjfs2tCyyCmVoa.HfqG3zzlfxPFqANWQzJrX1mCq2wrDDDj9xtjZAeYWEnz44xm5eZ8SCNUgTk7bP+NlQ+I.RqBaIOgp5wxOwMe76Ca4B+e7alAqNFq8eji8Th5P6.Zybg4xH.yHtEyIgKH6OTG5ByoTO4CvcUqYa9+zQA960ymWHG7elzpvy.HEAqc4vJk29iFYrigNBosWmL.ZYzXFou8Ivrb7DsslqWUlgmm1sF.oM.DsF66MU7OUowBS0HcJtHByJhF.c0DDQPpHhmpQ5TnHVLErZ2FcRyAw3tFcvPwN.sKlqESaPAzB+Od10Ae1Se1lYRu1jsUAeV3blYchmEwbHnB820Xrof4gMhyCyvT2BXpQwLkMXpYW.tRUFlB.oSjrC0MM3IuF1fDuCADhDqowKVdcKC4MVf7LglWUc67BEWiEJ93OpAzIKa6NVpcubMbmFJ5N1R1O.NiMnJas5pTMNSOMvI9Kkjg6A+ANfPDYhj7u9R5G8.yXGalc6WRPVp6nJ++.NkdUYbVkGnk246Zfn.aRX+lEDCvO2U1JchtEDsFze6saS9PQff79XFOiaz.yebSHynVju.Q97S467hg5whgfZl0fBvbf8yj6b26tAY.0CJT4fIFNlzP1F4eB0xh5MABtL7SCn.248.+InFr.BEwX.EPc50ZvdYBLTf0FHrEdRRRvTpTVB8AsfSeP4gqRP1U4+UARmW8C7huXCdO9PoUOktsD20TULzQIwIsf2sKOpAPiTd3PQ+VYNKS73qDgsPAbIpvRkpunTCVhTWbqrb1Sff8bF1YxJUVVQoUtJEjlXN4JG8LXEhqF8Lvgg8dg8Ww5ZLocKrmdgK1Je6XI29hLrY4KAYNt4HCSy6aZJG2Vj7pCGyu27UUR5LY2nJ442Zqqmsi3Uk7qbYqXbqXWTyVIcWk1zke13ZXSl3NojSZ7JoWLlmJ81zpkbIZIhi0uUmFBQ+JbQt1UWjL6G5O8DlAOG1jvgpRr88PnEAPV++HdI.gFP.iHHFAHtBHBfyPSpznqvYqhqssMK.ZL1e7I3sNKOrNHkLgJWHJWanaK99wb4vtPaWpfnhbxIFYz0fxctWrI1kai7PoXoikNWOAnq1U7uOVI5l33WGKkEyltvQWHKNPzMj.QawWHagWG0k9xPXWtQZ5Yl6BgcKGhyUYwLWrGkXMeB0bF0IUOvaPKtqFVuLrtZX82HjUN.uam74kErNuDcIwsuyuip1B2F57sVbTLlOLJeMeuPFnpAGMt7lhh.I7TC443gEJ3oEy4MeFeRQDZwHJLgnvhHR7njL8kLWrujv9B4G45dcd+pywrwv9Ug0PXuZbUiawTtc8nZNNNFS5HfTvt2fXPsgyAudyrzEvoKLOcHZlsEHQjrfdgfCiQ2C+r8NMPoxN6FN3EcPBiOOZbrSRvJwMVAiLs4wXkraViA.kL0f0oACcfNxiqkNKaQ1FfzKvFCbEXC0kKUM1YLQvJpAVXC3+xrzVhitncewDyAi5.RHzVsHljqBkrwHMHbyh8kBySlfHqUXTQ7A4YoIaDQLMnFd0KgjiWmN+z6s8ogmBE+Muplsq7f8Z0qZXq4X4d.9XXwGYdqvDXKCOITjvShTh4Y4gRhvFxB+7Fwbx6scljxzaTuWNXiQHt3rxhB6C6RjVOTVlBxxDZCEvTxgYEthgKx779vVNWvJFtnZlkony7fC1fyEX3EQEN2AxguvedgjhLmuUtgCi1DNc3LEwU4Lgys5a6Q+TKY1ITvgJjEmZznBmK5jNSpSgSiOCBmyiQYKeLgV77QjPoR2J66Gn7xe+.hOuAM9ORh.g1V6YY38oynVK6QOHE8KqHAqRjVAj5wdoAuQzKM3Are4VICcoRqTNpvZIlJK9JQjVF6GXZ.0VwX+UJim5mg2uc9e3eb448nL798+ke1e6Rw6aUN6OBz0S6eyxY9c7VTHjcj978dHD5guaiHXEfZ7gSXfdwROjFXnQ4uhiUJ+Pp6K7rmAhM4WzRpzqxW7WO9Mdf0nvV6aWlEF2VZN3nJUAVtvzU9WtC6mQKYfue3N7eyMwQfxdrOI9m9wEsFnUCR5.oEQhOMndcuau6dmM5d20ElnaxLs6rd6tRoUMmpK2dm3dVfwdke3G9g2gM7UvF6f1Xj31gtyOYsytB14ez6mt14Zh14u6mt1YOA6bv28+u1Y4r14STGlXmr+VL+TpmncpuiGctGRH+cNckDqX5v2NpHw9ZkOHoKpqjnEi.2wuHQWobzaKS6JIXQW9NeSQB9MKmTGO9F0tRRecwDmuqXDIsD3hD84dB9x7hFQB+MJydnUQOGvyS74Ogetn3Rk82ONIB4PxmXYO7pH0iVlWTPpGkWp29xWUvRxeSj+qWVj5o7WI6696+me1e9m+W+PnwZwrroF55lzAQWD2BkZjnbqboK1HU3+8uIqtsrUhriqGcFO6ub4NMaGqwwJRFMtWZHU5hJk+4m9Vk22VGu+wruFV7MCGMApDhOAU7AMZAJWn3Z+esmH6kUEe6xCLfsNJVGuQA5HTE2OF5XzCK90K+nQifPoTE7Vke7g+37Jhk3u3uwPSXNFX8i88md.b.fFEVcK7VvvZJuAdJ.+6132HBb.0Rm8AbNwODMYG76RQS1IdRoopZN1eUz6jBe5x2lMBnSVrm48Jk2G+lzYwxlkfvRciuRSKqnxwX2qKiqccYr20kw0utLdmqKiabcYbyKlQ7gteeeOa9C+.HX+AOh0lSoROh8RdYQqR+aPONMfL
-
@hujackus Nice, thank you for this!
-
@hujackus Nice one!
-
@hujackus awesome!