/* This file is part of darktable, Copyright (C) 2014-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/gdk_event_utils.h" #include "bauhaus/bauhaus.h" #include "common/imagebuf.h" #include "common/interpolation.h" #include "common/opencl.h" #include "common/math.h" #include "common/collection.h" #include "control/conf.h" #include "control/control.h" #include "develop/imageop.h" #include "develop/imageop_gui.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "iop/iop_api.h" #include #include #include #include #include #ifdef _OPENMP #include #endif // this is the version of the modules parameters, and includes version // information about compile-time dt DT_MODULE_INTROSPECTION(1, dt_iop_liquify_params_t) #define MAX_NODES 100 // max of nodes in one instance const int LOOKUP_OVERSAMPLE = 10; const int INTERPOLATION_POINTS = 100; // when interpolating bezier const float STAMP_RELOCATION = 0.1; // how many radii to move // stamp forward when // following a path #define CONF_RADIUS "plugins/darkroom/liquify/radius" #define CONF_ANGLE "plugins/darkroom/liquify/angle" #define CONF_STRENGTH "plugins/darkroom/liquify/strength" // enum of layers. sorted back to front. typedef enum { DT_LIQUIFY_LAYER_BACKGROUND, DT_LIQUIFY_LAYER_RADIUS, DT_LIQUIFY_LAYER_HARDNESS1, DT_LIQUIFY_LAYER_HARDNESS2, DT_LIQUIFY_LAYER_WARPS, DT_LIQUIFY_LAYER_PATH, DT_LIQUIFY_LAYER_CTRLPOINT1_HANDLE, DT_LIQUIFY_LAYER_CTRLPOINT2_HANDLE, DT_LIQUIFY_LAYER_RADIUSPOINT_HANDLE, DT_LIQUIFY_LAYER_HARDNESSPOINT1_HANDLE, DT_LIQUIFY_LAYER_HARDNESSPOINT2_HANDLE, DT_LIQUIFY_LAYER_STRENGTHPOINT_HANDLE, DT_LIQUIFY_LAYER_CENTERPOINT, DT_LIQUIFY_LAYER_CTRLPOINT1, DT_LIQUIFY_LAYER_CTRLPOINT2, DT_LIQUIFY_LAYER_RADIUSPOINT, DT_LIQUIFY_LAYER_HARDNESSPOINT1, DT_LIQUIFY_LAYER_HARDNESSPOINT2, DT_LIQUIFY_LAYER_STRENGTHPOINT, DT_LIQUIFY_LAYER_LAST } dt_liquify_layer_enum_t; typedef enum { DT_LIQUIFY_LAYER_FLAG_HIT_TEST = 1, ///< include layer in hit testing DT_LIQUIFY_LAYER_FLAG_PREV_SELECTED = 2, ///< show if previous node is selected DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED = 4, ///< show if node is selected DT_LIQUIFY_LAYER_FLAG_POINT_TOOL = 8, ///< show if point tool active DT_LIQUIFY_LAYER_FLAG_LINE_TOOL = 16, ///< show if line tool active DT_LIQUIFY_LAYER_FLAG_CURVE_TOOL = 32, ///< show if line tool active DT_LIQUIFY_LAYER_FLAG_NODE_TOOL = 64, ///< show if node tool active DT_LIQUIFY_LAYER_FLAG_ANY_TOOL = 8 + 16 + 32 + 64, } dt_liquify_layer_flag_enum_t; typedef struct { float red, green, blue, alpha; } dt_liquify_rgba_t; #define COLOR_NULL { 0.0, 0.0, 0.0, 0.8 } #define GREY { 0.3, 0.3, 0.3, 0.8 } #define LGREY { 0.8, 0.8, 0.8, 1.0 } #define COLOR_DEBUG { 0.9, 0.9, 0.0, 1.0 } static const dt_liquify_rgba_t DT_LIQUIFY_COLOR_SELECTED = { 1.0, 1.0, 1.0, 1.0 }; static const dt_liquify_rgba_t DT_LIQUIFY_COLOR_HOVER = { 1.0, 1.0, 1.0, 0.8 }; typedef struct { dt_liquify_layer_enum_t hover_master; ///< hover whenever master layer hovers, eg. to dt_liquify_rgba_t fg; ///< the foreground color for this layer dt_liquify_rgba_t bg; ///< the background color for this layer float opacity; ///< the opacity of this layer /// highlight the whole radius when only the /// radius point is hovered dt_liquify_layer_flag_enum_t flags; ///< various flags for layer const char *hint; ///< hint displayed when hovering } dt_liquify_layer_t; dt_liquify_layer_t dt_liquify_layers[] = { { DT_LIQUIFY_LAYER_BACKGROUND, COLOR_NULL, COLOR_NULL, 0.0, 0, }, { DT_LIQUIFY_LAYER_RADIUS, COLOR_DEBUG, COLOR_NULL, 0.25, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL, }, { DT_LIQUIFY_LAYER_HARDNESS1, COLOR_DEBUG, COLOR_NULL, 1.0, 0, }, { DT_LIQUIFY_LAYER_HARDNESS2, COLOR_DEBUG, COLOR_NULL, 1.0, 0, }, { DT_LIQUIFY_LAYER_WARPS, COLOR_DEBUG, LGREY, 0.5, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL, }, { DT_LIQUIFY_LAYER_PATH, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_CTRLPOINT1, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL, }, { DT_LIQUIFY_LAYER_CTRLPOINT2, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL, }, { DT_LIQUIFY_LAYER_RADIUSPOINT, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL, }, { DT_LIQUIFY_LAYER_HARDNESSPOINT1, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED, }, { DT_LIQUIFY_LAYER_HARDNESSPOINT2, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED, }, { DT_LIQUIFY_LAYER_STRENGTHPOINT, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL, }, { DT_LIQUIFY_LAYER_CENTERPOINT, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_CTRLPOINT1, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_CTRLPOINT2, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_RADIUSPOINT, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_HARDNESSPOINT1, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_HARDNESSPOINT2, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_NODE_TOOL | DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, }, { DT_LIQUIFY_LAYER_STRENGTHPOINT, GREY, LGREY, 1.0, DT_LIQUIFY_LAYER_FLAG_ANY_TOOL | DT_LIQUIFY_LAYER_FLAG_HIT_TEST, } }; typedef enum { DT_LIQUIFY_UI_WIDTH_THINLINE, DT_LIQUIFY_UI_WIDTH_THICKLINE, DT_LIQUIFY_UI_WIDTH_DOUBLELINE, DT_LIQUIFY_UI_WIDTH_GIZMO, DT_LIQUIFY_UI_WIDTH_GIZMO_SMALL, DT_LIQUIFY_UI_WIDTH_DEFAULT_RADIUS, DT_LIQUIFY_UI_WIDTH_DEFAULT_STRENGTH, DT_LIQUIFY_UI_WIDTH_MIN_DRAG, DT_LIQUIFY_UI_WIDTH_LAST } dt_liquify_ui_width_enum_t; float dt_liquify_ui_widths [] = { // value in 1/96 inch (that is: in pixels on a standard 96 dpi screen) 2.0, // DT_LIQUIFY_UI_WIDTH_THINLINE 3.0, // DT_LIQUIFY_UI_WIDTH_THICKLINE 3.0, // DT_LIQUIFY_UI_WIDTH_DOUBLELINE 9.0, // DT_LIQUIFY_UI_WIDTH_GIZMO 7.0, // DT_LIQUIFY_UI_WIDTH_GIZMO_SMALL 100.0, // DT_LIQUIFY_UI_WIDTH_DEFAULT_RADIUS, 50.0, // DT_LIQUIFY_UI_WIDTH_DEFAULT_STRENGTH, 3.0 // DT_LIQUIFY_UI_WIDTH_MIN_DRAG }; typedef enum { DT_LIQUIFY_WARP_TYPE_LINEAR, // $DESCRIPTION: "linear" A linear warp originating from one point. DT_LIQUIFY_WARP_TYPE_RADIAL_GROW, // $DESCRIPTION: "radial grow" A radial warp originating from one point. DT_LIQUIFY_WARP_TYPE_RADIAL_SHRINK, // $DESCRIPTION: "radial shrink" DT_LIQUIFY_WARP_TYPE_LAST } dt_liquify_warp_type_enum_t; typedef enum { DT_LIQUIFY_NODE_TYPE_CUSP, // $DESCRIPTION: "cusp" DT_LIQUIFY_NODE_TYPE_SMOOTH, // $DESCRIPTION: "smooth" DT_LIQUIFY_NODE_TYPE_SYMMETRICAL, // $DESCRIPTION: "symmetrical" DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH, // $DESCRIPTION: "autosmooth" DT_LIQUIFY_NODE_TYPE_LAST } dt_liquify_node_type_enum_t; typedef enum { DT_LIQUIFY_STATUS_NONE = 0, // $DESCRIPTION: "none" DT_LIQUIFY_STATUS_NEW = 1, // $DESCRIPTION: "new" DT_LIQUIFY_STATUS_INTERPOLATED = 2, // $DESCRIPTION: "interpolated" DT_LIQUIFY_STATUS_PREVIEW = 4, // $DESCRIPTION: "preview" DT_LIQUIFY_STATUS_LAST } dt_liquify_status_enum_t; // enumerates the shapes types we use. typedef enum { DT_LIQUIFY_PATH_INVALIDATED = 0, // $DESCRIPTION: "invalidated" DT_LIQUIFY_PATH_MOVE_TO_V1, // $DESCRIPTION: "move" DT_LIQUIFY_PATH_LINE_TO_V1, // $DESCRIPTION: "line" DT_LIQUIFY_PATH_CURVE_TO_V1, // $DESCRIPTION: "curve" } dt_liquify_path_data_enum_t; typedef struct { dt_liquify_path_data_enum_t type; dt_liquify_node_type_enum_t node_type; dt_liquify_layer_enum_t selected; dt_liquify_layer_enum_t hovered; int8_t prev; int8_t idx; int8_t next; } dt_liquify_path_header_t; // Scalars and vectors are represented here as points because the only // thing we can reasonably distort_transform are points. typedef struct { float complex point; float complex strength; ///< a point (the effective strength vector is: strength - point) float complex radius; ///< a point (the effective radius scalar is: cabs(radius - point)) float control1; ///< range 0.0 .. 1.0 == radius float control2; ///< range 0.0 .. 1.0 == radius dt_liquify_warp_type_enum_t type; dt_liquify_status_enum_t status; } dt_liquify_warp_t; typedef struct { float complex ctrl1; float complex ctrl2; } dt_liquify_node_t; // set up lots of alternative ways to get at the popular members. typedef struct { dt_liquify_path_header_t header; dt_liquify_warp_t warp; dt_liquify_node_t node; // extended node data } dt_liquify_path_data_t; typedef struct { dt_liquify_layer_enum_t layer; dt_liquify_path_data_t *elem; } dt_liquify_hit_t; static const dt_liquify_hit_t NOWHERE = { DT_LIQUIFY_LAYER_BACKGROUND, NULL }; typedef struct { dt_liquify_path_data_t nodes[MAX_NODES]; } dt_iop_liquify_params_t; typedef struct { int warp_kernel; } dt_iop_liquify_global_data_t; typedef struct { int node_index; // last node index inserted float complex last_mouse_pos; float complex last_button1_pressed_pos; GdkModifierType last_mouse_mods; ///< GDK modifiers at the time mouse button was pressed. dt_liquify_hit_t last_hit; ///< Element last hit with mouse button. dt_liquify_hit_t dragging; ///< Element being dragged with mouse button. dt_liquify_path_data_t *temp; ///< Points to the element under construction or NULL. dt_liquify_status_enum_t status; ///< Various flags. GtkLabel *label; GtkToggleButton *btn_point_tool, *btn_line_tool, *btn_curve_tool, *btn_node_tool; gboolean creation_continuous; gboolean just_started; } dt_iop_liquify_gui_data_t; // this returns a translatable name const char *name() { return _("liquify"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("distort parts of the image"), _("creative"), _("linear, RGB, scene-referred"), _("geometric, RGB"), _("linear, RGB, scene-referred")); } int default_group() { return IOP_GROUP_CORRECT | IOP_GROUP_EFFECTS; } int flags() { return IOP_FLAGS_SUPPORTS_BLENDING; } int operation_tags() { return IOP_TAG_DISTORT | IOP_TAG_GEOMETRY; } int operation_tags_filter() { // switch off cropping, we want to see the full image. return IOP_TAG_DECORATION | IOP_TAG_CROPPING; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } /******************************************************************************/ /* Code common to op-engine and gui. */ /******************************************************************************/ static inline float get_rot(const dt_liquify_warp_type_enum_t warp_type) { if(warp_type == DT_LIQUIFY_WARP_TYPE_RADIAL_SHRINK) return M_PI_F; else return 0.0f; } static dt_liquify_path_data_t *node_alloc(dt_iop_liquify_params_t *p, int *node_index) { for(int k=0; knodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) { *node_index = k; p->nodes[k].header.idx = k; p->nodes[k].header.next = p->nodes[k].header.prev = -1; p->nodes[k].header.selected = p->nodes[k].header.hovered = 0; return &p->nodes[k]; } return NULL; } static dt_liquify_path_data_t *node_prev(dt_iop_liquify_params_t *p, const dt_liquify_path_data_t *n) { if(n->header.prev == -1) return NULL; else return &p->nodes[n->header.prev]; } static dt_liquify_path_data_t *node_get(dt_iop_liquify_params_t *p, const int index) { if(index > -1 && index < MAX_NODES) return &p->nodes[index]; else return NULL; } static dt_liquify_path_data_t *node_next(dt_iop_liquify_params_t *p, const dt_liquify_path_data_t *n) { if(n->header.next == -1) return NULL; else return &p->nodes[n->header.next]; } static void node_insert_before(dt_iop_liquify_params_t *p, dt_liquify_path_data_t *this, dt_liquify_path_data_t *new) { new->header.next = this->header.idx; new->header.prev = this->header.prev; if(this->header.prev != -1) p->nodes[this->header.prev].header.next = new->header.idx; this->header.prev = new->header.idx; } static void node_gc(dt_iop_liquify_params_t *p) { int last=0; for(last=MAX_NODES-1; last>0; last--) if(p->nodes[last].header.type != DT_LIQUIFY_PATH_INVALIDATED) break; int k = 0; while(k<=last) { if(p->nodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) { for(int e=0; e= k) p->nodes[e] = p->nodes[e+1]; // update all pointers above position k if(e >= k) p->nodes[e].header.idx--; if(p->nodes[e].header.prev >= k) p->nodes[e].header.prev--; if(p->nodes[e].header.next >= k) p->nodes[e].header.next--; } last--; } else k++; } // invalidate all nodes beyond the last moved one for(int l=last+1; lnodes[l].header.type = DT_LIQUIFY_PATH_INVALIDATED; } static void node_delete(dt_iop_liquify_params_t *p, dt_liquify_path_data_t *this) { dt_liquify_path_data_t *prev = node_prev(p, this); dt_liquify_path_data_t *next = node_next(p, this); if(!prev && next) { next->header.prev = -1; next->header.type = DT_LIQUIFY_PATH_MOVE_TO_V1; } else if(prev) { prev->header.next = this->header.next; if(next) next->header.prev = prev->header.idx; } this->header.prev = this->header.next = - 1; this->header.type = DT_LIQUIFY_PATH_INVALIDATED; node_gc(p); } static void path_delete(dt_iop_liquify_params_t *p, dt_liquify_path_data_t *this) { dt_liquify_path_data_t *n = this; // clear next while(n) { n->header.type = DT_LIQUIFY_PATH_INVALIDATED; n = node_next(p, n); } // clear prev n = this; while(n) { n->header.type = DT_LIQUIFY_PATH_INVALIDATED; n = node_prev(p, n); } node_gc(p); } /** * The functions in this group help transform between coordinate * systems. (In darktable nomenclature this kind of transform is * called 'distort'). * * The transforms between coordinate systems are not necessarily * perspective transforms (eg. lensfun), therefore no transformation * matrix can be specified for them, instead all points to be * transformed have to be passed through a darktable function. * * Note: only points may be sensibly 'distorted'. Vectors and scalars * don't have a meaningful 'distort'. * * * Explanation of the coordinate systems used by this module: * * RAW: These are sensor coordinates. They go from x=0, y=0 to x=, y=. In a landscape picture (rotated 0°) x=0, * y=0 will be top left. In a portrait picture (rotated 90° * counter-clockwise) x=0, y=0 will be bottom left. * * The user probably wants liquified regions to be anchored to the * motive when more transformations are added, eg. a different * cropping of the image. For this to work, all coordinates we store * or pass between gui and pipe are RAW sensor coordinates. * * * PIECE: These are coordinates based on the size of our pipe piece. * They go from x=0, y=0 to x=, y=. * PIECE coordinates should only be used while processing an image. * * Note: Currently (as of darktable 1.7) there are no geometry * transforms between RAW and PIECE (our module coming very early in * the pipe), but this may change in a later release. By allowing for * them now, we are prepared for pipe order re-shuffeling. * * * CAIRO: These are coordinates based on the cairo view. The extent * of the longest side of the cooked picture is normalized to 1.0. * x=0, y=0 is the top left of the cooked picture. x=u, y=v is the * bottom right of a cooked picture with u<=1, v<=1 and either u==1 or * v==1 depending on orientation. Note that depending on pan and zoom * cairo view borders and cooked picture borders may intersect in many * ways. * * The normalized scale helps in choosing default values for vectors and * radii. * * VIEW: These are coordinates based on the cairo view. x=0, y=0 being * top left and x=, y= being bottom right. * The parameters to the mouse_moved, button_pressed, and * button_released functions are in this system. * * This system is also used for sizing ui-elements. They cannot be * expressed in CAIRO coordinates because they should not change size * when zooming the picture. * * To get sensible sizes for ui elements and default warps use this * relation between the scales: CAIRO * get_zoom_scale () == VIEW. * */ typedef struct { dt_develop_t *develop; dt_dev_pixelpipe_t *pipe; float from_scale; float to_scale; int transf_direction; } distort_params_t; static void _distort_paths_locked(const dt_iop_module_t *self, const distort_params_t *params, const dt_iop_liquify_params_t *p) { int len = 0; // count nodes for(int k = 0; k < MAX_NODES; k++) { const dt_liquify_path_data_t *data = (dt_liquify_path_data_t *) &p->nodes[k]; if(data->header.type == DT_LIQUIFY_PATH_INVALIDATED) break; switch(data->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: len += 2; // fall thru case DT_LIQUIFY_PATH_MOVE_TO_V1: case DT_LIQUIFY_PATH_LINE_TO_V1: len += 3; break; default: break; } } // create buffer with all points float *buffer = malloc(sizeof(float) * 2 * len); float *b = buffer; for(int k = 0; k < MAX_NODES; k++) { const dt_liquify_path_data_t *data = (dt_liquify_path_data_t *) &p->nodes[k]; if(data->header.type == DT_LIQUIFY_PATH_INVALIDATED) break; switch(data->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: *b++ = crealf(data->node.ctrl1) / params->from_scale; *b++ = cimagf(data->node.ctrl1) / params->from_scale; *b++ = crealf(data->node.ctrl2) / params->from_scale; *b++ = cimagf(data->node.ctrl2) / params->from_scale; // fall thru case DT_LIQUIFY_PATH_MOVE_TO_V1: case DT_LIQUIFY_PATH_LINE_TO_V1: *b++ = crealf(data->warp.point) / params->from_scale; *b++ = cimagf(data->warp.point) / params->from_scale; *b++ = crealf(data->warp.strength) / params->from_scale; *b++ = cimagf(data->warp.strength) / params->from_scale; *b++ = crealf(data->warp.radius) / params->from_scale; *b++ = cimagf(data->warp.radius) / params->from_scale; break; default: break; } } if(params->transf_direction == DT_DEV_TRANSFORM_DIR_ALL) { dt_dev_distort_transform_plus(params->develop, params->pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_EXCL, buffer, len); dt_dev_distort_transform_plus(params->develop, params->pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, buffer, len); } else dt_dev_distort_transform_plus(params->develop, params->pipe, self->iop_order, params->transf_direction, buffer, len); // record back the transformed points b = buffer; for(int k = 0; k < MAX_NODES; k++) { dt_liquify_path_data_t *data = (dt_liquify_path_data_t *) &p->nodes[k]; if(data->header.type == DT_LIQUIFY_PATH_INVALIDATED) break; switch(data->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: data->node.ctrl1 = (b[0] + b[1] * I) * params->to_scale; b += 2; data->node.ctrl2 = (b[0] + b[1] * I) * params->to_scale; b += 2; // fall thru case DT_LIQUIFY_PATH_MOVE_TO_V1: case DT_LIQUIFY_PATH_LINE_TO_V1: data->warp.point = (b[0] + b[1] * I) * params->to_scale; b += 2; data->warp.strength = (b[0] + b[1] * I) * params->to_scale; b += 2; data->warp.radius = (b[0] + b[1] * I) * params->to_scale; b += 2; break; default: break; } } free(buffer); } static void distort_paths_raw_to_piece(const dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const float roi_in_scale, dt_iop_liquify_params_t *p) { const distort_params_t params = { self->dev, pipe, pipe->iscale, roi_in_scale, DT_DEV_TRANSFORM_DIR_BACK_EXCL }; dt_pthread_mutex_lock(&self->dev->history_mutex); _distort_paths_locked(self, ¶ms, p); dt_pthread_mutex_unlock(&self->dev->history_mutex); } // op-engine code static inline float complex normalize(const float complex v) { if(cabsf(v) < 0.000001f) return 1.0f; return v / cabsf(v); } // calculate the linear blend of scalars a and b static inline float mix(const float a, const float b, const float t) { return a + (b - a) * t; } // calculate the linear blend of points p0 and p1 static inline float complex cmix(const float complex p0, const float complex p1, const float t) { return p0 + (p1 - p0) * t; } static void mix_warps(dt_liquify_warp_t *result, const dt_liquify_warp_t *warp1, const dt_liquify_warp_t *warp2, const complex float pt, const float t) { result->type = warp1->type; result->control1 = mix (warp1->control1, warp2->control1, t); result->control2 = mix (warp1->control2, warp2->control2, t); const float radius = mix(cabsf(warp1->radius - warp1->point), cabsf(warp2->radius - warp2->point), t); result->radius = pt + radius; const complex float p1 = warp1->strength - warp1->point; const complex float p2 = warp2->strength - warp2->point; float arg1 = cargf(p1); float arg2 = cargf(p2); gboolean invert = FALSE; if(arg1 > .0f && arg2 < -M_PI_2f) { invert = TRUE; arg1 = M_PI_F - arg1; arg2 = -M_PI_F - arg2; } else if(arg1 < -M_PI_2f && arg2 > .0f) { invert = TRUE; arg1 = -M_PI_F - arg1; arg2 = M_PI_F - arg2; } const float r = mix(cabsf(p1), cabsf(p2), t); const float phi = invert ? M_PI_F - mix(arg1, arg2, t) : mix(arg1, arg2, t); result->strength = pt + r * cexpf(phi * I); result->point = pt; } // Interpolate a cubic bezier spline into a series of points. static void interpolate_cubic_bezier(const float complex p0, const float complex p1, const float complex p2, const float complex p3, float complex buffer[], const int n) { // convert from bernstein basis to polynomial basis to get faster math // See: http://www.tinaja.com/glib/cubemath.pdf const float complex A = p3 - 3 * p2 + 3 * p1 - p0; const float complex B = 3 * p2 - 6 * p1 + 3 * p0; const float complex C = 3 * p1 - 3 * p0; const float complex D = p0; float complex *buf = buffer; const float step = 1.0f / n; float t = step; *buf++ = p0; for(int i = 1; i < n - 1; ++i) { *buf++ = ((A * t + B) * t + C) * t + D; t += step; } *buf = p3; } static GList *interpolate_paths(dt_iop_liquify_params_t *p); /* Get approx. arc length of a curve. Used to approximate the arc length of a bezier curve. */ static float get_arc_length(const float complex points[], const int n_points) { float length = 0.0f; for(int i = 1; i < n_points; i++) length += cabsf(points[i-1] - points[i]); return length; } typedef struct { int i; float length; } restart_cookie_t; /* Interpolate a point on a curve at a specified arc length. In a bezier curve the parameter t usually does not correspond to the arc length. */ static float complex point_at_arc_length(const float complex points[], const int n_points, const float arc_length, restart_cookie_t *restart) { float length = restart ? restart->length : 0.0f; for(int i = restart ? restart->i : 1; i < n_points; i++) { const float prev_length = length; length += cabsf(points[i-1] - points[i]); if(length >= arc_length) { const float t = (arc_length - prev_length) / (length - prev_length); if(restart) { restart->i = i; restart->length = prev_length; } return cmix(points[i - 1], points[i], t); } } return points[n_points - 1]; } /* Build a lookup table for the warp intensity. Lookup table for the warp intensity function: f(x). The warp intensity function determines how much a pixel is influenced by the warp depending from its distance from a central point. Boundary conditions: f(0) must be 1 and f(@a distance) must be 0. f'(0) and f'(@a distance) must both be 0 or we'll get artifacts on the picture. Implementation: a bezier curve with p0 = 0, 1 and p3 = 1, 0. p1 is defined by @a control1, 1 and p2 by @a control1, 0. Because a bezier is parameterized on t, we have to reparameterize on x, which we do by linear interpolation. Octave code: t = linspace(0,1,100); grid; hold on; for steps = 0:0.1:1 cpoints = [0,1; steps,1; steps,0; 1,0]; bezier = cbezier2poly(cpoints); x = polyval(bezier(1,:), t); y = polyval(bezier(2,:), t); plot(t, interp1(x, y, t)); end hold off; */ static float *build_lookup_table(const int distance, const float control1, const float control2) { float complex *clookup = dt_alloc_align_type(float complex, (distance + 2)); float *lookup = dt_alloc_align_float((size_t)(distance + 2)); if(!clookup || !lookup) { dt_free_align(clookup); dt_free_align(lookup); return NULL; } interpolate_cubic_bezier(I, control1 + I, control2, 1.0, clookup, distance + 2); // reparameterize bezier by x and keep only y values float *ptr = lookup; float complex *cptr = clookup + 1; const float complex *cptr_end = cptr + distance; const float step = 1.0f / (float) distance; float x = 0.0f; *ptr++ = 1.0f; for(int i = 1; i < distance && cptr < cptr_end; i++) { x += step; while(crealf(*cptr) < x && cptr < cptr_end) cptr++; const float dx1 = crealf(cptr[0] - cptr[-1]); const float dx2 = x - crealf(cptr[-1]); *ptr++ = cimagf(cptr[0]) +(dx2 / dx1) * (cimagf(cptr[0]) - cimagf(cptr[-1])); } *ptr++ = 0.0f; dt_free_align(clookup); return lookup; } static void compute_round_stamp_extent(cairo_rectangle_int_t *const restrict stamp_extent, const dt_liquify_warp_t *const restrict warp) { const int iradius = round(cabsf(warp->radius - warp->point)); assert(iradius > 0); stamp_extent->x = stamp_extent->y = -iradius; stamp_extent->x += crealf(warp->point); stamp_extent->y += cimagf(warp->point); stamp_extent->width = stamp_extent->height = 2 * iradius + 1; } /* Compute a round(circular) stamp. The stamp is a vector field of warp vectors around a center point. In a linear warp the center point gets a warp of @a strength, while points on the circumference of the circle get no warp at all. Between center and circumference the warp magnitude tapers off following a curve (see: build_lookup_table()). Note that when applying a linear stamp to a path, we will first rotate its vectors into the direction of the path. In a radial warp the center point and the points on the circumference get no warp. Between center and circumference the warp magnitude follows a curve with maximum at radius / 0.5 Our stamp is stored in a rectangular region. */ static void apply_round_stamp(const dt_liquify_warp_t *const restrict warp, float complex *global_map, const cairo_rectangle_int_t *const restrict global_map_extent) { const size_t iradius = round(cabsf(warp->radius - warp->point)); assert(iradius > 0); // 0.5 is factored in so the warp starts to degenerate when the // strength arrow crosses the warp radius. float complex strength = 0.5f * (warp->strength - warp->point); strength = (warp->status & DT_LIQUIFY_STATUS_INTERPOLATED) ? (strength * STAMP_RELOCATION) : strength; const float abs_strength = cabsf(strength) * (warp->type == DT_LIQUIFY_WARP_TYPE_RADIAL_SHRINK ? -1.0f : 1.0f); // lookup table: map of distance from center point => warp const size_t table_size = iradius * LOOKUP_OVERSAMPLE; const float *const restrict lookup_table = build_lookup_table(table_size, warp->control1, warp->control2); if(!lookup_table) { dt_free_align((void*)lookup_table); dt_print(DT_DEBUG_ALWAYS,"[liquify] out of memory, round stamp skipped"); return; } // point into the global distortion map at the center of the circle const size_t global_width = global_map_extent->width; const size_t stamp_x = (size_t) round(crealf(warp->point)); const size_t stamp_y = (size_t) round(cimagf(warp->point)); float complex *const center = (global_map + (stamp_y - global_map_extent->y) * global_width + stamp_x - global_map_extent->x); // The expensive operation here is hypotf (). By dividing the // circle in quadrants and doing only the inside we have to calculate // hypotf only for PI / 16 = 0.196 of the stamp area. // We don't do octants to avoid false sharing of cache lines between threads. DT_OMP_FOR(dt_omp_sharedconst(LOOKUP_OVERSAMPLE)) for(size_t y = 0; y <= iradius; y++) { const float complex y_i = y * I; const float y2 = y*y; for(size_t x = 0; x <= iradius; x++) { // faster than hypotf(), and we know we won't have overflow or denormals const float dist = sqrtf((float)x*x + y2); const size_t idist = round(dist * LOOKUP_OVERSAMPLE); if(idist >= table_size) // idist will only grow bigger in this row break; // pointers into the 4 quadrants of the circle // quadrant count is ccw from positive x-axis float complex *const q1 = center - y * global_width + x; float complex *const q2 = center - y * global_width - x; float complex *const q3 = center + y * global_width - x; float complex *const q4 = center + y * global_width + x; if(warp->type == DT_LIQUIFY_WARP_TYPE_LINEAR) { const float complex w_strength = -strength * lookup_table[idist]; *q1 += w_strength; if(x!=0) *q2 += w_strength; if(x!=0&&y!=0) *q3 += w_strength; if(y!=0) *q4 += w_strength; } else { // DT_LIQUIFY_WARP_TYPE_RADIAL_GROW or _SHRINK // abs_strength is negative for _SHRINK const float abs_lookup = abs_strength * lookup_table[idist] / iradius; *q1 -= abs_lookup * (x - y_i); if(x!=0) *q2 += abs_lookup * (x + y_i); if(x!=0&&y!=0) *q3 += abs_lookup * (x - y_i); if(y!=0) *q4 -= abs_lookup * (x + y_i); } } } dt_free_align((void*) lookup_table); } /* Applies the global distortion map to the picture. The distortion map maps points to the position from where the new color of the point should be sampled from. The distortion map is in relative device coords. */ static void _apply_global_distortion_map(dt_iop_module_t *self, const dt_dev_pixelpipe_iop_t *piece, const float *const restrict in, float *const restrict out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const float complex *const map, const cairo_rectangle_int_t *extent) { const int ch = piece->colors; const int ch_width = ch * roi_in->width; const dt_interpolation_t *const interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); const size_t min_y = MAX(roi_out->y, extent->y); const size_t max_y = MIN(roi_out->y + roi_out->height, extent->y + extent->height); DT_OMP_FOR() for(size_t y = min_y; y < max_y; y++) { const size_t min_x = MAX(roi_out->x, extent->x); const size_t max_x = MIN(roi_out->x + roi_out->width, extent->x + extent->width); const float complex *row = map + (y - extent->y) * extent->width + (min_x - extent->x); float* out_sample = out + ch * ((y - roi_out->y) * roi_out->width - roi_out->x); for(size_t x = min_x; x < max_x; x++) { if(*row != 0) // point actually warped? { if(ch == 1) // handle masks out_sample[x] = CLIP(dt_interpolation_compute_sample(interpolation, in, x + crealf(*row) - roi_in->x, y + cimagf(*row) - roi_in->y, roi_in->width, roi_in->height, 1, ch_width)); else dt_interpolation_compute_pixel4c( interpolation, in, out_sample + ch*x, x + crealf(*row) - roi_in->x, y + cimagf(*row) - roi_in->y, roi_in->width, roi_in->height, ch_width); } ++row; } } } // calculate the map extent. static GSList *_get_map_extent(const dt_iop_roi_t *roi_out, const GList *interpolated, cairo_rectangle_int_t *map_extent) { const cairo_rectangle_int_t roi_out_rect = { roi_out->x, roi_out->y, roi_out->width, roi_out->height }; cairo_region_t *roi_out_region = cairo_region_create_rectangle(&roi_out_rect); cairo_region_t *map_region = cairo_region_create(); GSList *in_roi = NULL; for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *warp = ((dt_liquify_warp_t *) i->data); cairo_rectangle_int_t r; compute_round_stamp_extent(&r, warp); // add extent if not entirely outside the roi if(cairo_region_contains_rectangle(roi_out_region, &r) != CAIRO_REGION_OVERLAP_OUT) { cairo_region_union_rectangle(map_region, &r); in_roi = g_slist_prepend(in_roi, i->data); } } // return the paths and the extent of all paths cairo_region_get_extents(map_region, map_extent); cairo_region_destroy(map_region); cairo_region_destroy(roi_out_region); return g_slist_reverse(in_roi); } static float complex *create_global_distortion_map(const cairo_rectangle_int_t *map_extent, const GSList *interpolated, const gboolean inverted) { const int mapsize = map_extent->width * map_extent->height; if(mapsize == 0) { // there are no pixels for which we need distortion info, so // return right away caller will see the NULL and bypass any // further processing of the points it wants to distort return NULL; } // allocate distortion map big enough to contain all paths float complex *map = dt_alloc_align_type(float complex, mapsize); memset(map, 0, sizeof(float complex) * mapsize); // build map for(const GSList *i = interpolated; i; i = g_slist_next(i)) { const dt_liquify_warp_t *warp = ((dt_liquify_warp_t *) i->data); apply_round_stamp(warp, map, map_extent); } if(inverted) { float complex * const imap = dt_alloc_align_type(float complex, mapsize); memset(imap, 0, sizeof(float complex) * mapsize); // copy map into imap(inverted map). // imap [ n + dx(map[n]) , n + dy(map[n]) ] = -map[n] DT_OMP_FOR() for(int y = 0; y < map_extent->height; y++) { const float complex *const row = map + y * map_extent->width; for(int x = 0; x < map_extent->width; x++) { const float complex d = row[x]; // compute new position (nx,ny) given the displacement d const int nx = x + (int)crealf(d); const int ny = y + (int)cimagf(d); // if the point falls into the extent, set it if(nx>0 && nxwidth && ny>0 && nyheight) imap[nx + ny * map_extent->width] = -d; } } dt_free_align((void *) map); // now just do a pass to avoid gap with a displacement of zero, // note that we do not need high precision here as the inverted // distortion mask is only used to compute a final displacement of // points. DT_OMP_FOR() for(int y = 0; y < map_extent->height; y++) { float complex *const row = imap + y * map_extent->width; float complex last[2] = { 0, 0 }; for(int x = 0; x < map_extent->width / 2 + 1; x++) { float complex *cl = row + x; float complex *cr = row + map_extent->width - x; if(x!=0) { if(*cl == 0) *cl = last[0]; if(*cr == 0) *cr = last[1]; } last[0] = *cl; last[1] = *cr; } } map = imap; } return map; } static void _build_global_distortion_map(const dt_iop_module_t *self, const dt_dev_pixelpipe_iop_t *piece, const float scale, const dt_iop_roi_t *roi, cairo_rectangle_int_t *map_extent, const gboolean inverted, float complex **map) { // copy params dt_iop_liquify_params_t copy_params; memcpy(©_params, piece->data, sizeof(dt_iop_liquify_params_t)); distort_paths_raw_to_piece(self, piece->pipe, scale, ©_params); GList *interpolated = interpolate_paths(©_params); GSList *interpolated_in_roi = _get_map_extent(roi, interpolated, map_extent); if(map) *map = create_global_distortion_map(map_extent, interpolated_in_roi, inverted); g_slist_free(interpolated_in_roi); g_list_free_full(interpolated, free); } void modify_roi_in(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in) { // Because we move pixels, and we may have to sample a pixel from // outside roi_in, we need to expand roi_in to contain all our // paths. But we may ignore paths completely outside of roi_out. *roi_in = *roi_out; cairo_rectangle_int_t extent; _build_global_distortion_map(self, piece, roi_in->scale, roi_out, &extent, FALSE, NULL); const cairo_rectangle_int_t pipe_rect = { 0, 0, lroundf(piece->buf_in.width * roi_in->scale), lroundf(piece->buf_in.height * roi_in->scale) }; cairo_rectangle_int_t roi_in_rect = { roi_in->x, roi_in->y, roi_in->width, roi_in->height }; cairo_region_t *roi_in_region = cairo_region_create_rectangle(&roi_in_rect); // (eventually) extend roi_in cairo_region_union_rectangle(roi_in_region, &extent); // and clamp to pipe extent cairo_region_intersect_rectangle(roi_in_region, &pipe_rect); // write new extent to roi_in cairo_region_get_extents(roi_in_region, &roi_in_rect); roi_in->x = roi_in_rect.x; roi_in->y = roi_in_rect.y; roi_in->width = roi_in_rect.width; roi_in->height = roi_in_rect.height; // cleanup cairo_region_destroy(roi_in_region); } static gboolean _distort_xtransform(const dt_iop_module_t *self, const dt_dev_pixelpipe_iop_t *piece, float *const restrict points, const size_t points_count, const gboolean inverted) { const float scale = piece->iscale; // compute the extent of all points (all computations are done in RAW coordinate) float xmin = FLT_MAX, xmax = FLT_MIN, ymin = FLT_MAX, ymax = FLT_MIN; DT_OMP_FOR(if(points_count > 100) reduction(min:xmin, ymin) reduction(max:xmax, ymax)) for(size_t i = 0; i < points_count * 2; i += 2) { const float x = points[i] * scale; const float y = points[i + 1] * scale; xmin = fmin(xmin, x); xmax = fmax(xmax, x); ymin = fmin(ymin, y); ymax = fmax(ymax, y); } cairo_rectangle_int_t extent = { .x = (int)(xmin - .5), .y = (int)(ymin - .5), .width = (int)(xmax - xmin + 2.5), .height = (int)(ymax - ymin + 2.5) }; if(extent.width > 0 && extent.height > 0) { // we need to adjust the extent to be the union enclosing all the // points (currently in extent) and the warps that are in // (possibly partly) in this same region. const dt_iop_roi_t roi_in = { .x = extent.x, .y = extent.y, .width = extent.width, .height = extent.height }; float complex *map = NULL; _build_global_distortion_map(self, piece, scale, &roi_in, &extent, inverted, &map); if(map == NULL) return FALSE; const int map_size = extent.width * extent.height; const int x_last = extent.x + extent.width; const int y_last = extent.y + extent.height; // apply distortion to all points (this is a simple displacement // given by a vector at this same point in the map) DT_OMP_FOR(if(points_count > 100)) for(size_t i = 0; i < points_count; i++) { float *px = &points[i*2]; float *py = &points[i*2+1]; const float x = *px * scale; const float y = *py * scale; const int map_offset = ((int)(x - 0.5) - extent.x) + ((int)(y - 0.5) - extent.y) * extent.width; if(x >= extent.x && x < x_last && y >= extent.y && y < y_last && map_offset >= 0 && map_offset < map_size) { const float complex dist = map[map_offset] / scale; *px += crealf(dist); *py += cimagf(dist); } } dt_free_align((void *) map); } return TRUE; } static void start_drag(dt_iop_liquify_gui_data_t *g, const dt_liquify_layer_enum_t layer, dt_liquify_path_data_t *elem) { g->dragging.layer = layer; g->dragging.elem = elem; } static void end_drag(dt_iop_liquify_gui_data_t *g) { g->dragging = NOWHERE; } static gboolean is_dragging(const dt_iop_liquify_gui_data_t *g) { return g->dragging.elem != NULL; } gboolean distort_transform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *const restrict points, const size_t points_count) { return _distort_xtransform(self, piece, points, points_count, TRUE); } gboolean distort_backtransform(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, float *const restrict points, const size_t points_count) { return _distort_xtransform(self, piece, points, points_count, FALSE); } void distort_mask(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const float *const in, float *const out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { // 1. copy the whole image (we'll change only a small part of it) dt_iop_copy_image_roi(out, in, 1, roi_in, roi_out); // 2. build the distortion map cairo_rectangle_int_t map_extent; float complex *map = NULL; _build_global_distortion_map(self, piece, roi_in->scale, roi_out, &map_extent, FALSE, &map); if(map == NULL) return; // 3. apply the map if(map_extent.width != 0 && map_extent.height != 0) { const int ch = piece->colors; piece->colors = 1; _apply_global_distortion_map(self, piece, in, out, roi_in, roi_out, map, &map_extent); piece->colors = ch; } dt_free_align((void *) map); } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const in, void *const out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, in, out, roi_in, roi_out)) return; // 1. copy the whole image (we'll change only a small part of it) dt_iop_copy_image_roi(out, in, piece->colors, roi_in, roi_out); // 2. build the distortion map cairo_rectangle_int_t map_extent; float complex *map = NULL; _build_global_distortion_map(self, piece, roi_in->scale, roi_out, &map_extent, FALSE, &map); if(map == NULL) return; // 3. apply the map if(map_extent.width != 0 && map_extent.height != 0) _apply_global_distortion_map(self, piece, in, out, roi_in, roi_out, map, &map_extent); dt_free_align((void *)map); } #ifdef HAVE_OPENCL // compute lanczos kernel. See: // https://en.wikipedia.org/wiki/Lanczos_resampling#Lanczos_kernel static inline float lanczos(const float a, const float x) { if(fabsf(x) >= a) return 0.0f; if(fabsf(x) < FLT_EPSILON) return 1.0f; return (a * sinf(M_PI_F * x) * sinf(M_PI_F * x / a)) / (M_PI_F * M_PI_F * x * x); } // compute bicubic kernel. See: // https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm static inline float bicubic(const float a, const float x) { const float absx = fabsf(x); if(absx <= 1) return ((a + 2) * absx - (a + 3)) * absx * absx + 1; if(absx < 2) return ((a * absx - 5 * a) * absx + 8 * a) * absx - 4 * a; return 0.0f; } typedef struct { int size; int resolution; } dt_liquify_kernel_descriptor_t; typedef cl_mem cl_mem_t; typedef cl_int cl_int_t; static cl_int_t _apply_global_distortion_map_cl(const dt_iop_module_t *self, const dt_dev_pixelpipe_iop_t *piece, const cl_mem_t dev_in, const cl_mem_t dev_out, const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out, const float complex *map, const cairo_rectangle_int_t *map_extent) { cl_int_t err = CL_MEM_OBJECT_ALLOCATION_FAILURE; const dt_iop_liquify_global_data_t *gd = self->global_data; const int devid = piece->pipe->devid; const dt_interpolation_t *interpolation = dt_interpolation_new(DT_INTERPOLATION_USERPREF_WARP); dt_liquify_kernel_descriptor_t kdesc = { .size = 0, .resolution = 100 }; float *k = NULL; switch(interpolation->id) { case DT_INTERPOLATION_BILINEAR: kdesc.size = 1; kdesc.resolution = 1; k = malloc(sizeof(float) * 2); k[0] = 1.0f; k[1] = 0.0f; break; case DT_INTERPOLATION_BICUBIC: kdesc.size = 2; k = malloc(sizeof(float) * ((size_t)kdesc.size * kdesc.resolution + 1)); for(int i = 0; i <= kdesc.size * kdesc.resolution; ++i) k[i] = bicubic(-0.5f, (float) i / kdesc.resolution); break; case DT_INTERPOLATION_LANCZOS2: kdesc.size = 2; k = malloc(sizeof(float) * ((size_t)kdesc.size * kdesc.resolution + 1)); for(int i = 0; i <= kdesc.size * kdesc.resolution; ++i) k[i] = lanczos(2, (float) i / kdesc.resolution); break; case DT_INTERPOLATION_LANCZOS3: kdesc.size = 3; k = malloc(sizeof(float) * ((size_t)kdesc.size * kdesc.resolution + 1)); for(int i = 0; i <= kdesc.size * kdesc.resolution; ++i) k[i] = lanczos(3, (float) i / kdesc.resolution); break; default: return DT_OPENCL_PROCESS_CL; } const cl_mem_t dev_roi_in = dt_opencl_copy_host_to_device_constant (devid, sizeof(dt_iop_roi_t), (void *) roi_in); const cl_mem_t dev_roi_out = dt_opencl_copy_host_to_device_constant (devid, sizeof(dt_iop_roi_t), (void *) roi_out); const cl_mem_t dev_map = dt_opencl_copy_host_to_device_constant (devid, sizeof(float complex) * map_extent->width * map_extent->height, (void *) map); const cl_mem_t dev_map_extent = dt_opencl_copy_host_to_device_constant (devid, sizeof(cairo_rectangle_int_t), (void *) map_extent); const cl_mem_t dev_kdesc = dt_opencl_copy_host_to_device_constant (devid, sizeof(dt_liquify_kernel_descriptor_t), (void *) &kdesc); const cl_mem_t dev_kernel = dt_opencl_copy_host_to_device_constant (devid, sizeof(float) * (kdesc.size * kdesc.resolution + 1), (void *) k); if(dev_roi_in == NULL || dev_roi_out == NULL || dev_map == NULL || dev_map_extent == NULL || dev_kdesc == NULL || dev_kernel == NULL) goto error; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->warp_kernel, map_extent->width, map_extent->height, CLARG(dev_in), CLARG(dev_out), CLARG(dev_roi_in), CLARG(dev_roi_out), CLARG(dev_map), CLARG(dev_map_extent), CLARG(dev_kdesc), CLARG(dev_kernel)); error: dt_opencl_release_mem_object(dev_kernel); dt_opencl_release_mem_object(dev_kdesc); dt_opencl_release_mem_object(dev_map_extent); dt_opencl_release_mem_object(dev_map); dt_opencl_release_mem_object(dev_roi_out); dt_opencl_release_mem_object(dev_roi_in); if(k) free(k); return err; } int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const cl_mem_t dev_in, const cl_mem_t dev_out, const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out) { cl_int_t err = DT_OPENCL_DEFAULT_ERROR; const int devid = piece->pipe->devid; const int height = MIN(roi_in->height, roi_out->height); const int width = MIN(roi_in->width, roi_out->width); // 1. copy the whole image (we'll change only a small part of it) { const size_t src[2] = { roi_out->x - roi_in->x, roi_out->y - roi_in->y }; const size_t extent[2] = { width, height }; err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, src, CLIMG_ORIGIN, extent); if(err != CL_SUCCESS) return err; } // 2. build the distortion map cairo_rectangle_int_t map_extent; float complex *map = NULL; _build_global_distortion_map(self, piece, roi_in->scale, roi_out, &map_extent, FALSE, &map); if(map == NULL) return CL_SUCCESS; // 3. apply the map if(map_extent.width != 0 && map_extent.height != 0) err = _apply_global_distortion_map_cl(self, piece, dev_in, dev_out, roi_in, roi_out, map, &map_extent); dt_free_align((void *) map); return err; } #endif void init_global(dt_iop_module_so_t *self) { // called once at startup const int program = 17; // from programs.conf dt_iop_liquify_global_data_t *gd = malloc(sizeof(dt_iop_liquify_global_data_t)); self->data = gd; gd->warp_kernel = dt_opencl_create_kernel(program, "warp_kernel"); } void cleanup_global(dt_iop_module_so_t *self) { // called once at shutdown const dt_iop_liquify_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->warp_kernel); free(self->data); self->data = NULL; } // calculate the dot product of 2 vectors. static float cdot(const float complex p0, const float complex p1) { #ifdef FP_FAST_FMA return fma(crealf(p0), crealf(p1), cimagf(p0) * cimagf(p1)); #else return crealf(p0) * crealf(p1) + cimagf(p0) * cimagf(p1); #endif } static void draw_rectangle(cairo_t *cr, const float complex pt, const double theta, const double size) { const double x = creal(pt), y = cimag(pt); cairo_save(cr); cairo_translate(cr, x, y); cairo_rotate(cr, theta); cairo_rectangle(cr, -size / 2.0, -size / 2.0, size, size); cairo_restore(cr); } static void draw_triangle(cairo_t *cr, const float complex pt, const double theta, const double size) { const double x = creal(pt), y = cimag(pt); cairo_save(cr); cairo_translate(cr, x, y); cairo_rotate(cr, theta); cairo_move_to(cr, -size, -size / 2.0); cairo_line_to(cr, 0, 0 ); cairo_line_to(cr, -size, +size / 2.0); cairo_close_path(cr); cairo_restore(cr); } static void draw_circle(cairo_t *cr, const float complex pt, const double diameter) { const double x = creal(pt), y = cimag(pt); cairo_save(cr); cairo_new_sub_path(cr); cairo_arc(cr, x, y, diameter / 2.0, 0, 2 * M_PI); cairo_restore(cr); } static void set_source_rgba(cairo_t *cr, const dt_liquify_rgba_t rgba) { cairo_set_source_rgba(cr, rgba.red, rgba.green, rgba.blue, rgba.alpha); } static float get_ui_width(const float scale, const dt_liquify_ui_width_enum_t w) { assert(w >= 0 && w < DT_LIQUIFY_UI_WIDTH_LAST); return scale * DT_PIXEL_APPLY_DPI(dt_liquify_ui_widths[w]); } #define GET_UI_WIDTH(a) (get_ui_width(scale, DT_LIQUIFY_UI_WIDTH_##a)) static void set_line_width(cairo_t *cr, const double scale, const dt_liquify_ui_width_enum_t w) { const double width = get_ui_width(scale, w); cairo_set_line_width(cr, width * (dt_iop_canvas_not_sensitive(darktable.develop) ? 0.5 : 1.0)); } static gboolean _layers_showing(const dt_iop_liquify_gui_data_t *g) { return gtk_toggle_button_get_active(g->btn_node_tool) || gtk_toggle_button_get_active(g->btn_point_tool) || gtk_toggle_button_get_active(g->btn_line_tool) || gtk_toggle_button_get_active(g->btn_curve_tool); } static gboolean detect_drag(const dt_iop_liquify_gui_data_t *g, const double scale, const float complex pt) { // g->last_button1_pressed_pos is valid only while BUTTON1 is down return g->last_button1_pressed_pos != -1.0 && cabsf(pt - g->last_button1_pressed_pos) >= (GET_UI_WIDTH(MIN_DRAG) / scale); } static void update_warp_count(const dt_iop_module_t *self) { const dt_iop_liquify_gui_data_t *g = self->gui_data; const dt_iop_liquify_params_t *p = self->params; guint warp = 0, node = 0; for(int k=0; knodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; else { node++; if(p->nodes[k].header.type == DT_LIQUIFY_PATH_MOVE_TO_V1) warp++; } char str[10]; snprintf(str, sizeof(str), "%u | %u", warp, node); // update the label, but only if it has changed -- otherwise we get // into an infinite redraw loop while the module is visible and // expanded if(strcmp(str,gtk_label_get_text(g->label)) != 0) gtk_label_set_text(g->label, str); } static GList *interpolate_paths(dt_iop_liquify_params_t *p) { GList *l = NULL; for(int k=0; knodes[k]; if(data->header.type == DT_LIQUIFY_PATH_INVALIDATED) break; const float complex *p2 = &data->warp.point; const dt_liquify_warp_t *warp2 = &data->warp; if(data->header.type == DT_LIQUIFY_PATH_MOVE_TO_V1) { if(data->header.next == -1) { dt_liquify_warp_t *w = malloc(sizeof(dt_liquify_warp_t)); *w = *warp2; l = g_list_append(l, w); } continue; } const dt_liquify_path_data_t *prev = node_prev(p, data); const dt_liquify_warp_t *warp1 = &prev->warp; const float complex *p1 = &prev->warp.point; if(data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) { const float total_length = cabsf(*p1 - *p2); float arc_length = 0.0f; while(arc_length < total_length) { dt_liquify_warp_t *w = malloc(sizeof(dt_liquify_warp_t)); const float t = arc_length / total_length; const float complex pt = cmix(*p1, *p2, t); mix_warps(w, warp1, warp2, pt, t); w->status = DT_LIQUIFY_STATUS_INTERPOLATED; arc_length += cabsf(w->radius - w->point) * STAMP_RELOCATION; l = g_list_append(l, w); } continue; } if(data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { float complex *buffer = malloc(sizeof(float complex) * INTERPOLATION_POINTS); interpolate_cubic_bezier(*p1, data->node.ctrl1, data->node.ctrl2, *p2, buffer, INTERPOLATION_POINTS); const float total_length = get_arc_length(buffer, INTERPOLATION_POINTS); float arc_length = 0.0f; restart_cookie_t restart = { 1, 0.0 }; while(arc_length < total_length) { dt_liquify_warp_t *w = malloc(sizeof(dt_liquify_warp_t)); const float t = arc_length / total_length; const float complex pt = point_at_arc_length(buffer, INTERPOLATION_POINTS, arc_length, &restart); mix_warps(w, warp1, warp2, pt, t); w->status = DT_LIQUIFY_STATUS_INTERPOLATED; arc_length += cabsf(w->radius - w->point) * STAMP_RELOCATION; l = g_list_append(l, w); } free((void *) buffer); continue; } } return l; } #define FG_COLOR set_source_rgba(cr, fg_color) #define BG_COLOR set_source_rgba(cr, bg_color) #define VERYTHINLINE set_line_width (cr, scale / 2.0f, DT_LIQUIFY_UI_WIDTH_THINLINE) #define THINLINE set_line_width (cr, scale, DT_LIQUIFY_UI_WIDTH_THINLINE) #define THICKLINE set_line_width (cr, scale, DT_LIQUIFY_UI_WIDTH_THICKLINE) static void _draw_paths(const dt_iop_module_t *self, cairo_t *cr, const float scale, dt_iop_liquify_params_t *p, const GList *layers) { const dt_iop_liquify_gui_data_t *g = self->gui_data; cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); const gboolean showhandle = dt_iop_canvas_not_sensitive(darktable.develop) == FALSE; // do not display any interpolated items as slow when: // - we are dragging (pan) // - the button one is pressed // - exception for DT_LIQUIFY_LAYER_STRENGTHPOINT where we want to see the // interpolated strength lines. GList *interpolated = (is_dragging(g) || g->last_button1_pressed_pos != -1) && (g->last_hit.layer != DT_LIQUIFY_LAYER_STRENGTHPOINT) ? NULL : interpolate_paths(p); for(const GList *l = layers; l; l = g_list_next(l)) { const dt_liquify_layer_enum_t layer = (dt_liquify_layer_enum_t) GPOINTER_TO_INT(l->data); dt_liquify_rgba_t fg_color = dt_liquify_layers[layer].fg; dt_liquify_rgba_t bg_color = dt_liquify_layers[layer].bg; if(dt_liquify_layers[layer].opacity < 1.0) cairo_push_group(cr); for(int k=0; knodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; const dt_liquify_path_data_t *data = &p->nodes[k]; const dt_liquify_path_data_t *prev = node_prev(p, data); if((dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED) && !data->header.selected) continue; if((dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_PREV_SELECTED) && (!prev || !prev->header.selected)) continue; fg_color = dt_liquify_layers[layer].fg; bg_color = dt_liquify_layers[layer].bg; if(data->header.selected == layer) fg_color = DT_LIQUIFY_COLOR_SELECTED; if(data->header.hovered == dt_liquify_layers[layer].hover_master) fg_color = DT_LIQUIFY_COLOR_HOVER; cairo_new_path(cr); const float complex point = data->warp.point; if(data->header.type == DT_LIQUIFY_PATH_MOVE_TO_V1) cairo_move_to(cr, crealf(point), cimagf(point)); if(layer == DT_LIQUIFY_LAYER_RADIUS) { for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *pwarp = ((dt_liquify_warp_t *) i->data); draw_circle(cr, pwarp->point, 2.0f * cabsf(pwarp->radius - pwarp->point)); } draw_circle(cr, point, 2.0f * cabsf(data->warp.radius - data->warp.point)); FG_COLOR; cairo_fill(cr); } else if(layer == DT_LIQUIFY_LAYER_HARDNESS1) { for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *pwarp = ((dt_liquify_warp_t *) i->data); draw_circle(cr, pwarp->point, 2.0f * cabsf(pwarp->radius - pwarp->point) * pwarp->control1); } FG_COLOR; cairo_fill(cr); } else if(layer == DT_LIQUIFY_LAYER_HARDNESS2) { for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *pwarp = ((dt_liquify_warp_t *) i->data); draw_circle(cr, pwarp->point, 2.0f * cabsf(pwarp->radius - pwarp->point) * pwarp->control2); } FG_COLOR; cairo_fill(cr); } else if(layer == DT_LIQUIFY_LAYER_WARPS) { VERYTHINLINE; FG_COLOR; for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *pwarp = ((dt_liquify_warp_t *) i->data); cairo_move_to(cr, crealf(pwarp->point), cimagf(pwarp->point)); cairo_line_to(cr, crealf(pwarp->strength), cimagf(pwarp->strength)); } cairo_stroke(cr); for(const GList *i = interpolated; i; i = g_list_next(i)) { const dt_liquify_warp_t *pwarp = ((dt_liquify_warp_t *) i->data); const float rot = get_rot(pwarp->type); draw_circle(cr, pwarp->point, GET_UI_WIDTH(GIZMO_SMALL)); draw_triangle(cr, pwarp->strength, cargf(pwarp->strength - pwarp->point) + rot, GET_UI_WIDTH(GIZMO_SMALL) / 3.0); } BG_COLOR; cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } else if(layer == DT_LIQUIFY_LAYER_PATH) { if((data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) || (data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1)) { assert(prev); cairo_move_to(cr, crealf(prev->warp.point), cimagf(prev->warp.point)); if(data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) cairo_line_to(cr, crealf(point), cimagf(point)); if(data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { cairo_curve_to(cr, crealf(data->node.ctrl1), cimagf(data->node.ctrl1), crealf(data->node.ctrl2), cimagf(data->node.ctrl2), crealf(point), cimagf(point)); } THICKLINE; FG_COLOR; cairo_stroke_preserve(cr); THINLINE; BG_COLOR; cairo_stroke(cr); } } else if(layer == DT_LIQUIFY_LAYER_CENTERPOINT) { if(data->header.type == DT_LIQUIFY_PATH_MOVE_TO_V1 || data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1 || data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { const float w = GET_UI_WIDTH(GIZMO); switch(data->header.node_type) { case DT_LIQUIFY_NODE_TYPE_CUSP: draw_triangle(cr, point - w / 2.0 * I, -M_PI_2, w); break; case DT_LIQUIFY_NODE_TYPE_SMOOTH: draw_rectangle(cr, point, M_PI_4, w); break; case DT_LIQUIFY_NODE_TYPE_SYMMETRICAL: draw_rectangle(cr, point, 0, w); break; case DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH: draw_circle(cr, point, w); break; default: break; } THINLINE; BG_COLOR; cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } } if(data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { if(layer == DT_LIQUIFY_LAYER_CTRLPOINT1_HANDLE && showhandle && !(prev && prev->header.node_type == DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH)) { THINLINE; FG_COLOR; cairo_move_to(cr, crealf(prev->warp.point), cimagf(prev->warp.point)); cairo_line_to(cr, crealf(data->node.ctrl1), cimagf(data->node.ctrl1)); cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_CTRLPOINT2_HANDLE && showhandle && data->header.node_type != DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH) { THINLINE; FG_COLOR; cairo_move_to(cr, crealf(data->warp.point), cimagf(data->warp.point)); cairo_line_to(cr, crealf(data->node.ctrl2), cimagf(data->node.ctrl2)); cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_CTRLPOINT1 && !(prev && prev->header.node_type == DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH)) { THINLINE; BG_COLOR; draw_circle(cr, data->node.ctrl1, GET_UI_WIDTH(GIZMO_SMALL)); cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_CTRLPOINT2 && data->header.node_type != DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH) { THINLINE; BG_COLOR; draw_circle(cr, data->node.ctrl2, GET_UI_WIDTH(GIZMO_SMALL)); cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } } const dt_liquify_warp_t *warp = &data->warp; if(layer == DT_LIQUIFY_LAYER_RADIUSPOINT_HANDLE && showhandle) { draw_circle(cr, point, 2.0 * cabsf(warp->radius - point)); THICKLINE; FG_COLOR; cairo_stroke_preserve(cr); THINLINE; BG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_RADIUSPOINT) { THINLINE; BG_COLOR; draw_circle(cr, warp->radius, GET_UI_WIDTH(GIZMO_SMALL)); cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT1_HANDLE && showhandle) { draw_circle(cr, point, 2.0 * cabsf(warp->radius - point) * warp->control1); THICKLINE; FG_COLOR; cairo_stroke_preserve(cr); THINLINE; BG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT2_HANDLE && showhandle) { draw_circle(cr, point, 2.0 * cabsf(warp->radius - point) * warp->control2); THICKLINE; FG_COLOR; cairo_stroke_preserve(cr); THINLINE; BG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT1) { draw_triangle(cr, cmix(point, warp->radius, warp->control1), cargf(warp->radius - point), GET_UI_WIDTH(GIZMO_SMALL)); THINLINE; BG_COLOR; cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT2) { draw_triangle(cr, cmix(point, warp->radius, warp->control2), cargf(-(warp->radius - point)), GET_UI_WIDTH(GIZMO_SMALL)); THINLINE; BG_COLOR; cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_STRENGTHPOINT_HANDLE && showhandle) { cairo_move_to(cr, crealf(point), cimagf(point)); if(warp->type == DT_LIQUIFY_WARP_TYPE_LINEAR) { const float complex pt = cmix(point, warp->strength, 1.0 - 0.5 * (GET_UI_WIDTH(GIZMO_SMALL) / cabsf(warp->strength - point))); cairo_line_to(cr, crealf(pt), cimagf(pt)); } else draw_circle(cr, point, 2.0 * cabsf(warp->strength - warp->point)); THICKLINE; FG_COLOR; cairo_stroke_preserve(cr); THINLINE; BG_COLOR; cairo_stroke(cr); } if(layer == DT_LIQUIFY_LAYER_STRENGTHPOINT) { cairo_move_to(cr, crealf(warp->strength), cimagf(warp->strength)); const float rot = get_rot(warp->type); draw_triangle(cr, warp->strength, cargf(warp->strength - warp->point) + rot, GET_UI_WIDTH(GIZMO_SMALL)); THINLINE; BG_COLOR; cairo_fill_preserve(cr); FG_COLOR; cairo_stroke(cr); } } if(dt_liquify_layers[layer].opacity < 1.0) { cairo_pop_group_to_source(cr); cairo_paint_with_alpha(cr, dt_liquify_layers[layer].opacity); } } g_list_free_full(interpolated, free); } /* Find the nearest point on a cubic bezier curve. Return the curve parameter t of the point on a cubic bezier curve that is nearest to another arbitrary point. Uses interpolation. FIXME: Implement a faster method, see: http://tog.acm.org/resources/GraphicsGems/gems/NearestPoint.c */ static float find_nearest_on_curve_t(const float complex p0, const float complex p1, const float complex p2, const float complex p3, const float complex x, const int n) { float min_t = 0.0f, min_dist = cabsf(x - p0); for(int i = 0; i < n; i++) { const float t = (1.0 * i) / n; const float t1 = 1.0 - t; const float complex ip = t1 * t1 * t1 * p0 + 3 * t1 * t1 * t * p1 + 3 * t1 * t * t * p2 + t * t * t * p3; const float dist = cabsf(x - ip); if(dist < min_dist) { min_dist = dist; min_t = t; } } return min_t; } /* Find the nearest point on a line. Return the line parameter t of the point on a line that is nearest to another arbitrary point. */ static float find_nearest_on_line_t(const float complex p0, const float complex p1, const float complex x) { // scalar projection const float b = cabsf(p1 - p0); // |b| const float dotab = cdot(x - p0, p1 - p0); // |a| * |b| * cos(phi) return dotab / (b * b); // |a| / |b| * cos(phi) } // split a cubic bezier at t into two cubic beziers. static void casteljau(const float complex *p0, float complex *p1, float complex *p2, float complex *p3, const float t) { const float complex p01 = *p0 + (*p1 - *p0) * t; const float complex p12 = *p1 + (*p2 - *p1) * t; const float complex p23 = *p2 + (*p3 - *p2) * t; const float complex p012 = p01 + (p12 - p01) * t; const float complex p123 = p12 + (p23 - p12) * t; const float complex p0123 = p012 + (p123 - p012) * t; *p1 = p01; *p2 = p012; *p3 = p0123; } #define CHECK_HIT_PT(point) \ const float d = cabsf(point - (*pt)); \ if(d < distance) \ { \ distance = d; \ hit->layer = layer; \ hit->elem = data; \ } static void _hit_paths(dt_iop_module_t *self, dt_iop_liquify_params_t *p, const GList *layers, const float complex *pt, dt_liquify_hit_t *hit) { float distance = FLT_MAX; for(const GList *l = layers; l; l = g_list_next(l)) { const dt_liquify_layer_enum_t layer = (dt_liquify_layer_enum_t)GPOINTER_TO_INT(l->data); if((dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_HIT_TEST) == 0) continue; for(int k=0; knodes[k]; const dt_liquify_path_data_t *prev = node_prev(p, data); if(p->nodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; if((dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_NODE_SELECTED) && !data->header.selected) continue; if((dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_PREV_SELECTED) && (!prev || !prev->header.selected)) continue; const dt_liquify_warp_t *warp = &data->warp; const float complex point = data->warp.point; if(layer == DT_LIQUIFY_LAYER_PATH) { if(data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) { // remove 5% from start and end of line as non sensible area // this is to avoid wrong interaction for center point on both // sides. const float complex deadzone = (point - prev->warp.point) / 20.0f; const float complex lp1 = prev->warp.point + deadzone; const float complex lp2 = point - deadzone; const float t = find_nearest_on_line_t(lp1, lp2, *pt); if(t > 0.0f && t < 1.0f) { const float complex linepoint = cmix(lp1, lp2, t); CHECK_HIT_PT(linepoint); } } else if(data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { // remove 5% from start and end of line as non sensible area // this is to avoid wrong interaction for center point on both // sides. const float complex deadzone = (point - prev->warp.point) / 20.0f; const float complex lp1 = prev->warp.point + deadzone; const float complex lp2 = point - deadzone; const float t = find_nearest_on_curve_t(lp1, data->node.ctrl1, data->node.ctrl2, lp2, *pt, INTERPOLATION_POINTS); if(t > 0.0f && t < 1.0f) { float complex curvepoint = lp2; float complex p1 = data->node.ctrl1; float complex p2 = data->node.ctrl2; casteljau(&lp1, &p1, &p2, &curvepoint, t); CHECK_HIT_PT(curvepoint); } } } else if(layer == DT_LIQUIFY_LAYER_CENTERPOINT) { if(data->header.type == DT_LIQUIFY_PATH_MOVE_TO_V1 || data->header.type == DT_LIQUIFY_PATH_LINE_TO_V1 || data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { CHECK_HIT_PT(point); } } else if(layer == DT_LIQUIFY_LAYER_RADIUSPOINT) { CHECK_HIT_PT(warp->radius); } else if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT1) { CHECK_HIT_PT(cmix(point, warp->radius, warp->control1)); } else if(layer == DT_LIQUIFY_LAYER_HARDNESSPOINT2) { CHECK_HIT_PT(cmix(point, warp->radius, warp->control2)); } else if(layer == DT_LIQUIFY_LAYER_STRENGTHPOINT) { const float complex wp = warp->point - warp->strength; CHECK_HIT_PT(warp->strength + (float)DT_PIXEL_APPLY_DPI(5) * (wp / cabsf(wp))); } if(data->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { if(layer == DT_LIQUIFY_LAYER_CTRLPOINT1 && !(prev && prev->header.node_type == DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH)) { CHECK_HIT_PT(data->node.ctrl1); } if(layer == DT_LIQUIFY_LAYER_CTRLPOINT2 && data->header.node_type != DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH) { CHECK_HIT_PT(data->node.ctrl2); } } } } if(distance > DT_PIXEL_APPLY_DPI(25)) { memcpy(hit, &NOWHERE, sizeof(dt_liquify_hit_t)); } } static void draw_paths(const dt_iop_module_t *self, cairo_t *cr, const float scale, dt_iop_liquify_params_t *params) { const dt_iop_liquify_gui_data_t *g = self->gui_data; GList *layers = NULL; for(dt_liquify_layer_enum_t layer = 0; layer < DT_LIQUIFY_LAYER_LAST; ++layer) { if(gtk_toggle_button_get_active(g->btn_point_tool) && (dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_POINT_TOOL)) layers = g_list_prepend(layers, GINT_TO_POINTER(layer)); if(gtk_toggle_button_get_active(g->btn_line_tool) && (dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_LINE_TOOL)) layers = g_list_prepend(layers, GINT_TO_POINTER(layer)); if(gtk_toggle_button_get_active(g->btn_curve_tool) && (dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_CURVE_TOOL)) layers = g_list_prepend(layers, GINT_TO_POINTER(layer)); if(gtk_toggle_button_get_active(g->btn_node_tool) && (dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_NODE_TOOL)) layers = g_list_prepend(layers, GINT_TO_POINTER(layer)); } layers = g_list_reverse(layers); // list was built in reverse order, so un-reverse it _draw_paths(self, cr, scale, params, layers); g_list_free(layers); } static void _hit_test_paths(dt_iop_module_t *self, dt_iop_liquify_params_t *params, const float complex pt, dt_liquify_hit_t *hit) { GList *layers = NULL; for(dt_liquify_layer_enum_t layer = 0; layer < DT_LIQUIFY_LAYER_LAST; ++layer) { if(dt_liquify_layers[layer].flags & DT_LIQUIFY_LAYER_FLAG_HIT_TEST) layers = g_list_prepend(layers, GINT_TO_POINTER(layer)); } layers = g_list_reverse(layers); // list was built in reverse order, so un-reverse it _hit_paths(self, params, layers, &pt, hit); g_list_free(layers); } /** * Smooth a bezier spline through prescribed points. * * Smooth a bezier spline through prescribed points by solving a * linear system. First we build a tridiagonal matrix and then we * solve it using the Thomas algorithm. (FIXME: A tridiagonal matrix * is easy to solve in O(n) but you cannot write a closed path as a * tridiagonal. To solve closed paths we will have to use a different * solver. Use the GSL?) * * Here is an article that explains the math: * http://www.particleincell.com/blog/2012/bezier-splines/ * * Basically we find all the ctrl1 points when we solve the linear * system, then we calculate each ctrl2 from the ctrl1. * * We build the linear system choosing for each segment of the path an * equation among following 9 equations. "Straight" is a path that * goes straight in to the knot (2nd derivative == 0 at the knot). * "Smooth" means a path that goes smoothly through the knot, makes no * corner and curves the same amount just before and just after the * knot (1st and 2nd derivatives are constant around the knot.) * "Keep" means to keep the control point as the user set it. * * | | start | end of path * | -- | ----------- | --------------- * | 1 | straight | smooth * | 2 | smooth | smooth * | 3 | smooth | straight * | 4 | keep | smooth * | 5 | keep | keep * | 6 | smooth | keep * | 7 | keep | straight * | 8 | straight | straight (yields a line) * | 9 | straight | keep * * The equations are (close your eyes): * * \f{eqnarray}{ * 2P_{1,i} + P_{1,i+1} &=& K_i + 2K_{i+1} \label{1} \\ * P_{1,i-1} + 4P_{1,i} + P_{1,i+1} &=& 4K_i + 2K_{i+1} \label{2} \\ * 2P_{1,i-1} + 7P_{1,i} &=& 8K_i + K_{i+1} \label{3} \\ * P_{1,i} &=& C1_i \label{4} \\ * P_{1,i} &=& C1_i \label{5} \\ * P_{1,i-1} + 4P_{1,i} &=& C2_i + 4K_i \label{6} \\ * P_{1,i} &=& C1_i \label{7} \\ * 3P_{1,i} &=& 2K_i + K_{i+1} \label{8} \\ * 2P_{1,i} &=& K_i + C2_i \label{9} * \f} * * Some of these are the same and differ only in the way we calculate * c2. (You may open your eyes again.) */ static void smooth_path_linsys(size_t n, const float complex *k, float complex *c1, float complex *c2, const int *equation) { --n; float *a = malloc(sizeof(float) * n); // subdiagonal float *b = malloc(sizeof(float) * n); // main diagonal float *c = malloc(sizeof(float) * n); // superdiagonal float complex *d = malloc(sizeof(float complex) * n); // right hand side // Build the tridiagonal matrix. for(int i = 0; i < n; i++) { switch(equation[i]) { #define ABCD(A,B,C,D) { { a[i] = A; b[i] = B; c[i] = C; d[i] = D; continue; } } case 1: ABCD(0, 2, 1, k[i] + 2 * k[i+1] ); break; case 2: ABCD(1, 4, 1, 4 * k[i] + 2 * k[i+1] ); break; case 3: ABCD(2, 7, 0, 8 * k[i] + k[i+1] ); break; case 4: ABCD(0, 1, 0, c1[i] ); break; case 5: ABCD(0, 1, 0, c1[i] ); break; case 6: ABCD(1, 4, 0, 4 * k[i] + c2[i] ); break; case 7: ABCD(0, 1, 0, c1[i] ); break; case 8: ABCD(0, 3, 0, 2 * k[i] + k[i+1] ); break; case 9: ABCD(0, 2, 0, k[i] + c2[i] ); break; #undef ABCD } } // Solve with the Thomas algorithm to compute c1's. See: // http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm for(int i = 1; i < n; i++) { const float m = a[i] / b[i-1]; b[i] = b[i] - m * c[i-1]; d[i] = d[i] - m * d[i-1]; } c1[n-1] = d[n-1] / b[n-1]; for(int i = n - 2; i >= 0; i--) c1[i] = (d[i] - c[i] * c1[i+1]) / b[i]; // Now compute the c2's. for(int i = 0; i < n; i++) { switch(equation[i]) { // keep end: c2 does not change case 5: case 6: case 9: break; // straight end: put c2[i] halfway between c1[i] and k[i+1] case 3: case 7: case 8: c2[i] = (c1[i] + k[i+1]) / 2; break; // smooth end: c2 and c1 are symmetrical around the knot default: c2[i] = 2 * k[i+1] - c1[i+1]; } } free(a); free(b); free(c); free(d); } static int path_length(const dt_iop_liquify_params_t *p, const dt_liquify_path_data_t *n) { int count = 1; while(n->header.next != -1) { count++; n = &p->nodes[n->header.next]; } return count; } static void smooth_paths_linsys(dt_iop_liquify_params_t *params) { for(int k = 0; k < MAX_NODES; k++) { if(params->nodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; if(params->nodes[k].header.prev != -1) continue; dt_liquify_path_data_t *node = ¶ms->nodes[k]; const size_t n = path_length(params, node); if(n < 2) continue; float complex *pt = calloc(n, sizeof(float complex)); float complex *c1 = calloc(n, sizeof(float complex)); float complex *c2 = calloc(n, sizeof(float complex)); int *eqn = calloc(n, sizeof(int)); size_t idx = 0; while(node) { const dt_liquify_path_data_t *d = (dt_liquify_path_data_t *) node; const dt_liquify_path_data_t *p = node_prev(params, node); const dt_liquify_path_data_t *nx = node_next(params, node); const dt_liquify_path_data_t *nn = nx ? node_next(params, nx) : NULL; pt[idx] = node->warp.point; if(d->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { c1[idx-1] = d->node.ctrl1; c2[idx-1] = d->node.ctrl2; } const int autosmooth = d->header.node_type == DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; const int next_autosmooth = nx && nx->header.node_type == DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; const int firstseg = !p || d->header.type != DT_LIQUIFY_PATH_CURVE_TO_V1; const int lastseg = !nn || nn->header.type != DT_LIQUIFY_PATH_CURVE_TO_V1; const int lineseg = nx && nx->header.type == DT_LIQUIFY_PATH_LINE_TO_V1; // Program the linear system with equations: // // START END // -------------------------- // 1: straight smooth // 2: smooth smooth // 3: smooth straight // 4: keep smooth // 5: keep keep // 6: smooth keep // 7: keep straight // 8: straight straight (== line) // 9: straight keep if(lineseg) eqn[idx] = 5; else if(!autosmooth && !next_autosmooth) eqn[idx] = 5; else if(firstseg && lastseg && !autosmooth && next_autosmooth) eqn[idx] = 7; else if(firstseg && lastseg && autosmooth && next_autosmooth) eqn[idx] = 8; else if(firstseg && lastseg && autosmooth && !next_autosmooth) eqn[idx] = 9; else if(firstseg && autosmooth && !next_autosmooth) eqn[idx] = 5; else if(firstseg && autosmooth) eqn[idx] = 1; else if(lastseg && autosmooth && next_autosmooth) eqn[idx] = 3; else if(lastseg && !autosmooth && next_autosmooth) eqn[idx] = 7; else if(autosmooth && !next_autosmooth) eqn[idx] = 6; else if(!autosmooth && next_autosmooth) eqn[idx] = 4; else eqn[idx] = 2; ++idx; node = node_next(params, node); } smooth_path_linsys(n, pt, c1, c2, eqn); // write calculated control points back to list structure node = ¶ms->nodes[k]; node = node_next(params, node); idx = 0; while(node) { dt_liquify_path_data_t *d = (dt_liquify_path_data_t *) node; if(d->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { d->node.ctrl1 = c1[idx]; d->node.ctrl2 = c2[idx]; } ++idx; node = node_next(params, node); } free(pt); free(c1); free(c2); free(eqn); } } static dt_liquify_path_data_t *_find_hovered(dt_iop_liquify_params_t *p) { for(int k=0; knodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; else if(p->nodes[k].header.hovered) return &p->nodes[k]; return NULL; } static void init_warp(dt_liquify_warp_t *warp, const float complex point) { warp->type = DT_LIQUIFY_WARP_TYPE_LINEAR; warp->point = point; warp->radius = point; warp->strength = point; warp->control1 = 0.5; warp->control2 = 0.75; warp->status = DT_LIQUIFY_STATUS_NONE; } static dt_liquify_path_data_t *alloc_move_to(const dt_iop_module_t *self, const float complex start_point) { dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; dt_liquify_path_data_t* m = (dt_liquify_path_data_t*)node_alloc(p, &g->node_index); if(m) { m->header.type = DT_LIQUIFY_PATH_MOVE_TO_V1; m->header.node_type = DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; init_warp(&m->warp, start_point); } return (dt_liquify_path_data_t *)m; } static dt_liquify_path_data_t *alloc_line_to(const dt_iop_module_t *self, const float complex end_point) { dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; dt_liquify_path_data_t* l = (dt_liquify_path_data_t*)node_alloc(p, &g->node_index); if(l) { l->header.type = DT_LIQUIFY_PATH_LINE_TO_V1; l->header.node_type = DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; init_warp(&l->warp, end_point); } return (dt_liquify_path_data_t *)l; } static dt_liquify_path_data_t *alloc_curve_to(const dt_iop_module_t *self, const float complex end_point) { dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; dt_liquify_path_data_t* c = (dt_liquify_path_data_t*)node_alloc(p, &g->node_index); if(c) { c->header.type = DT_LIQUIFY_PATH_CURVE_TO_V1; c->header.node_type = DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; c->node.ctrl1 = c->node.ctrl2 = 0.0; init_warp(&c->warp, end_point); } return (dt_liquify_path_data_t *)c; } static void unselect_all(dt_iop_liquify_params_t *p) { for(int k=0; knodes[k].header.type == DT_LIQUIFY_PATH_INVALIDATED) break; else p->nodes[k].header.selected = 0; } void gui_post_expose(dt_iop_module_t *self, cairo_t *cr, const float bb_width, const float bb_height, const float pointerx, const float pointery, const float zoom_scale) { dt_develop_t *develop = self->dev; dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; if(!g) return; const float iscale = develop->preview_pipe->iscale; const float scale = MAX(bb_width, bb_height); if(bb_width < 1.0 || bb_height < 1.0) return; // get a copy of all iop params dt_iop_gui_enter_critical_section(self); update_warp_count(self); smooth_paths_linsys(p); dt_iop_liquify_params_t copy_params; memcpy(©_params, p, sizeof(dt_iop_liquify_params_t)); dt_iop_gui_leave_critical_section(self); // check whether we need to draw an overlay with the warps if(_layers_showing(g)) { // distort all points const distort_params_t d_params = { develop, develop->preview_pipe, iscale, 1.0 / scale, DT_DEV_TRANSFORM_DIR_ALL }; _distort_paths_locked(self, &d_params, ©_params); cairo_scale(cr, scale, scale); draw_paths(self, cr, 1.0 / (scale * zoom_scale), ©_params); } } static void _btn_make_radio_cb(GtkToggleButton *btn, gboolean ctrl_pressed, dt_iop_module_t *self); static void btn_make_radio_callback(GtkGestureSingle *gesture, int n_press, double x, double y, dt_iop_module_t *self); void gui_focus(dt_iop_module_t *self, const gboolean in) { if(!in) { dt_collection_hint_message(darktable.collection); _btn_make_radio_cb(NULL, FALSE, self); } } static void sync_pipe(dt_iop_module_t *self, const gboolean history) { if(history) { dt_iop_liquify_params_t *p = self->params; // something definitive has happened like button release ... so // redraw pipe smooth_paths_linsys(p); dt_dev_add_history_item(darktable.develop, self, TRUE); } else { // only moving mouse around, pointing at things or dragging ... so // give some cairo feedback, but don't redraw pipe dt_control_queue_redraw_center(); } } /* right-click on node: Delete node. right-click on path: Delete whole path. ctrl+click on node: Cycle symmetrical, smooth, cusp, autosmooth ctrl+click on path: Add node ctrl+alt+click on path: Change line / bezier ctrl+click on strength: Cycle linear, grow, shrink */ static void get_point_scale(const dt_iop_module_t *self, const float pzx, const float pzy, float complex *pt, float *scale) { float wd, ht; dt_dev_get_preview_size(self->dev, &wd, &ht); float pts[2] = { pzx * wd, pzy * ht }; dt_dev_distort_backtransform_plus(darktable.develop, darktable.develop->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1); dt_dev_distort_backtransform_plus(darktable.develop, darktable.develop->preview_pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_EXCL, pts, 1); const float nx = pts[0] / darktable.develop->preview_pipe->iwidth; const float ny = pts[1] / darktable.develop->preview_pipe->iheight; *scale = darktable.develop->preview_pipe->iscale * (dt_dev_get_zoom_scale_full()); *pt = (nx * darktable.develop->full.pipe->iwidth) + (ny * darktable.develop->full.pipe->iheight) * I; } int mouse_moved(dt_iop_module_t *self, const float x, const float y, const double pressure, const int which, const float zoom_scale) { dt_iop_liquify_gui_data_t *g = self->gui_data; const gboolean layer_display = _layers_showing(g); if(!self->enabled && !layer_display) return FALSE; dt_iop_liquify_params_t *pa = self->params; gboolean handled = FALSE; float complex pt = 0.0f; float scale = 0.0f; get_point_scale(self, x, y, &pt, &scale); dt_iop_gui_enter_critical_section(self); g->last_mouse_pos = pt; // Don't hit test while dragging, you'd only hit the dragged thing // anyway. if(!is_dragging(g) && _layers_showing(g)) { dt_liquify_hit_t hit = NOWHERE; _hit_test_paths(self, pa, pt, &hit); dt_liquify_path_data_t *last_hovered = _find_hovered(pa); if(hit.elem != last_hovered || (last_hovered && hit.elem && hit.elem->header.hovered != last_hovered->header.hovered)) { if(hit.elem) hit.elem->header.hovered = hit.layer; if(last_hovered) last_hovered->header.hovered = 0; // change in hover display dt_control_hinter_message(dt_liquify_layers[hit.layer].hint); // also use when dragging later dt_liquify_layers[DT_LIQUIFY_LAYER_BACKGROUND].hint = dt_liquify_layers[hit.layer].hint; handled = TRUE; goto done; } const gboolean dragged = detect_drag(g, scale, pt); if(dragged && g->last_hit.elem) { // start dragging start_drag(g, g->last_hit.layer, g->last_hit.elem); // nothing more to do, we will refresh on the next call anyway // this makes the initial move of a node a bit more fluid. handled = TRUE; goto done; } if(g->last_hit.elem) { // an item is selected, so this movement is handled and must // not trigger any panning. handled = TRUE; } else if(hit.elem == DT_LIQUIFY_LAYER_BACKGROUND && gtk_toggle_button_get_active(g->btn_node_tool)) dt_control_hinter_message(_("click to edit nodes")); } else if(is_dragging(g)) // we are dragging { dt_control_hinter_message(dt_liquify_layers[DT_LIQUIFY_LAYER_BACKGROUND].hint); dt_liquify_path_data_t *d = g->dragging.elem; dt_liquify_path_data_t *n = node_next(pa, d); dt_liquify_path_data_t *p = node_prev(pa, d); const float complex *start_pt = &d->warp.point; switch(g->dragging.layer) { case DT_LIQUIFY_LAYER_CENTERPOINT: switch(d->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: d->node.ctrl2 += pt - d->warp.point; // fall thru case DT_LIQUIFY_PATH_MOVE_TO_V1: case DT_LIQUIFY_PATH_LINE_TO_V1: if(n && n->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) n->node.ctrl1 += pt - d->warp.point; if(p && p->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) p->node.ctrl2 += pt - d->warp.point; d->warp.radius += pt - d->warp.point; d->warp.strength += pt - d->warp.point; d->warp.point = pt; break; default: break; } break; case DT_LIQUIFY_LAYER_CTRLPOINT1: switch(d->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: d->node.ctrl1 = pt; if(p && p->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { switch(p->header.node_type) { case DT_LIQUIFY_NODE_TYPE_SMOOTH: p->node.ctrl2 = p->warp.point + cabsf(p->warp.point - p->node.ctrl2) * cexpf(cargf(p->warp.point - pt) * I); break; case DT_LIQUIFY_NODE_TYPE_SYMMETRICAL: p->node.ctrl2 = 2 * p->warp.point - pt; break; default: break; } } break; default: break; } break; case DT_LIQUIFY_LAYER_CTRLPOINT2: switch(d->header.type) { case DT_LIQUIFY_PATH_CURVE_TO_V1: d->node.ctrl2 = pt; if(n && n->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { switch(d->header.node_type) { case DT_LIQUIFY_NODE_TYPE_SMOOTH: n->node.ctrl1 = d->warp.point + cabsf(d->warp.point - n->node.ctrl1) * cexpf(cargf(d->warp.point - pt) * I); break; case DT_LIQUIFY_NODE_TYPE_SYMMETRICAL: n->node.ctrl1 = 2 * d->warp.point - pt; break; default: break; } } break; default: break; } break; case DT_LIQUIFY_LAYER_RADIUSPOINT: d->warp.radius = pt; dt_conf_set_float(CONF_RADIUS, cabsf(d->warp.radius - d->warp.point)); break; case DT_LIQUIFY_LAYER_STRENGTHPOINT: d->warp.strength = pt; dt_conf_set_float(CONF_STRENGTH, cabsf(d->warp.strength - d->warp.point)); dt_conf_set_float(CONF_ANGLE, cargf(d->warp.strength - d->warp.point)); break; case DT_LIQUIFY_LAYER_HARDNESSPOINT1: d->warp.control1 = MIN(1.0, cabsf(pt - *start_pt) / cabsf(d->warp.radius - *start_pt)); break; case DT_LIQUIFY_LAYER_HARDNESSPOINT2: d->warp.control2 = MIN(1.0, cabsf(pt - *start_pt) / cabsf(d->warp.radius - *start_pt)); break; default: break; } handled = TRUE; } done: dt_iop_gui_leave_critical_section(self); if(handled) { sync_pipe(self, FALSE); } return handled; } static float dt_conf_get_sanitize_float(const char *name, const float min, const float max, const float default_value) { const float value = dt_conf_get_float(name); float new_value = CLAMP(value, min, max); if(default_value != 0.0f && new_value != value) new_value = 0.25f * default_value + 0.75f * value; dt_conf_set_float(name, new_value); return new_value; } static void get_stamp_params(dt_iop_module_t *self, float *radius, float *r_strength, float *phi) { GtkWidget *widget = dt_ui_main_window(darktable.gui->ui); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int last_win_min = MIN(allocation.width, allocation.height); const dt_dev_pixelpipe_t *devpipe = darktable.develop->preview_pipe; const float iwd_min = MIN(devpipe->iwidth, devpipe->iheight); const float proc_wdht_min = MIN(devpipe->processed_width, devpipe->processed_height); const float scale = devpipe->iscale / dt_dev_get_zoom_scale_full(); const float im_scale = 0.09f * iwd_min * last_win_min * scale / proc_wdht_min; *radius = dt_conf_get_sanitize_float(CONF_RADIUS, 0.1f*im_scale, 3.0f*im_scale, im_scale); *r_strength = dt_conf_get_sanitize_float(CONF_STRENGTH, 0.5f * *radius, 2.0f * *radius, 1.5f * *radius); *phi = dt_conf_get_sanitize_float(CONF_ANGLE, -M_PI, M_PI, 0.0f); } /* add support for changing the radius and the strength vector for the temp node */ int scrolled(dt_iop_module_t *self, const float x, const float y, const int up, const uint32_t state) { const dt_iop_liquify_gui_data_t *g = self->gui_data; const gboolean incr = dt_mask_scroll_increases(up); if(g->temp) { dt_liquify_warp_t *warp = &g->temp->warp; const float complex strength_v = warp->strength - warp->point; if(dt_modifier_is(state, 0)) { // change size float radius = 0.0f, r = 0.0f, phi = 0.0f; get_stamp_params(self, &radius, &r, &phi); r = dt_masks_change_size(incr, r, 10.0f, FLT_MAX); radius = dt_masks_change_size(incr, radius, 10.0f, FLT_MAX); warp->radius = warp->point + (dt_masks_change_size(incr, radius, 10.0f, FLT_MAX)); warp->strength = warp->point + r * cexpf(phi * I); dt_conf_set_float(CONF_RADIUS, radius); dt_conf_set_float(CONF_STRENGTH, r); return 1; } else if(dt_modifier_is(state, GDK_CONTROL_MASK)) { // change the strength direction const float phi = dt_masks_change_rotation(incr, cargf(strength_v), FALSE); const float r = cabsf(strength_v); warp->strength = warp->point + r * cexpf(phi * I); dt_conf_set_float(CONF_STRENGTH, r); dt_conf_set_float(CONF_ANGLE, phi); return 1; } else if(dt_modifier_is(state, GDK_SHIFT_MASK)) { // change the strength const float phi = cargf(strength_v); const float r = dt_masks_change_size(incr, cabsf(strength_v), 0.0001f, FLT_MAX); warp->strength = warp->point + r * cexpf(phi * I); dt_conf_set_float(CONF_STRENGTH, r); dt_conf_set_float(CONF_ANGLE, phi); return 1; } } return 0; } int button_pressed(dt_iop_module_t *self, const float x, const float y, const double pressure, const int which, const int type, const uint32_t state, const float zoom_scale) { // avoid unexpected back to lighttable mode on double-click if(type == GDK_2BUTTON_PRESS && which == GDK_BUTTON_PRIMARY) return 1; dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; int handled = 0; float complex pt = 0.0f; float scale = 0.0f; get_point_scale(self, x, y, &pt, &scale); dt_iop_gui_enter_critical_section(self); g->last_mouse_pos = pt; g->last_mouse_mods = state; if(which == GDK_BUTTON_PRIMARY) g->last_button1_pressed_pos = pt; if(!is_dragging(g)) // while dragging you would always hit the dragged thing _hit_test_paths(self, p, pt, &g->last_hit); if(which == GDK_BUTTON_MIDDLE) goto done; // Point tool if(which == GDK_BUTTON_PRIMARY && gtk_toggle_button_get_active(g->btn_point_tool)) { // always end dragging before manipulating the path list to avoid // dangling pointers end_drag(g); if(!g->temp) goto done; g->status |= DT_LIQUIFY_STATUS_NEW; g->status &= ~DT_LIQUIFY_STATUS_PREVIEW; start_drag(g, DT_LIQUIFY_LAYER_STRENGTHPOINT, g->temp); g->last_hit = NOWHERE; handled = 1; goto done; } // Line tool or curve tool if(which == GDK_BUTTON_PRIMARY && (gtk_toggle_button_get_active(g->btn_line_tool) || gtk_toggle_button_get_active(g->btn_curve_tool))) { // always end dragging before manipulating the path list to avoid // dangling pointers end_drag(g); if(!g->temp) { if(g->last_hit.layer == DT_LIQUIFY_LAYER_CENTERPOINT) { // continue path g->temp = g->last_hit.elem; } else { if(!g->temp) goto done; } } g->last_hit = NOWHERE; if(gtk_toggle_button_get_active(g->btn_curve_tool)) { start_drag(g, DT_LIQUIFY_LAYER_CTRLPOINT1, g->temp); } g->status |= DT_LIQUIFY_STATUS_NEW; g->status &= ~DT_LIQUIFY_STATUS_PREVIEW; handled = 1; goto done; } // right-click is handled on release if(which == GDK_BUTTON_SECONDARY) { handled = 1; goto done; } done: dt_iop_gui_leave_critical_section(self); return handled; } static void _start_new_shape(dt_iop_module_t *self) { dt_iop_liquify_gui_data_t *g = self->gui_data; // create initial shape at the center float complex pt = 0.0f; float scale = 1.0f; get_point_scale(self, 0.5f * darktable.develop->full.width, 0.5f * darktable.develop->full.height, &pt, &scale); float radius = 0.0f, r = 1.0f, phi = 0.0f; get_stamp_params(self, &radius, &r, &phi); // start a new path g->temp = alloc_move_to(self, pt); g->temp->warp.radius = pt + radius; g->temp->warp.strength = pt + r * cexpf(phi * I); g->status |= DT_LIQUIFY_STATUS_PREVIEW; g->status |= DT_LIQUIFY_STATUS_NEW; g->just_started = TRUE; start_drag(g, DT_LIQUIFY_LAYER_CENTERPOINT, g->temp); g->last_hit = NOWHERE; } int button_released(dt_iop_module_t *self, const float x, const float y, const int which, const uint32_t state, const float zoom_scale) { dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; int handled = 0; float complex pt = 0.0f; float scale = 0.0f; get_point_scale(self, x, y, &pt, &scale); dt_iop_gui_enter_critical_section(self); g->last_mouse_pos = pt; const gboolean dragged = detect_drag(g, scale, pt); if(which == GDK_BUTTON_PRIMARY && g->temp && (g->status & DT_LIQUIFY_STATUS_NEW)) { end_drag(g); if(gtk_toggle_button_get_active(g->btn_point_tool)) { g->temp = NULL; // a point is done if(g->creation_continuous) _start_new_shape(self); else _btn_make_radio_cb(GTK_TOGGLE_BUTTON(g->btn_node_tool), FALSE, self); handled = 2; } else if(gtk_toggle_button_get_active(g->btn_line_tool)) { const int prev_index = g->node_index; const float complex strength = (g->temp->warp.strength - g->temp->warp.point); const float radius = cabsf(g->temp->warp.radius - g->temp->warp.point); g->temp = alloc_line_to(self, pt); if(!g->temp) goto done; g->temp->warp.radius = pt + radius; g->temp->warp.strength = pt + strength; // links g->temp->header.prev = prev_index; node_get(p, prev_index)->header.next = g->node_index; start_drag(g, DT_LIQUIFY_LAYER_CENTERPOINT, g->temp); g->just_started = FALSE; handled = 1; } else if(gtk_toggle_button_get_active(g->btn_curve_tool)) { const int prev_index = g->node_index; const float complex strength = (g->temp->warp.strength - g->temp->warp.point); const float radius = cabsf(g->temp->warp.radius - g->temp->warp.point); g->temp = alloc_curve_to(self, pt); if(!g->temp) goto done; g->temp->warp.radius = pt + radius; g->temp->warp.strength = pt + strength; // links g->temp->header.prev = prev_index; node_get(p, prev_index)->header.next = g->node_index; start_drag(g, DT_LIQUIFY_LAYER_CENTERPOINT, g->temp); g->just_started = FALSE; handled = 1; } g->status &= ~DT_LIQUIFY_STATUS_NEW; goto done; } if(which == GDK_BUTTON_PRIMARY && is_dragging(g)) { end_drag(g); handled = 2; goto done; } // right click == cancel or delete if(which == GDK_BUTTON_SECONDARY) { end_drag(g); // cancel line or curve creation if(g->temp) { node_delete(p, g->temp); g->temp = NULL; if(g->creation_continuous && !g->just_started) _start_new_shape(self); else { g->status &= ~DT_LIQUIFY_STATUS_PREVIEW; _btn_make_radio_cb(GTK_TOGGLE_BUTTON(g->btn_node_tool), FALSE, self); } handled = 2; goto done; } // right click on background toggles node tool if(g->last_hit.layer == DT_LIQUIFY_LAYER_BACKGROUND) { _btn_make_radio_cb(GTK_TOGGLE_BUTTON(g->btn_node_tool), FALSE, self); handled = 1; goto done; } // delete node if(g->last_hit.layer == DT_LIQUIFY_LAYER_CENTERPOINT) { node_delete(p, g->last_hit.elem); g->last_hit = NOWHERE; handled = 2; goto done; } // delete shape if(g->last_hit.layer == DT_LIQUIFY_LAYER_PATH) { path_delete(p, g->last_hit.elem); g->last_hit = NOWHERE; handled = 2; goto done; } goto done; } // Node tool if(gtk_toggle_button_get_active(g->btn_node_tool)) { if(which == GDK_BUTTON_PRIMARY && dt_modifier_is(g->last_mouse_mods, 0) && !dragged) { // select/unselect start/endpoint and clear previous selections if(g->last_hit.layer == DT_LIQUIFY_LAYER_CENTERPOINT) { const int oldsel = !!g->last_hit.elem->header.selected; unselect_all(p); g->last_hit.elem->header.selected = oldsel ? 0 : g->last_hit.layer; handled = 1; goto done; } // unselect all if(g->last_hit.layer == DT_LIQUIFY_LAYER_BACKGROUND) { unselect_all(p); handled = 1; goto done; } } if(which == GDK_BUTTON_PRIMARY && dt_modifier_is(g->last_mouse_mods, GDK_SHIFT_MASK) && !dragged) { // select/unselect start/endpoint and keep previous selections if(g->last_hit.layer == DT_LIQUIFY_LAYER_CENTERPOINT) { const int oldsel = !!g->last_hit.elem->header.selected; g->last_hit.elem->header.selected = oldsel ? 0 : g->last_hit.layer; handled = 1; goto done; } } if(which == GDK_BUTTON_PRIMARY && dt_modifier_is(g->last_mouse_mods, GDK_CONTROL_MASK) && !dragged) { // add node if(g->last_hit.layer == DT_LIQUIFY_LAYER_PATH) { dt_liquify_path_data_t *e = g->last_hit.elem; const dt_liquify_path_data_t *prev = node_prev(p, e); if(prev && e->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { // add node to curve dt_liquify_path_data_t *curve1 = (dt_liquify_path_data_t *) e; dt_liquify_path_data_t *curve2 = alloc_curve_to(self, 0); if(!curve2) goto done; curve2->node.ctrl1 = curve1->node.ctrl1; curve2->node.ctrl2 = curve1->node.ctrl2; const dt_liquify_warp_t *warp1 = &prev->warp; dt_liquify_warp_t *warp2 = &curve2->warp; const dt_liquify_warp_t *warp3 = &e->warp; const float t = find_nearest_on_curve_t(warp1->point, curve1->node.ctrl1, curve1->node.ctrl2, warp3->point, pt, INTERPOLATION_POINTS); float complex midpoint = warp3->point; casteljau(&warp1->point, &curve1->node.ctrl1, &curve1->node.ctrl2, &midpoint, t); midpoint = warp1->point; casteljau(&warp3->point, &curve2->node.ctrl2, &curve2->node.ctrl1, &midpoint, 1.0 - t); mix_warps(warp2, warp1, warp3, midpoint, t); node_insert_before(p, e, (dt_liquify_path_data_t *)curve2); handled = 2; goto done; } if(prev && e->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) { // add node to line const dt_liquify_warp_t *warp1 = &prev->warp; const dt_liquify_warp_t *warp3 = &e->warp; const float t = find_nearest_on_line_t(warp1->point, warp3->point, pt); dt_liquify_path_data_t *tmp = alloc_line_to(self, e->warp.point); if(!tmp) goto done; dt_liquify_warp_t *warp2 = &tmp->warp; const float complex midpoint = cmix(warp1->point, warp3->point, t); mix_warps(warp2, warp1, warp3, midpoint, t); node_insert_before(p, e, tmp); } } else if(g->last_hit.elem && g->last_hit.elem->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1 && g->last_hit.layer == DT_LIQUIFY_LAYER_CENTERPOINT) { // cycle node type: smooth -> cusp etc. dt_liquify_path_data_t *e = g->last_hit.elem; e->header.node_type = (e->header.node_type + 1) % DT_LIQUIFY_NODE_TYPE_LAST; handled = 2; goto done; } else if(g->last_hit.layer == DT_LIQUIFY_LAYER_STRENGTHPOINT) { // cycle warp type: linear -> radial etc. dt_liquify_path_data_t *e = g->last_hit.elem; if(e->header.type == DT_LIQUIFY_PATH_MOVE_TO_V1) { dt_liquify_warp_t *warp = &e->warp; warp->type = (warp->type + 1) % DT_LIQUIFY_WARP_TYPE_LAST; handled = 2; goto done; } } } if(which == GDK_BUTTON_PRIMARY && dt_modifier_is(g->last_mouse_mods, GDK_MOD1_MASK | GDK_CONTROL_MASK) && !dragged) { if(g->last_hit.layer == DT_LIQUIFY_LAYER_PATH) { // change segment dt_liquify_path_data_t *e = g->last_hit.elem; const dt_liquify_path_data_t *prev = node_prev(p, e); if(prev && e->header.type == DT_LIQUIFY_PATH_CURVE_TO_V1) { // curve -> line e->header.type = DT_LIQUIFY_PATH_LINE_TO_V1; e->header.node_type = DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; e->header.selected = e->header.hovered = 0; handled = 2; goto done; } if(prev && e->header.type == DT_LIQUIFY_PATH_LINE_TO_V1) { // line -> curve const float complex p0 = prev->warp.point; const float complex p1 = e->warp.point; dt_liquify_path_data_t *c = (dt_liquify_path_data_t *)e; e->header.type = DT_LIQUIFY_PATH_CURVE_TO_V1; e->header.node_type = DT_LIQUIFY_NODE_TYPE_AUTOSMOOTH; c->node.ctrl1 = (2 * p0 + p1) / 3.0; c->node.ctrl2 = ( p0 + 2 * p1) / 3.0; handled = 2; goto done; } } } } done: dt_iop_gui_leave_critical_section(self); if(which == GDK_BUTTON_PRIMARY) g->last_button1_pressed_pos = -1; g->last_hit = NOWHERE; if(handled) { update_warp_count(self); sync_pipe(self, handled == 2); } return handled; } static void _liquify_cairo_paint_point_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data); static void _liquify_cairo_paint_line_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data); static void _liquify_cairo_paint_curve_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data); static void _liquify_cairo_paint_node_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data); // we need this only because darktable has no radiobutton support static void _btn_make_radio_cb(GtkToggleButton *btn, gboolean ctrl_pressed, dt_iop_module_t *self) { dt_iop_liquify_gui_data_t *g = self->gui_data; dt_iop_liquify_params_t *p = self->params; // if currently dragging and a form (line or node) has been started, // does nothing (expect resetting the toggle button status). if(is_dragging(g) && g->temp && node_prev(p, g->temp)) { return; } g->creation_continuous = ctrl_pressed; dt_control_hinter_message(""); // if we are on a preview, it means that a form (point, line, curve) // has been started, but no node has yet been placed. in this case // we abort the current preview and let the new tool activated. if(g->status & DT_LIQUIFY_STATUS_PREVIEW) { node_delete(p, g->temp); g->temp = NULL; g->status &= ~DT_LIQUIFY_STATUS_PREVIEW; } /* Called from GtkGestureMultiPress::pressed (via our CAPTURE-phase * gesture that claims the sequence). The signal fires BEFORE the * togglebutton toggles, so we manage all toggle states explicitly. */ if(!btn) { /* NULL btn from gui_focus() / gui_reset() — disable all tools. */ gtk_toggle_button_set_active(g->btn_point_tool, FALSE); gtk_toggle_button_set_active(g->btn_line_tool, FALSE); gtk_toggle_button_set_active(g->btn_curve_tool, FALSE); gtk_toggle_button_set_active(g->btn_node_tool, FALSE); dt_control_hinter_message(""); return; } // now, let's enable and start a new form safely if(!gtk_toggle_button_get_active(btn)) { gtk_toggle_button_set_active(g->btn_point_tool, btn == g->btn_point_tool); gtk_toggle_button_set_active(g->btn_line_tool, btn == g->btn_line_tool); gtk_toggle_button_set_active(g->btn_curve_tool, btn == g->btn_curve_tool); gtk_toggle_button_set_active(g->btn_node_tool, btn == g->btn_node_tool); dt_liquify_layers[DT_LIQUIFY_LAYER_BACKGROUND].hint = btn == g->btn_point_tool ? _("add point: click and drag\nsize: scroll - " "strength: shift+scroll - direction: ctrl+scroll") : btn == g->btn_line_tool ? _("add line: click\nsize: scroll - " "strength: shift+scroll - direction: ctrl+scroll") : btn == g->btn_curve_tool ? _("add curve: click\nsize: scroll - " "strength: shift+scroll - direction: ctrl+scroll") : ""; // start the preview mode to show the shape that will be created if(btn == g->btn_point_tool || btn == g->btn_line_tool || btn == g->btn_curve_tool) { _start_new_shape(self); } if(btn) dt_iop_request_focus(self); } else { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), FALSE); } sync_pipe(self, FALSE); } static void btn_make_radio_callback(GtkGestureSingle *gesture, int n_press, double x, double y, dt_iop_module_t *self) { /* read the modifier state from the gesture: for a real click this is * the live event state, for a shortcut-activated press it is the * declared effect carried by _action_effect_button_state() (see * DT_ACTION_GESTURE_SYNTH_KEY in gtk.h); gtk_get_current_event() would * return the key event during shortcut dispatch and lose the declared * ctrl variant */ const gboolean ctrl_pressed = dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK); GtkWidget *btn = dt_gui_get_widget(gesture); _btn_make_radio_cb(GTK_TOGGLE_BUTTON(btn), ctrl_pressed, self); } void gui_update(dt_iop_module_t *self) { update_warp_count(self); } void gui_init(dt_iop_module_t *self) { dt_iop_liquify_gui_data_t *g = IOP_GUI_ALLOC(liquify); // A dummy surface for calculations only, no drawing. cairo_surface_t *cs = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); cairo_surface_destroy(cs); g->dragging = NOWHERE; g->temp = NULL; g->status = 0; g->last_mouse_pos = g->last_button1_pressed_pos = -1; g->last_hit = NOWHERE; g->node_index = 0; GtkWidget *label = dt_ui_label_new(_("warps|nodes count:")); g->label = GTK_LABEL(dt_ui_label_new("-")); GtkWidget *count = dt_gui_hbox(label, g->label); gtk_widget_set_tooltip_text (count, _("use a tool to add warps\nremove a warp: right-click")); GtkWidget *hbox = dt_gui_hbox(); self->widget = dt_gui_vbox(count, hbox); g->btn_node_tool = GTK_TOGGLE_BUTTON(dt_iop_togglebutton_new (self, NULL, N_("edit, add and delete nodes"), NULL, G_CALLBACK(btn_make_radio_callback), TRUE, 0, 0, _liquify_cairo_paint_node_tool, hbox)); g->btn_curve_tool = GTK_TOGGLE_BUTTON(dt_iop_togglebutton_new (self, N_("shapes"), N_("draw curves"), N_("draw multiple curves"), G_CALLBACK(btn_make_radio_callback), TRUE, 0, 0, _liquify_cairo_paint_curve_tool, hbox)); g->btn_line_tool = GTK_TOGGLE_BUTTON(dt_iop_togglebutton_new (self, N_("shapes"), N_("draw lines"), N_("draw multiple lines"), G_CALLBACK(btn_make_radio_callback), TRUE, 0, 0, _liquify_cairo_paint_line_tool, hbox)); g->btn_point_tool = GTK_TOGGLE_BUTTON(dt_iop_togglebutton_new (self, N_("shapes"), N_("draw points"), N_("draw multiple points"), G_CALLBACK(btn_make_radio_callback), TRUE, 0, 0, _liquify_cairo_paint_point_tool, hbox)); dt_liquify_layers[DT_LIQUIFY_LAYER_BACKGROUND].hint = ""; dt_liquify_layers[DT_LIQUIFY_LAYER_PATH].hint = _("add node: ctrl+click - remove path: right-click\n" "toggle line/curve: ctrl+alt+click"); dt_liquify_layers[DT_LIQUIFY_LAYER_CENTERPOINT].hint = _("move: click and drag - show/hide feathering controls: click\n" "autosmooth, cusp, smooth, symmetrical: ctrl+click" " - remove: right-click"); dt_liquify_layers[DT_LIQUIFY_LAYER_CTRLPOINT1].hint = _("shape of path: drag"); dt_liquify_layers[DT_LIQUIFY_LAYER_CTRLPOINT2].hint = _("shape of path: drag"); dt_liquify_layers[DT_LIQUIFY_LAYER_RADIUSPOINT].hint = _("radius: drag"); dt_liquify_layers[DT_LIQUIFY_LAYER_HARDNESSPOINT1].hint = _("hardness (center): drag"); dt_liquify_layers[DT_LIQUIFY_LAYER_HARDNESSPOINT2].hint = _("hardness (feather): drag"); dt_liquify_layers[DT_LIQUIFY_LAYER_STRENGTHPOINT].hint = _("strength: drag\n" "linear, grow, and shrink: ctrl+click"); } void gui_reset(dt_iop_module_t *self) { dt_iop_liquify_gui_data_t *g = self->gui_data; g->dragging = NOWHERE; g->temp = NULL; g->status = 0; _btn_make_radio_cb(NULL, FALSE, self); } // defgroup Button paint functions #define PREAMBLE \ cairo_save(cr); \ const gint s = MIN(w, h); \ cairo_translate(cr, x + (w / 2.0) - (s / 2.0), \ y + (h / 2.0) - (s / 2.0)); \ cairo_scale(cr, s, s); \ cairo_push_group(cr); \ cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); \ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); \ cairo_set_line_width(cr, 0.2); #define POSTAMBLE \ cairo_pop_group_to_source(cr); \ cairo_paint_with_alpha(cr, flags & CPF_ACTIVE ? 1.0 : 0.5); \ cairo_restore(cr); static void _liquify_cairo_paint_point_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data) { PREAMBLE; cairo_new_sub_path(cr); cairo_arc(cr, 0.5, 0.5, 0.2, 0.0, 2 * M_PI); cairo_fill(cr); POSTAMBLE; } static void _liquify_cairo_paint_line_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data) { PREAMBLE; cairo_move_to(cr, 0.1, 0.9); cairo_line_to(cr, 0.9, 0.1); cairo_stroke(cr); POSTAMBLE; } static void _liquify_cairo_paint_curve_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data) { PREAMBLE; cairo_move_to(cr, 0.1, 0.9); cairo_curve_to(cr, 0.1, 0.5, 0.5, 0.1, 0.9, 0.1); cairo_stroke(cr); POSTAMBLE; } static void _liquify_cairo_paint_node_tool(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data) { PREAMBLE; const double dashed[] = {0.2, 0.2}; cairo_set_dash(cr, dashed, 2, 0); cairo_set_line_width(cr, 0.1); cairo_arc(cr, 0.75, 0.75, 0.75, 2.8, 4.7124); cairo_stroke(cr); cairo_rectangle(cr, 0.2, 0.0, 0.4, 0.4); cairo_fill(cr); cairo_move_to(cr, 0.4, 0.2); cairo_line_to(cr, 0.5, 1.0); cairo_line_to(cr, 0.9, 0.7); cairo_close_path(cr); cairo_fill(cr); POSTAMBLE; } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on