aboutsummaryrefslogtreecommitdiff
path: root/site/code/input-knobs.js
diff options
context:
space:
mode:
authorRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
committerRichard <q@1bpm.net>2025-04-13 18:48:02 +0100
commit9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22 (patch)
tree291bd79ce340e67affa755a8a6b4f6a83cce93ea /site/code/input-knobs.js
downloadapps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.gz
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.tar.bz2
apps.csound.1bpm.net-9fbf91db06a6d4f4b5cd8bb45389a731bb86bf22.zip
initial
Diffstat (limited to 'site/code/input-knobs.js')
-rw-r--r--site/code/input-knobs.js342
1 files changed, 342 insertions, 0 deletions
diff --git a/site/code/input-knobs.js b/site/code/input-knobs.js
new file mode 100644
index 0000000..65aefb1
--- /dev/null
+++ b/site/code/input-knobs.js
@@ -0,0 +1,342 @@
+window.addEventListener("load",()=>{
+ let op=window.inputKnobsOptions||{};
+ op.knobWidth=op.knobWidth||op.knobDiameter||64;
+ op.knobHeight=op.knobHeight||op.knobDiameter||64;
+ op.sliderWidth=op.sliderWidth||op.sliderDiameter||128;
+ op.sliderHeight=op.sliderHeight||op.sliderDiameter||20;
+ op.switchWidth=op.switchWidth||op.switchDiameter||24;
+ op.switchHeight=op.switchHeight||op.switchDiameter||24;
+ op.fgcolor=op.fgcolor||"#f00";
+ op.bgcolor=op.bgcolor||"#000";
+ op.knobMode=op.knobMode||"linear";
+ op.sliderMode=op.sliderMode||"relative";
+ let styles=document.createElement("style");
+ styles.innerHTML=
+`input[type=range].input-knob,input[type=range].input-slider{
+ -webkit-appearance:none;
+ -moz-appearance:none;
+ border:none;
+ box-sizing:border-box;
+ overflow:hidden;
+ background-repeat:no-repeat;
+ background-size:100% 100%;
+ background-position:0px 0%;
+ background-color:transparent;
+ touch-action:none;
+}
+input[type=range].input-knob{
+ width:${op.knobWidth}px; height:${op.knobHeight}px;
+}
+input[type=range].input-slider{
+ width:${op.sliderWidth}px; height:${op.sliderHeight}px;
+}
+input[type=range].input-knob::-webkit-slider-thumb,input[type=range].input-slider::-webkit-slider-thumb{
+ -webkit-appearance:none;
+ opacity:0;
+}
+input[type=range].input-knob::-moz-range-thumb,input[type=range].input-slider::-moz-range-thumb{
+ -moz-appearance:none;
+ height:0;
+ border:none;
+}
+input[type=range].input-knob::-moz-range-track,input[type=range].input-slider::-moz-range-track{
+ -moz-appearance:none;
+ height:0;
+ border:none;
+}
+input[type=checkbox].input-switch,input[type=radio].input-switch {
+ width:${op.switchWidth}px;
+ height:${op.switchHeight}px;
+ -webkit-appearance:none;
+ -moz-appearance:none;
+ background-size:100% 200%;
+ background-position:0% 0%;
+ background-repeat:no-repeat;
+ border:none;
+ border-radius:0;
+ background-color:transparent;
+}
+input[type=checkbox].input-switch:checked,input[type=radio].input-switch:checked {
+ background-position:0% 100%;
+}`;
+ document.head.appendChild(styles);
+ let makeKnobFrames=(fr,fg,bg)=>{
+ let r=
+`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="${fr*64}" viewBox="0 0 64 ${fr*64}" preserveAspectRatio="none">
+<defs><g id="K"><circle cx="32" cy="32" r="30" fill="${bg}"/>
+<line x1="32" y1="28" x2="32" y2="7" stroke-linecap="round" stroke-width="6" stroke="${fg}"/></g></defs>
+<use xlink:href="#K" transform="rotate(-135,32,32)"/>`;
+ for(let i=1;i<fr;++i)
+ r+=`<use xlink:href="#K" transform="translate(0,${64*i}) rotate(${-135+270*i/fr},32,32)"/>`;
+ return r+"</svg>";
+ }
+ let makeHSliderFrames=(fr,fg,bg,w,h)=>{
+ let r=
+`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${w}" height="${fr*h}" viewBox="0 0 ${w} ${fr*h}" preserveAspectRatio="none">
+<defs><g id="B"><rect x="0" y="0" width="${w}" height="${h}" rx="${h/2}" ry="${h/2}" fill="${bg}"/></g>
+<g id="K"><circle x="${w/2}" y="0" r="${h/2*0.9}" fill="${fg}"/></g></defs>`;
+ for(let i=0;i<fr;++i){
+ r+=`<use xlink:href="#B" transform="translate(0,${h*i})"/>`;
+ r+=`<use xlink:href="#K" transform="translate(${h/2+(w-h)*i/100},${h/2+h*i})"/>`;
+ }
+ return r+"</svg>";
+ }
+ let makeVSliderFrames=(fr,fg,bg,w,h)=>{
+ let r=
+`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${w}" height="${fr*h}" viewBox="0 0 ${w} ${fr*h}" preserveAspectRatio="none">
+<defs><rect id="B" x="0" y="0" width="${w}" height="${h}" rx="${w/2}" ry="${w/2}" fill="${bg}"/>
+<circle id="K" x="0" y="0" r="${w/2*0.9}" fill="${fg}"/></defs>`;
+ for(let i=0;i<fr;++i){
+ r+=`<use xlink:href="#B" transform="translate(0,${h*i})"/>`;
+ r+=`<use xlink:href="#K" transform="translate(${w/2} ${h*(i+1)-w/2-i*(h-w)/100})"/>`;
+ }
+ return r+"</svg>";
+ }
+ let initSwitches=(el)=>{
+ let w,h,d,fg,bg;
+ if(el.inputKnobs)
+ return;
+ el.inputKnobs={};
+ el.refresh=()=>{
+ let src=el.getAttribute("data-src");
+ d=+el.getAttribute("data-diameter");
+ let st=document.defaultView.getComputedStyle(el,null);
+ w=parseFloat(el.getAttribute("data-width")||d||st.width);
+ h=parseFloat(el.getAttribute("data-height")||d||st.height);
+ bg=el.getAttribute("data-bgcolor")||op.bgcolor;
+ fg=el.getAttribute("data-fgcolor")||op.fgcolor;
+ el.style.width=w+"px";
+ el.style.height=h+"px";
+ if(src)
+ el.style.backgroundImage="url("+src+")";
+ else {
+ let minwh=Math.min(w,h);
+ let svg=
+`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h*2}" viewBox="0 0 ${w} ${h*2}" preserveAspectRatio="none">
+<g><rect fill="${bg}" x="1" y="1" width="${w-2}" height="${h-2}" rx="${minwh*0.25}" ry="${minwh*0.25}"/>
+<rect fill="${bg}" x="1" y="${h+1}" width="${w-2}" height="${h-2}" rx="${minwh*0.25}" ry="${minwh*0.25}"/>
+<circle fill="${fg}" cx="${w*0.5}" cy="${h*1.5}" r="${minwh*0.25}"/></g></svg>`;
+ el.style.backgroundImage="url(data:image/svg+xml;base64,"+btoa(svg)+")";
+ }
+ };
+ el.refresh();
+ };
+ let initKnobs=(el)=>{
+ let w,h,d,fg,bg;
+ if(el.inputKnobs){
+ el.redraw();
+ return;
+ }
+ let ik=el.inputKnobs={};
+ el.refresh=()=>{
+ d=+el.getAttribute("data-diameter");
+ let st=document.defaultView.getComputedStyle(el,null);
+ w=parseFloat(el.getAttribute("data-width")||d||st.width);
+ h=parseFloat(el.getAttribute("data-height")||d||st.height);
+ bg=el.getAttribute("data-bgcolor")||op.bgcolor;
+ fg=el.getAttribute("data-fgcolor")||op.fgcolor;
+ ik.sensex=ik.sensey=200;
+ if(el.className.indexOf("input-knob")>=0)
+ ik.itype="k";
+ else{
+ if(w>=h){
+ ik.itype="h";
+ ik.sensex=w-h;
+ ik.sensey=Infinity;
+ el.style.backgroundSize="auto 100%";
+ }
+ else{
+ ik.itype="v";
+ ik.sensex=Infinity;
+ ik.sensey=h-w;
+ el.style.backgroundSize="100% auto";
+ }
+ }
+ el.style.width=w+"px";
+ el.style.height=h+"px";
+ ik.frameheight=h;
+ let src=el.getAttribute("data-src");
+ if(src){
+ el.style.backgroundImage=`url(${src})`;
+ let sp=+el.getAttribute("data-sprites");
+ if(sp)
+ ik.sprites=sp;
+ else
+ ik.sprites=0;
+ if(ik.sprites>=1)
+ el.style.backgroundSize=`100% ${(ik.sprites+1)*100}%`;
+ else if(ik.itype!="k"){
+ el.style.backgroundColor=bg;
+ el.style.borderRadius=Math.min(w,h)*0.25+"px";
+ }
+ }
+ else{
+ let svg;
+ switch(ik.itype){
+ case "k": svg=makeKnobFrames(101,fg,bg); break;
+ case "h": svg=makeHSliderFrames(101,fg,bg,w,h); break;
+ case "v": svg=makeVSliderFrames(101,fg,bg,w,h); break;
+ }
+ ik.sprites=100;
+ el.style.backgroundImage="url(data:image/svg+xml;base64,"+btoa(svg)+")";
+ el.style.backgroundSize=`100% ${(ik.sprites+1)*100}%`;
+ }
+ ik.valrange={min:+el.min, max:(el.max=="")?100:+el.max, step:(el.step=="")?1:+el.step};
+ el.redraw(true);
+ };
+ el.setValue=(v)=>{
+ v=(Math.round((v-ik.valrange.min)/ik.valrange.step))*ik.valrange.step+ik.valrange.min;
+ if(v<ik.valrange.min) v=ik.valrange.min;
+ if(v>ik.valrange.max) v=ik.valrange.max;
+ el.value=v;
+ if(el.value!=ik.oldvalue){
+ el.setAttribute("value",el.value);
+ el.redraw();
+ let event=document.createEvent("HTMLEvents");
+ event.initEvent("input",false,true);
+ el.dispatchEvent(event);
+ ik.oldvalue=el.value;
+ }
+ };
+ ik.pointerdown=(ev)=>{
+ el.focus();
+ const evorg = ev;
+ if(ev.touches)
+ ev = ev.touches[0];
+ let rc=el.getBoundingClientRect();
+ let cx=(rc.left+rc.right)*0.5,cy=(rc.top+rc.bottom)*0.5;
+ let dx=ev.clientX,dy=ev.clientY;
+ let da=Math.atan2(ev.clientX-cx,cy-ev.clientY);
+ if(ik.itype=="k"&&op.knobMode=="circularabs"){
+ dv=ik.valrange.min+(da/Math.PI*0.75+0.5)*(ik.valrange.max-ik.valrange.min);
+ el.setValue(dv);
+ }
+ if(ik.itype!="k"&&op.sliderMode=="abs"){
+ dv=(ik.valrange.min+ik.valrange.max)*0.5+((dx-cx)/ik.sensex-(dy-cy)/ik.sensey)*(ik.valrange.max-ik.valrange.min);
+ el.setValue(dv);
+ }
+ ik.dragfrom={x:ev.clientX,y:ev.clientY,a:Math.atan2(ev.clientX-cx,cy-ev.clientY),v:+el.value};
+ document.addEventListener("mousemove",ik.pointermove);
+ document.addEventListener("mouseup",ik.pointerup);
+ document.addEventListener("touchmove",ik.pointermove);
+ document.addEventListener("touchend",ik.pointerup);
+ document.addEventListener("touchcancel",ik.pointerup);
+ document.addEventListener("touchstart",ik.preventScroll);
+ evorg.preventDefault();
+ evorg.stopPropagation();
+ };
+ ik.pointermove=(ev)=>{
+ let dv;
+ let rc=el.getBoundingClientRect();
+ let cx=(rc.left+rc.right)*0.5,cy=(rc.top+rc.bottom)*0.5;
+ if(ev.touches)
+ ev = ev.touches[0];
+ let dx=ev.clientX-ik.dragfrom.x,dy=ev.clientY-ik.dragfrom.y;
+ let da=Math.atan2(ev.clientX-cx,cy-ev.clientY);
+ switch(ik.itype){
+ case "k":
+ switch(op.knobMode){
+ case "linear":
+ dv=(dx/ik.sensex-dy/ik.sensey)*(ik.valrange.max-ik.valrange.min);
+ if(ev.shiftKey)
+ dv*=0.2;
+ el.setValue(ik.dragfrom.v+dv);
+ break;
+ case "circularabs":
+ if(!ev.shiftKey){
+ dv=ik.valrange.min+(da/Math.PI*0.75+0.5)*(ik.valrange.max-ik.valrange.min);
+ el.setValue(dv);
+ break;
+ }
+ case "circularrel":
+ if(da>ik.dragfrom.a+Math.PI) da-=Math.PI*2;
+ if(da<ik.dragfrom.a-Math.PI) da+=Math.PI*2;
+ da-=ik.dragfrom.a;
+ dv=da/Math.PI/1.5*(ik.valrange.max-ik.valrange.min);
+ if(ev.shiftKey)
+ dv*=0.2;
+ el.setValue(ik.dragfrom.v+dv);
+ }
+ break;
+ case "h":
+ case "v":
+ dv=(dx/ik.sensex-dy/ik.sensey)*(ik.valrange.max-ik.valrange.min);
+ if(ev.shiftKey)
+ dv*=0.2;
+ el.setValue(ik.dragfrom.v+dv);
+ break;
+ }
+ };
+ ik.pointerup=()=>{
+ document.removeEventListener("mousemove",ik.pointermove);
+ document.removeEventListener("touchmove",ik.pointermove);
+ document.removeEventListener("mouseup",ik.pointerup);
+ document.removeEventListener("touchend",ik.pointerup);
+ document.removeEventListener("touchcancel",ik.pointerup);
+ document.removeEventListener("touchstart",ik.preventScroll);
+ let event=document.createEvent("HTMLEvents");
+ event.initEvent("change",false,true);
+ el.dispatchEvent(event);
+ };
+ ik.preventScroll=(ev)=>{
+ ev.preventDefault();
+ };
+ ik.keydown=()=>{
+ el.redraw();
+ };
+ ik.wheel=(ev)=>{
+ let delta=ev.deltaY>0?-ik.valrange.step:ik.valrange.step;
+ if(!ev.shiftKey)
+ delta*=5;
+ el.setValue(+el.value+delta);
+ ev.preventDefault();
+ ev.stopPropagation();
+ };
+ el.redraw=(f)=>{
+ if(f||ik.valueold!=el.value){
+ let v=(el.value-ik.valrange.min)/(ik.valrange.max-ik.valrange.min);
+ if(ik.sprites>=1)
+ el.style.backgroundPosition="0px "+(-((v*ik.sprites)|0)*ik.frameheight)+"px";
+ else{
+ switch(ik.itype){
+ case "k":
+ el.style.transform="rotate("+(270*v-135)+"deg)";
+ break;
+ case "h":
+ el.style.backgroundPosition=((w-h)*v)+"px 0px";
+ break;
+ case "v":
+ el.style.backgroundPosition="0px "+(h-w)*(1-v)+"px";
+ break;
+ }
+ }
+ ik.valueold=el.value;
+ }
+ };
+ el.refresh();
+ el.redraw(true);
+ el.addEventListener("keydown",ik.keydown);
+ el.addEventListener("mousedown",ik.pointerdown);
+ el.addEventListener("touchstart",ik.pointerdown);
+ el.addEventListener("wheel",ik.wheel);
+ }
+ let refreshque=()=>{
+ let elem=document.querySelectorAll("input.input-knob,input.input-slider");
+ for(let i=0;i<elem.length;++i)
+ procque.push([initKnobs,elem[i]]);
+ elem=document.querySelectorAll("input[type=checkbox].input-switch,input[type=radio].input-switch");
+ for(let i=0;i<elem.length;++i){
+ procque.push([initSwitches,elem[i]]);
+ }
+ }
+ let procque=[];
+ refreshque();
+ setInterval(()=>{
+ for(let i=0;procque.length>0&&i<8;++i){
+ let q=procque.shift();
+ q[0](q[1]);
+ }
+ if(procque.length<=0)
+ refreshque();
+ },50);
+});