Page 1 of 2

create knobs with cairo

Posted: Fri Dec 23, 2016 4:58 am
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;
}

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 5:31 am
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.

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 6:35 am
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.

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 12:48 pm
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.

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 2:11 pm
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.

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 5:30 pm
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.

Re: create knobs with cairo

Posted: Fri Dec 23, 2016 5:33 pm
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!

Re: create knobs with cairo

Posted: Sun Dec 25, 2016 5:55 am
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.

Re: create knobs with cairo

Posted: Mon Dec 26, 2016 4:32 pm
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

Re: create knobs with cairo

Posted: Wed Oct 24, 2018 4:46 am
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;
}

Re: create knobs with cairo

Posted: Fri Oct 26, 2018 9:06 am
by tramp
I've created a project on github now, and pushed this stuff there.

https://github.com/brummer10/knobmake

Re: create knobs with cairo

Posted: Fri Oct 26, 2018 1:23 pm
by SpotlightKid
Cool stuff.

Re: create knobs with cairo

Posted: Sun Nov 11, 2018 6:39 am
by tramp
SpotlightKid wrote:Cool stuff.
Thanks.

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

Image

Re: create knobs with cairo

Posted: Wed Dec 05, 2018 8:54 pm
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]

Re: create knobs with cairo

Posted: Thu Dec 06, 2018 2:16 am
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. :)