]> git.cworth.org Git - akamaru/blob - akamaru.c
First commit of akamaru.
[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 = 10;
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, j;
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[j].b->position.x - x;
189     dy = model->sticks[j].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[j].b->position.x = x + dx * (1 - fraction);
195     model->sticks[j].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 < 10; 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_objects; i++) {
309     cairo_line_to (cr,
310                    model->objects[i].position.x,
311                    model->objects[i].position.y);
312   }
313
314   cairo_stroke (cr);
315 }
316
317 static void
318 draw_constraints (cairo_t *cr,
319                   Model   *model,
320                   Color   *color)
321 {
322   cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
323
324   cairo_move_to (cr, 0, ground_level);
325   cairo_line_to (cr, 1500, ground_level);
326   cairo_line_to (cr, 1500, ground_level + 10);
327   cairo_line_to (cr, 0, ground_level + 10);
328   cairo_close_path (cr);
329
330   cairo_move_to (cr, 0, box_top);
331   cairo_line_to (cr, box_left, box_top);
332   cairo_line_to (cr, box_left, box_bottom);
333   cairo_line_to (cr, 0, box_bottom);
334   cairo_close_path (cr);
335
336   cairo_fill (cr);
337 }
338
339 static Color blue = { 0, 0, 1 };
340 static Color red = { 1, 0, 0 };
341
342 static gboolean
343 sproing_expose_event (GtkWidget      *widget,
344                       GdkEventExpose *event,
345                       gpointer        data)
346 {
347   Model *model = data;
348   int i;
349   cairo_t *cr;
350
351   cr = gdk_cairo_create (widget->window);
352
353   cairo_set_source_rgb (cr, 1, 1, 1);
354   cairo_paint (cr);
355
356   draw_constraints (cr, model, &red);
357   draw_lines (cr, model, &blue);
358
359 #if 0
360   for (i = 0; i < model->num_objects; i++) {
361     draw_star (widget, model->objects[i].position.x,
362                model->objects[i].position.y, model->objects[i].theta, &blue);
363   }
364 #endif
365
366   cairo_destroy (cr);
367
368   return TRUE;
369 }
370
371 static gboolean
372 sproing_button_press_event (GtkWidget      *widget,
373                             GdkEventButton *event,
374                             gpointer        data)
375 {
376   Model *model = data;
377
378   if (event->button != 1)
379     return TRUE;
380
381   model->anchor_position.x = event->x;
382   model->anchor_position.y = event->y;
383   model->anchor_object = model_find_nearest (model, event->x, event->y);
384
385   return TRUE;
386 }
387
388 static gboolean
389 sproing_button_release_event (GtkWidget      *widget,
390                               GdkEventButton *event,
391                               gpointer        data)
392 {
393   Model *model = data;
394
395   if ((event->state & GDK_BUTTON1_MASK) == 0)
396     return TRUE;
397
398   model->anchor_object = NULL;
399
400   return TRUE;
401 }
402
403 static gboolean
404 sproing_motion_notify_event (GtkWidget      *widget,
405                              GdkEventMotion *event,
406                              gpointer        data)
407 {
408   Model *model = data;
409   int x, y;
410   GdkModifierType state;
411
412   gdk_window_get_pointer (event->window, &x, &y, &state);
413
414   model->anchor_position.x = x + 0.5;
415   model->anchor_position.y = y + 0.5;
416
417   return TRUE;
418 }
419
420 static void
421 spring_constant_changed (GtkSpinButton *spinbutton, gpointer user_data)
422 {
423   Model *model = user_data;
424
425   model->k = gtk_spin_button_get_value (spinbutton);
426 }
427
428 static void
429 friction_changed (GtkSpinButton *spinbutton, gpointer user_data)
430 {
431   Model *model = user_data;
432
433   model->friction = gtk_spin_button_get_value (spinbutton);
434 }
435
436 static GtkWidget *
437 create_spinners (Model *model)
438 {
439   GtkWidget *hbox;
440   GtkWidget *spinner, *label;
441
442   hbox = gtk_hbox_new (FALSE, 8);
443
444   label = gtk_label_new_with_mnemonic ("_Spring constant:");
445   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
446   spinner = gtk_spin_button_new_with_range  (0.05, 30.00, 0.05);
447   gtk_label_set_mnemonic_widget (GTK_LABEL (label), spinner);
448   gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
449   gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinner), model->k);
450   g_signal_connect (spinner, "value-changed",
451                     G_CALLBACK (spring_constant_changed), model);
452
453   label = gtk_label_new_with_mnemonic ("_Friction:");
454   gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
455   spinner = gtk_spin_button_new_with_range  (0.05, 15.00, 0.05);
456   gtk_label_set_mnemonic_widget (GTK_LABEL (label), spinner);
457   gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, FALSE, 0);
458   gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinner), model->friction);
459   g_signal_connect (spinner, "value-changed",
460                     G_CALLBACK (friction_changed), model);
461
462   return hbox;
463 }
464
465 static GtkWidget *
466 create_window (Model *model)
467 {
468   GtkWidget *window;
469   GtkWidget *frame;
470   GtkWidget *vbox;
471   GtkWidget *da;
472   GtkWidget *spinners;
473
474   window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
475   gtk_window_set_title (GTK_WINDOW (window), "Drawing Area");
476
477   g_signal_connect (window, "destroy",
478                     G_CALLBACK (gtk_main_quit), &window);
479
480   gtk_container_set_border_width (GTK_CONTAINER (window), 8);
481
482   vbox = gtk_vbox_new (FALSE, 8);
483   gtk_container_set_border_width (GTK_CONTAINER (vbox), 8);
484   gtk_container_add (GTK_CONTAINER (window), vbox);
485
486   /*
487    * Create the drawing area
488    */
489       
490   frame = gtk_frame_new (NULL);
491   gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN);
492   gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0);
493       
494   da = gtk_drawing_area_new ();
495   /* set a minimum size */
496   gtk_widget_set_size_request (da, 600, 500);
497
498   gtk_container_add (GTK_CONTAINER (frame), da);
499
500   /* Signals used to handle backing pixmap */
501       
502   g_signal_connect (da, "expose_event",
503                     G_CALLBACK (sproing_expose_event), model);
504       
505   /* Event signals */
506       
507   g_signal_connect (da, "motion_notify_event",
508                     G_CALLBACK (sproing_motion_notify_event), model);
509   g_signal_connect (da, "button_press_event",
510                     G_CALLBACK (sproing_button_press_event), model);
511   g_signal_connect (da, "button_release_event",
512                     G_CALLBACK (sproing_button_release_event), model);
513
514   /* Ask to receive events the drawing area doesn't normally
515    * subscribe to
516    */
517   gtk_widget_set_events (da, gtk_widget_get_events (da)
518                          | GDK_LEAVE_NOTIFY_MASK
519                          | GDK_BUTTON_PRESS_MASK
520                          | GDK_BUTTON_RELEASE_MASK
521                          | GDK_POINTER_MOTION_MASK
522                          | GDK_POINTER_MOTION_HINT_MASK);
523
524   spinners = create_spinners (model);
525   gtk_box_pack_start (GTK_BOX (vbox), spinners, FALSE, FALSE, 0);
526
527   return da;
528 }
529
530 typedef struct _Closure Closure;
531 struct _Closure {
532   GtkWidget *drawing_area;
533   Model *model;
534   int i;
535 };
536
537 static gint
538 timeout_callback (gpointer data)
539 {
540   Closure *closure = data;
541   int i;
542
543   for (i = 0; i < 3; i++)
544     model_step (closure->model, 0.5);
545
546   closure->i++;
547   if (closure->i == 1) {
548     gtk_widget_queue_draw (closure->drawing_area);
549     closure->i = 0;
550   }
551
552   return TRUE;
553 }
554
555 int
556 main (int argc, char *argv[])
557 {
558   Closure closure;
559   Model model;
560
561   gtk_init (&argc, &argv);
562   model_init (&model);
563   closure.drawing_area = create_window (&model);
564   closure.i = 0;
565   gtk_widget_show_all (gtk_widget_get_toplevel (closure.drawing_area));
566   closure.model = &model;
567   g_timeout_add (100, timeout_callback, &closure);
568   gtk_main ();
569
570   return 0;
571 }