create knobs with cairo

Programming applications for making music on Linux.

Moderators: MattKingUSA, khz

tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

create knobs with cairo

Post by tramp »

On your application GUI you may need knobs as control elements. Mostly therefore been framed horizontal png images in use.
Now, were do you get them from? Sure, there is knobman which runs fine in wine or as java version, but, I'm not really satisfied with the results. A much better result could be reached with blender, just blender has a somewhat step learning curve that I give up on it, special because I'm not a designer, more a developer.
So, cairo comes on the plan, and I wrote a little prog which creates basic knob images for me, as shown below.
Image
The prog just cover the basics, and only takes 3 args on command line , that's the size of the knob, the frame count and, optional a offset, which means space around the knob. Other parameters like knob colour, border colour, 3d shading, pointer size and stuff like that, needs to be edited in the source, to cover the needs. So this is a tool for developers. The prog create the image in svg and png format.
I place the source below in public domain, so, you could grab it and do with it what ever you wont. Maybe you are the one who extend it to make it a knobman like tool for linux? :D

The gif above was created with a size of 61 and 65 frames.
So here it is:

Code: Select all

#include <cairo.h>
#include <cairo-svg.h>
#include <math.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>

// gcc -Wall -g make_knob_image.c -lm `pkg-config --cflags --libs cairo` -o knobmake

#ifndef min
#define min(x, y) ((x) < (y) ? (x) : (y))
#endif
#ifndef max
#define max(x, y) ((x) < (y) ? (y) : (x))
#endif

const double scale_zero = 20 * (M_PI/180); // defines "dead zone" for knobs

static void paint_knob_state(cairo_t *cr, int knob_size, int knob_offset, double knobstate)
{
    /** set knob size **/
    int arc_offset = knob_offset;
    double knob_x = knob_size-arc_offset;
    double knob_y = knob_size-arc_offset;
    double knobx = arc_offset/2;
    double knobx1 = knob_x/2;
    double knoby = arc_offset/2;
    double knoby1 = knob_y/2;

    /** create the knob, set the knob and border color to your needs,
     *  or set knob color alpa to 0.0 to draw only the border **/
    cairo_arc(cr,knobx1+arc_offset/2, knoby1+arc_offset/2, knob_x/2.1, 0, 2 * M_PI );
    cairo_set_source_rgba (cr, 0.6, 0.6, 0.6, 1.0); // knob color
    cairo_fill_preserve (cr);
    
    cairo_set_source_rgb (cr, 0.2, 0.2, 0.2); // knob border color
    cairo_set_line_width(cr,min(5, max(2,knob_x/30)));
    cairo_stroke(cr);

    /** calculate the pointer **/
    double angle = scale_zero + knobstate * 2 * (M_PI - scale_zero);
    double pointer_off =knob_x/10;
    double radius = min(knob_x-pointer_off, knob_y-pointer_off) / 2;
    double length_x = (knobx+radius+pointer_off/2) - radius * sin(angle);
    double length_y = (knoby+radius+pointer_off/2) + radius * cos(angle);
    double radius_x = (knobx+radius+pointer_off/2) - radius/ 1.5 * sin(angle);
    double radius_y = (knoby+radius+pointer_off/2) + radius/ 1.5 * cos(angle);

    /** create the rotating pointer on the knob, 
     * set the color to your needs **/
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); 
    cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL);
    cairo_move_to(cr, radius_x, radius_y);
    cairo_line_to(cr,length_x,length_y);

    cairo_set_source_rgb (cr, 0.2, 0.2, 0.2); // knob pointer color
    cairo_set_line_width(cr,min(5, max(2,knob_x/30)));
    cairo_stroke(cr);

    /** 3d shading comment out, or set alpa to 0.0, for flat knobs 
     * or set alpa to a higher value for more shading effect **/
    cairo_arc(cr,knobx1+arc_offset/2, knoby1+arc_offset/2, knob_x/2.1, 0, 2 * M_PI );
    cairo_pattern_t*pat =
        cairo_pattern_create_radial (knobx1+arc_offset-knob_x/6,knoby1+arc_offset-knob_x/6, 1,knobx1+arc_offset,knoby1+arc_offset,knob_x/2.1 );
    cairo_pattern_add_color_stop_rgba (pat, 0,  0.8, 0.8, 0.8, 0.2);
    cairo_pattern_add_color_stop_rgba (pat, 1,  0.0, 0.0, 0.0, 0.2);
    cairo_set_source (cr, pat);
    cairo_fill (cr);
    cairo_pattern_destroy (pat);
}

