PLUG'N SCRIPT
rapid plugin development
Tutorial
DSP
KUIML
How-to
Scripts
GUI
  • Drawing
  • Simple knobs
Library
  • Params
Rave Generation
  • Preamp
ScriptsGUISimple knobs
November 04, 2025   Ilya Orlov

Simple knobs

About

This is an example script of how to make a simple knob in KUIML using CANVAS element for drawing and INVISIBLE_PARAM_KNOB for mouse behaviour.

This idea is further developed in LM Skin, where CANVAS-based knobs and faders drawn-on-the fly can have multiple shadows based on light sources.

Download

simple_knobs.cxx  ·  simple_knobs.kuiml

Contents

simple_knobs.cxx

  1. // THE MAIN CODE IS IN KUIML FILE
  2. string name="Very simple knobs";

simple_knobs.kuiml

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <SKIN layout_type="column" h_margin="0" v_margin="0">
  3. <!--
  4. SIMPLE KNOBS using CANVAS
  5. =========================
  6. Demo by Ilya Orlov (LetiMix.com) | St. Petersburg, Russia, 2019-20
  7. Original url: https://pns.letimix.com/scripts/gui/simple_knobs
  8. =========================
  9. Get cool knobs with multiple shadows in Letimix Skin: pns.letimix.com/skin
  10. -->

  11. <!-- couple variables used to generate new element id for each knob -->
  12. <VARIABLE id="LMPR" value="test" override="true" />
  13. <VARIABLE id="LMID" value="500" override="true" />

  14. <!-- main template to create a knob -->
  15. <TEMPLATE id="LMR_TPL_SIMPLE_KNOB_TEST"
  16. param_id="_" visible="true" opacity="1" positions_count="201" pixel_range="128" zoom="1" offset="0" reverse="false"
  17. size="52" body_size="0.95" color="#000000" body_opacity="1"
  18. marker_start="0.0" marker_end="0.56" marker_width="1" marker_color="#FFFFFF" marker_opacity="1" marker_stroke_width="1" marker_type="1"
  19. angle_start="-135" angle_end="135"
  20. >

  21. <!-- generate unique widget_id for each knob, or take it from `id` attr (if set) -->
  22. <LOCAL_VARIABLE id="WID" script="if (&quot;$id$&quot; == &quot;$&quot;+&quot;id$&quot;) return &quot;knob_$LMPR$_$LMID$&quot;; else return &quot;$id$&quot;;" /><VARIABLE id="LMID" formula="$LMID$ + 1" override="true" />

  23. <!-- create param with normalized value (from 0 to 1), also supporting zoom, offset and reverse attributes -->
  24. <FORMULA_PARAM id="$WID$_nval" min="0" max="1" formula="$WID$.zoom*(abs($reverse$-($param_id$-$param_id$.min)/($param_id$.max-$param_id$.min))) - ($WID$.zoom - 1)*$WID$.offset" exposed="true" />

  25. <!-- create knob object on loading, pass all the params into it -->
  26. <SCRIPT script="SimpleKnob obj_$WID$(&quot;$size$;$angle_start$;$angle_end$;$color$;$body_opacity$;$body_size$;$marker_start$;$marker_end$;$marker_width$;$marker_color$;$marker_opacity$;$marker_type$;$marker_stroke_width$&quot;); " />

  27. <!-- now display the knob -->
  28. <CELL>
  29. <LAYER_STACK>

  30. <!-- the canvas element, where all the drawing happens -->
  31. <CANVAS opacity="$opacity$" id="cnv_$WID$" width="$size$" height="$size$" render_script="obj_$WID$.render($WID$_nval);" />

  32. <!-- the invisible knob on top of it to support mouse events -->
  33. <INVISIBLE_PARAM_KNOB id="$WID$" param_id="$param_id$" opacity="$opacity$" width="$size$" height="$size$" positions_count="$positions_count$" visible="$visible$" pixel_range="$pixel_range$" cursor="system::hand" reverse="$reverse$" zoom="$zoom$" offset="$offset$">
  34. <TEMPLATE_INNER_CONTENT /> <!-- in case we want to insert something inside of our knob -->
  35. </INVISIBLE_PARAM_KNOB>

  36. </LAYER_STACK>

  37. <!-- redraw canvas on param changes -->
  38. <ACTION_TRIGGER event_id="$param_id$.value_changed" script="cnv_$WID$.Invalidate()" requires="cnv_$WID$.Invalidate" />
  39. </CELL>

  40. </TEMPLATE>

  41. <!-- add value of pi into variable PI to use un script -->
  42. <VARIABLE id="PI" formula="pi" override="true" />

  43. <!-- script that used for rendering -->
  44. <SCRIPT script=" 

  45. /* enumerated constants to unparse params from string `s_params` in SimpleKnob constructor. RSK_ stands for RenderSimpleKnob */
  46. enum SimpleKnobParams{
  47. RSK_SIZE, RSK_ANGLE_START, RSK_ANGLE_END, RSK_BODY_COLOR, RSK_BODY_OPACITY, RSK_BODY_SIZE, RSK_MARKER_START, RSK_MARKER_END, RSK_MARKER_WIDTH, RSK_MARKER_COLOR, RSK_MARKER_OPACITY, RSK_MARKER_TYPE, RSK_MARKER_STROKE_WIDTH
  48. }

  49. /* different marker types */
  50. enum SimpleKnobMarkers { RSK_MT_NONE, RSK_MT_LINE, RSK_MT_ROUNDED, RSK_MT_ROUNDED_FILLED, RSK_MT_CIRCLE, RSK_MT_CIRCLE_FILLED }

  51. /* now the main class for a knob object */
  52. class SimpleKnob{
  53. double pi = $PI$;
  54. double marker_start, marker_end, marker_width = 3, marker_stroke_width;
  55. double angle_start = -135, angle_end = 135, angle_center = 0, angle_width = 270;
  56. double cw, ch, cw2, ch2, body_radius;
  57. double bodyR, bodyG, bodyB, bodyA;
  58. double markerR, markerG, markerB, markerA;
  59. int marker_type = 1;

  60. /* knob constructor - unparse params from `s_params` and put into vars */
  61. SimpleKnob(string s_params){

  62. /* split incoming parameters */
  63. array&lt;string> ar = s_params.split(&quot;;&quot;);
  64. ar.resize(RSK_MARKER_STROKE_WIDTH+1); /* just to prevent errors, make sure array is always large enough */

  65. this.cw = parseFloat(ar[RSK_SIZE]); /* canvas width and height */
  66. this.ch = this.cw;
  67. cw2 = cw/2; ch2 = ch/2;

  68. body_radius = cw2*parseFloat(ar[RSK_BODY_SIZE]);
  69. marker_width = this.cw*0.1*parseFloat(ar[RSK_MARKER_WIDTH]);
  70. marker_start = cw2*(1-parseFloat(ar[RSK_MARKER_START]))*parseFloat(ar[RSK_BODY_SIZE]);
  71. marker_end = cw2*(1-parseFloat(ar[RSK_MARKER_END]))*parseFloat(ar[RSK_BODY_SIZE]);
  72. marker_type = parseInt(ar[RSK_MARKER_TYPE]);
  73. marker_stroke_width = parseFloat(ar[RSK_MARKER_STROKE_WIDTH])*0.025*this.cw;

  74. angle_start = parseFloat(ar[RSK_ANGLE_START]);
  75. angle_end = parseFloat(ar[RSK_ANGLE_END]);
  76. if (angle_end &lt; angle_start) {
  77. angle_start = -parseFloat(ar[RSK_ANGLE_START]);
  78. angle_end = -parseFloat(ar[RSK_ANGLE_END]);
  79. }

  80. angle_width = (angle_end-angle_start);
  81. angle_center = 90 - (angle_start+angle_end)/2 ;

  82. bodyR = double(parseInt(ar[RSK_BODY_COLOR].substr(1,2),16))/255.0;
  83. bodyG = double(parseInt(ar[RSK_BODY_COLOR].substr(3,2),16))/255.0;
  84. bodyB = double(parseInt(ar[RSK_BODY_COLOR].substr(5,2),16))/255.0;
  85. bodyA = parseFloat(ar[RSK_BODY_OPACITY]);

  86. markerR = double(parseInt(ar[RSK_MARKER_COLOR].substr(1,2),16))/255.0;
  87. markerG = double(parseInt(ar[RSK_MARKER_COLOR].substr(3,2),16))/255.0;
  88. markerB = double(parseInt(ar[RSK_MARKER_COLOR].substr(5,2),16))/255.0;
  89. markerA = parseFloat(ar[RSK_MARKER_OPACITY]);
  90. }

  91. /* the main function to render the knob, uses precalculated values from constructor */
  92. void render(double nval){
  93. /* get graphics object */
  94. auto ctx=Kt::Graphics::GetCurrentContext();

  95. /* do some precalculation, convert angles, etc */
  96. if (nval&lt;0) nval=0;
  97. if (nval>1) nval=1;
  98. double nvalc = nval-0.5;
  99. double adeg = (angle_center - nvalc*angle_width);
  100. double arad = adeg*pi/180;

  101. /* calculate coordinates for the marker */
  102. double si = sin(arad), co = cos(arad);
  103. double dxme = cw2 + co*marker_end;
  104. double dyme = ch2 - si*marker_end;
  105. double dxms = cw2 + co*marker_start;
  106. double dyms = ch2 - si*marker_start;
  107. double mdeg = 270-adeg;

  108. /* now start drawing */
  109. ctx.path.Clear();

  110. /* knob body (filled circle) */
  111. ctx.source.SetRGBA(bodyR, bodyG, bodyB, bodyA);
  112. ctx.path.Arc(cw2, ch2, body_radius, 0.001, 0);
  113. ctx.FillPath();

  114. /* prepare to draw marker */
  115. ctx.path.Clear();
  116. ctx.source.SetRGBA(markerR, markerG, markerB, markerA);

  117. switch(marker_type) {
  118. case RSK_MT_ROUNDED:
  119. case RSK_MT_ROUNDED_FILLED:
  120. case RSK_MT_CIRCLE:
  121. case RSK_MT_CIRCLE_FILLED:
  122. if ((marker_type == RSK_MT_CIRCLE) or (marker_type == RSK_MT_CIRCLE_FILLED)) {
  123. ctx.path.Arc(dxms, dyms, marker_width*0.5, 0.001, 0);
  124. } else {
  125. ctx.path.Arc(dxms, dyms, marker_width*0.5, mdeg, mdeg+180);
  126. ctx.path.Arc(dxme, dyme, marker_width*0.5, mdeg+180, mdeg+360);
  127. }
  128. ctx.path.Close();
  129. if ((marker_type == RSK_MT_ROUNDED_FILLED) or (marker_type == RSK_MT_CIRCLE_FILLED)) {
  130. ctx.FillPath();
  131. } else {
  132. ctx.settings.set_lineWidth(marker_stroke_width);
  133. ctx.StrokePath();
  134. }
  135. break;

  136. default:
  137. ctx.settings.set_lineWidth(marker_width);
  138. ctx.path.MoveTo(dxme, dyme);
  139. ctx.path.LineTo(dxms, dyms);
  140. ctx.StrokePath();
  141. }
  142. }
  143. }

  144.  " />


  145. <!-- make some demo params in kuiml (we can indeed use `custom_param0,1,2` from DSP) -->
  146. <PARAM id="param1" min="-100" max="100" default="0" exposed="true" />
  147. <PARAM id="param2" min="0" max="10" default="3" exposed="true" />
  148. <PARAM id="param3" min="0" max="1" default="3" exposed="true" />

  149. <!-- create some basic knob element using our template (this it optional, we could use template directly) -->
  150. <DEFINE>
  151. <MY_KNOB base_type="LMR_TPL_SIMPLE_KNOB_TEST" size="50" color="#2c9fdf" marker_color="#EEEEEE" marker_end="0.56" />
  152. </DEFINE>

  153. <!-- show the knobs -->
  154. <ROW spacing="20">
  155. <CELL>
  156. <MY_KNOB size="100" param_id="param1" />
  157. <PARAM_TEXT param_id="param1" />
  158. </CELL>
  159. <CELL>
  160. <MY_KNOB size="100" param_id="param2" color="#DD3355" marker_type="3" marker_start="0.2" angle_start="-90" angle_end="90" />
  161. <PARAM_TEXT param_id="param2" />
  162. </CELL>
  163. <CELL>
  164. <MY_KNOB size="100" param_id="param3" color="#22AA44" marker_type="4" marker_start="0.25" angle_start="-180" angle_end="180">
  165. <PARAM_TOOLTIP param_id="param3" content="Value: {value}
  166. This knob is reversly linked to the blue one" />
  167. </MY_KNOB>
  168. <PARAM_TEXT param_id="param3" />

  169. </CELL>
  170. </ROW>

  171. <!-- just for fun, let's link last knob to the first one -->
  172. <PARAM_LINK from="param3" to="param1" normalized="true" reverse="true" />

  173. </SKIN>

Comments

Please, authorize to view and post comments.

2020 - 2026 © Site by LetiMix · Donate  |  Plug'n Script and KUIML by Blue Cat Audio