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