int main(int argc, char* argv[])
{
    if (argc < 3) {
        fprintf(stdout, "usage: %s knob_size frame_count [offset] \n", basename(argv[0]));
        return 1;
    }

    int knob_size = atoi(argv[1]); 
    int knob_frames = atoi(argv[2]);
    int knob_image_width = knob_size * knob_frames;
    int knob_offset = 0;
    if (argc >= 4) {
        knob_offset = atoi(argv[3]);
    }
    
    char* sz = argv[1];
    char* fr = argv[2];
    char png_file[80];
    char svg_file[80];
    sprintf(png_file, "knob_%sx%s.png", sz,fr);
    sprintf(svg_file, "knob_%sx%s.svg", sz,fr);

    /** use this instead the svg surface when you don't need the svg format **/
    //cairo_surface_t *frame = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, knob_size, knob_size);
    cairo_surface_t *frame = cairo_svg_surface_create(NULL, knob_size, knob_size);
    cairo_t *crf = cairo_create(frame);
    /** use this instead the svg surface when you don't need the svg format **/
    //cairo_surface_t *knob_img = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, knob_image_widht, knob_size);
    cairo_surface_t *knob_img = cairo_svg_surface_create(svg_file, knob_image_width, knob_size);
    cairo_t *cr = cairo_create(knob_img);

    /** draw the knob per frame to image **/
    for (int i = 0; i < knob_frames; i++) {
        paint_knob_state(crf, knob_size, knob_offset, (double)((double)i/ knob_frames));
        cairo_set_source_surface(cr, frame, knob_size*i, 0);
        cairo_paint(cr);
        cairo_set_operator(crf,CAIRO_OPERATOR_CLEAR);
        cairo_paint(crf);
        cairo_set_operator(crf,CAIRO_OPERATOR_OVER);
    }

    /** save to png file **/
    cairo_surface_flush(knob_img);
    cairo_surface_write_to_png(knob_img, png_file);

    /** clean up **/
    cairo_destroy(crf);
    cairo_destroy(cr);
    cairo_surface_destroy(knob_img);
    cairo_surface_destroy(frame);
    return 0;
}
On the road again.
User avatar
sadko4u
Established Member
Posts: 986
Joined: Mon Sep 28, 2015 9:03 pm
Has thanked: 2 times
Been thanked: 359 times

Re: create knobs with cairo

Post by sadko4u »

And why not to draw knob directly with cairo in the UI?
For example, in LSP plugins there is no built-in graphic resources (exception is KVRDC entry where the KVRDC logo was built-in), all is drawn dynamically.
LSP (Linux Studio Plugins) Developer and Maintainer.
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

sadko4u wrote:And why not to draw knob directly with cairo in the UI?
Well, that's what I do in my plugs as well. But, for example, you cant draw direct with cairo in web based UI's.
On the road again.
User avatar
sadko4u
Established Member
Posts: 986
Joined: Mon Sep 28, 2015 9:03 pm
Has thanked: 2 times
Been thanked: 359 times

Re: create knobs with cairo

Post by sadko4u »

