diff options
Diffstat (limited to 'src/modules/book')
-rw-r--r-- | src/modules/book/Makefile.am | 3 | ||||
-rw-r--r-- | src/modules/book/book.c | 384 |
2 files changed, 387 insertions, 0 deletions
diff --git a/src/modules/book/Makefile.am b/src/modules/book/Makefile.am new file mode 100644 index 0000000..77066a7 --- /dev/null +++ b/src/modules/book/Makefile.am @@ -0,0 +1,3 @@ +noinst_LTLIBRARIES = libbook.la +libbook_la_SOURCES = book.c +libbook_la_CPPFLAGS = -I@top_srcdir@/src diff --git a/src/modules/book/book.c b/src/modules/book/book.c new file mode 100644 index 0000000..1a6f790 --- /dev/null +++ b/src/modules/book/book.c @@ -0,0 +1,384 @@ +#include <math.h> + +#include "til.h" +#include "til_fb.h" +#include "til_module_context.h" +#include "til_settings.h" +#include "til_stream.h" +#include "til_str.h" +#include "til_tap.h" +#include "til_util.h" + +/* Copyright (C) 2023 - Vito Caputo <vcaputo@pengaru.com> */ + +#define BOOK_DEFAULT_PAGE_MODULE "roto" +#define BOOK_DEFAULT_FLIP_RATE "10" +#define BOOK_DEFAULT_FLIP_DIRECTION "1.0" /* negative for reverse */ + +/* this was derived from modules/compose.c (layers became pages) */ +typedef struct book_page_t { + til_module_context_t *module_ctxt; + /* XXX: it's expected that pages will get more settable attributes to stick in here */ +} book_page_t; + +typedef struct book_context_t { + til_module_context_t til_module_context; + + struct { + til_tap_t rate; + til_tap_t direction; + til_tap_t page; + } taps; + + struct { + float rate; + float direction; + float page; + } vars; + + float *rate; + float *direction; + float *page; + + size_t n_pages; + book_page_t pages[]; +} book_context_t; + +typedef struct book_page_setup_t { + til_setup_t *module_setup; +} book_page_setup_t; + +typedef struct book_setup_t { + til_setup_t til_setup; + float rate; + float direction; + size_t n_pages; + book_page_setup_t pages[]; +} book_setup_t; + + +static void book_update_taps(book_context_t *ctxt, til_stream_t *stream, float dt) +{ + book_setup_t *s = (book_setup_t *)ctxt->til_module_context.setup; + + if (!til_stream_tap_context(stream, &ctxt->til_module_context, NULL, &ctxt->taps.direction)) + *ctxt->direction = s->direction; + else + ctxt->vars.direction = *ctxt->direction; + + if (!til_stream_tap_context(stream, &ctxt->til_module_context, NULL, &ctxt->taps.rate)) + *ctxt->rate = s->rate; + else + ctxt->vars.rate = *ctxt->rate; + + if (!til_stream_tap_context(stream, &ctxt->til_module_context, NULL, &ctxt->taps.page)) + *ctxt->page = fmodf(*ctxt->page + dt * (ctxt->vars.rate * ctxt->vars.direction), ctxt->n_pages); + else + ctxt->vars.page = *ctxt->page; +} + + +static til_module_context_t * book_create_context(const til_module_t *module, til_stream_t *stream, unsigned seed, unsigned ticks, unsigned n_cpus, til_setup_t *setup) +{ + book_setup_t *s = (book_setup_t *)setup; + book_context_t *ctxt; + + assert(setup); + + ctxt = til_module_context_new(module, sizeof(book_context_t) + s->n_pages * sizeof(book_page_t), stream, seed, ticks, n_cpus, setup); + if (!ctxt) + return NULL; + + /* Get the page track before any potential per-page tracks in RocketEditor. */ + ctxt->taps.page = til_tap_init_float(ctxt, &ctxt->page, 1, &ctxt->vars.page, "page"); + ctxt->taps.rate = til_tap_init_float(ctxt, &ctxt->rate, 1, &ctxt->vars.rate, "rate"); + ctxt->taps.direction = til_tap_init_float(ctxt, &ctxt->direction, 1, &ctxt->vars.direction, "direction"); + + for (size_t i = 0; i < s->n_pages; i++) { + const til_module_t *page_module; + + /* FIXME TODO: + * If someone does something like pages=moire,moire,moire,moire, should they be different or the same at the seed level? + * As-is, this _always_ varies the seed across the pages. There's no generic settings syntax currently for overriding + * that behavior where one can just specify the seed explicitly to make it the same, or even toggle this source of randomness. + * That's a more fundamental til thing to fix, though it could also just be a bespoke module setting to control this. + * Maybe for starters I should just implement it ad-hoc on a per-module basis but use the same syntax there, then if + * from the UX perspective that works satisfactorily, work on a more generic implementation within the settings code and + * deduplicate all the ad-hoc stuff from the relevant modules that will hopefully have implemented a common set of behaviors+syntax. + */ + page_module = s->pages[i].module_setup->creator; + if (til_module_create_context(page_module, stream, rand_r(&seed), ticks, n_cpus, s->pages[i].module_setup, &ctxt->pages[i].module_ctxt) < 0) + return til_module_context_free(&ctxt->til_module_context); + + ctxt->n_pages++; + } + + book_update_taps(ctxt, stream, 0.f); + + return &ctxt->til_module_context; +} + + +static void book_destroy_context(til_module_context_t *context) +{ + book_context_t *ctxt = (book_context_t *)context; + + for (size_t i = 0; i < ctxt->n_pages; i++) + til_module_context_free(ctxt->pages[i].module_ctxt); + + free(context); +} + + +static void book_render_fragment(til_module_context_t *context, til_stream_t *stream, unsigned ticks, unsigned cpu, til_fb_fragment_t **fragment_ptr) +{ + book_context_t *ctxt = (book_context_t *)context; + til_fb_fragment_t *fragment = *fragment_ptr; + size_t i = floorf((*ctxt->page >= 0) ? *ctxt->page : ctxt->n_pages + *ctxt->page); + float dt = ((float)(ticks - context->last_ticks)) * .001f; + + if (i >= ctxt->n_pages) + i = ctxt->n_pages - 1; + + book_update_taps(ctxt, stream, dt); + til_module_render(ctxt->pages[i].module_ctxt, stream, ticks, &fragment); + + *fragment_ptr = fragment; +} + + +static int book_setup(const til_settings_t *settings, til_setting_t **res_setting, const til_setting_desc_t **res_desc, til_setup_t **res_setup); + + +til_module_t book_module = { + .create_context = book_create_context, + .destroy_context = book_destroy_context, + .render_fragment = book_render_fragment, + .setup = book_setup, + .name = "book", + .description = "Flipbook module", + .author = "Vito Caputo <vcaputo@pengaru.com>", + .flags = TIL_MODULE_OVERLAYABLE, +}; + + +static void book_setup_free(til_setup_t *setup) +{ + book_setup_t *s = (book_setup_t *)setup; + + for (size_t i = 0; i < s->n_pages; i++) + til_setup_free(s->pages[i].module_setup); + + free(setup); +} + + +static int book_page_module_setup(const til_settings_t *settings, til_setting_t **res_setting, const til_setting_desc_t **res_desc, til_setup_t **res_setup) +{ + const char *exclusions[] = { "none", "book" /* XXX: prevent infinite recursion */, NULL }; + + /* nested book might be interesting, but there needs to be guards to prevent the potential infinite recursion. + * note you can still access it via the ':' override prefix + */ + + return til_module_setup_full(settings, + res_setting, + res_desc, + res_setup, + "Page module name", + BOOK_DEFAULT_PAGE_MODULE, + (TIL_MODULE_EXPERIMENTAL | TIL_MODULE_HERMETIC | TIL_MODULE_AUDIO_ONLY), + exclusions); +} + + +static char * book_random_rate(unsigned seed) +{ + const char *rate_values[] = { + "60", + "30", + "15", + "10", + "5", + "2", + "1", + ".75", + ".5", + ".25", + ".1", + ".01", + }; + + return strdup(rate_values[rand_r(&seed) % nelems(rate_values)]); +} + + +static char * book_random_module_setting(unsigned seed) +{ + til_str_t *str; + size_t n_pages = 2; /* minimum of two pages */ + const char *candidates[] = { + "blinds", + "checkers", + "drizzle", + "julia", + "meta2d", + "moire", + "pixbounce", + "plasma", + "plato", + "roto", + "shapes", + "sparkler", + "spiro", + "stars", + "submit", + "swab", + "swarm", + "voronoi", + }; + + str = til_str_new(""); + if (!str) + return NULL; + + n_pages += rand_r(&seed) % 7; + for (size_t i = 0; i < n_pages; i++) { + size_t c = rand_r(&seed) % nelems(candidates); + + til_str_appendf(str, "%s%s", i == 0 ? "" : ",", candidates[c]); + } + + return til_str_to_buf(str, NULL); +} + + +static int book_setup(const til_settings_t *settings, til_setting_t **res_setting, const til_setting_desc_t **res_desc, til_setup_t **res_setup) +{ + til_setting_t *rate; + til_setting_t *direction; + const til_settings_t *pages_settings; + til_setting_t *pages; + int r; + + r = til_settings_get_and_describe_setting(settings, + &(til_setting_spec_t){ + .name = "Page flipping rate (N.N (Hz))", + .key = "rate", + .preferred = BOOK_DEFAULT_FLIP_RATE, + .random = book_random_rate, + }, + &rate, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_setting(settings, + &(til_setting_spec_t){ + .name = "Page flipping direction multiplier (+-N.N)", + .key = "direction", + .preferred = BOOK_DEFAULT_FLIP_DIRECTION, + }, + &direction, + res_setting, + res_desc); + if (r) + return r; + + r = til_settings_get_and_describe_setting(settings, + &(til_setting_spec_t){ + .name = "Comma-separated list of ordered module pages", + .key = "pages", + .preferred = "plasma,roto,moire", + .annotations = NULL, + /* TODO: .values = could have a selection of interesting preset compositions... */ + .random = book_random_module_setting, + .as_nested_settings = 1, + }, + &pages, /* XXX: unused in raw-value form, we want the settings instance */ + res_setting, + res_desc); + if (r) + return r; + + pages_settings = pages->value_as_nested_settings; + assert(pages_settings); + { + til_setting_t *page_setting; + + /* + * Note this relies on til_settings_get_value_by_idx() returning NULL once idx runs off the end, + * which is indistinguishable from a NULL-valued setting, so if the user were to fat-finger + * an empty page like "pages=foo,,bar" maybe we'd never reach bar. This could be made more robust + * by explicitly looking at the number of settings and just ignoring NULL values, but maybe + * instead we should just prohibit such settings constructions? Like an empty value should still get + * "" not NULL put in it. FIXME TODO XXX verify/clarify/assert this in code + */ + for (size_t i = 0; til_settings_get_value_by_idx(pages_settings, i, &page_setting); i++) { + if (!page_setting->value_as_nested_settings) { + r = til_setting_desc_new(pages_settings, + &(til_setting_spec_t){ + .as_nested_settings = 1, + }, res_desc); + if (r < 0) + return r; + + *res_setting = page_setting; + + return 1; + } + } + + for (size_t i = 0; til_settings_get_value_by_idx(pages_settings, i, &page_setting); i++) { + r = book_page_module_setup(page_setting->value_as_nested_settings, + res_setting, + res_desc, + NULL); /* XXX: note no res_setup, must defer finalize */ + if (r) + return r; + } + } + + if (res_setup) { /* turn pages settings into an array of book_page_setup_t's {name,til_setup_t} */ + size_t n_pages = til_settings_get_count(pages_settings); + til_setting_t *page_setting; + book_setup_t *setup; + + if (n_pages < 2) { + *res_setting = pages; + + return -EINVAL; + } + + setup = til_setup_new(settings, sizeof(*setup) + n_pages * sizeof(*setup->pages), book_setup_free, &book_module); + if (!setup) + return -ENOMEM; + + if (sscanf(rate->value, "%f", &setup->rate) != 1) + return til_setup_free_with_failed_setting_ret_err(&setup->til_setup, rate, res_setting, -EINVAL); + + if (setup->rate < 0) + return til_setup_free_with_failed_setting_ret_err(&setup->til_setup, rate, res_setting, -EINVAL); + + if (sscanf(direction->value, "%f", &setup->direction) != 1) + return til_setup_free_with_failed_setting_ret_err(&setup->til_setup, direction, res_setting, -EINVAL); + + setup->n_pages = n_pages; + + for (size_t i = 0; til_settings_get_value_by_idx(pages_settings, i, &page_setting); i++) { + r = book_page_module_setup(page_setting->value_as_nested_settings, + res_setting, + res_desc, + &setup->pages[i].module_setup); /* finalize! */ + if (r < 0) + return til_setup_free_with_ret_err(&setup->til_setup, r); + + assert(r == 0); + } + + *res_setup = &setup->til_setup; + } + + return 0; +} |