]> git.cworth.org Git - cairo-spline/commitdiff
Add cairo-spline demonstration program. master
authorCarl Worth <cworth@cworth.org>
Sat, 23 Jun 2012 17:07:22 +0000 (10:07 -0700)
committerCarl Worth <cworth@cworth.org>
Sat, 23 Jun 2012 17:21:45 +0000 (10:21 -0700)
This is an old program from the beginnings of cairo, which I used to
interactively explore the quality of cairo's spline rendering.

I'm bringing it back now to use it to demonstrate a bug in cairo's new
"contour" spline stroking as of cairo 1.12.

.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
cairo-spline.c [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..10879c3
--- /dev/null
@@ -0,0 +1,3 @@
+*.o
+*~
+cairo-spline
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..1bea967
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+PROGS=cairo-spline
+
+# I'd like to put a bunch of compiler-specific warning flags here, but
+# I don't know a good way to choose the right flags based on the
+# compiler in use.
+#
+# So, for now, if you want more warnings, set them in CFLAGS before
+# calling make. For example, for gcc:
+#
+# CFLAGS="-Wall -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations -Wnested-externs -fno-strict-aliasing" make
+
+MYCFLAGS=`pkg-config --cflags cairo-xlib`
+MYLDFLAGS=`pkg-config --libs cairo-xlib`
+
+all: $(PROGS)
+
+%.o: %.c
+       $(CC) -c $(CFLAGS) $(CPPFLAGS) ${MYCFLAGS} $< -o $@
+
+%: %.c
+       $(CC) $(CFLAGS) $(CPPFLAGS) ${MYCFLAGS} ${MYLDFLAGS} $^ -o $@
+
+clean:
+       rm -f $(PROGS) *.o
diff --git a/cairo-spline.c b/cairo-spline.c
new file mode 100644 (file)
index 0000000..335e36a
--- /dev/null
@@ -0,0 +1,736 @@
+/* cc `pkg-config --cflags --libs cairo-xlib` cairo-spline.c -o cairo-spline */
+
+/* Copyright © 2005 Carl Worth
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <math.h>
+
+#include <cairo.h>
+#include <cairo-xlib.h>
+#include <X11/extensions/Xrender.h>
+
+#define EPSILON (1.0 / (2<<16))
+#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))
+
+typedef struct color {
+    double red;
+    double green;
+    double blue;
+} color_t;
+
+typedef struct pt {
+    double x;
+    double y;
+} pt_t;
+
+typedef struct spline {
+    pt_t pt[4];
+} spline_t;
+
+typedef struct quadr {
+    pt_t pt[4];
+} quadr_t;
+
+typedef struct win {
+    Display *dpy;
+    int scr;
+    Window win;
+    GC gc;
+    Pixmap pix;
+    int width, height;
+    long event_mask;
+
+    int needs_refresh;
+
+    double tolerance;
+    double line_width;
+    cairo_line_cap_t line_cap;
+    spline_t spline;
+    double zoom;
+    double xtrans;
+    double ytrans;
+
+    int click;
+    pt_t drag_pt;
+    int active;
+} win_t;
+
+typedef struct callback_doc {
+    void *callback;
+    char *doc;
+} callback_doc_t;
+
+typedef int (*key_callback_t)(win_t *win);
+
+typedef struct key_binding
+{
+    char *key;
+    int is_alias;
+    KeyCode keycode;
+    key_callback_t callback;
+} key_binding_t;
+
+static void win_init(win_t *win, Display *dpy);
+static void win_deinit(win_t *win);
+static void win_refresh(win_t *win);
+static void win_select_events(win_t *win);
+static void win_handle_events(win_t *win);
+static void win_print_help(win_t *win);
+
+static int quit_cb(win_t *win);
+static int print_spline_cb(win_t *win);
+static int zoom_in_cb(win_t *win);
+static int zoom_out_cb(win_t *win);
+static int trans_left_cb(win_t *win);
+static int trans_right_cb(win_t *win);
+static int trans_up_cb(win_t *win);
+static int trans_down_cb(win_t *win);
+static int flatten_cb(win_t *win);
+static int smooth_cb(win_t *win);
+static int widen_line(win_t *win);
+static int narrow_line(win_t *win);
+static int butt_cap_cb(win_t *win);
+static int round_cap_cb(win_t *win);
+
+static const double DEFAULT_XTRANS = 0.0;
+static const double DEFAULT_YTRANS = 0.0;
+static const double DEFAULT_TOLERANCE = .1;
+static const double DEFAULT_ZOOM = 1.0;
+static const cairo_line_cap_t DEFAULT_LINE_CAP = CAIRO_LINE_CAP_BUTT;
+
+/* Showing off the problems with wide butt-capped splines that turn
+   sharply at the end. */
+static const spline_t funky_fangs = {
+ { { 69.25, 48.185 }, { 40.225, 43.06 }, { 59.5, 34.5 }, { 59.4998, 35.2514 } }
+};
+
+/* A simple looping spline. */
+static const spline_t ribbon = {
+    { {110, 20}, {310, 300}, {10, 310}, {210, 20} }
+};
+
+/* An even more difficult spline, (showing bugs in the "contour-based
+ * stroking" in cairo 1.12) */
+static const spline_t contour_bugs = {
+  { { 207.25, 221.185 }, { 212.225, 229.06 }, { 211.5, 222.5 }, { 210.5, 224.5 } }
+};
+
+#define DEFAULT_SPLINE contour_bugs
+static const double DEFAULT_LINE_WIDTH = 160;
+
+static const callback_doc_t callback_doc[] = {
+    { quit_cb,         "Exit the program" },
+    { print_spline_cb, "Print current spline coordinates on stdout" },
+    { zoom_in_cb,      "Zoom in" },
+    { zoom_out_cb,     "Zoom out" },
+    { trans_left_cb,   "Translate left" },
+    { trans_right_cb,  "Translate right" },
+    { trans_up_cb,     "Translate up" },
+    { trans_down_cb,   "Translate down" },
+    { flatten_cb,      "Decrease rendering accuracy, (tolerance *= 10)" },
+    { smooth_cb,       "Increase rendering accuracy, (tolerance /= 10)" },
+    { widen_line,      "Widen line width" },
+    { narrow_line,     "Narrow line width" },
+    { butt_cap_cb,     "Set butt cap" },
+    { round_cap_cb,    "Set round cap" },
+};
+
+static key_binding_t key_binding[] = {
+    /* Keysym, Alias, Keycode, callback */
+    { "Q",     0, 0, quit_cb },
+    { "Left",  0, 0, trans_left_cb },
+    { "Right", 0, 0, trans_right_cb },
+    { "Up",    0, 0, trans_up_cb },
+    { "Down",  0, 0, trans_down_cb },
+    { "Return",        0, 0, print_spline_cb },
+    { "plus",  0, 0, zoom_in_cb },
+    { "equal", 1, 0, zoom_in_cb },
+    { "minus", 0, 0, zoom_out_cb },
+    { "greater",0, 0, smooth_cb },
+    { "period",        1, 0, smooth_cb },
+    { "less",  0, 0, flatten_cb },
+    { "comma", 1, 0, flatten_cb },
+    { "W",     0, 0, widen_line },
+    { "N",     0, 0, narrow_line },
+    { "B",     0, 0, butt_cap_cb },
+    { "R",     0, 0, round_cap_cb },
+};
+
+int
+main(int argc, char *argv[])
+{
+    win_t win;
+
+    Display *dpy = XOpenDisplay(0);
+
+    if (dpy == NULL) {
+       fprintf(stderr, "Failed to open display: %s\n", XDisplayName(0));
+       return 1;
+    }
+
+    win_init(&win, dpy);
+
+    win_print_help(&win);
+
+    win_handle_events(&win);
+
+    win_deinit(&win);
+
+    XCloseDisplay(dpy);
+
+    return 0;
+}
+
+static void
+draw_control_line(cairo_t *cr, pt_t *a, pt_t *b, double width)
+{
+    cairo_save(cr);
+
+    cairo_set_source_rgb(cr, 0, 0, 1);
+    cairo_set_line_width(cr, width);
+
+    cairo_move_to(cr, a->x, a->y);
+    cairo_line_to(cr, b->x, b->y);
+    cairo_stroke(cr);
+
+    cairo_restore(cr);
+}
+
+static void
+draw_spline(cairo_t *cr, win_t *win)
+{
+    spline_t *spline = &win->spline;
+    double zoom = win->zoom;
+    double drag_user_x = win->drag_pt.x;
+    double drag_user_y = win->drag_pt.y;
+
+    int i;
+
+    cairo_device_to_user (cr, &drag_user_x, &drag_user_y);
+
+    cairo_save(cr);
+
+    cairo_move_to(cr, spline->pt[0].x, spline->pt[0].y);
+    cairo_curve_to(cr,
+             spline->pt[1].x, spline->pt[1].y,
+             spline->pt[2].x, spline->pt[2].y,
+             spline->pt[3].x, spline->pt[3].y);
+
+
+    if (win->click && cairo_in_stroke (cr, drag_user_x, drag_user_y)) {
+       win->active = 0xf;
+    }
+
+    cairo_save (cr);
+    {
+       cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
+
+       cairo_stroke(cr);
+    }
+    cairo_restore (cr);
+
+    cairo_save (cr);
+    cairo_set_line_width (cr, 2);
+    cairo_move_to(cr, spline->pt[0].x, spline->pt[0].y);
+    cairo_curve_to(cr,
+             spline->pt[1].x, spline->pt[1].y,
+             spline->pt[2].x, spline->pt[2].y,
+             spline->pt[3].x, spline->pt[3].y);
+    cairo_set_source_rgb (cr, 1.0, 1.0, 0.0);
+    cairo_stroke (cr);
+    cairo_restore (cr);
+
+    draw_control_line(cr, &spline->pt[0], &spline->pt[1], 2.0 / zoom);
+    draw_control_line(cr, &spline->pt[3], &spline->pt[2], 2.0 / zoom);
+
+    for (i=0; i < 4; i++) {
+       cairo_save(cr);
+
+       cairo_set_source_rgba (cr, 1, 0, 0, 0.5);
+
+       cairo_new_path (cr);
+       cairo_arc (cr,
+                  spline->pt[i].x, spline->pt[i].y,
+                  10, 0, 2 * M_PI);
+       if (win->click && cairo_in_fill (cr, drag_user_x, drag_user_y)) {
+           win->active = (1<<i);
+           win->click = 0;
+       }
+       cairo_fill (cr);
+
+       cairo_restore(cr);
+    }
+
+    cairo_restore(cr);
+}
+
+static void
+win_refresh(win_t *win)
+{
+    Display *dpy = win->dpy;
+
+    cairo_surface_t *surface;
+    cairo_t *cr;
+    cairo_status_t status;
+
+    XFillRectangle(dpy, win->pix, win->gc, 0, 0, win->width, win->height);
+
+    surface = cairo_xlib_surface_create (dpy, win->pix,
+                                        DefaultVisual (dpy, win->scr),
+                                        win->width, win->height);
+    cr = cairo_create(surface);
+
+    cairo_set_source_rgb(cr, 0, 0, 0);
+
+    cairo_set_line_width(cr, win->line_width);
+    cairo_set_line_cap(cr, win->line_cap);
+    cairo_translate(cr, win->xtrans, win->ytrans);
+    cairo_scale(cr, win->zoom, win->zoom);
+    cairo_set_tolerance(cr, win->tolerance);
+
+    draw_spline(cr, win);
+
+    status = cairo_status(cr);
+    if (status) {
+       fprintf(stderr, "Cairo is unhappy: %s\n",
+               cairo_status_to_string(status));
+    }
+
+    cairo_destroy(cr);
+    cairo_surface_destroy (surface);
+
+    XCopyArea(win->dpy, win->pix, win->win, win->gc,
+             0, 0, win->width, win->height,
+             0, 0);
+}
+
+int
+find_true_color_visual (Display *dpy, int scr, XVisualInfo *result)
+{
+    XVisualInfo        *xvi;
+    XVisualInfo        template;
+    int nvi;
+    XRenderPictFormat *format;
+    Visual *visual;
+
+    template.screen = scr;
+    template.class = TrueColor;
+    xvi = XGetVisualInfo (dpy, 
+                         VisualScreenMask |
+                         VisualClassMask,
+                         &template,
+                         &nvi);
+    if (xvi == NULL)
+       return 0;
+
+    *result = xvi[0];
+
+    XFree (xvi);
+
+    return 1;
+}
+
+static void
+win_init(win_t *win, Display *dpy)
+{
+    int i;
+    Window root;
+    XGCValues gcv;
+    XVisualInfo xvi;
+    XSetWindowAttributes wattr;
+    unsigned long wmask;
+
+    win->dpy = dpy;
+    win->width = 400;
+    win->height = 400;
+
+    root = DefaultRootWindow(dpy);
+    win->scr = DefaultScreen(dpy);
+
+    if (! find_true_color_visual (dpy, DefaultScreen (dpy), &xvi)) {
+       printf ("No TrueColor visual available, exiting.\n");
+       exit (1);
+    }
+
+    wattr.event_mask = ExposureMask | StructureNotifyMask;
+    wattr.background_pixel = 0;
+    wattr.border_pixel = 0;
+    wattr.colormap = XCreateColormap (dpy, root, xvi.visual, AllocNone);
+    wmask = CWEventMask | CWBackPixel | CWBorderPixel | CWColormap;
+
+    win->win = XCreateWindow(dpy, root, 0, 0,
+                            win->width, win->height, 0,
+                            xvi.depth, InputOutput, xvi.visual,
+                            wmask, &wattr);
+
+    win->pix = XCreatePixmap(dpy, win->win, win->width, win->height, xvi.depth);
+    gcv.foreground = WhitePixel(dpy, win->scr);
+    win->gc = XCreateGC(dpy, win->pix, GCForeground, &gcv);
+    XFillRectangle(dpy, win->pix, win->gc, 0, 0, win->width, win->height);
+
+    for (i=0; i < ARRAY_SIZE(key_binding); i++) {
+       KeySym keysym;
+       keysym = XStringToKeysym(key_binding[i].key);
+       if (keysym == NoSymbol)
+           fprintf(stderr, "ERROR: No keysym for \"%s\"\n", key_binding[i].key);
+       else
+           key_binding[i].keycode = XKeysymToKeycode(dpy, keysym);
+    }
+
+    win->active = 0;
+    win->spline = DEFAULT_SPLINE;
+    win->tolerance = DEFAULT_TOLERANCE;
+    win->line_width = DEFAULT_LINE_WIDTH;
+    win->line_cap = DEFAULT_LINE_CAP;
+    win->zoom = DEFAULT_ZOOM;
+    win->xtrans = DEFAULT_XTRANS;
+    win->ytrans = DEFAULT_YTRANS;
+
+    win->click = 0;
+    win->drag_pt.x = 0.0;
+    win->drag_pt.y = 0.0;
+
+    win_refresh(win);
+    win->needs_refresh = 0;
+
+    win_select_events(win);
+
+    XMapWindow(dpy, win->win);
+}
+
+static void
+win_deinit(win_t *win)
+{
+    XFreeGC(win->dpy, win->gc);
+    XFreePixmap(win->dpy, win->pix);
+    XDestroyWindow(win->dpy, win->win);
+}
+
+static void
+win_select_events(win_t *win)
+{
+    win->event_mask = 
+         ButtonPressMask
+       | ButtonReleaseMask
+       | PointerMotionMask
+       | KeyPressMask
+       | StructureNotifyMask
+       | ExposureMask;
+    XSelectInput(win->dpy, win->win, win->event_mask);
+
+}
+
+static char *
+get_callback_doc(void *callback)
+{
+    int i;
+
+    for (i=0; i < ARRAY_SIZE(callback_doc); i++)
+       if (callback_doc[i].callback == callback)
+           return callback_doc[i].doc;
+
+    return "<undocumented function>";
+}
+
+static void
+win_print_help(win_t *win)
+{
+    int i;
+
+    printf("A cairo spline demonstration\n");
+    printf("Click and drag to move the spline or adjust its controls. Or:\n\n");
+
+    for (i=0; i < ARRAY_SIZE(key_binding); i++)
+       if (! key_binding[i].is_alias)
+           printf("%s:\t%s\n",
+                  key_binding[i].key,
+                  get_callback_doc(key_binding[i].callback));
+}
+
+static void
+win_handle_button_press(win_t *win, XButtonEvent *bev)
+{
+    win->click = 1;
+    win->drag_pt.x = bev->x;
+    win->drag_pt.y = bev->y;
+    win->needs_refresh = 1;
+}
+
+static void
+win_handle_motion(win_t *win, XMotionEvent *mev)
+{
+    int i;
+
+    if (win->active == 0)
+       return;
+
+    for (i = 0; i < 4; i++) {
+       if (((1<<i) & win->active) == 0)
+           continue;
+       win->spline.pt[i].x += (mev->x - win->drag_pt.x) / win->zoom;
+       win->spline.pt[i].y += (mev->y - win->drag_pt.y) / win->zoom;
+
+       win->needs_refresh = 1;
+    }
+
+    win->drag_pt.x = mev->x;
+    win->drag_pt.y = mev->y;
+}
+
+static int
+win_handle_key_press(win_t *win, XKeyEvent *kev)
+{
+    int i;
+
+    for (i=0; i < ARRAY_SIZE(key_binding); i++)
+       if (key_binding[i].keycode == kev->keycode)
+           return (key_binding[i].callback)(win);
+       
+    return 0;
+}
+
+static void
+win_grow_pixmap(win_t *win)
+{
+    Pixmap new;
+
+    new = XCreatePixmap(win->dpy, win->win, win->width, win->height, DefaultDepth (win->dpy, win->scr));
+    XFillRectangle(win->dpy, new, win->gc, 0, 0, win->width, win->height);
+    XCopyArea(win->dpy, win->pix, new, win->gc, 0, 0, win->width, win->height, 0, 0);
+    XFreePixmap(win->dpy, win->pix);
+    win->pix = new;
+    win->needs_refresh = 1;
+}
+
+static void
+win_handle_configure(win_t *win, XConfigureEvent *cev)
+{
+    int has_grown = 0;
+
+    if (cev->width > win->width || cev->height > win->height) {
+       has_grown = 1;
+    }
+
+    win->width = cev->width;
+    win->height = cev->height;
+
+    if (has_grown) {
+       win_grow_pixmap(win);
+    }
+}
+
+static void
+win_handle_expose(win_t *win, XExposeEvent *eev)
+{
+    XCopyArea(win->dpy, win->pix, win->win, win->gc,
+             eev->x, eev->y, eev->width, eev->height,
+             eev->x, eev->y);
+}
+
+static void
+win_handle_events(win_t *win)
+{
+    int done;
+    XEvent xev;
+
+    while (1) {
+
+       if (!XPending(win->dpy) && win->needs_refresh) {
+           win_refresh(win);
+           win->needs_refresh = 0;
+       }
+
+       XNextEvent(win->dpy, &xev);
+
+       switch(xev.type) {
+       case ButtonPress:
+           win_handle_button_press(win, &xev.xbutton);
+           break;
+       case MotionNotify:
+           win_handle_motion(win, &xev.xmotion);
+           break;
+       case ButtonRelease:
+           win->click = 0;
+           win->active = 0;
+           break;
+       case KeyPress:
+           done = win_handle_key_press(win, &xev.xkey);
+           if (done)
+               return;
+           break;
+       case ConfigureNotify:
+           win_handle_configure(win, &xev.xconfigure);
+           break;
+       case Expose:
+           win_handle_expose(win, &xev.xexpose);
+           break;
+       }
+    }
+}
+
+
+/* Callbacks */
+
+static int
+quit_cb(win_t *win)
+{
+    return 1;
+}
+
+static int
+print_spline_cb(win_t *win)
+{
+    pt_t *pt = win->spline.pt;
+
+    printf("{ { %.20g, %.20g }, { %.20g, %.20g }, { %.20g, %.20g }, { %.20g, %.20g } }\n",
+          pt[0].x, pt[0].y,
+          pt[1].x, pt[1].y,
+          pt[2].x, pt[2].y,
+          pt[3].x, pt[3].y);
+
+    return 0;
+}
+
+static int
+zoom_in_cb(win_t *win)
+{
+    win->zoom *= 1.1;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+trans_left_cb(win_t *win)
+{
+    win->xtrans -= win->width / 16.0;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+trans_right_cb(win_t *win)
+{
+    win->xtrans += win->width / 16.0;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+trans_up_cb(win_t *win)
+{
+    win->ytrans -= win->height / 16.0;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+trans_down_cb(win_t *win)
+{
+    win->ytrans += win->height / 16.0;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+zoom_out_cb(win_t *win)
+{
+    win->zoom /= 1.1;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+flatten_cb(win_t *win)
+{
+    win->tolerance *= 10;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+smooth_cb(win_t *win)
+{
+    win->tolerance /= 10;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+widen_line(win_t *win)
+{
+    win->line_width *= 2;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+narrow_line(win_t *win)
+{
+    win->line_width /= 2;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+butt_cap_cb(win_t *win)
+{
+    win->line_cap = CAIRO_LINE_CAP_BUTT;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}
+
+static int
+round_cap_cb(win_t *win)
+{
+    win->line_cap = CAIRO_LINE_CAP_ROUND;
+
+    win->needs_refresh = 1;
+
+    return 0;
+}