1 /* -*- mode: c; c-basic-offset: 2 -*-
4 * gcc -Wall -g $(pkg-config --cflags --libs gtk+-2.0 cairo) \
9 * http://en.wikipedia.org/wiki/Verlet_integration
10 * http://www.teknikus.dk/tj/gdc2001.htm
14 * - Fix box collision test
15 * - Add stick objects instead of hardcoding sticks
16 * - Add code to add boxes
22 #include <cairo-xlib.h>
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;
32 typedef struct _xy_pair Point;
33 typedef struct _xy_pair Vector;
38 typedef struct _Object Object;
39 typedef struct _Stick Stick;
40 typedef struct _Model Model;
46 Point previous_position;
65 Object *anchor_object;
66 Vector anchor_position;
72 model_init (Model *model)
74 const int num_objects = 20;
75 const int num_sticks = 40 - 3;
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;
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;
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];
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];
99 model->anchor_object = NULL;
103 model_accumulate_forces (Model *model)
107 for (i = 0; i < model->num_objects; i++) {
108 model->objects[i].force.x = 0;
109 model->objects[i].force.y = 3;
114 model_integrate (Model *model, double step)
120 for (i = 0; i < model->num_objects; i++) {
121 o = &model->objects[i];
126 x + (x - o->previous_position.x) + o->force.x * step * step;
128 y + (y - o->previous_position.y) + o->force.y * step * step;
130 o->previous_position.x = x;
131 o->previous_position.y = y;
136 model_constrain (Model *model, double step)
138 double dx, dy, x, y, distance, fraction, squared;
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;
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 &&
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;
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;
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;
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;
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);
198 /* Stick constraints, without square roots. */
199 squared = stick_length * stick_length;
200 for (i = 0; i < model->num_objects - 1; i++) {
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);
216 model_step (Model *model, double delta_t)
220 model_accumulate_forces (model);
221 model_integrate (model, delta_t);
223 for (i = 0; i < 10; i++)
224 model_constrain (model, delta_t);
226 model->theta += delta_t;
230 object_distance (Object *object, double x, double y)
234 dx = object->position.x - x;
235 dy = object->position.y - y;
237 return sqrt (dx*dx + dy*dy);
241 model_find_nearest (Model *model, double x, double y)
244 double distance, min_distance;
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];
258 typedef struct _Color Color;
260 double red, green, blue;
264 draw_star (cairo_t *cr,
270 const int spike_count = 5;
271 const int inner_radius = 2;
272 const int outer_radius = 4;
276 cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
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;
283 cairo_move_to (cr, x, y);
285 cairo_line_to (cr, x, y);
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;
290 cairo_line_to (cr, x, y);
296 draw_lines (cairo_t *cr,
302 cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
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);
308 for (i = 0; i < model->num_objects; i++) {
310 model->objects[i].position.x,
311 model->objects[i].position.y);
318 draw_constraints (cairo_t *cr,
322 cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
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);
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);
339 static Color blue = { 0, 0, 1 };
340 static Color red = { 1, 0, 0 };
343 sproing_expose_event (GtkWidget *widget,
344 GdkEventExpose *event,
351 cr = gdk_cairo_create (widget->window);
353 cairo_set_source_rgb (cr, 1, 1, 1);
356 draw_constraints (cr, model, &red);
357 draw_lines (cr, model, &blue);
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);
372 sproing_button_press_event (GtkWidget *widget,
373 GdkEventButton *event,
378 if (event->button != 1)
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);
389 sproing_button_release_event (GtkWidget *widget,
390 GdkEventButton *event,
395 if ((event->state & GDK_BUTTON1_MASK) == 0)
398 model->anchor_object = NULL;
404 sproing_motion_notify_event (GtkWidget *widget,
405 GdkEventMotion *event,
410 GdkModifierType state;
412 gdk_window_get_pointer (event->window, &x, &y, &state);
414 model->anchor_position.x = x + 0.5;
415 model->anchor_position.y = y + 0.5;
421 spring_constant_changed (GtkSpinButton *spinbutton, gpointer user_data)
423 Model *model = user_data;
425 model->k = gtk_spin_button_get_value (spinbutton);
429 friction_changed (GtkSpinButton *spinbutton, gpointer user_data)
431 Model *model = user_data;
433 model->friction = gtk_spin_button_get_value (spinbutton);
437 create_spinners (Model *model)
440 GtkWidget *spinner, *label;
442 hbox = gtk_hbox_new (FALSE, 8);
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);
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);
466 create_window (Model *model)
474 window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
475 gtk_window_set_title (GTK_WINDOW (window), "Drawing Area");
477 g_signal_connect (window, "destroy",
478 G_CALLBACK (gtk_main_quit), &window);
480 gtk_container_set_border_width (GTK_CONTAINER (window), 8);
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);
487 * Create the drawing area
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);
494 da = gtk_drawing_area_new ();
495 /* set a minimum size */
496 gtk_widget_set_size_request (da, 600, 500);
498 gtk_container_add (GTK_CONTAINER (frame), da);
500 /* Signals used to handle backing pixmap */
502 g_signal_connect (da, "expose_event",
503 G_CALLBACK (sproing_expose_event), model);
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);
514 /* Ask to receive events the drawing area doesn't normally
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);
524 spinners = create_spinners (model);
525 gtk_box_pack_start (GTK_BOX (vbox), spinners, FALSE, FALSE, 0);
530 typedef struct _Closure Closure;
532 GtkWidget *drawing_area;
538 timeout_callback (gpointer data)
540 Closure *closure = data;
543 for (i = 0; i < 3; i++)
544 model_step (closure->model, 0.5);
547 if (closure->i == 1) {
548 gtk_widget_queue_draw (closure->drawing_area);
556 main (int argc, char *argv[])
561 gtk_init (&argc, &argv);
563 closure.drawing_area = create_window (&model);
565 gtk_widget_show_all (gtk_widget_get_toplevel (closure.drawing_area));
566 closure.model = &model;
567 g_timeout_add (100, timeout_callback, &closure);