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
- // THE MAIN CODE IS IN KUIML FILE
- string name="Very simple knobs";
simple_knobs.kuiml
- <?xml version="1.0" encoding="utf-8" ?>
- <SKIN layout_type="column" h_margin="0" v_margin="0">
- <!--
- SIMPLE KNOBS using CANVAS
- =========================
- Demo by Ilya Orlov (LetiMix.com) | St. Petersburg, Russia, 2019-20
- Original url: https://pns.letimix.com/scripts/gui/simple_knobs
- =========================
- Get cool knobs with multiple shadows in Letimix Skin: pns.letimix.com/skin
- -->
- <!-- couple variables used to generate new element id for each knob -->
- <VARIABLE id="LMPR" value="test" override="true" />
- <VARIABLE id="LMID" value="500" override="true" />
- <!-- main template to create a knob -->
- <TEMPLATE id="LMR_TPL_SIMPLE_KNOB_TEST"
- param_id="_" visible="true" opacity="1" positions_count="201" pixel_range="128" zoom="1" offset="0" reverse="false"
- size="52" body_size="0.95" color="#000000" body_opacity="1"
- marker_start="0.0" marker_end="0.56" marker_width="1" marker_color="#FFFFFF" marker_opacity="1" marker_stroke_width="1" marker_type="1"
- angle_start="-135" angle_end="135"
- >
- <!-- generate unique widget_id for each knob, or take it from `id` attr (if set) -->
- <LOCAL_VARIABLE id="WID" script="if ("$id$" == "$"+"id$") return "knob_$LMPR$_$LMID$"; else return "$id$";" /><VARIABLE id="LMID" formula="$LMID$ + 1" override="true" />
- <!-- create param with normalized value (from 0 to 1), also supporting zoom, offset and reverse attributes -->
- <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" />
- <!-- create knob object on loading, pass all the params into it -->
- <SCRIPT script="SimpleKnob obj_$WID$("$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$"); " />
- <!-- now display the knob -->
- <CELL>
- <!-- the canvas element, where all the drawing happens -->
- <CANVAS opacity="$opacity$" id="cnv_$WID$" width="$size$" height="$size$" render_script="obj_$WID$.render($WID$_nval);" />
- <!-- the invisible knob on top of it to support mouse events -->
- <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$">
- <TEMPLATE_INNER_CONTENT /> <!-- in case we want to insert something inside of our knob -->
- </LAYER_STACK>
- <!-- redraw canvas on param changes -->
- <ACTION_TRIGGER event_id="$param_id$.value_changed" script="cnv_$WID$.Invalidate()" requires="cnv_$WID$.Invalidate" />
- </CELL>
- </TEMPLATE>
- <!-- add value of pi into variable PI to use un script -->
- <VARIABLE id="PI" formula="pi" override="true" />
- <!-- script that used for rendering -->
- <SCRIPT script="
- /* enumerated constants to unparse params from string `s_params` in SimpleKnob constructor. RSK_ stands for RenderSimpleKnob */
- enum SimpleKnobParams{
- 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
- }
- /* different marker types */
- enum SimpleKnobMarkers { RSK_MT_NONE, RSK_MT_LINE, RSK_MT_ROUNDED, RSK_MT_ROUNDED_FILLED, RSK_MT_CIRCLE, RSK_MT_CIRCLE_FILLED }
- /* now the main class for a knob object */
- class SimpleKnob{
- double pi = $PI$;
- double marker_start, marker_end, marker_width = 3, marker_stroke_width;
- double angle_start = -135, angle_end = 135, angle_center = 0, angle_width = 270;
- double cw, ch, cw2, ch2, body_radius;
- double bodyR, bodyG, bodyB, bodyA;
- double markerR, markerG, markerB, markerA;
- int marker_type = 1;
- /* knob constructor - unparse params from `s_params` and put into vars */
- SimpleKnob(string s_params){
- /* split incoming parameters */
- array<string> ar = s_params.split(";");
- ar.resize(RSK_MARKER_STROKE_WIDTH+1); /* just to prevent errors, make sure array is always large enough */
- this.cw = parseFloat(ar[RSK_SIZE]); /* canvas width and height */
- this.ch = this.cw;
- cw2 = cw/2; ch2 = ch/2;
- body_radius = cw2*parseFloat(ar[RSK_BODY_SIZE]);
- marker_width = this.cw*0.1*parseFloat(ar[RSK_MARKER_WIDTH]);
- marker_start = cw2*(1-parseFloat(ar[RSK_MARKER_START]))*parseFloat(ar[RSK_BODY_SIZE]);
- marker_end = cw2*(1-parseFloat(ar[RSK_MARKER_END]))*parseFloat(ar[RSK_BODY_SIZE]);
- marker_type = parseInt(ar[RSK_MARKER_TYPE]);
- marker_stroke_width = parseFloat(ar[RSK_MARKER_STROKE_WIDTH])*0.025*this.cw;
- angle_start = parseFloat(ar[RSK_ANGLE_START]);
- angle_end = parseFloat(ar[RSK_ANGLE_END]);
- if (angle_end < angle_start) {
- angle_start = -parseFloat(ar[RSK_ANGLE_START]);
- angle_end = -parseFloat(ar[RSK_ANGLE_END]);
- }
- angle_width = (angle_end-angle_start);
- angle_center = 90 - (angle_start+angle_end)/2 ;
- bodyR = double(parseInt(ar[RSK_BODY_COLOR].substr(1,2),16))/255.0;
- bodyG = double(parseInt(ar[RSK_BODY_COLOR].substr(3,2),16))/255.0;
- bodyB = double(parseInt(ar[RSK_BODY_COLOR].substr(5,2),16))/255.0;
- bodyA = parseFloat(ar[RSK_BODY_OPACITY]);
- markerR = double(parseInt(ar[RSK_MARKER_COLOR].substr(1,2),16))/255.0;
- markerG = double(parseInt(ar[RSK_MARKER_COLOR].substr(3,2),16))/255.0;
- markerB = double(parseInt(ar[RSK_MARKER_COLOR].substr(5,2),16))/255.0;
- markerA = parseFloat(ar[RSK_MARKER_OPACITY]);
- }
- /* the main function to render the knob, uses precalculated values from constructor */
- void render(double nval){
- /* get graphics object */
- auto ctx=Kt::Graphics::GetCurrentContext();
- /* do some precalculation, convert angles, etc */
- if (nval<0) nval=0;
- if (nval>1) nval=1;
- double nvalc = nval-0.5;
- double adeg = (angle_center - nvalc*angle_width);
- double arad = adeg*pi/180;
- /* calculate coordinates for the marker */
- double si = sin(arad), co = cos(arad);
- double dxme = cw2 + co*marker_end;
- double dyme = ch2 - si*marker_end;
- double dxms = cw2 + co*marker_start;
- double dyms = ch2 - si*marker_start;
- double mdeg = 270-adeg;
- /* now start drawing */
- ctx.path.Clear();
- /* knob body (filled circle) */
- ctx.source.SetRGBA(bodyR, bodyG, bodyB, bodyA);
- ctx.path.Arc(cw2, ch2, body_radius, 0.001, 0);
- ctx.FillPath();
- /* prepare to draw marker */
- ctx.path.Clear();
- ctx.source.SetRGBA(markerR, markerG, markerB, markerA);
- switch(marker_type) {
- case RSK_MT_ROUNDED:
- case RSK_MT_ROUNDED_FILLED:
- case RSK_MT_CIRCLE:
- case RSK_MT_CIRCLE_FILLED:
- if ((marker_type == RSK_MT_CIRCLE) or (marker_type == RSK_MT_CIRCLE_FILLED)) {
- ctx.path.Arc(dxms, dyms, marker_width*0.5, 0.001, 0);
- } else {
- ctx.path.Arc(dxms, dyms, marker_width*0.5, mdeg, mdeg+180);
- ctx.path.Arc(dxme, dyme, marker_width*0.5, mdeg+180, mdeg+360);
- }
- ctx.path.Close();
- if ((marker_type == RSK_MT_ROUNDED_FILLED) or (marker_type == RSK_MT_CIRCLE_FILLED)) {
- ctx.FillPath();
- } else {
- ctx.settings.set_lineWidth(marker_stroke_width);
- ctx.StrokePath();
- }
- break;
- default:
- ctx.settings.set_lineWidth(marker_width);
- ctx.path.MoveTo(dxme, dyme);
- ctx.path.LineTo(dxms, dyms);
- ctx.StrokePath();
- }
- }
- }
- " />
- <!-- make some demo params in kuiml (we can indeed use `custom_param0,1,2` from DSP) -->
- <PARAM id="param1" min="-100" max="100" default="0" exposed="true" />
- <PARAM id="param2" min="0" max="10" default="3" exposed="true" />
- <PARAM id="param3" min="0" max="1" default="3" exposed="true" />
- <!-- create some basic knob element using our template (this it optional, we could use template directly) -->
- <DEFINE>
- <MY_KNOB base_type="LMR_TPL_SIMPLE_KNOB_TEST" size="50" color="#2c9fdf" marker_color="#EEEEEE" marker_end="0.56" />
- </DEFINE>
- <!-- show the knobs -->
- <ROW spacing="20">
- <CELL>
- <MY_KNOB size="100" param_id="param1" />
- <PARAM_TEXT param_id="param1" />
- </CELL>
- <CELL>
- <MY_KNOB size="100" param_id="param2" color="#DD3355" marker_type="3" marker_start="0.2" angle_start="-90" angle_end="90" />
- <PARAM_TEXT param_id="param2" />
- </CELL>
- <CELL>
- <MY_KNOB size="100" param_id="param3" color="#22AA44" marker_type="4" marker_start="0.25" angle_start="-180" angle_end="180">
- <PARAM_TOOLTIP param_id="param3" content="Value: {value}
- This knob is reversly linked to the blue one" />
- </MY_KNOB>
- <PARAM_TEXT param_id="param3" />
- </CELL>
- </ROW>
- <!-- just for fun, let's link last knob to the first one -->
- <PARAM_LINK from="param3" to="param1" normalized="true" reverse="true" />
- </SKIN>