+/* 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;
+}