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