599 lines
18 KiB
C
599 lines
18 KiB
C
/* PIKA - Photo and Image Kooker Application
|
|
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
|
|
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
|
|
*
|
|
* Original copyright, applying to most contents (license remains unchanged):
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <gegl.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "libpikamath/pikamath.h"
|
|
|
|
#include "display-types.h"
|
|
|
|
#include "core/pikacoords.h"
|
|
#include "core/pikacoords-interpolate.h"
|
|
#include "core/pikamarshal.h"
|
|
|
|
#include "pikamotionbuffer.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 pika_motion_buffer_dispose (GObject *object);
|
|
static void pika_motion_buffer_finalize (GObject *object);
|
|
static void pika_motion_buffer_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void pika_motion_buffer_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
static void pika_motion_buffer_push_event_history (PikaMotionBuffer *buffer,
|
|
const PikaCoords *coords);
|
|
static void pika_motion_buffer_pop_event_queue (PikaMotionBuffer *buffer,
|
|
PikaCoords *coords);
|
|
|
|
static void pika_motion_buffer_interpolate_stroke (PikaMotionBuffer *buffer,
|
|
PikaCoords *coords);
|
|
static gboolean pika_motion_buffer_event_queue_timeout (PikaMotionBuffer *buffer);
|
|
|
|
|
|
G_DEFINE_TYPE (PikaMotionBuffer, pika_motion_buffer, PIKA_TYPE_OBJECT)
|
|
|
|
#define parent_class pika_motion_buffer_parent_class
|
|
|
|
static guint motion_buffer_signals[LAST_SIGNAL] = { 0 };
|
|
|
|
|
|
static void
|
|
pika_motion_buffer_class_init (PikaMotionBufferClass *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 (PikaMotionBufferClass, stroke),
|
|
NULL, NULL,
|
|
pika_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 (PikaMotionBufferClass, hover),
|
|
NULL, NULL,
|
|
pika_marshal_VOID__POINTER_FLAGS_BOOLEAN,
|
|
G_TYPE_NONE, 3,
|
|
G_TYPE_POINTER,
|
|
GDK_TYPE_MODIFIER_TYPE,
|
|
G_TYPE_BOOLEAN);
|
|
|
|
object_class->dispose = pika_motion_buffer_dispose;
|
|
object_class->finalize = pika_motion_buffer_finalize;
|
|
object_class->set_property = pika_motion_buffer_set_property;
|
|
object_class->get_property = pika_motion_buffer_get_property;
|
|
}
|
|
|
|
static void
|
|
pika_motion_buffer_init (PikaMotionBuffer *buffer)
|
|
{
|
|
buffer->event_history = g_array_new (FALSE, FALSE, sizeof (PikaCoords));
|
|
buffer->event_queue = g_array_new (FALSE, FALSE, sizeof (PikaCoords));
|
|
}
|
|
|
|
static void
|
|
pika_motion_buffer_dispose (GObject *object)
|
|
{
|
|
PikaMotionBuffer *buffer = PIKA_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
|
|
pika_motion_buffer_finalize (GObject *object)
|
|
{
|
|
PikaMotionBuffer *buffer = PIKA_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
|
|
pika_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
|
|
pika_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 */
|
|
|
|
PikaMotionBuffer *
|
|
pika_motion_buffer_new (void)
|
|
{
|
|
return g_object_new (PIKA_TYPE_MOTION_BUFFER,
|
|
NULL);
|
|
}
|
|
|
|
void
|
|
pika_motion_buffer_begin_stroke (PikaMotionBuffer *buffer,
|
|
guint32 time,
|
|
PikaCoords *last_motion)
|
|
{
|
|
g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
|
|
g_return_if_fail (last_motion != NULL);
|
|
|
|
buffer->last_read_motion_time = time;
|
|
|
|
*last_motion = buffer->last_coords;
|
|
}
|
|
|
|
void
|
|
pika_motion_buffer_end_stroke (PikaMotionBuffer *buffer)
|
|
{
|
|
g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
|
|
|
|
if (buffer->event_delay_timeout)
|
|
{
|
|
g_source_remove (buffer->event_delay_timeout);
|
|
buffer->event_delay_timeout = 0;
|
|
}
|
|
|
|
pika_motion_buffer_event_queue_timeout (buffer);
|
|
}
|
|
|
|
/**
|
|
* pika_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 PikaCoords 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.
|
|
*
|
|
* Returns: %TRUE if the motion was significant enough to be
|
|
* processed, %FALSE otherwise.
|
|
**/
|
|
gboolean
|
|
pika_motion_buffer_motion_event (PikaMotionBuffer *buffer,
|
|
PikaCoords *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 (PIKA_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
|
|
{
|
|
PikaCoords 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,
|
|
PikaCoords, 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 = pika_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)
|
|
{
|
|
pika_motion_buffer_interpolate_stroke (buffer, coords);
|
|
}
|
|
else
|
|
{
|
|
buffer->event_delay = TRUE;
|
|
pika_motion_buffer_push_event_history (buffer, coords);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (buffer->event_delay)
|
|
buffer->event_delay = FALSE;
|
|
|
|
pika_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
|
|
pika_motion_buffer_get_last_motion_time (PikaMotionBuffer *buffer)
|
|
{
|
|
g_return_val_if_fail (PIKA_IS_MOTION_BUFFER (buffer), 0);
|
|
|
|
return buffer->last_read_motion_time;
|
|
}
|
|
|
|
void
|
|
pika_motion_buffer_request_stroke (PikaMotionBuffer *buffer,
|
|
GdkModifierType state,
|
|
guint32 time)
|
|
{
|
|
GdkModifierType event_state;
|
|
gint keep = 0;
|
|
|
|
g_return_if_fail (PIKA_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)
|
|
{
|
|
PikaCoords buf_coords;
|
|
|
|
pika_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) pika_motion_buffer_event_queue_timeout,
|
|
buffer);
|
|
}
|
|
}
|
|
|
|
void
|
|
pika_motion_buffer_request_hover (PikaMotionBuffer *buffer,
|
|
GdkModifierType state,
|
|
gboolean proximity)
|
|
{
|
|
g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
|
|
|
|
if (buffer->event_queue->len > 0)
|
|
{
|
|
PikaCoords buf_coords = g_array_index (buffer->event_queue,
|
|
PikaCoords,
|
|
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
|
|
pika_motion_buffer_push_event_history (PikaMotionBuffer *buffer,
|
|
const PikaCoords *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
|
|
pika_motion_buffer_pop_event_queue (PikaMotionBuffer *buffer,
|
|
PikaCoords *coords)
|
|
{
|
|
*coords = g_array_index (buffer->event_queue, PikaCoords, 0);
|
|
|
|
g_array_remove_index (buffer->event_queue, 0);
|
|
}
|
|
|
|
static void
|
|
pika_motion_buffer_interpolate_stroke (PikaMotionBuffer *buffer,
|
|
PikaCoords *coords)
|
|
{
|
|
PikaCoords 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 (PikaCoords));
|
|
|
|
catmull[0] = g_array_index (buffer->event_history, PikaCoords, i - 1);
|
|
catmull[1] = g_array_index (buffer->event_history, PikaCoords, i);
|
|
catmull[2] = g_array_index (buffer->event_queue, PikaCoords, 0);
|
|
catmull[3] = *coords;
|
|
|
|
pika_coords_interpolate_catmull (catmull, EVENT_FILL_PRECISION / 2,
|
|
ret_coords, NULL);
|
|
|
|
/* Push the last actual event in history */
|
|
pika_motion_buffer_push_event_history (buffer,
|
|
&g_array_index (buffer->event_queue,
|
|
PikaCoords, 0));
|
|
|
|
g_array_set_size (buffer->event_queue, 0);
|
|
|
|
g_array_append_vals (buffer->event_queue,
|
|
&g_array_index (ret_coords, PikaCoords, 0),
|
|
ret_coords->len);
|
|
|
|
g_array_free (ret_coords, TRUE);
|
|
}
|
|
|
|
static gboolean
|
|
pika_motion_buffer_event_queue_timeout (PikaMotionBuffer *buffer)
|
|
{
|
|
buffer->event_delay = FALSE;
|
|
buffer->event_delay_timeout = 0;
|
|
|
|
if (buffer->event_queue->len > 0)
|
|
{
|
|
PikaCoords last_coords = g_array_index (buffer->event_queue,
|
|
PikaCoords,
|
|
buffer->event_queue->len - 1);
|
|
|
|
pika_motion_buffer_push_event_history (buffer, &last_coords);
|
|
|
|
pika_motion_buffer_request_stroke (buffer,
|
|
buffer->last_active_state,
|
|
buffer->last_read_motion_time);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|