tramp wrote:Well, that's what I do in my plugs as well. But, for example, you cant draw direct with cairo in web based UI's.
Hmmm. If you use this generator for web, it's reasonable. Also if you use canvas extension available in HTML5, you may draw 2d-graphics well without using pre-rendered sprites.
LSP (Linux Studio Plugins) Developer and Maintainer.
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

sadko4u wrote:Hmmm. If you use this generator for web, it's reasonable.
There may be more reasons to use images as well in usual GUI's for rendering knobs:
Image
doing this one dynamical in cairo will properly use more CPU then processing the image, in special when you've use the image from a gresource.
On the road again.
User avatar
sadko4u
Established Member
Posts: 986
Joined: Mon Sep 28, 2015 9:03 pm
Has thanked: 2 times
Been thanked: 359 times

Re: create knobs with cairo

Post by sadko4u »

tramp wrote:doing this one dynamical in cairo will properly use more CPU then processing the image, in special when you've use the image from a gresource.
That's right. But we always may cache the current image in the back-buffer. Indeed, the image you've attached is very hard to draw with cairo. For this purposes easier is to use openGL.
LSP (Linux Studio Plugins) Developer and Maintainer.
ssj71
Established Member
Posts: 1294
Joined: Tue Sep 25, 2012 6:36 pm
Has thanked: 1 time

Re: create knobs with cairo

Post by ssj71 »

this will come in handy for me for getting my plugins on the mod. I was going to write the same thing myself. Thanks!
_ssj71

music: https://soundcloud.com/ssj71
My plugins are Infamous! http://ssj71.github.io/infamousPlugins
I just want to get back to making music!
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

ssj71 wrote:this will come in handy for me for getting my plugins on the mod. I was going to write the same thing myself. Thanks!
So I'm glad I've posted it here. By the way, it's exactly the reason why I wrote it.
sadko4u wrote:Indeed, the image you've attached is very hard to draw with cairo. For this purposes easier is to use openGL.
Even with openGL you'll waste CPU to do it dynamical.
Just look at the images as if it be interpolation tables.
On the road again.
folderol
Established Member
Posts: 2069
Joined: Mon Sep 28, 2015 8:06 pm
Location: Here, of course!
Has thanked: 224 times
Been thanked: 400 times
Contact:

Re: create knobs with cairo

Post by folderol »

Another thing you can do to improve apparent drawing performance is to stuff the incoming control settings into a ring buffer at whatever rate they come in, but read them in the gui at refresh rate, and only actually draw the most recent of each control. e.g. a MIDI volume ramp coming in at 3mS intervals but gui is refreshed every 20mS you only need to draw 1 in 6
The Yoshimi guy {apparently now an 'elderly'}
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

and here is now a little app to test the knobs you've created with the above prog.

To make them interact smooth together, you need to edit the above one and add

Code: Select all

#include <unistd.h>
on top of the file, and then add

Code: Select all

    unlink ("knob.png");
    symlink(png_file,"knob.png");
after

Code: Select all

    cairo_surface_write_to_png(knob_img, png_file);
this will create a symlink to your last created knob image which then will be used in knob_view.

now finally replace the line

Code: Select all

return 0;
with

Code: Select all

    char *arg[]={"./knob_view",NULL}; 
    return execvp(arg[0],arg);
to watch the knob directly after you've created it.

Code: Select all

#include <stdio.h>
#include <string.h>
#include <cairo.h>
#include <cairo-xlib.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

// gcc `pkg-config --cflags --libs cairo` -o knob_view knob_view.c -lX11

#ifndef min
#define min(x, y) ((x) < (y) ? (x) : (y))
#endif
#ifndef max
#define max(x, y) ((x) < (y) ? (y) : (x))
#endif

// define controller position in window
typedef struct {
	int x;
	int y;
	int width;
	int height;
} alinment;

// define controller adjustment
typedef struct {
	float std_value;
	float value;
	float min_value;
	float max_value;
	float step;
} adjustment;

