]> git.cworth.org Git - akamaru/blob - akamaru.c
Draw sticks instead of just lines between points.
[akamaru] / akamaru.c
1 /*                                           -*- mode: c; c-basic-offset: 2 -*-
2  * To compile:
3  *
4  *     gcc -Wall -g $(pkg-config --cflags --libs gtk+-2.0 cairo) \
5  *             akamaru.c -o akamaru
6  *
7  * See:
8  *
9  *     http://en.wikipedia.org/wiki/Verlet_integration
10  *     http://www.teknikus.dk/tj/gdc2001.htm
11  *
12  * TODO:
13  *
14  *     - Fix box collision test
15  *     - Add stick objects instead of hardcoding sticks
16  *     - Add code to add boxes
17  *     - Add circle object
18  */
19
20 #include <gtk/gtk.h>
21 #include <cairo.h>
22 #include <cairo-xlib.h>
23 #include <gdk/gdkx.h>
24 #include <math.h>
25
26 const double stick_length = 30;
27 const double ground_friction = 0.1, ground_level = 400;
28 const double box_left = 200, box_top = 200, box_bottom = 210;
29 const double elasticity = 0.7;
30 const double edge_fuzz = 1;
31
32 typedef struct _xy_pair Point;
33 typedef struct _xy_pair Vector;
34 struct _xy_pair {
35   double x, y;
36 };
37
38 typedef struct _Object Object;
39 typedef struct _Stick Stick;
40 typedef struct _Model Model;
41
42 struct _Object {
43   Vector force;
44
45   Point position;
46   Point previous_position;
47   Vector velocity;
48
49   double mass;
50   double theta;
51 };
52
53 struct _Stick {
54   Object *a, *b;
55 };
56
57 struct _Model {
58   int num_objects;
59   Object *objects;
60   int num_sticks;
61   Stick *sticks;
62   double k;
63   double friction;
64
65   Object *anchor_object;
66   Vector anchor_position;
67
68   double theta;
69 };
70
71 static void
72 model_init (Model *model)
73 {
74   const int num_objects = 20;
75   const int num_sticks = 40 - 3;
76   int i;
77
78   model->objects = g_new (Object, num_objects);
79   model->num_objects = num_objects;
80   model->sticks = g_new (Stick, num_sticks);
81   model->num_sticks = num_sticks;
82
83   for (i = 0; i < num_objects; i++) {
84     model->objects[i].position.x = 200;
85     model->objects[i].position.y = i * stick_length + 40;
86     model->objects[i].previous_position.x = 200;
87     model->objects[i].previous_position.y = i * stick_length + 40;
88
89     if (i + 1 < num_objects) {
90       model->sticks[i * 2].a = &model->objects[i];
91       model->sticks[i * 2].b = &model->objects[i + 1];
92     }
93     if (i + 2 < num_objects) {
94       model->sticks[i * 2 + 1].a = &model->objects[i];
95       model->sticks[i * 2 + 1].b = &model->objects[i + 2];
96     }
97   }
98
99   model->anchor_object = NULL;
100 }
101
102 static void
103 model_accumulate_forces (Model *model)
104 {
105   int i;
106
107   for (i = 0; i < model->num_objects; i++) {
108     model->objects[i].force.x = 0;
109     model->objects[i].force.y = 3;
110   }
111 }
112
113 static void
114 model_integrate (Model *model, double step)
115 {
116   double x, y;
117   Object *o;
118   int i;
119
120   for (i = 0; i < model->num_objects; i++) {
121     o = &model->objects[i];
122     x = o->position.x;
123     y = o->position.y;
124     
125     o->position.x =
126       x + (x - o->previous_position.x) + o->force.x * step * step;
127     o->position.y =
128       y + (y - o->previous_position.y) + o->force.y * step * step;
129
130     o->previous_position.x = x;
131     o->previous_position.y = y;
132   }
133 }
134
135 static void
136 model_constrain (Model *model, double step)
137 {
138   double dx, dy, x, y, distance, fraction, squared;
139   int i;
140
141   /* Anchor object constraint. */
142   if (model->anchor_object != NULL) {
143     model->anchor_object->position.x = model->anchor_position.x;
144     model->anchor_object->position.y = model->anchor_position.y;
145     model->anchor_object->previous_position.x = model->anchor_position.x;
146     model->anchor_object->previous_position.y = model->anchor_position.y;
147   }
148
149   /* FIXME: this should be "is point inside box" test instead. Figure
150    * out from previous_position which edge the point has passed
151    * through and reflect in that. */
152   for (i = 0; i < model->num_objects; i++) {
153     x = model->objects[i].position.x;
154     y = model->objects[i].position.y;
155     if (box_top - edge_fuzz <= y &&
156         model->objects[i].previous_position.y <= box_top + edge_fuzz &&
157         x < box_left) {
158       model->objects[i].position.y = box_top - (y - box_top) * elasticity;
159       model->objects[i].previous_position.y =
160         box_top - (model->objects[i].previous_position.y - box_top) * elasticity;
161     }
162   }
163
164   /* Ground collision detection constraints.  This puts a ground level
165    * in to make sure the points don't fall off the screen. */
166   for (i = 0; i < model->num_objects; i++) {
167     x = model->objects[i].position.x;
168     y = model->objects[i].position.y;
169
170     if (model->objects[i].position.y > ground_level) {
171       model->objects[i].position.y =
172         ground_level - (model->objects[i].position.y - ground_level) * elasticity;
173       model->objects[i].previous_position.y =
174         ground_level - (model->objects[i].previous_position.y - ground_level) * elasticity;
175
176       /* Friction on impact */
177       model->objects[i].position.x =
178         model->objects[i].position.x * (1 - ground_friction) +
179         model->objects[i].previous_position.x * ground_friction;
180     }
181   }
182
183 #if 1
184   /* Stick constraints. */
185   for (i = 0; i < model->num_sticks; i++) {
186     x = model->sticks[i].a->position.x;
187     y = model->sticks[i].a->position.y;
188     dx = model->sticks[i].b->position.x - x;
189     dy = model->sticks[i].b->position.y - y;
190     distance = sqrt (dx * dx + dy * dy);
191     fraction = (distance - stick_length) / distance / 2;
192     model->sticks[i].a->position.x = x + dx * fraction;
193     model->sticks[i].a->position.y = y + dy * fraction;
194     model->sticks[i].b->position.x = x + dx * (1 - fraction);
195     model->sticks[i].b->position.y = y + dy * (1 - fraction);
196   }
197 #else
198   /* Stick constraints, without square roots. */
199   squared = stick_length * stick_length;
200   for (i = 0; i < model->num_objects - 1; i++) {
201     j = i + 1;
202     x = model->objects[i].position.x;
203     y = model->objects[i].position.y;
204     dx = model->objects[j].position.x - x;
205     dy = model->objects[j].position.y - y;
206     fraction = squared / (dx * dx + dy * dy + squared) - 0.5;
207     model->objects[i].position.x = x + dx * fraction;
208     model->objects[i].position.y = y + dy * fraction;
209     model->objects[j].position.x = x + dx * (1 - fraction);
210     model->objects[j].position.y = y + dy * (1 - fraction);
211   }
212 #endif
213 }
214
215 static void
216 model_step (Model *model, double delta_t)
217 {
218   int i;
219
220   model_accumulate_forces (model);
221   model_integrate (model, delta_t);
222
223   for (i = 0; i < 50; i++)
224     model_constrain (model, delta_t);
225
226   model->theta += delta_t;
227 }
228
229 static double
230 object_distance (Object *object, double x, double y)
231 {
232   double dx, dy;
233
234   dx = object->position.x - x;
235   dy = object->position.y - y;
236
237   return sqrt (dx*dx + dy*dy);
238 }
239
240 static Object *
241 model_find_nearest (Model *model, double x, double y)
242 {
243   Object *object;
244   double distance, min_distance;
245   int i;
246
247   for (i = 0; i < model->num_objects; i++) {
248     distance = object_distance (&model->objects[i], x, y);
249     if (i == 0 || distance < min_distance) {
250       min_distance = distance;
251       object = &model->objects[i];
252     }
253   }
254
255   return object;
256 }
257
258 typedef struct _Color Color;
259 struct _Color {
260   double red, green, blue;
261 };
262
263 static void
264 draw_star (cairo_t      *cr,
265            gdouble      cx,
266            gdouble      cy,
267            double       theta,
268            Color        *color)
269 {
270   const int spike_count = 5;
271   const int inner_radius = 2;
272   const int outer_radius = 4;
273   double x, y;
274   int i;
275
276   cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
277   cairo_new_path (cr);
278   for (i = 0; i < spike_count; i++) {
279     x = cx + cos ((i * 2) * M_PI / spike_count + theta) * inner_radius;
280     y = cy + sin ((i * 2) * M_PI / spike_count + theta) * inner_radius;
281
282     if (i == 0)
283       cairo_move_to (cr, x, y);
284     else
285       cairo_line_to (cr, x, y);
286                  
287     x = cx + cos ((i * 2 + 1) * M_PI / spike_count + theta) * outer_radius;
288     y = cy + sin ((i * 2 + 1) * M_PI / spike_count + theta) * outer_radius;
289
290     cairo_line_to (cr, x, y);
291   }
292   cairo_fill (cr);
293 }
294
295 static void
296 draw_lines (cairo_t *cr,
297             Model   *model,
298             Color   *color)
299 {
300   int i;
301
302   cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
303   cairo_new_path (cr);
304   cairo_set_line_width (cr, 4);
305   cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
306   cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
307
308   for (i = 0; i < model->num_sticks; i++) {
309     cairo_move_to (cr,
310                    model->sticks[i].a->position.x,
311                    model->sticks[i].a->position.y);
312     cairo_line_to (cr,
313                    model->sticks[i].b->position.x,
314                    model->sticks[i].b->position.y);
315   }
316
317   cairo_stroke (cr);
318 }
319
320 static void
321 draw_constraints (cairo_t *cr,
322                   Model   *model,
323                   Color   *color)
324 {
325   cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
326
327   cairo_move_to (cr, 0, ground_level);
328   cairo_line_to (cr, 1500, ground_level);
329   cairo_line_to (cr, 1500, ground_level + 10);
330   cairo_line_to (cr, 0, ground_level + 10);
331   cairo_close_path (cr);
332
333   cairo_move_to (cr, 0, box_top);
334   cairo_line_to (cr, box_left, box_top);
335   cairo_line_to (cr, box_left, box_bottom);
336   cairo_line_to (cr, 0, box_bottom);
337   cairo_close_path (cr);
338
339   cairo_fill (cr);
340 }
341
342 static Color blue = { 0, 0, 1 };
343 static Color red = { 1, 0, 0 };
344
345 static gboolean
346 sproing_expose_event (GtkWidget      *widget,
347                       GdkEventExpose *event,
348                       gpointer        data)
349 {
350   Model *model = data;
351   cairo_t *cr;
352
353   cr = gdk_cairo_create (widget->window);
354
355   cairo_set_source_rgb (cr, 1, 1, 1);
356   cairo_paint (cr);
357
358   draw_constraints (cr, model, &red);
359   draw_lines (cr, model, &blue);
360
361 #if 0
362   for (i = 0; i < model->num_objects; i++) {
363     draw_star (widget, model->objects[i].position.x,
364                model->objects[i].position.y, model->objects[i].theta, &blue);
365   }
366 #endif
367
368   cairo_destroy (cr);
369
370   return TRUE;
371 }
372
373 static gboolean
374 sproing_button_press_event (GtkWidget      *widget,
375                             GdkEventButton *event,
376                             gpointer        data)
377 {
378   Model *model = data;
379
380   if (event->button != 1)
381     return TRUE;
382
383   model->anchor_position.x = event->x;
384   model->anchor_position.y = event->y;
385   model->anchor_object = model_find_nearest (model, event->x, event->y);
386
387   return TRUE;
388 }
389
390 static gboolean
391 sproing_button_release_event (GtkWidget      *widget,
392                               GdkEventButton *event,
393                               gpointer        data)
394 {
395   Model *model = data;
396
397   if ((event->state & GDK_BUTTON1_MASK) == 0)
398     return TRUE;
399
400   model->anchor_object = NULL;
401
402   return TRUE;
403 }
404
405 static gboolean
406 sproing_motion_notify_event (GtkWidget      *widget,
407                              GdkEventMotion *event,
408                              gpointer        data)
409 {
410   Model *model = data;
411   int x, y;
412   GdkModifierType state;
413
414   gdk_window_get_pointer (event->window, &x, &y, &state);
415
416   model->anchor_position.x = x + 0.5;
417   model->anchor_position.y = y + 0.5;
418
419   return TRUE;
420 }
421
422 static void
423 spring_constant_changed (GtkSpinButton *spinbutton, gpointer user_data)
424 {
425   Model *model = user_data;
426
427   model->k = gtk_spin_button_get_value (spinbutton);
428 }
429
430 static void
431 friction_changed (GtkSpinButton *spinbutton, gpointer user_data)
432 {
433   Model *model = user_data;
434
435   model->friction = gtk_spin_button_get_value (spinbutton);
436 }
437
438 static GtkWidget *
439 create_spinners (Model *model)
440 {
441   GtkWidget *hbox;
442   GtkWidget *spinner, *label;
443
444   hbox = gtk_hbox_new (FALSE, 8);
445
446   label = gtk_label_new_with_mnemonic ("_Spring constant:");
447   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
448   spinner = gtk_spin_button_new_with_range  (0.05, 30.00, 0.05);
449   gtk_label_set_mnemonic_widget (GTK_LABEL (label), spinner);
450   gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
451   gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinner), model->k);
452   g_signal_connect (spinner, "value-changed",
453                     G_CALLBACK (spring_constant_changed), model);
454
455   label = gtk_label_new_with_mnemonic ("_Friction:");
456   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
457   spinner = gtk_spin_button_new_with_range  (0.05, 15.00, 0.05);
458   gtk_label_set_mnemonic_widget (GTK_LABEL (label), spinner);
459   gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
460   gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinner), model->friction);
461   g_signal_connect (spinner, "value-changed",
462                     G_CALLBACK (friction_changed), model);
463
464   return hbox;
465 }
466
467 static GtkWidget *
468 create_window (Model *model)
469 {
470   GtkWidget *window;
471   GtkWidget *frame;
472   GtkWidget *vbox;
473   GtkWidget *da;
474   GtkWidget *spinners;
475
476   window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
477   gtk_window_set_title (GTK_WINDOW (window), "Drawing Area");
478
479   g_signal_connect (window, "destroy",
480                     G_CALLBACK (gtk_main_quit), &window);
481
482   gtk_container_set_border_width (GTK_CONTAINER (window), 8);
483
484   vbox = gtk_vbox_new (FALSE, 8);
485   gtk_container_set_border_width (GTK_CONTAINER (vbox), 8);
486   gtk_container_add (GTK_CONTAINER (window), vbox);
487
488   /*
489    * Create the drawing area
490    */
491       
492   frame = gtk_frame_new (NULL);
493   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
494   gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
495       
496   da = gtk_drawing_area_new ();
497   /* set a minimum size */
498   gtk_widget_set_size_request (da, 600, 500);
499
500   gtk_container_add (GTK_CONTAINER (frame), da);
501
502   /* Signals used to handle backing pixmap */
503       
504   g_signal_connect (da, "expose_event",
505                     G_CALLBACK (sproing_expose_event), model);
506       
507   /* Event signals */
508       
509   g_signal_connect (da, "motion_notify_event",
510                     G_CALLBACK (sproing_motion_notify_event), model);
511   g_signal_connect (da, "button_press_event",
512                     G_CALLBACK (sproing_button_press_event), model);
513   g_signal_connect (da, "button_release_event",
514                     G_CALLBACK (sproing_button_release_event), model);
515
516   /* Ask to receive events the drawing area doesn't normally
517    * subscribe to
518    */
519   gtk_widget_set_events (da, gtk_widget_get_events (da)
520                          | GDK_LEAVE_NOTIFY_MASK
521                          | GDK_BUTTON_PRESS_MASK
522                          | GDK_BUTTON_RELEASE_MASK
523                          | GDK_POINTER_MOTION_MASK
524                          | GDK_POINTER_MOTION_HINT_MASK);
525
526   spinners = create_spinners (model);
527   gtk_box_pack_start (GTK_BOX (vbox), spinners, FALSE, FALSE, 0);
528
529   return da;
530 }
531
532 typedef struct _Closure Closure;
533 struct _Closure {
534   GtkWidget *drawing_area;
535   Model *model;
536   int i;
537 };
538
539 static gint
540 timeout_callback (gpointer data)
541 {
542   Closure *closure = data;
543   int i;
544
545   for (i = 0; i < 3; i++)
546     model_step (closure->model, 0.5);
547
548   closure->i++;
549   if (closure->i == 1) {
550     gtk_widget_queue_draw (closure->drawing_area);
551     closure->i = 0;
552   }
553
554   return TRUE;
555 }
556
557 int
558 main (int argc, char *argv[])
559 {
560   Closure closure;
561   Model model;
562
563   gtk_init (&argc, &argv);
564   model_init (&model);
565   closure.drawing_area = create_window (&model);
566   closure.i = 0;
567   gtk_widget_show_all (gtk_widget_get_toplevel (closure.drawing_area));
568   closure.model = &model;
569   g_timeout_add (100, timeout_callback, &closure);
570   gtk_main ();
571
572   return 0;
573 }