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 = 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;
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[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);
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 < 50; 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_sticks; i++) {
310 model->sticks[i].a->position.x,
311 model->sticks[i].a->position.y);
313 model->sticks[i].b->position.x,
314 model->sticks[i].b->position.y);
321 draw_constraints (cairo_t *cr,
325 cairo_set_source_rgba (cr, color->red, color->green, color->blue, 0.5);
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);
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);
342 static Color blue = { 0, 0, 1 };
343 static Color red = { 1, 0, 0 };
346 sproing_expose_event (GtkWidget *widget,
347 GdkEventExpose *event,
353 cr = gdk_cairo_create (widget->window);
355 cairo_set_source_rgb (cr, 1, 1, 1);
358 draw_constraints (cr, model, &red);
359 draw_lines (cr, model, &blue);
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);
374 sproing_button_press_event (GtkWidget *widget,
375 GdkEventButton *event,
380 if (event->button != 1)
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);
391 sproing_button_release_event (GtkWidget *widget,
392 GdkEventButton *event,
397 if ((event->state & GDK_BUTTON1_MASK) == 0)
400 model->anchor_object = NULL;
406 sproing_motion_notify_event (GtkWidget *widget,
407 GdkEventMotion *event,
412 GdkModifierType state;
414 gdk_window_get_pointer (event->window, &x, &y, &state);
416 model->anchor_position.x = x + 0.5;
417 model->anchor_position.y = y + 0.5;
423 spring_constant_changed (GtkSpinButton *spinbutton, gpointer user_data)
425 Model *model = user_data;
427 model->k = gtk_spin_button_get_value (spinbutton);
431 friction_changed (GtkSpinButton *spinbutton, gpointer user_data)
433 Model *model = user_data;
435 model->friction = gtk_spin_button_get_value (spinbutton);
439 create_spinners (Model *model)
442 GtkWidget *spinner, *label;
444 hbox = gtk_hbox_new (FALSE, 8);
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);
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);
468 create_window (Model *model)
476 window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
477 gtk_window_set_title (GTK_WINDOW (window), "Drawing Area");
479 g_signal_connect (window, "destroy",
480 G_CALLBACK (gtk_main_quit), &window);
482 gtk_container_set_border_width (GTK_CONTAINER (window), 8);
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);
489 * Create the drawing area
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);
496 da = gtk_drawing_area_new ();
497 /* set a minimum size */
498 gtk_widget_set_size_request (da, 600, 500);
500 gtk_container_add (GTK_CONTAINER (frame), da);
502 /* Signals used to handle backing pixmap */
504 g_signal_connect (da, "expose_event",
505 G_CALLBACK (sproing_expose_event), model);
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);
516 /* Ask to receive events the drawing area doesn't normally
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);
526 spinners = create_spinners (model);
527 gtk_box_pack_start (GTK_BOX (vbox), spinners, FALSE, FALSE, 0);
532 typedef struct _Closure Closure;
534 GtkWidget *drawing_area;
540 timeout_callback (gpointer data)
542 Closure *closure = data;
545 for (i = 0; i < 3; i++)
546 model_step (closure->model, 0.5);
549 if (closure->i == 1) {
550 gtk_widget_queue_draw (closure->drawing_area);
558 main (int argc, char *argv[])
563 gtk_init (&argc, &argv);
565 closure.drawing_area = create_window (&model);
567 gtk_widget_show_all (gtk_widget_get_toplevel (closure.drawing_area));
568 closure.model = &model;
569 g_timeout_add (100, timeout_callback, &closure);