// controller struct
typedef struct {
	adjustment adj;
	alinment al;
} controller;

// redraw the window
static void _expose(cairo_t *cr, cairo_surface_t *image, controller *knob, int h, int s) {
	double knobstate = (knob->adj.value - knob->adj.min_value) / (knob->adj.max_value - knob->adj.min_value);
	int findex = (int)(s * knobstate);

	// push and pop to avoid any flicker (offline drawing)
	cairo_push_group (cr);
	// draw background
	cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0);
	cairo_rectangle(cr,0, 0, h, h);
	cairo_fill(cr);

	// draw knob image
	cairo_set_source_surface (cr, image, -h*findex, 0);
	cairo_rectangle(cr,0, 0, h, h);
	cairo_fill(cr);

	cairo_pop_group_to_source (cr);
	cairo_paint (cr);
}

// send expose event to own window
static void send_expose(Display* display,Window win) {
	XEvent exppp;
	memset(&exppp, 0, sizeof(exppp));
	exppp.type = Expose;
	exppp.xexpose.window = win;
	XSendEvent(display,win,False,ExposureMask,&exppp);
	// we don't need to flush the desktop in a single threated application
	//XFlush(display);
}

// mouse wheel scroll event
static void scroll_event(controller *knob, int direction) {
	knob->adj.value = min(knob->adj.max_value,max(knob->adj.min_value, 
	  knob->adj.value + (knob->adj.step * direction)));
}

// mouse move while left button is pressed
static void motion_event(controller *knob, double start_value, int pos_y, int m_y) {
	static const double scaling = 0.5;
	// transfer any range to a range from 0 to 1 and get position in this range
	double knobstate = (start_value - knob->adj.min_value) /
					   (knob->adj.max_value - knob->adj.min_value);
	// calculate the step size to use in 0 . . 1
	double nsteps = knob->adj.step / (knob->adj.max_value-knob->adj.min_value);
	// calculate the new position in the range 0 . . 1
	double nvalue = min(1.0,max(0.0,knobstate - ((double)(pos_y - m_y)*scaling *nsteps)));
	// set new value to the knob in the knob range
	knob->adj.value = nvalue * (knob->adj.max_value-knob->adj.min_value) + knob->adj.min_value;
}

