[gtk/matthiasc/lottie: 1/7] Add operations
- From: Matthias Clasen <matthiasc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gtk/matthiasc/lottie: 1/7] Add operations
- Date: Sat, 21 Nov 2020 15:36:44 +0000 (UTC)
commit ca4d610bf2bca3b000c953f9338bd324f54d562a
Author: Matthias Clasen <mclasen redhat com>
Date: Fri Nov 20 15:35:46 2020 -0500
Add operations
Allow setting the type of segments to move, line or
curve, from the context menu, and do the necessary
adjustments to maintain smoothness, in particular
between curve and line segments.
tests/curve.c | 433 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 378 insertions(+), 55 deletions(-)
---
diff --git a/tests/curve.c b/tests/curve.c
index cb2aec08a7..bc8c3a18a0 100644
--- a/tests/curve.c
+++ b/tests/curve.c
@@ -1,12 +1,94 @@
+/* TODO
+ * - point insert/remove
+ * - rename to CurveEditor
+ * - add properties
+ */
+
#include <gtk/gtk.h>
+/* Set q to the projection of p onto the line through a and b */
+static void
+closest_point (const graphene_point_t *p,
+ const graphene_point_t *a,
+ const graphene_point_t *b,
+ graphene_point_t *q)
+{
+ graphene_vec2_t n;
+ graphene_vec2_t ap;
+ float t;
+
+ graphene_vec2_init (&n, b->x - a->x, b->y - a->y);
+ graphene_vec2_init (&ap, p->x - a->x, p->y - a->y);
+
+ t = graphene_vec2_dot (&ap, &n) / graphene_vec2_dot (&n, &n);
+
+ q->x = a->x + t * (b->x - a->x);
+ q->y = a->y + t * (b->y - a->y);
+}
+
+/* Set q to the point on the line through p and a that is
+ * at a distance of d from p, on the opposite side
+ */
+static void
+opposite_point (const graphene_point_t *p,
+ const graphene_point_t *a,
+ float d,
+ graphene_point_t *q)
+{
+ graphene_vec2_t ap;
+ float t;
+
+ graphene_vec2_init (&ap, p->x - a->x, p->y - a->y);
+
+ t = - sqrt (d * d / graphene_vec2_dot (&ap, &ap));
+
+ q->x = p->x + t * (a->x - p->x);
+ q->y = p->y + t * (a->y - p->y);
+}
#define RADIUS 5
G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget)
+typedef enum
+{
+ MOVE,
+ LINE,
+ CURVE
+} Operation;
+
+static const char *
+op_to_string (Operation op)
+{
+ switch (op)
+ {
+ case MOVE:
+ return "move";
+ case LINE:
+ return "line";
+ case CURVE:
+ return "curve";
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static Operation
+op_from_string (const char *s)
+{
+ if (strcmp (s, "move") == 0)
+ return MOVE;
+ else if (strcmp (s, "line") == 0)
+ return LINE;
+ else if (strcmp (s, "curve") == 0)
+ return CURVE;
+ else
+ g_assert_not_reached ();
+}
+
typedef struct
{
+ Operation op;
gboolean edit;
gboolean smooth;
} PointData;
@@ -76,6 +158,7 @@ drag_update (GtkGestureDrag *gesture,
double x, y;
double dx, dy;
graphene_point_t *c, *p, *d;
+ double l1, l2;
if (self->dragged == -1)
return;
@@ -89,53 +172,174 @@ drag_update (GtkGestureDrag *gesture,
d = &self->points[self->dragged];
+ /* before moving the point, record the distances to its neighbors, since
+ * we may want to preserve those
+ */
+ c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
+ l1 = dist (d, c);
+ c = &self->points[(self->dragged + 1) % self->n_points];
+ l2 = dist (d, c);
+
dx = x - d->x;
dy = y - d->y;
- d->x += dx;
- d->y += dy;
-
if (self->dragged % 3 == 0)
{
- /* point is on curve */
+ /* dragged point is on curve */
+
+ Operation op, op1, op2;
+
+ /* first move the point itself */
+ d->x = x;
+ d->y = y;
+
+ /* adjust control points as needed */
+ op = self->point_data[self->dragged / 3].op;
+ op1 = self->point_data[((self->dragged - 1 + self->n_points) % self->n_points) / 3].op;
+
+ if (op1 == LINE)
+ {
+ /* the other endpoint of the line */
+ p = &self->points[(self->dragged - 3 + self->n_points) % self->n_points];
+
+ if (op == CURVE && self->point_data[self->dragged / 3].smooth)
+ {
+ /* adjust the control point after the line segment */
+ c = &self->points[(self->dragged + 1) % self->n_points];
+ opposite_point (d, p, l2, c);
+ }
+ else
+ {
+ c = &self->points[(self->dragged + 1) % self->n_points];
+ c->x += dx;
+ c->y += dy;
+ }
+
+ c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
+ c->x += dx;
+ c->y += dy;
+
+ op2 = self->point_data[((self->dragged - 4 + self->n_points) % self->n_points) / 3].op;
+ if (op2 == CURVE && self->point_data[((self->dragged - 3 + self->n_points) % self->n_points) /
3].smooth)
+ {
+ double l;
+
+ /* adjust the control point before the line segment */
+ c = &self->points[((self->dragged - 4 + self->n_points) % self->n_points)];
+
+ l = dist (c, p);
+ opposite_point (p, d, l, c);
+ }
+ }
+
+ if (op == LINE)
+ {
+ /* the other endpoint of the line */
+ p = &self->points[(self->dragged + 3) % self->n_points];
+
+ if (op1 == CURVE && self->point_data[self->dragged / 3].smooth)
+ {
+ /* adjust the control point before the line segment */
+ c = &self->points[(self->dragged - 1 + self->n_points) % self->n_points];
+ opposite_point (d, p, l1, c);
+ }
+ else
+ {
+ c = &self->points[(self->dragged -1 + self->n_points) % self->n_points];
+ c->x += dx;
+ c->y += dy;
+ }
+
+ c = &self->points[(self->dragged + 1) % self->n_points];
+ c->x += dx;
+ c->y += dy;
- self->points[(self->dragged - 1 + self->n_points) % self->n_points].x += dx;
- self->points[(self->dragged - 1 + self->n_points) % self->n_points].y += dy;
+ op2 = self->point_data[((self->dragged + 3) % self->n_points) / 3].op;
+ if (op2 == CURVE && self->point_data[((self->dragged + 3) % self->n_points) / 3].smooth)
+ {
+ double l;
+
+ /* adjust the control point after the line segment */
+ c = &self->points[((self->dragged + 4) % self->n_points)];
+
+ l = dist (c, p);
+ opposite_point (p, d, l, c);
+ }
+ }
- self->points[(self->dragged + 1) % self->n_points].x += dx;
- self->points[(self->dragged + 1) % self->n_points].y += dy;
+ if (op1 != LINE && op != LINE)
+ {
+ self->points[(self->dragged - 1 + self->n_points) % self->n_points].x += dx;
+ self->points[(self->dragged - 1 + self->n_points) % self->n_points].y += dy;
+
+ self->points[(self->dragged + 1) % self->n_points].x += dx;
+ self->points[(self->dragged + 1) % self->n_points].y += dy;
+ }
}
else
{
+ /* dragged point is a control point */
+
int point;
+ graphene_point_t *p1;
+ Operation op, op1;
if (self->dragged % 3 == 1)
{
point = (self->dragged - 1 + self->n_points) % self->n_points;
c = &self->points[(self->dragged - 2 + self->n_points) % self->n_points];
p = &self->points[point];
+
+ op = self->point_data[point / 3].op;
+ op1 = self->point_data[((self->dragged - 4 + self->n_points) % self->n_points) / 3].op;
+ p1 = &self->points[(self->dragged - 4 + self->n_points) % self->n_points];
}
else if (self->dragged % 3 == 2)
{
point = (self->dragged + 1) % self->n_points;
c = &self->points[(self->dragged + 2) % self->n_points];
p = &self->points[point];
+
+ op = self->point_data[self->dragged / 3].op;
+ op1 = self->point_data[point / 3].op;
+ p1 = &self->points[(self->dragged + 4) % self->n_points];
}
else
g_assert_not_reached ();
- if (self->point_data[point / 3].smooth)
+ if (op == CURVE && self->point_data[point / 3].smooth)
{
- double a, l;
+ if (op1 == CURVE)
+ {
+ double l;
- a = atan2 (self->points[self->dragged].y - p->y, self->points[self->dragged].x - p->x) + M_PI;
+ /* first move the point itself */
+ d->x = x;
+ d->y = y;
+
+ /* then adjust the other control point */
+ if (self->symmetric)
+ l = dist (d, p);
+ else
+ l = dist (c, p);
- if (self->symmetric)
- l = dist (d, p);
+ opposite_point (p, d, l, c);
+ }
+ else if (op1 == LINE)
+ {
+ graphene_point_t m = GRAPHENE_POINT_INIT (x, y);
+ closest_point (&m, p, p1, d);
+ }
else
- l = dist (c, p);
- c->x = p->x + l * cos (a);
- c->y = p->y + l * sin (a);
+ {
+ d->x = x;
+ d->y = y;
+ }
+ }
+ else
+ {
+ d->x = x;
+ d->y = y;
}
}
@@ -153,29 +357,85 @@ drag_end (GtkGestureDrag *gesture,
self->symmetric = FALSE;
}
+static void
+maintain_smoothness (DemoWidget *self,
+ int point)
+{
+ gboolean smooth;
+ Operation op, op1;
+
+ smooth = self->point_data[point / 3].smooth;
+
+ op = self->point_data[point / 3].op;
+ op1 = self->point_data[((point - 1 + self->n_points) % self->n_points) / 3].op;
+
+ if (smooth)
+ {
+ graphene_point_t *p;
+
+ p = &self->points[point];
+
+ if (op == CURVE && op1 == CURVE)
+ {
+ graphene_point_t *c, *c2;
+ float d;
+
+ c = &self->points[(point - 1 + self->n_points) % self->n_points];
+ c2 = &self->points[(point + 1) % self->n_points];
+
+ d = dist (c, p);
+ opposite_point (p, c2, d, c);
+ }
+ else if (op == CURVE && op1 == LINE)
+ {
+ graphene_point_t *c, *p2;
+ float d;
+
+ c = &self->points[(point + 1) % self->n_points];
+ p2 = &self->points[(point - 3 + self->n_points) % self->n_points];
+
+ d = dist (c, p);
+ opposite_point (p, p2, d, c);
+ }
+ else if (op == LINE && op1 == CURVE)
+ {
+ graphene_point_t *c, *p2;
+ float d;
+
+ c = &self->points[(point - 1 + self->n_points) % self->n_points];
+ p2 = &self->points[(point + 3) % self->n_points];
+
+ d = dist (c, p);
+ opposite_point (p, p2, d, c);
+ }
+ }
+}
+
static void
toggle_smooth (GSimpleAction *action,
GVariant *value,
gpointer data)
{
DemoWidget *self = DEMO_WIDGET (data);
- gboolean smooth = g_variant_get_boolean (value);
- self->point_data[self->context / 3].smooth = smooth;
- if (smooth)
- {
- graphene_point_t *p, *c, *c2;
- float a, d;
+ self->point_data[self->context / 3].smooth = g_variant_get_boolean (value);
- p = &self->points[self->context];
- c = &self->points[(self->context - 1 + self->n_points) % self->n_points];
- c2 = &self->points[(self->context + 1 + self->n_points) % self->n_points];
+ maintain_smoothness (self, self->context);
- a = atan2 (c2->y - p->y, c2->x - p->x) + M_PI;
- d = dist (c, p);
- c->x = p->x + d * cos (a);
- c->y = p->y + d * sin (a);
- }
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+set_operation (GSimpleAction *action,
+ GVariant *value,
+ gpointer data)
+{
+ DemoWidget *self = DEMO_WIDGET (data);
+
+ self->point_data[self->context / 3].op = op_from_string (g_variant_get_string (value, NULL));
+
+ maintain_smoothness (self, self->context);
+ maintain_smoothness (self, (self->context + 3) % self->n_points);
gtk_widget_queue_draw (GTK_WIDGET (self));
}
@@ -211,6 +471,10 @@ pressed (GtkGestureClick *gesture,
action = g_action_map_lookup_action (self->actions, "smooth");
g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_boolean (self->point_data[i /
3].smooth));
+ action = g_action_map_lookup_action (self->actions, "operation");
+
+ g_simple_action_set_state (G_SIMPLE_ACTION (action), g_variant_new_string (op_to_string
(self->point_data[i / 3].op)));
+
gtk_popover_set_pointing_to (GTK_POPOVER (self->menu),
&(const GdkRectangle){ x, y, 1, 1 });
gtk_popover_popup (GTK_POPOVER (self->menu));
@@ -250,16 +514,14 @@ released (GtkGestureClick *gesture,
if (self->point_data[i / 3].smooth)
{
graphene_point_t *p, *c, *c2;
- float a, d;
+ float d;
p = &self->points[i];
c = &self->points[(i - 1 + self->n_points) % self->n_points];
c2 = &self->points[(i + 1 + self->n_points) % self->n_points];
- a = atan2 (c2->y - p->y, c2->x - p->x) + M_PI;
d = dist (c, p);
- c->x = p->x + d * cos (a);
- c->y = p->y + d * sin (a);
+ opposite_point (p, c2, d, c);
}
}
}
@@ -308,6 +570,7 @@ init_points (DemoWidget *self)
{
self->point_data[i].edit = FALSE;
self->point_data[i].smooth = TRUE;
+ self->point_data[i].op = CURVE;
}
}
@@ -316,6 +579,7 @@ demo_widget_init (DemoWidget *self)
{
GtkGesture *gesture;
GMenu *menu;
+ GMenu *section;
GMenuItem *item;
GSimpleAction *action;
@@ -338,16 +602,41 @@ demo_widget_init (DemoWidget *self)
init_points (self);
self->actions = G_ACTION_MAP (g_simple_action_group_new ());
+
action = g_simple_action_new_stateful ("smooth", NULL, g_variant_new_boolean (FALSE));
g_signal_connect (action, "change-state", G_CALLBACK (toggle_smooth), self);
g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
gtk_widget_insert_action_group (GTK_WIDGET (self), "point", G_ACTION_GROUP (self->actions));
+ action = g_simple_action_new_stateful ("operation", G_VARIANT_TYPE_STRING, g_variant_new_string ("curve"));
+ g_signal_connect (action, "change-state", G_CALLBACK (set_operation), self);
+ g_action_map_add_action (G_ACTION_MAP (self->actions), G_ACTION (action));
+
+ gtk_widget_insert_action_group (GTK_WIDGET (self), "point", G_ACTION_GROUP (self->actions));
+
menu = g_menu_new ();
+
item = g_menu_item_new ("Smooth", "point.smooth");
g_menu_append_item (menu, item);
g_object_unref (item);
+ section = g_menu_new ();
+
+ item = g_menu_item_new ("Move", "point.operation::move");
+ g_menu_append_item (section, item);
+ g_object_unref (item);
+
+ item = g_menu_item_new ("Line", "point.operation::line");
+ g_menu_append_item (section, item);
+ g_object_unref (item);
+
+ item = g_menu_item_new ("Curve", "point.operation::curve");
+ g_menu_append_item (section, item);
+ g_object_unref (item);
+
+ g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+ g_object_unref (section);
+
self->menu = gtk_popover_menu_new_from_model (G_MENU_MODEL (menu));
g_object_unref (menu);
@@ -373,19 +662,27 @@ demo_widget_snapshot (GtkWidget *widget,
if (self->edit)
{
+ /* Add the skeleton */
+
gsk_path_builder_move_to (builder, self->points[0].x, self->points[0].y);
for (i = 1; i < self->n_points; i++)
{
gboolean edit;
+ gboolean line;
if (i % 3 == 2)
edit = self->point_data[((i + 3) % self->n_points) / 3].edit;
else
edit = self->point_data[i / 3].edit;
+ if (i % 3 == 0)
+ line = self->point_data[((i - 1 + self->n_points) % self->n_points) / 3].op != CURVE;
+ else
+ line = self->point_data[i / 3].op != CURVE;
+
if (edit)
{
- if (i % 3 == 2)
+ if (i % 3 == 2 || line)
gsk_path_builder_move_to (builder, self->points[i].x, self->points[i].y);
else
gsk_path_builder_line_to (builder, self->points[i].x, self->points[i].y);
@@ -395,15 +692,36 @@ demo_widget_snapshot (GtkWidget *widget,
gsk_path_builder_line_to (builder, self->points[0].x, self->points[0].y);
}
+ /* Add the curve itself */
+
gsk_path_builder_move_to (builder, self->points[0].x, self->points[0].y);
for (i = 1; i < self->n_points; i += 3)
{
- gsk_path_builder_curve_to (builder,
- self->points[i].x, self->points[i].y,
- self->points[(i + 1) % self->n_points].x, self->points[(i + 1) %
self->n_points].y,
- self->points[(i + 2) % self->n_points].x, self->points[(i + 2) %
self->n_points].y);
+ switch (self->point_data[i / 3].op)
+ {
+ case MOVE:
+ gsk_path_builder_move_to (builder,
+ self->points[(i + 2) % self->n_points].x, self->points[(i + 2) %
self->n_points].y);
+ break;
+
+ case LINE:
+ gsk_path_builder_line_to (builder,
+ self->points[(i + 2) % self->n_points].x, self->points[(i + 2) %
self->n_points].y);
+ break;
+
+ case CURVE:
+ gsk_path_builder_curve_to (builder,
+ self->points[i].x, self->points[i].y,
+ self->points[(i + 1) % self->n_points].x, self->points[(i + 1) %
self->n_points].y,
+ self->points[(i + 2) % self->n_points].x, self->points[(i + 2) %
self->n_points].y);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
}
+ /* Stroke everything we have so far */
+
path = gsk_path_builder_free_to_path (builder);
stroke = gsk_stroke_new (1);
gtk_snapshot_push_stroke (snapshot, path, stroke);
@@ -418,6 +736,8 @@ demo_widget_snapshot (GtkWidget *widget,
if (self->edit)
{
+ /* Draw the circles, in several passes, one for each color */
+
const char *colors[] = {
"red",
"green",
@@ -434,30 +754,33 @@ demo_widget_snapshot (GtkWidget *widget,
switch (j)
{
case 0:
- if (i % 3 == 0 &&
- self->edit &&
- self->point_data[i / 3].smooth)
- break;
- else
+ if (!(i % 3 == 0 &&
+ self->point_data[i / 3].smooth))
continue;
+ break;
case 1:
- if (i % 3 == 0 &&
- self->edit &&
- !self->point_data[i / 3].smooth)
- break;
- else
+ if (!(i % 3 == 0 &&
+ !self->point_data[i / 3].smooth))
continue;
+ break;
case 2:
- if (self->edit &&
- ((i % 3 == 1 &&
- self->point_data[i / 3].edit) ||
- (i % 3 == 2 &&
- self->point_data[((i + 3) % self->n_points) / 3].edit)))
- break;
+ if (i % 3 == 1)
+ {
+ if (!(self->point_data[i / 3].edit &&
+ self->point_data[i / 3].op == CURVE))
+ continue;
+ }
+ else if (i % 3 == 2)
+ {
+ if (!(self->point_data[((i + 3) % self->n_points) / 3].edit &&
+ self->point_data[i / 3].op == CURVE))
+ continue;
+ }
else
continue;
+ break;
default:
g_assert_not_reached ();
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]