/* trochoids.c Magic tools to draw various centered trochoids; similar to art generated by devices like the Spirograph and Wondergraph. by Bill Kendrick with help from Pere Pujal Carabantes January 6, 2024 - January 26, 2024 */ #include #include #include #include "tp_magic_api.h" #include "SDL_image.h" #include "SDL_mixer.h" #define deg_cos(x) cosf((float) (x) * M_PI / 180.0) #define deg_sin(x) sinf((float) (x) * M_PI / 180.0) /* All _possible_ tools */ enum { TOOL_EPITROCHOID_SIZES, TOOL_EPITROCHOID_NOSIZES_1, TOOL_EPITROCHOID_NOSIZES_2, TOOL_EPITROCHOID_NOSIZES_3, TOOL_HYPOTROCHOID_SIZES, TOOL_HYPOTROCHOID_NOSIZES_1, TOOL_HYPOTROCHOID_NOSIZES_2, TOOL_HYPOTROCHOID_NOSIZES_3, NUM_TOOLS }; Uint8 tp_offers_sizes = 1; int num_tools[2] = { 6, /* when sizes not available */ 2, /* when sizes available */ }; int *which_to_tool; int which_to_tool_per_size_availability[2][6] = { /* when sizes not available */ { TOOL_EPITROCHOID_NOSIZES_1, TOOL_EPITROCHOID_NOSIZES_2, TOOL_EPITROCHOID_NOSIZES_3, TOOL_HYPOTROCHOID_NOSIZES_1, TOOL_HYPOTROCHOID_NOSIZES_2, TOOL_HYPOTROCHOID_NOSIZES_3, }, /* when sizes available */ { TOOL_EPITROCHOID_SIZES, TOOL_HYPOTROCHOID_SIZES, -1, -1, -1, -1, }, }; #define SIZE_WELLINSIDE 0.33 #define SIZE_INSIDE 0.66 #define SIZE_EDGE 1.00 #define SIZE_OUTSIDE 1.50 #define SIZE_WELLOUTSIDE 2.00 #define NUM_SIZES 5 float trochoids_sizes_per_size_setting[NUM_SIZES] = { SIZE_WELLINSIDE, SIZE_INSIDE, SIZE_EDGE, SIZE_OUTSIDE, SIZE_WELLOUTSIDE, }; /* Default to "SIZE_EDGE" */ #define DEFAULT_SIZE 3 float trochoids_sizes_per_tool[NUM_TOOLS] = { 0, // N/A; size controls available SIZE_INSIDE, SIZE_EDGE, SIZE_WELLOUTSIDE, 0, // N/A; size controls available SIZE_INSIDE, SIZE_EDGE, SIZE_WELLOUTSIDE, }; const char *icon_filenames[NUM_TOOLS] = { "epitrochoid_edge.png", /* use "edge" variation when sizes are available */ "epitrochoid_inside.png", "epitrochoid_edge.png", "epitrochoid_outside.png", "hypotrochoid_edge.png", /* use "edge" variation when sizes are available */ "hypotrochoid_inside.png", "hypotrochoid_edge.png", "hypotrochoid_outside.png", }; const char *tool_names[NUM_TOOLS] = { gettext_noop("Epitrochoid"), gettext_noop("Epitrochoid Inside"), gettext_noop("Epitrochoid Edge"), gettext_noop("Epitrochoid Outside"), gettext_noop("Hypotrochoid"), gettext_noop("Hypotrochoid Inside"), gettext_noop("Hypotrochoid Edge"), gettext_noop("Hypotrochoid Outside"), }; const char *tool_descriptions[NUM_TOOLS] = { /* Epitrochoids */ gettext_noop ("Click to start drawing an epitrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling outside of it. Use the size option to change where the pen is."), gettext_noop ("Click to start drawing an epitrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling outside of it. Then pen is within the rolling circle."), gettext_noop ("Click to start drawing an epitrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling outside of it. Then pen is on the edge of the rolling circle."), gettext_noop ("Click to start drawing an epitrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling outside of it. Then pen is outside the rolling circle."), /* Hypotrochoids */ gettext_noop ("Click to start drawing a hypotrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling inside it. Use the size option to change where the pen is."), gettext_noop ("Click to start drawing a hypotrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling inside it. Then pen is within the rolling circle."), gettext_noop ("Click to start drawing a hypotrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling inside it. Then pen is on the edge of the rolling circle."), gettext_noop ("Click to start drawing a hypotrochoid. Drag left/right to change the size of the fixed circle, and up/down to change the size of the circle rolling inside it. Then pen is outside the rolling circle."), }; /* Sound effects (same for everyone) */ enum { SND_DRAG, SND_RELEASE_EPITROCHOID, SND_RELEASE_HYPOTROCHOID, NUM_SNDS }; Mix_Chunk *sound_effects[NUM_SNDS]; const char *sound_filenames[NUM_SNDS] = { "trochoids_drag.ogg", "epitrochoid.ogg", "hypotrochoid.ogg", }; Uint8 trochoids_size = 1; int trochoids_x, trochoids_y, dragged = 0; Uint32 trochoids_color = 0x00000000; int rotator_anim_a = 0; /* Function prototypes: */ Uint32 trochoids_api_version(void); int trochoids_init(magic_api * api, Uint8 disabled_features, Uint8 complexity_level); int trochoids_get_tool_count(magic_api * api); SDL_Surface *trochoids_get_icon(magic_api * api, int which); char *trochoids_get_name(magic_api * api, int which); int trochoids_get_group(magic_api * api, int which); int trochoids_get_order(int which); char *trochoids_get_description(magic_api * api, int which, int mode); int trochoids_requires_colors(magic_api * api, int which); int trochoids_modes(magic_api * api, int which); Uint8 trochoids_accepted_sizes(magic_api * api, int which, int mode); Uint8 trochoids_default_size(magic_api * api, int which, int mode); void trochoids_shutdown(magic_api * api); void trochoids_click(magic_api * api, int which, int mode, SDL_Surface * canvas, SDL_Surface * snapshot, int x, int y, SDL_Rect * update_rect); void trochoids_drag(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * snapshot, int old_x, int old_y, int x, int y, SDL_Rect * update_rect); void trochoids_work(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * snapshot, int x, int y, SDL_Rect * update_rect, int guides); void trochoids_release(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * snapshot, int x, int y, SDL_Rect * update_rect); void trochoids_sound(magic_api * api, int snd_idx, int x, int y); void trochoids_set_color(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * snapshot, Uint8 r, Uint8 g, Uint8 b, SDL_Rect * update_rect); void trochoids_set_size(magic_api * api, int which, int mode, SDL_Surface * canvas, SDL_Surface * last, Uint8 size, SDL_Rect * update_rect); void trochoids_line_callback(void *pointer, int tool, SDL_Surface * canvas, SDL_Surface * snapshot, int x, int y); void trochoids_xorline_callback(void *pointer, int tool, SDL_Surface * canvas, SDL_Surface * snapshot, int x, int y); void trochoids_switchin(magic_api * api, int which, int mode, SDL_Surface * canvas); void trochoids_switchout(magic_api * api, int which, int mode, SDL_Surface * canvas); int calc_lcm(int a, int b); Uint32 trochoids_api_version(void) { return (TP_MAGIC_API_VERSION); } int trochoids_init(magic_api *api, Uint8 disabled_features, Uint8 complexity_level ATTRIBUTE_UNUSED) { int i; char filename[1024]; for (i = 0; i < NUM_SNDS; i++) { sound_effects[i] = NULL; } if (disabled_features & MAGIC_FEATURE_SIZE) { tp_offers_sizes = 0; } else { tp_offers_sizes = 1; } which_to_tool = which_to_tool_per_size_availability[tp_offers_sizes]; for (i = 0; i < NUM_SNDS; i++) { snprintf(filename, sizeof(filename), "%ssounds/magic/%s", api->data_directory, sound_filenames[i]); sound_effects[i] = Mix_LoadWAV(filename); } return (1); } int trochoids_get_tool_count(magic_api *api ATTRIBUTE_UNUSED) { return (num_tools[tp_offers_sizes]); } SDL_Surface *trochoids_get_icon(magic_api *api, int which) { char filename[1024]; snprintf(filename, sizeof(filename), "%simages/magic/%s", api->data_directory, icon_filenames[which_to_tool[which]]); return (IMG_Load(filename)); } char *trochoids_get_name(magic_api *api ATTRIBUTE_UNUSED, int which) { return (strdup(gettext(tool_names[which_to_tool[which]]))); } int trochoids_get_group(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { return (MAGIC_TYPE_ARTISTIC); } int trochoids_get_order(int which) { /* Group in their own spot, ordered the way the appear in the "TOOL_..." `enum` */ return 10000 + which; } char *trochoids_get_description(magic_api *api ATTRIBUTE_UNUSED, int which, int mode ATTRIBUTE_UNUSED) { return (strdup(gettext(tool_descriptions[which_to_tool[which]]))); } int trochoids_requires_colors(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { return 1; } int trochoids_modes(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { return MODE_PAINT; } Uint8 trochoids_accepted_sizes(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED) { return NUM_SIZES; } Uint8 trochoids_default_size(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED) { return DEFAULT_SIZE; } void trochoids_shutdown(magic_api *api ATTRIBUTE_UNUSED) { int i; for (i = 0; i < NUM_SNDS; i++) { if (sound_effects[i] != NULL) { Mix_FreeChunk(sound_effects[i]); } } } void trochoids_click(magic_api *api, int which, int mode ATTRIBUTE_UNUSED, SDL_Surface *canvas, SDL_Surface *snapshot, int x, int y, SDL_Rect *update_rect) { trochoids_x = x; trochoids_y = y; trochoids_drag(api, which, canvas, snapshot, x, y, x + (canvas->w / 20), y + (canvas->h / 20), update_rect); dragged = 0; } /* Affect the canvas on drag: */ void trochoids_drag(magic_api *api, int which, SDL_Surface *canvas, SDL_Surface *snapshot, int old_x ATTRIBUTE_UNUSED, int old_y ATTRIBUTE_UNUSED, int x, int y, SDL_Rect *update_rect) { dragged = 1; trochoids_work(api, which, canvas, snapshot, x, y, update_rect, 1); trochoids_sound(api, SND_DRAG, x, y); } void trochoids_work(magic_api *api, int which, SDL_Surface *canvas, SDL_Surface *snapshot, int x, int y, SDL_Rect *update_rect, int guides) { int R, r, d, LCM; int px, py, px2, py2; float a, r_ratio, size; int xx, yy; which = which_to_tool[which]; /* Drag left/right to change radius of stator (fixed circle) */ R = abs(trochoids_x - x); if (R < 20) { R = 20; } R = (R / 10) * 10; /* Drag down to increase radius of rotator (rolling circle) */ r = abs(y - trochoids_y); if (r < 10) { r = 10; } r = (r / 10) * 10; if (which == TOOL_HYPOTROCHOID_SIZES || which == TOOL_HYPOTROCHOID_NOSIZES_1 || which == TOOL_HYPOTROCHOID_NOSIZES_2 || which == TOOL_HYPOTROCHOID_NOSIZES_3) { /* Hypotrochoid */ if (R == r) { r += 10; } r_ratio = (float)(R - r) / (float)r; } else { /* Epitrochoid */ r_ratio = (float)(R + r) / (float)r; } /* Size option (or use of alternate tools, if --nomagicsizes) determines the distance from the center of the rotator that the pen draws */ if (tp_offers_sizes) { size = trochoids_sizes_per_size_setting[trochoids_size]; } else { size = trochoids_sizes_per_tool[which]; } d = r * size; /* Erase old before drawing new */ update_rect->x = 0; update_rect->y = 0; update_rect->w = canvas->w; update_rect->h = canvas->h; SDL_BlitSurface(snapshot, update_rect, canvas, update_rect); /* Draw the lines */ LCM = calc_lcm(r, R); for (a = 0; a < 360.0 * (float)(LCM / R); a++) { float a2 = (a + 1); if (which == TOOL_HYPOTROCHOID_SIZES || which == TOOL_HYPOTROCHOID_NOSIZES_1 || which == TOOL_HYPOTROCHOID_NOSIZES_2 || which == TOOL_HYPOTROCHOID_NOSIZES_3) { /* Hypotrochoid */ px = trochoids_x + (((R - r) * deg_cos(a)) + (d * deg_cos(r_ratio * a))); py = trochoids_y + (((R - r) * deg_sin(a)) - (d * deg_sin(r_ratio * a))); px2 = trochoids_x + (((R - r) * deg_cos((a2))) + (d * deg_cos(r_ratio * a2))); py2 = trochoids_y + (((R - r) * deg_sin((a2))) - (d * deg_sin(r_ratio * a2))); } else { /* Epitrochoid */ px = trochoids_x + (((R + r) * deg_cos(a)) - (d * deg_cos(r_ratio * a))); py = trochoids_y + (((R + r) * deg_sin(a)) - (d * deg_sin(r_ratio * a))); px2 = trochoids_x + (((R + r) * deg_cos((a2))) - (d * deg_cos(r_ratio * a2))); py2 = trochoids_y + (((R + r) * deg_sin((a2))) - (d * deg_sin(r_ratio * a2))); } api->line((void *)api, which, canvas, snapshot, px, py, px2, py2, (20 * (guides && (a >= 360.0))) + 1, trochoids_line_callback); } if (guides) { int guide_spacing; /* When still dragging (before release), draw some "guides", showing the mechanism that would be used to generate the pattern */ rotator_anim_a = (int)(atan2(y - trochoids_y, x - trochoids_x) / M_PI * 180.0); /* Stator (fixed circle) */ guide_spacing = 360 / R; if (guide_spacing < 2) { guide_spacing = 2; } for (a = 0; a < 360; a = a + guide_spacing) { px = (int)((float)trochoids_x + ((float)R * deg_cos(a))); py = (int)((float)trochoids_y - ((float)R * deg_sin(a))); api->putpixel(canvas, px, py, 0); api->putpixel(canvas, px + 1, py, 0xff); api->putpixel(canvas, px, py + 1, 0); api->putpixel(canvas, px + 1, py + 1, 0xff); } /* Rotator (rolling circle) */ guide_spacing = 360 / r; if (guide_spacing < 2) { guide_spacing = 2; } for (a = 0; a < 360; a = a + guide_spacing) { if (which == TOOL_HYPOTROCHOID_SIZES || which == TOOL_HYPOTROCHOID_NOSIZES_1 || which == TOOL_HYPOTROCHOID_NOSIZES_2 || which == TOOL_HYPOTROCHOID_NOSIZES_3) { /* Hypotrochoid */ px = (int)((float)trochoids_x + ((R - r) * deg_cos(rotator_anim_a)) + ((float)-r * deg_cos(a))); py = (int)((float)trochoids_y + ((R - r) * deg_sin(rotator_anim_a)) - ((float)-r * deg_sin(a))); } else { /* Epitrochoid */ px = (int)((float)trochoids_x + ((R + r) * deg_cos(rotator_anim_a)) + ((float)r * deg_cos(a))); py = (int)((float)trochoids_y + ((R + r) * deg_sin(rotator_anim_a)) - ((float)r * deg_sin(a))); } api->xorpixel(canvas, px, py); api->xorpixel(canvas, px + 1, py); api->xorpixel(canvas, px, py + 1); api->xorpixel(canvas, px + 1, py + 1); } /* Pen */ if (which == TOOL_HYPOTROCHOID_SIZES || which == TOOL_HYPOTROCHOID_NOSIZES_1 || which == TOOL_HYPOTROCHOID_NOSIZES_2 || which == TOOL_HYPOTROCHOID_NOSIZES_3) { /* Hypotrochoid */ px = trochoids_x + (((R - r) * deg_cos(rotator_anim_a)) + (d * deg_cos(360 - rotator_anim_a))); py = trochoids_y + (((R - r) * deg_sin(rotator_anim_a)) - (d * deg_sin(360 - rotator_anim_a))); px2 = trochoids_x + (((R - r) * deg_cos(rotator_anim_a))); py2 = trochoids_y + (((R - r) * deg_sin(rotator_anim_a))); } else { /* Epitrochoid */ px = trochoids_x + (((R + r) * deg_cos(rotator_anim_a)) - (d * deg_cos(360 - rotator_anim_a))); py = trochoids_y + (((R + r) * deg_sin(rotator_anim_a)) - (d * deg_sin(360 - rotator_anim_a))); px2 = trochoids_x + (((R + r) * deg_cos(rotator_anim_a))); py2 = trochoids_y + (((R + r) * deg_sin(rotator_anim_a))); } api->line((void *)api, which, canvas, snapshot, px, py, px2, py2, 2, trochoids_line_callback); for (yy = -2; yy <= 2; yy++) { for (xx = -2; xx <= 2; xx++) { api->putpixel(canvas, px + xx, py + yy, trochoids_color); } } } } void trochoids_release(magic_api *api, int which, SDL_Surface *canvas, SDL_Surface *snapshot, int x, int y, SDL_Rect *update_rect) { int tool, snd_idx; /* (Stop dragging sound, or previous release sound if they just clicked (no drag) to make another shape quickly; the release sound effect lingers & we want to start playing again, otherwise it will seem like the sound only happens intermittently) */ api->stopsound(); /* Pick which sound to play & play it */ tool = which_to_tool[which]; if (tool == TOOL_EPITROCHOID_SIZES || tool == TOOL_EPITROCHOID_NOSIZES_1 || tool == TOOL_EPITROCHOID_NOSIZES_2 || tool == TOOL_EPITROCHOID_NOSIZES_3) { snd_idx = SND_RELEASE_EPITROCHOID; } else { snd_idx = SND_RELEASE_HYPOTROCHOID; } trochoids_sound(api, snd_idx, x, y); /* If they clicked & released with no drag, ignore the (x,y) we received; we want the 'default' offset to get a reasonably pleasant shape -- for users who tried clicking w/o dragging */ if (dragged == 0) { if (tool == TOOL_EPITROCHOID_SIZES || tool == TOOL_EPITROCHOID_NOSIZES_1 || tool == TOOL_EPITROCHOID_NOSIZES_2 || tool == TOOL_EPITROCHOID_NOSIZES_3) { x = trochoids_x + 50; y = trochoids_y + 20; } else { x = trochoids_x + 70; y = trochoids_y + 30; } } /* Draw it (no guides, this time!) */ trochoids_work(api, which, canvas, snapshot, x, y, update_rect, 0); } /* Play a sound; volume and panning will be based on the size and position of the shape being generated by the user's UI interaction */ void trochoids_sound(magic_api *api, int snd_idx, int x, int y) { int R, vol, pan; /* Volume based on the radii of the stator (fixed circle) and the rotator (rolling circle), combined; larger = louder */ R = abs(trochoids_x - x) + abs(trochoids_y - y); if (R < 20) { R = 20; } else if (R > api->canvas_w) { R = api->canvas_w; } vol = (255 * R * 2) / api->canvas_w; if (vol > 255) { vol = 255; } /* Panning based on the left/right position of the center of the shape */ pan = (trochoids_x * 255) / api->canvas_w; api->playsound(sound_effects[snd_idx], pan, vol); } void trochoids_set_color(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, SDL_Surface *canvas ATTRIBUTE_UNUSED, SDL_Surface *snapshot ATTRIBUTE_UNUSED, Uint8 r, Uint8 g, Uint8 b, SDL_Rect *update_rect ATTRIBUTE_UNUSED) { trochoids_color = SDL_MapRGB(canvas->format, r, g, b); } void trochoids_set_size(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface *canvas ATTRIBUTE_UNUSED, SDL_Surface *last ATTRIBUTE_UNUSED, Uint8 size, SDL_Rect *update_rect ATTRIBUTE_UNUSED) { trochoids_size = (size - 1); /* array index is 0-based, but Tux Paint returns between 1...{accepted sizes} */ } void trochoids_line_callback(void *pointer ATTRIBUTE_UNUSED, int tool ATTRIBUTE_UNUSED, SDL_Surface *canvas, SDL_Surface *snapshot ATTRIBUTE_UNUSED, int x, int y) { magic_api *api = (magic_api *) pointer; api->putpixel(canvas, x, y, trochoids_color); } void trochoids_xorline_callback(void *pointer ATTRIBUTE_UNUSED, int tool ATTRIBUTE_UNUSED, SDL_Surface *canvas, SDL_Surface *snapshot ATTRIBUTE_UNUSED, int x, int y) { magic_api *api = (magic_api *) pointer; api->xorpixel(canvas, x, y); api->xorpixel(canvas, x + 1, y); api->xorpixel(canvas, x, y + 1); api->xorpixel(canvas, x + 1, y + 1); } void trochoids_switchin(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface *canvas ATTRIBUTE_UNUSED) { } void trochoids_switchout(magic_api *api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface *canvas ATTRIBUTE_UNUSED) { } int calc_lcm(int a, int b) { int max; if (a > b) { max = a; } else { max = b; } while ((max % a) != 0 || (max % b) != 0) { max++; } return max; }