/* GIMP - The GNU Image Manipulation Program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * This program 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. * * This program 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 this program. If not, see . */ #include "config.h" #include #include #include #include "libgimpmath/gimpmath.h" #include "display-types.h" #include "core/gimpcoords.h" #include "core/gimpcoords-interpolate.h" #include "core/gimpmarshal.h" #include "gimpmotionbuffer.h" /* Velocity unit is screen pixels per millisecond we pass to tools as 1. */ #define VELOCITY_UNIT 3.0 #define EVENT_FILL_PRECISION 6.0 #define DIRECTION_RADIUS (1.0 / MAX (scale_x, scale_y)) #define SMOOTH_FACTOR 0.3 enum { PROP_0 }; enum { STROKE, HOVER, LAST_SIGNAL }; /* local function prototypes */ static void gimp_motion_buffer_dispose (GObject *object); static void gimp_motion_buffer_finalize (GObject *object); static void gimp_motion_buffer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void gimp_motion_buffer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void gimp_motion_buffer_push_event_history (GimpMotionBuffer *buffer, const GimpCoords *coords); static void gimp_motion_buffer_pop_event_queue (GimpMotionBuffer *buffer, GimpCoords *coords); static void gimp_motion_buffer_interpolate_stroke (GimpMotionBuffer *buffer, GimpCoords *coords); static gboolean gimp_motion_buffer_event_queue_timeout (GimpMotionBuffer *buffer); G_DEFINE_TYPE (GimpMotionBuffer, gimp_motion_buffer, GIMP_TYPE_OBJECT) #define parent_class gimp_motion_buffer_parent_class static guint motion_buffer_signals[LAST_SIGNAL] = { 0 }; static void gimp_motion_buffer_class_init (GimpMotionBufferClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); motion_buffer_signals[STROKE] = g_signal_new ("stroke", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GimpMotionBufferClass, stroke), NULL, NULL, gimp_marshal_VOID__POINTER_UINT_FLAGS, G_TYPE_NONE, 3, G_TYPE_POINTER, G_TYPE_UINT, GDK_TYPE_MODIFIER_TYPE); motion_buffer_signals[HOVER] = g_signal_new ("hover", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (GimpMotionBufferClass, hover), NULL, NULL, gimp_marshal_VOID__POINTER_FLAGS_BOOLEAN, G_TYPE_NONE, 3, G_TYPE_POINTER, GDK_TYPE_MODIFIER_TYPE, G_TYPE_BOOLEAN); object_class->dispose = gimp_motion_buffer_dispose; object_class->finalize = gimp_motion_buffer_finalize; object_class->set_property = gimp_motion_buffer_set_property; object_class->get_property = gimp_motion_buffer_get_property; } static void gimp_motion_buffer_init (GimpMotionBuffer *buffer) { buffer->event_history = g_array_new (FALSE, FALSE, sizeof (GimpCoords)); buffer->event_queue = g_array_new (FALSE, FALSE, sizeof (GimpCoords)); } static void gimp_motion_buffer_dispose (GObject *object) { GimpMotionBuffer *buffer = GIMP_MOTION_BUFFER (object); if (buffer->event_delay_timeout) { g_source_remove (buffer->event_delay_timeout); buffer->event_delay_timeout = 0; } G_OBJECT_CLASS (parent_class)->dispose (object); } static void gimp_motion_buffer_finalize (GObject *object) { GimpMotionBuffer *buffer = GIMP_MOTION_BUFFER (object); if (buffer->event_history) { g_array_free (buffer->event_history, TRUE); buffer->event_history = NULL; } if (buffer->event_queue) { g_array_free (buffer->event_queue, TRUE); buffer->event_queue = NULL; } G_OBJECT_CLASS (parent_class)->finalize (object); } static void gimp_motion_buffer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void gimp_motion_buffer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } /* public functions */ GimpMotionBuffer * gimp_motion_buffer_new (void) { return g_object_new (GIMP_TYPE_MOTION_BUFFER, NULL); } void gimp_motion_buffer_begin_stroke (GimpMotionBuffer *buffer, guint32 time, GimpCoords *last_motion) { g_return_if_fail (GIMP_IS_MOTION_BUFFER (buffer)); g_return_if_fail (last_motion != NULL); buffer->last_read_motion_time = time; *last_motion = buffer->last_coords; } void gimp_motion_buffer_end_stroke (GimpMotionBuffer *buffer) { g_return_if_fail (GIMP_IS_MOTION_BUFFER (buffer)); if (buffer->event_delay_timeout) { g_source_remove (buffer->event_delay_timeout); buffer->event_delay_timeout = 0; } gimp_motion_buffer_event_queue_timeout (buffer); } /** * gimp_motion_buffer_motion_event: * @buffer: * @coords: * @time: * @event_fill: * * This function evaluates the event to decide if the change is big * enough to need handling and returns FALSE, if change is less than * set filter level taking a whole lot of load off any draw tools that * have no use for these events anyway. If the event is seen fit at * first look, it is evaluated for speed and smoothed. Due to lousy * time resolution of events pretty strong smoothing is applied to * timestamps for sensible speed result. This function is also ideal * for other event adjustment like pressure curve or calculating other * derived dynamics factors like angular velocity calculation from * tilt values, to allow for even more dynamic brushes. Calculated * distance to last event is stored in GimpCoords because its a * sideproduct of velocity calculation and is currently calculated in * each tool. If they were to use this distance, more resources on * recalculating the same value would be saved. * * Return value: %TRUE if the motion was significant enough to be * processed, %FALSE otherwise. **/ gboolean gimp_motion_buffer_motion_event (GimpMotionBuffer *buffer, GimpCoords *coords, guint32 time, gboolean event_fill) { gdouble delta_time = 0.001; gdouble delta_x = 0.0; gdouble delta_y = 0.0; gdouble distance = 1.0; gdouble scale_x = coords->xscale; gdouble scale_y = coords->yscale; g_return_val_if_fail (GIMP_IS_MOTION_BUFFER (buffer), FALSE); g_return_val_if_fail (coords != NULL, FALSE); /* the last_read_motion_time most be set unconditionally, so set * it early */ buffer->last_read_motion_time = time; delta_time = (buffer->last_motion_delta_time * (1 - SMOOTH_FACTOR) + (time - buffer->last_motion_time) * SMOOTH_FACTOR); if (buffer->last_motion_time == 0) { /* First pair is invalid to do any velocity calculation, so we * apply a constant value. */ coords->velocity = 1.0; } else { GimpCoords last_dir_event = buffer->last_coords; gdouble filter; gdouble dist; gdouble delta_dir; gdouble dir_delta_x = 0.0; gdouble dir_delta_y = 0.0; delta_x = last_dir_event.x - coords->x; delta_y = last_dir_event.y - coords->y; /* Events with distances less than the screen resolution are * not worth handling. */ filter = MIN (1.0 / scale_x, 1.0 / scale_y) / 2.0; if (fabs (delta_x) < filter && fabs (delta_y) < filter) { return FALSE; } distance = dist = sqrt (SQR (delta_x) + SQR (delta_y)); /* If even smoothed time resolution does not allow to guess for * speed, use last velocity. */ if (delta_time == 0) { coords->velocity = buffer->last_coords.velocity; } else { /* We need to calculate the velocity in screen coordinates * for human interaction */ gdouble screen_distance = (distance * MIN (scale_x, scale_y)); /* Calculate raw valocity */ coords->velocity = ((screen_distance / delta_time) / VELOCITY_UNIT); /* Adding velocity dependent smoothing, feels better in tools. */ coords->velocity = (buffer->last_coords.velocity * (1 - MIN (SMOOTH_FACTOR, coords->velocity)) + coords->velocity * MIN (SMOOTH_FACTOR, coords->velocity)); /* Speed needs upper limit */ coords->velocity = MIN (coords->velocity, 1.0); } if (((fabs (delta_x) > DIRECTION_RADIUS) && (fabs (delta_y) > DIRECTION_RADIUS)) || (buffer->event_history->len < 4)) { dir_delta_x = delta_x; dir_delta_y = delta_y; } else { gint x = CLAMP ((buffer->event_history->len - 1), 3, 15); while (((fabs (dir_delta_x) < DIRECTION_RADIUS) || (fabs (dir_delta_y) < DIRECTION_RADIUS)) && (x >= 0)) { last_dir_event = g_array_index (buffer->event_history, GimpCoords, x); dir_delta_x = last_dir_event.x - coords->x; dir_delta_y = last_dir_event.y - coords->y; x--; } } if ((fabs (dir_delta_x) < DIRECTION_RADIUS) || (fabs (dir_delta_y) < DIRECTION_RADIUS)) { coords->direction = buffer->last_coords.direction; } else { coords->direction = gimp_coords_direction (&last_dir_event, coords); } coords->direction = coords->direction - floor (coords->direction); delta_dir = coords->direction - buffer->last_coords.direction; if (delta_dir < -0.5) { coords->direction = (0.5 * coords->direction + 0.5 * (buffer->last_coords.direction - 1.0)); } else if (delta_dir > 0.5) { coords->direction = (0.5 * coords->direction + 0.5 * (buffer->last_coords.direction + 1.0)); } else { coords->direction = (0.5 * coords->direction + 0.5 * buffer->last_coords.direction); } coords->direction = coords->direction - floor (coords->direction); /* do event fill for devices that do not provide enough events */ if (distance >= EVENT_FILL_PRECISION && event_fill && buffer->event_history->len >= 2) { if (buffer->event_delay) { gimp_motion_buffer_interpolate_stroke (buffer, coords); } else { buffer->event_delay = TRUE; gimp_motion_buffer_push_event_history (buffer, coords); } } else { if (buffer->event_delay) buffer->event_delay = FALSE; gimp_motion_buffer_push_event_history (buffer, coords); } #ifdef EVENT_VERBOSE g_printerr ("DIST: %f, DT:%f, Vel:%f, Press:%f,smooth_dd:%f, POS: (%f, %f)\n", distance, delta_time, buffer->last_coords.velocity, coords->pressure, distance - dist, coords->x, coords->y); #endif } g_array_append_val (buffer->event_queue, *coords); buffer->last_coords = *coords; buffer->last_motion_time = time; buffer->last_motion_delta_time = delta_time; buffer->last_motion_delta_x = delta_x; buffer->last_motion_delta_y = delta_y; buffer->last_motion_distance = distance; return TRUE; } guint32 gimp_motion_buffer_get_last_motion_time (GimpMotionBuffer *buffer) { g_return_val_if_fail (GIMP_IS_MOTION_BUFFER (buffer), 0); return buffer->last_read_motion_time; } void gimp_motion_buffer_request_stroke (GimpMotionBuffer *buffer, GdkModifierType state, guint32 time) { GdkModifierType event_state; gint keep = 0; g_return_if_fail (GIMP_IS_MOTION_BUFFER (buffer)); if (buffer->event_delay) { /* If we are in delay we use LAST state, not current */ event_state = buffer->last_active_state; keep = 1; /* Holding one event in buf */ } else { /* Save the state */ event_state = state; } if (buffer->event_delay_timeout) { g_source_remove (buffer->event_delay_timeout); buffer->event_delay_timeout = 0; } buffer->last_active_state = state; while (buffer->event_queue->len > keep) { GimpCoords buf_coords; gimp_motion_buffer_pop_event_queue (buffer, &buf_coords); g_signal_emit (buffer, motion_buffer_signals[STROKE], 0, &buf_coords, time, event_state); } if (buffer->event_delay) { buffer->event_delay_timeout = g_timeout_add (50, (GSourceFunc) gimp_motion_buffer_event_queue_timeout, buffer); } } void gimp_motion_buffer_request_hover (GimpMotionBuffer *buffer, GdkModifierType state, gboolean proximity) { g_return_if_fail (GIMP_IS_MOTION_BUFFER (buffer)); if (buffer->event_queue->len > 0) { GimpCoords buf_coords = g_array_index (buffer->event_queue, GimpCoords, buffer->event_queue->len - 1); g_signal_emit (buffer, motion_buffer_signals[HOVER], 0, &buf_coords, state, proximity); g_array_set_size (buffer->event_queue, 0); } } /* private functions */ static void gimp_motion_buffer_push_event_history (GimpMotionBuffer *buffer, const GimpCoords *coords) { if (buffer->event_history->len == 4) g_array_remove_index (buffer->event_history, 0); g_array_append_val (buffer->event_history, *coords); } static void gimp_motion_buffer_pop_event_queue (GimpMotionBuffer *buffer, GimpCoords *coords) { *coords = g_array_index (buffer->event_queue, GimpCoords, 0); g_array_remove_index (buffer->event_queue, 0); } static void gimp_motion_buffer_interpolate_stroke (GimpMotionBuffer *buffer, GimpCoords *coords) { GimpCoords catmull[4]; GArray *ret_coords; gint i = buffer->event_history->len - 1; /* Note that there must be exactly one event in buffer or bad things * can happen. This must never get called under other circumstances. */ ret_coords = g_array_new (FALSE, FALSE, sizeof (GimpCoords)); catmull[0] = g_array_index (buffer->event_history, GimpCoords, i - 1); catmull[1] = g_array_index (buffer->event_history, GimpCoords, i); catmull[2] = g_array_index (buffer->event_queue, GimpCoords, 0); catmull[3] = *coords; gimp_coords_interpolate_catmull (catmull, EVENT_FILL_PRECISION / 2, ret_coords, NULL); /* Push the last actual event in history */ gimp_motion_buffer_push_event_history (buffer, &g_array_index (buffer->event_queue, GimpCoords, 0)); g_array_set_size (buffer->event_queue, 0); g_array_append_vals (buffer->event_queue, &g_array_index (ret_coords, GimpCoords, 0), ret_coords->len); g_array_free (ret_coords, TRUE); } static gboolean gimp_motion_buffer_event_queue_timeout (GimpMotionBuffer *buffer) { buffer->event_delay = FALSE; buffer->event_delay_timeout = 0; if (buffer->event_queue->len > 0) { GimpCoords last_coords = g_array_index (buffer->event_queue, GimpCoords, buffer->event_queue->len - 1); gimp_motion_buffer_push_event_history (buffer, &last_coords); gimp_motion_buffer_request_stroke (buffer, buffer->last_active_state, buffer->last_read_motion_time); } return FALSE; }