int main(int argc, char* argv[])
{
	Display* display = XOpenDisplay(NULL);
	Window win;
	Atom wm_delete_window;
	XEvent event;
	long event_mask;

	cairo_t *cr;
	cairo_surface_t *surface;
	cairo_surface_t *image;
	int w, h, s;

	controller knob;
	double start_value;
	int pos_x;
	int pos_y;

	image = cairo_image_surface_create_from_png ("./knob.png");
	w = cairo_image_surface_get_width (image);
	h = cairo_image_surface_get_height (image);
	if (!w ||!h) {
		XCloseDisplay(display);
		fprintf(stderr, "./knob.png not found\n");
		return 1;
	}
	s = (w/h)-1;
	fprintf(stderr, "width %i height %i steps %i\n", w,h,s);

	knob = (controller) {{0.5,0.5,0.0,1.0, 0.01},{0,0,w,h}};

	win = XCreateWindow(display, DefaultRootWindow(display), 0, 0, h,h, 0,
						CopyFromParent, InputOutput, CopyFromParent, CopyFromParent, 0);

	event_mask = StructureNotifyMask|ExposureMask|KeyPressMask 
					|EnterWindowMask|LeaveWindowMask|ButtonReleaseMask
					|ButtonPressMask|Button1MotionMask;

	XSelectInput(display, win, event_mask);

	wm_delete_window = XInternAtom(display, "WM_DELETE_WINDOW", 0);
	XSetWMProtocols(display, win, &wm_delete_window, 1);

	surface = cairo_xlib_surface_create (display, win,DefaultVisual(display, DefaultScreen (display)), h, h);
	cr = cairo_create(surface);

	XMapWindow(display, win);

	int keep_running = 1;

	while (keep_running) {
		XNextEvent(display, &event);

		switch(event.type) {
			case Expose:
				// only redraw on the last expose event
				if (event.xexpose.count == 0) {
					_expose(cr, image, &knob, h, s);
				}
			break;
			case ButtonPress:
				// save mouse position and knob value
				pos_x = event.xbutton.x;
				pos_y = event.xbutton.y;
				start_value = knob.adj.value;

				switch(event.xbutton.button) {
					case  Button4:
						// mouse wheel scroll up
						scroll_event(&knob, 1);
						send_expose(display,win);
					break;
					case Button5:
						// mouse wheel scroll down
						scroll_event(&knob, -1);
						send_expose(display,win);
					break;
					default:
					break;
				}
			break;
			case MotionNotify:
				// mouse move while button1 is pressed
				if(event.xmotion.state == Button1MotionMask) {
					motion_event(&knob, start_value, event.xmotion.y, pos_y);
					send_expose(display,win);
				}
			break;
			case KeyPress:
				if (event.xkey.keycode == XKeysymToKeycode(display,XK_Up)) {
					scroll_event(&knob, 1);
					send_expose(display,win);
				} else if (event.xkey.keycode == XKeysymToKeycode(display,XK_Right)) {
					scroll_event(&knob, 1);
					send_expose(display,win);
				} else if (event.xkey.keycode == XKeysymToKeycode(display,XK_Down)) {
					scroll_event(&knob, -1);
					send_expose(display,win);
				} else if (event.xkey.keycode == XKeysymToKeycode(display,XK_Left)) {
					scroll_event(&knob, -1);
					send_expose(display,win);
				}
			break;
			case ClientMessage:
				if (event.xclient.message_type == XInternAtom(display, "WM_PROTOCOLS", 1) &&
				   (Atom)event.xclient.data.l[0] == XInternAtom(display, "WM_DELETE_WINDOW", 1))
					keep_running = 0;
				break;
			default:
				break;
		}
	}

	cairo_destroy(cr);
	cairo_surface_destroy(surface);
	cairo_surface_destroy(image);
	XDestroyWindow(display, win);
	XCloseDisplay(display);
	return 0;
}
On the road again.
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

I've created a project on github now, and pushed this stuff there.

https://github.com/brummer10/knobmake
On the road again.
User avatar
SpotlightKid
Established Member
Posts: 250
Joined: Sun Jul 02, 2017 1:24 pm
Has thanked: 48 times
Been thanked: 54 times

Re: create knobs with cairo

Post by SpotlightKid »

Cool stuff.
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

SpotlightKid wrote:Cool stuff.
Thanks.

I've just added a cairo snippet for creating a switch.

Image
On the road again.
CrocoDuck
Established Member
Posts: 1133
Joined: Sat May 05, 2012 6:12 pm
Been thanked: 17 times

Re: create knobs with cairo

Post by CrocoDuck »

tramp wrote:I place the source below in public domain, so, you could grab it and do with it what ever you wont. Maybe you are the one who extend it to make it a knobman like tool for linux? :D
[offTopicJoke]

I don't think I will be the one, but if anybody steps in, please, whoever you are, call it "What a Knob".

[/offTopicJoke]
tramp
Established Member
Posts: 2335
Joined: Mon Jul 01, 2013 8:13 am
Has thanked: 9 times
Been thanked: 454 times

Re: create knobs with cairo

Post by tramp »

CrocoDuck wrote:I don't think I will be the one, but if anybody steps in, please, whoever you are, call it "What a Knob".
It becomes more a Interface Designer.
Image

this one is completely generated with those cairo snippets.
So one could decide if one would use the resulting images, or the cairo algorithms in the app.

For me, it is a way faster and easier to write a interface in cairo, then trying to draw something with gimp or incscape. :)
On the road again.
Post Reply