[gtk/matthiasc/lottie] Add operations



commit da413a8261e78df2ed2f85584694c7c0cccfbfc9
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 | 431 ++++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 376 insertions(+), 55 deletions(-)
---
diff --git a/tests/curve.c b/tests/curve.c
index cb2aec08a7..7ec1a5834d 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,172 @@ 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);
+            }
+        }
+      else 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;
+
+          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;
 
-      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;
+              /* adjust the control point after the line segment */
+              c = &self->points[((self->dragged + 4) % self->n_points)];
 
-      self->points[(self->dragged + 1) % self->n_points].x += dx;
-      self->points[(self->dragged + 1) % self->n_points].y += dy;
+              l = dist (c, p);
+              opposite_point (p, d, l, c);
+            }
+        }
+      else
+        {
+          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;
 
-          if (self->symmetric)
-            l = dist (d, p);
+              /* then adjust the other control point */
+              if (self->symmetric)
+                l = dist (d, p);
+              else
+                l = dist (c, 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 +355,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 +469,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 +512,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 +568,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 +577,7 @@ demo_widget_init (DemoWidget *self)
 {
   GtkGesture *gesture;
   GMenu *menu;
+  GMenu *section;
   GMenuItem *item;
   GSimpleAction *action;
 
@@ -338,16 +600,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 +660,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 +690,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 +734,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 +752,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]