[cairo] Coordinate from device to user space without a context

Bryce W. Harrington b.harrington at samsung.com
Wed Nov 6 20:25:55 CET 2013


Hi Donn,

You're on the right path with the cairo_matrix_transform_point()
function, but if you're not comfortable with matrix math it may seem
more intimidating than it actually is.  So ignore matrices for the
moment and just consider the math.


Essentially, you have two sets of coordinates, one of which is offset
from the other, and perhaps multiplied by some factor, and maybe rotated
or skewed.  To calculate the x,y coordinates in one system from the
other, you use this formula:

 x_new = xx * x + xy * y + x0;
 y_new = yx * x + yy * y + y0;

But let's say your two coordinates aren't rotated or skewed, they're
just straight multiples with an offset.  The formula simplifies to this:

 x_new = xx * x + x0;
 y_new = yy * y + y0;

xx and yy are the multiplication factors, and x0,y0 are the offsets.
(If you're rotating by 180 degrees, just switch the sign of xx or yy.)

If you need to go the other way round, just do the basic algebra:

 x = (x_new - x0)/xx
 y = (y_new - y0)/yy

The matrix object you saved from the previous drawing operation will
give you xx, yy, and x0, y0 directly (i.e. mymatrix.xx), no need for a
cairo context.  You can use the above equation directly to compute the
user space coordinates for the mouse x,y.  Examine the elements, and
doublecheck that xy and yx are zero; if they aren't, you'll need the
first set of formulae.

Once you understand the above, then you'll understand exactly what
the cairo_matrix_transform_point() and cairo_matrix_invert() functions
do.

Hope this gives some insights into what's going on!

Bryce

On Wed, Nov 06, 2013 at 04:14:03PM +0200, Donn wrote:
> Hi,
> (I'm using vala - fwiw)
> 
> My code is in an event handler that does not have a Cairo.Context --
> I am in a drag operation and I want to pass my mouse x,y into the
> drag-object.
> 
>  The drag-object has pre-stored its Cairo.Matrix - when it was asked
> to draw() in the past, it had a Context passed to it. In there, a SM
> = cr.get_matrix() was done.
> 
>  I now want the object to transform the mouse x,y into coordinates
> that are in user space according to the stored matrix (SM).
> 
> (The drag object may have been under an arbitrary number of matrix
> transforms in the past.)
> 
> I'm just not math smart and I don't even know if what I'm asking is
> possible. I see these kinds of funcs in Cairo:
> cairo_matrix_transform_point
> cairo_matrix_multiply
> .. but I don't know what they do.
> 
> I have tried the transform_point one. Something like this:
> (In the object's move method)
> move(mousex,mousey)
>  SM.transform_point(mousex,mousey) <-- they do change. Chaotically.
>  this.x = mousex
>  this.y = mousey
>  (later, in the draw method - those x,y vars are used in a translate call.)
> 
> .. but - I get weird offsets and such.
> 
> I want the drag-object to move along under my mouse coords.
> 
> I'm shooting in the dark really. Can someone help this
> math-deficient amateur hacker?
> 
> I attached my test code - in case.
> 
> 
> \d

> //valac --pkg gee-1.0 --pkg gtk+-3.0 -X -lm -X -w "$1" -o test
> using Gtk;
> using Cairo;
> 
> public void grid(Cairo.Context cr, bool white=false) {
>     cr.move_to(0,0);
>     double m=0f;
>     double w=0f;
>     double x=0f;
>     //for (x=10f; x <= 210f; x+=10f) {
>     for (x=0f; x <= 500f; x+=10f) {
>       m++;
>       w = 10 + ( Math.fmod(m,2) * 10); //fluctuating height
>       //w = (x==100) ? 80 : w; //major stroke
>       cr.rectangle (x,-w/2,1,w); //right arm
>       cr.rectangle (-x,-w/2,1,w); //left arm
>       cr.rectangle (-w/2,-x,w,1); //down arm
>       cr.rectangle (-w/2,x,w,1); //up arm
>     }
>     if (white) {
>       cr.set_source_rgb(1,1,1);
>     } else {
>       cr.set_source_rgba(0,0,0,0.2);
>     }
>     cr.fill();
> }
> 
> public double c(int col) { return (double)col / 256.0; }
> 
> public class Dot: Object {
>   public double x = 0;
>   public double y = 0;
>   public double sx = 1;
>   public double sy = 1;
>   public double rot = 0;
>   public double radius = 8;
>   public double r=0;
>   public double g=0;
>   public double b=0;
>   public string name;
>   public double device_x=0; 
>   public double device_y=0; 
>   
>   //Device-space bounding box coords
>   public double dbbx1;
>   public double dbbx2;
>   public double dbby1;
>   public double dbby2;
> 
>   public bool is_hit = false;
> 
>   private Cairo.Matrix? mymatrix;
> 
> 
>   public Dot(string n) {
>     name = n;
>   }
>   
>   public void move(double mx, double my) {
>     //MESSY MYSTERY STUFF> My math brain is dribbly goo.
> 
>     double px = mx; double py=my;
>     stdout.printf("BEFORE mx my: %G | %G\n", px, py);
>     mymatrix.transform_point(ref px, ref py);
>     stdout.printf("AFTER mx my: %G | %G\n", px, py);
> 
>     //Set my obj vars for a later draw()
>     x=px; y=py;
>   }
> 
>   public bool bbox_hit( double mx, double my) {
>     /*Crude box test.
>     Turns-out, I can just cmp the mouse x,y to my pre-saved
>     device bounding box coords!
>     */
> 
>     is_hit = (
>       (mx > dbbx1) && 
>       (mx < dbbx2) && 
>       (my > dbby1) && 
>       (my < dbby2)
>     );
>     return is_hit;
>   }
> 
>   public void set_matrix(Cairo.Context cr) {
> 
>     //Super NB that we store the u2d coords BEFORE the matrix stuff
>     device_x = x; 
>     device_y = y; //prep in-vars
>     cr.user_to_device(ref device_x, ref device_y); //stores into refs
>     //stdout.printf("%s stored %G,%G\n", name, device_x, device_y);
> 
> 
>     //NOTE - goes after the above code.
>     cr.translate(x,y);
>     cr.scale(sx,sy);
>     cr.rotate((Math.PI/180.0)*rot);
> 
>         cr.save();
>           //cr.identity_matrix();
>           mymatrix = cr.get_matrix();
>         cr.restore();
> 
>   }
> 
>   private void _record_bbox(Cairo.Context cr) {
>     cr.save();
>     //Oookay... getting weird:
>     //I want a bounding box that fits me.
>     //Step 1: According to random gmane list: set identity matrix
>     //Step 2: use path_extents func and store into obj vars.
>     //        these coords are now in DEVICE space, due to that
>     //        matrix voodoo. (Even tho docs say user space.)
>     //Oh, PS, do this all BEFORE a fill() - i.e. there must
>     //be a current path.
>     cr.identity_matrix();
>     cr.path_extents (out dbbx1, out dbby1, out dbbx2, out dbby2);
>     //stdout.printf("%G,%G,%G,%G\n",x1,y1,x2,y2);
>     cr.restore();
>   }
> 
>   public void draw_geom(Cairo.Context cr) {
>       cr.arc (20 ,20, radius, 0, 2 * Math.PI);
>       if (is_hit) {
>         cr.set_source_rgb (1, 0, 0);
>       } else {
>         cr.set_source_rgb (r, g, b);
>       }
>       cr.rectangle(-radius+20,-radius+20,radius,radius);
> 
>       //Get myy bbox:
>       //Have to do this here, before any commands that 
>       //end the current path.
>       _record_bbox(cr);
>   }
> 
>   public void draw(Cairo.Context cr) {
> 
>       //random grid
>       _grid(cr);
> 
>       draw_geom(cr);
>       cr.fill ();
> 
>       //random label.
>       cr.move_to(radius+5,0);
>       cr.save(); //isolate the text from the current rotation and scale?
>         cr.identity_matrix();
>         cr.select_font_face ("Adventure", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
>         cr.show_text(@"d$(this.name) u($x,$y) d($device_x,$device_y)");
>       cr.restore();
> 
>   }
> 
>   public void draw_dbb(Cairo.Context cr) {
>     //To draw the bb - the identity matrix must be applied again.
>     //I dunno why. I tried drawing it in the main draw() event (of the
>     //GraphicsArea) under that default matrix, but the rects get drawn 
>     //way down the screen. I dunno and I know that I dunno. :(
>     /*
> "Resets the current transformation matrix (CTM) by setting it equal to the identity matrix. That is, the user-space and device-space axes will be aligned and one user-space unit will transform to one device-space unit."
> 
>     */
>     cr.save();
>     cr.identity_matrix();
> 
>     //stdout.printf("BB of %s: %g | %G | %G | %G\n",name, dbbx1,dbby1,dbbx2,dbby2);
>     cr.rectangle(dbbx1,dbby1,(dbbx2-dbbx1),(dbby2-dbby1));
>     cr.set_line_width(2);
>     cr.set_source_rgb(1,1,1);
>     cr.stroke();
>     cr.restore();
>   }
> 
> 
>   private void _grid(Cairo.Context cr) {
>     grid(cr);
>   }
> 
> }
> 
> /*-------------------------------------------------*/
> public class SceneGraphWidget : Gtk.DrawingArea {
> 
>   public Dot d1;
>   public Dot d2;
>   public Dot d3;
>   public List<Dot> dlist;
> 
> 
>   public const uint flag_button_press = 1<<0;
>   public const uint flag_button_release = 1<<1;
>   public const uint flag_motion_notify = 1<<2;
>   public const uint flag_in_drag = 1<<3;
>   
>   public const uint pattern_click = flag_button_press | flag_button_release;
>   public const uint pattern_drag_start = flag_button_press | flag_motion_notify;
>   public const uint pattern_in_drag = pattern_drag_start | flag_in_drag;
>   public const uint pattern_drop = flag_button_press | flag_in_drag | flag_button_release;
> 
> 
>   private uint bitstate;
> 
>   private Dot? dotBeingDragged;
>   private Dot? overDot;
> 
>   construct {
> 
> 
>     d1 = new Dot("1");
>     d2 = new Dot("2");
>     d3 = new Dot("3");
> 
>     d1.r=0.02; d1.g=0.5; d1.b=0.02; d1.x=55; d1.y=55;
> 
>     d2.r=0; d2.g=0; d2.b=0; d2.x=17; d2.y=-112; d2.rot=0; 
>     d2.radius = 40; d2.sx=0.5; d2.sy=0.5;
> 
>     d3.r=0; d3.g=0.7; d3.b=0.7; d3.radius=20;
>     d3.x=-50; d3.y=0;
>     
>     dlist = new List<Dot>();
>     dlist.append(d1);
>     dlist.append(d2);
>     dlist.append(d3);
> 
>     this.set_double_buffered (true);
> 
>     this.set_size_request (500,500);
>     this.can_focus = true; //In order to receive keybd signals
> 
>     this.add_events (
>     Gdk.EventMask.BUTTON_PRESS_MASK |
>     Gdk.EventMask.BUTTON_RELEASE_MASK |
>     /*Gdk.EventMask.KEY_PRESS_MASK */
>     Gdk.EventMask.POINTER_MOTION_MASK
>     /*Gdk.EventMask.POINTER_MOTION_HINT_MASK */
>     );
> 
>     //"event" here is a SIGNAL available in a Gtk.Widget!
>     //So, I can finally have a single handler for many event types!
>     event.connect(handle_events); //!!! Check it out!
>   }
> 
>   private void initmatrix ( Cairo.Context cr ) {
> 	/*
>      Scale to window's size and shift the origin of the page to the center.
> 		  -y | -y
> 		  -x | +x
> 		 ----0------
> 		  -x | +x
> 		  +y | +y
> 
> 
>     TODO: Figure out how to keep the window proportional...
> 		*/
> 
>     int w,h;
>     double pw, ph;
> 
>     h = get_allocated_height ();
>     w = get_allocated_width ();
> 
>     pw = (float) w / 500f; 
>     ph = (float) h / 500f;
> 
> 		var matrix = Cairo.Matrix (1, 0, 0, 1, w/2,h/2 ); //translate to middle.
> 		
>     matrix.scale (pw, ph); //Scale to the window's size.
> 		cr.transform(matrix);
>   }
> 
>   /* Override of the draw() callback, GTK3 requires this. */
>   public override bool draw ( Cairo.Context cr ) {
>     initmatrix(cr);
>     //We are in Matrix A
>      
>      //Just draw some guides for sanity
>     cr.set_source_rgb (1,1,1);
>     cr.paint ();
> 
>     cr.rectangle(0,0,-250,-250);
>     cr.set_source_rgb(c(255), c(204), c(102));
>     cr.fill();
> 
>     cr.rectangle(0,0,250,-250);
>     cr.set_source_rgb(c(153), c(204), c(255));
>     cr.fill();
>     
>     cr.rectangle(0,0,250,250);
>     cr.set_source_rgb(c(204), c(244), c(255));
>     cr.fill();
>     
>     cr.rectangle(0,0,-250,250);
>     cr.set_source_rgb(c(51), c(255), c(153));
>     cr.fill();
>     
> 
>     grid(cr, true);
> 
> 
>       //Begin the pain
>       cr.save();
> 
>       cr.save();
>         d1.set_matrix(cr);
>         d1.draw(cr);
>         d1.draw_dbb(cr);
> 
> 
>             cr.save();
>               d2.set_matrix(cr);
>               d2.draw(cr);
>               d2.draw_dbb(cr);
> 
>             cr.restore();
>       
>       cr.restore();  
> 
>       d3.set_matrix(cr);
>       d3.draw(cr);
>       d3.draw_dbb(cr);
> 
> 
>   
>     cr.restore(); 
>     //back in device space
> 
> 
> 
>     return true;
>   }
> 
>  
> 
>   private bool handle_events(Gdk.Event event) {
>     //stdout.printf("%s\n", event.type.to_string());
> 
>     if (event.type == Gdk.EventType.MOTION_NOTIFY) {
> 
>       bitstate |= flag_motion_notify;
> 
>           double mx = event.motion.x;
>           double my = event.motion.y;
>           //stdout.printf("Mouse at:(%G | %G)\n",mx,my);
> 
>           var some_hit = false;
> 
>           foreach(Dot d in dlist) {
>             //If we are dragging AND
>             //the dotBeingDragged is the one in the loop, then
>             //skip the hit_test.
>             //i.e. we don't need to hittest the object we are already dragging.
>             if ( (bitstate == pattern_in_drag) && (dotBeingDragged == d) ) continue;
>               if (d.bbox_hit(mx,my)) {
>                 some_hit = true;
>                 overDot = d;
>               }
>             }
>           
> 
>           d2.rot += 1;d2.sx+=0.001;d2.sy+=0.001;
> 
>           //if we did not get some kind of hit, nullify the overDot - cos
>           //there's no Dot we are over right now.
>           if (!some_hit) overDot = null;
> 
>           //if ((!some_hit) && (bitstate != pattern_in_drag)) dotBeingDragged = null;
>     }
> 
> 
>     if (event.type == Gdk.EventType.BUTTON_PRESS) {
>       //Set our flag:
>       bitstate = bitstate | flag_button_press;
> 
>       //stdout.printf("  state : %d \n", event.key.state);
> 
>       Gdk.ModifierType modifiers;
>       modifiers = Gtk.accelerator_get_default_mod_mask ();
>       //stdout.printf ("  Gdk.ModifierType.CONTROL_MASK : %d \n", Gdk.ModifierType.CONTROL_MASK);
>       //stdout.printf ("  state & modifiers %d\n", (event.button.state & modifiers) );
> 
>       if ( (event.button.state & modifiers) == Gdk.ModifierType.CONTROL_MASK ) {
>         stdout.printf (" Control Key\n");
>         }
>     }
> 
>     if (event.type == Gdk.EventType.BUTTON_RELEASE) {
>       //Set our flag:
>       bitstate = bitstate | flag_button_release;
>     }
> 
>     //Use our flags!
> 
>     if (bitstate == pattern_click) {
>       stdout.printf("CLICK!\n");
>     }
> 
>     if (bitstate == pattern_drag_start) {
>       //stdout.printf("DRAG START\n");
>       bitstate |= flag_in_drag; //signal we are dragging flag
>       if (overDot != null ) dotBeingDragged = overDot;
>       if (dotBeingDragged != null) stdout.printf("DRAGSTART %s\n", dotBeingDragged.name);
>     }
>     if (bitstate == pattern_in_drag) {
>       //stdout.printf("IN DRAG\n");
>       if (dotBeingDragged != null) { 
>         stdout.printf("DRAGGING %s\n", dotBeingDragged.name);
>         dotBeingDragged.move(event.motion.x, event.motion.y);
>       //d2.x = mx; d2.y = my;
>       }
>     }
> 
>     if (bitstate == pattern_drop) {
>       //stdout.printf("DROP!!!!!!!!!!!\n");
>       if (dotBeingDragged != null) {
>         stdout.printf("DROPPED %s\n", dotBeingDragged.name);
>         dotBeingDragged = null;
>       }
>     }
> 
> 
>     //switch off flags after we've used them.
>     if (event.type == Gdk.EventType.MOTION_NOTIFY) {
>       bitstate &= ~flag_motion_notify;
>     }
> 
>     if (event.type == Gdk.EventType.BUTTON_PRESS) {
>       //self.ui_state = self.ui_state & ~Mouserizer.__FLAG_RELEASING # clear bit
>       bitstate = bitstate & ~flag_button_release; //clear release flag
>     }
>     if (event.type == Gdk.EventType.BUTTON_RELEASE) {
>       bitstate &= ~flag_button_press; //clear press state
>       bitstate &= ~flag_in_drag; //no drag either.
>     }
> 
> 
>     queue_draw ();
> 
>     return true; //True - stops the event here. False propogates it up.
>   }
> 
> 
> }
> 
> int main(string[] args) {
> 
>   Gtk.init ( ref args ); //ok = Gtk.init ... 
> 
>   var sgw = new SceneGraphWidget ();
> 
>   var w = new Gtk.Window();
> 
> 
>   w.add( sgw );
>   w.show_all ();
>   w.destroy.connect (Gtk.main_quit);
>   w.key_press_event.connect( (e) => { 
>     if (e.keyval == Gdk.Key.Escape) {
>       Gtk.main_quit(); 
>       return true; 
>     }
>     return false;
>   } );
> 
>   Gtk.main ();
> 
>   return 0;
> }

> -- 
> cairo mailing list
> cairo at cairographics.org
> http://lists.cairographics.org/mailman/listinfo/cairo


More information about the cairo mailing list