diff --git a/NEWS.md b/NEWS.md index 2719703ae..8d10262c8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,9 +18,9 @@ rearrangements of Notcurses. any meaning, and has been removed. * The `fadecb` typedef now accepts as its third argument a `const struct timespec`. This is the absolute deadline through which the frame ought - be displayed. Like the changes to `ncvisual_stream()`, this gives more - flexibility to the callback, and allows more precise timing. There will - be further changes to the Fade API before API freeze (see #659). + be displayed. New functions have been added to the Fade API: like the + changes to `ncvisual_stream()`, this gives more flexibility, and allows + more precise timing. All old functions remain available. * 1.4.3 (2020-05-22) * Plot: make 8x1 the default, instead of 1x1. diff --git a/USAGE.md b/USAGE.md index 5139aa8d4..b2fd05341 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1189,34 +1189,64 @@ int ncplane_stain(struct ncplane* n, int ystop, int xstop, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr); ``` -My 14 year-old self would never forgive me if we didn't have sweet palette fades. +My 14 year-old self would never forgive me if we didn't have sweet palette +fades. The simple fade API runs the operation over a time interval, adapting +to the actual runtime, invoking a callback at each iteration. ```c -// Called for each delta performed in a fade on ncp. If anything but 0 is returned, -// the fading operation ceases immediately, and that value is propagated out. If provided -// and not NULL, the faders will not themselves call notcurses_render(). -typedef int (*fadecb)(struct notcurses* nc, struct ncplane* ncp, void* curry); +// Called for each fade iteration on 'ncp'. If anything but 0 is returned, +// the fading operation ceases immediately, and that value is propagated out. +// The recommended absolute display time target is passed in 'tspec'. +typedef int (*fadecb)(struct notcurses* nc, struct ncplane* ncp, + const struct timespec*, void* curry); -// Fade the ncplane out over the provided time, calling the specified function -// when done. Requires a terminal which supports truecolor, or at least palette +// Fade the ncplane out over the provided time, calling 'fader' at each +// iteration. Requires a terminal which supports truecolor, or at least palette // modification (if the terminal uses a palette, our ability to fade planes is // limited, and affected by the complexity of the rest of the screen). -int ncplane_fadeout(struct ncplane* n, const struct timespec* ts, fadecb fader, void* curry); +int ncplane_fadeout(struct ncplane* n, const struct timespec* ts, + fadecb fader, void* curry); // Fade the ncplane in over the specified time. Load the ncplane with the // target cells without rendering, then call this function. When it's done, the // ncplane will have reached the target levels, starting from zeroes. -int ncplane_fadein(struct ncplane* n, const struct timespec* ts, fadecb fader, void* curry); +int ncplane_fadein(struct ncplane* n, const struct timespec* ts, + fadecb fader, void* curry); +// Rather than the simple ncplane_fade{in/out}(), ncfadectx_setup() can be // Pulse the plane in and out until the callback returns non-zero, relying on // the callback 'fader' to initiate rendering. 'ts' defines the half-period // (i.e. the transition from black to full brightness, or back again). Proper // use involves preparing (but not rendering) an ncplane, then calling // ncplane_pulse(), which will fade in from black to the specified colors. int ncplane_pulse(struct ncplane* n, const struct timespec* ts, fadecb fader, void* curry); + ``` -Finally, a raw stream of RGBA or BGRx data can be blitted directly to an ncplane: +The more flexible fade API allows for fine control of the process. + +```c +// paired with a loop over ncplane_fade{in/out}_iteration() + ncfadectx_free(). +struct ncfadectx* ncfadectx_setup(struct ncplane* n, const struct timespec* ts); + +// Return the number of iterations through which 'nctx' will fade. +int ncfadectx_iterations(const struct ncfadectx* nctx); + +// Fade out through 'iter' iterations, where +// 'iter' < 'ncfadectx_iterations(nctx)'. +int ncplane_fadeout_iteration(struct ncplane* n, struct ncfadectx* nctx, + int iter, fadecb fader, void* curry); + +// Fade in through 'iter' iterations, where +// 'iter' < 'ncfadectx_iterations(nctx)'. +int ncplane_fadein_iteration(struct ncplane* n, struct ncfadectx* nctx, + int iter, fadecb fader, void* curry); + +// Release the resources associated with 'nctx'. +void ncfadectx_free(struct ncfadectx* nctx); +``` + +Raw streams of RGBA or BGRx data can be blitted directly to an ncplane: ```c // Blit a flat array 'data' of BGRx 32-bit values to the ncplane 'nc', offset diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index 20ec4a0f0..4100fcf5b 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -39,6 +39,7 @@ struct ncsubproc; // ncfdplane wrapper with subprocess management struct ncselector;// widget supporting selecting 1 from a list of options struct ncmultiselector; // widget supporting selecting 0..n from n options struct ncreader; // widget supporting free string input ala readline +struct ncfadectx; // context for a palette fade operation // Initialize a direct-mode notcurses context on the connected terminal at 'fp'. // 'fp' must be a tty. You'll usually want stdout. Direct mode supportes a @@ -1971,8 +1972,8 @@ API unsigned ncplane_styles(const struct ncplane* n); typedef int (*fadecb)(struct notcurses* nc, struct ncplane* ncp, const struct timespec*, void* curry); -// Fade the ncplane out over the provided time, calling the specified function -// when done. Requires a terminal which supports truecolor, or at least palette +// Fade the ncplane out over the provided time, calling 'fader' at each +// iteration. Requires a terminal which supports truecolor, or at least palette // modification (if the terminal uses a palette, our ability to fade planes is // limited, and affected by the complexity of the rest of the screen). API int ncplane_fadeout(struct ncplane* n, const struct timespec* ts, @@ -1984,6 +1985,23 @@ API int ncplane_fadeout(struct ncplane* n, const struct timespec* ts, API int ncplane_fadein(struct ncplane* n, const struct timespec* ts, fadecb fader, void* curry); +// Rather than the simple ncplane_fade{in/out}(), ncfadectx_setup() can be +// paired with a loop over ncplane_fade{in/out}_iteration() + ncfadectx_free(). +API struct ncfadectx* ncfadectx_setup(struct ncplane* n, const struct timespec* ts); + +// Return the number of iterations through which 'nctx' will fade. +API int ncfadectx_iterations(const struct ncfadectx* nctx); + +// Fade out through 'iter' iterations, where +// 'iter' < 'ncfadectx_iterations(nctx)'. +API int ncplane_fadeout_iteration(struct ncplane* n, struct ncfadectx* nctx, + int iter, fadecb fader, void* curry); + +// Fade in through 'iter' iterations, where +// 'iter' < 'ncfadectx_iterations(nctx)'. +API int ncplane_fadein_iteration(struct ncplane* n, struct ncfadectx* nctx, + int iter, fadecb fader, void* curry); + // Pulse the plane in and out until the callback returns non-zero, relying on // the callback 'fader' to initiate rendering. 'ts' defines the half-period // (i.e. the transition from black to full brightness, or back again). Proper @@ -1991,6 +2009,9 @@ API int ncplane_fadein(struct ncplane* n, const struct timespec* ts, // ncplane_pulse(), which will fade in from black to the specified colors. API int ncplane_pulse(struct ncplane* n, const struct timespec* ts, fadecb fader, void* curry); +// Release the resources associated with 'nctx'. +API void ncfadectx_free(struct ncfadectx* nctx); + // load up six cells with the EGCs necessary to draw a box. returns 0 on // success, -1 on error. on error, any cells this function might // have loaded before the error are cell_release()d. There must be at least diff --git a/src/lib/fade.c b/src/lib/fade.c index ffdb1d0bb..70d4028d2 100644 --- a/src/lib/fade.c +++ b/src/lib/fade.c @@ -11,6 +11,10 @@ typedef struct ncfadectx { uint64_t* channels; // all channels from the framebuffer } ncfadectx; +int ncfadectx_iterations(const ncfadectx* nctx){ + return nctx->maxsteps; +} + // These arrays are too large to be safely placed on the stack. Get an atomic // snapshot of all channels on the plane. While copying the snapshot, determine // the maxima across each of the six components. @@ -53,7 +57,6 @@ alloc_ncplane_palette(ncplane* n, ncfadectx* pp, const struct timespec* ts){ } } } - // FIXME factor this duplication out channels = n->basecell.channels; pp->channels[y * pp->cols] = channels; channels_fg_rgb(channels, &r, &g, &b); @@ -84,9 +87,14 @@ alloc_ncplane_palette(ncplane* n, ncfadectx* pp, const struct timespec* ts){ if(pp->maxsteps == 0){ pp->maxsteps = 1; } - uint64_t nanosecs_total = timespec_to_ns(ts); - pp->nanosecs_step = nanosecs_total / pp->maxsteps; - if(pp->nanosecs_step == 0){ + uint64_t nanosecs_total; + if(ts){ + nanosecs_total = timespec_to_ns(ts); + pp->nanosecs_step = nanosecs_total / pp->maxsteps; + if(pp->nanosecs_step == 0){ + pp->nanosecs_step = 1; + } + }else{ pp->nanosecs_step = 1; } struct timespec times; @@ -96,11 +104,54 @@ alloc_ncplane_palette(ncplane* n, ncfadectx* pp, const struct timespec* ts){ return 0; } +int ncplane_fadein_iteration(ncplane* n, ncfadectx* nctx, int iter, + fadecb fader, void* curry){ + int y, x; + // each time through, we need look each cell back up, due to the + // possibility of a resize event :/ + int dimy, dimx; + ncplane_dim_yx(n, &dimy, &dimx); + for(y = 0 ; y < nctx->rows && y < dimy ; ++y){ + for(x = 0 ; x < nctx->cols && x < dimx; ++x){ + unsigned r, g, b; + channels_fg_rgb(nctx->channels[nctx->cols * y + x], &r, &g, &b); + unsigned br, bg, bb; + channels_bg_rgb(nctx->channels[nctx->cols * y + x], &br, &bg, &bb); + cell* c = &n->fb[dimx * y + x]; + if(!cell_fg_default_p(c)){ + r = r * iter / nctx->maxsteps; + g = g * iter / nctx->maxsteps; + b = b * iter / nctx->maxsteps; + cell_set_fg_rgb(c, r, g, b); + } + if(!cell_bg_default_p(c)){ + br = br * iter / nctx->maxsteps; + bg = bg * iter / nctx->maxsteps; + bb = bb * iter / nctx->maxsteps; + cell_set_bg_rgb(c, br, bg, bb); + } + } + } + uint64_t nextwake = (iter + 1) * nctx->nanosecs_step + nctx->startns; + struct timespec sleepspec; + sleepspec.tv_sec = nextwake / NANOSECS_IN_SEC; + sleepspec.tv_nsec = nextwake % NANOSECS_IN_SEC; + int ret = 0; + if(fader){ + ret |= fader(n->nc, n, &sleepspec, curry); + }else{ + ret |= notcurses_render(n->nc); + // clock_nanosleep() has no love for CLOCK_MONOTONIC_RAW, at least as + // of Glibc 2.29 + Linux 5.3 (or FreeBSD 12) :/. + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &sleepspec, NULL); + } + return ret; +} + static int ncplane_fadein_internal(ncplane* n, fadecb fader, ncfadectx* pp, void* curry){ // Current time, sampled each iteration uint64_t curns; - int ret = 0; do{ struct timespec times; clock_gettime(CLOCK_MONOTONIC, ×); @@ -109,49 +160,13 @@ ncplane_fadein_internal(ncplane* n, fadecb fader, ncfadectx* pp, void* curry){ if(iter > pp->maxsteps){ break; } - int y, x; - // each time through, we need look each cell back up, due to the - // possibility of a resize event :/ - int dimy, dimx; - ncplane_dim_yx(n, &dimy, &dimx); - for(y = 0 ; y < pp->rows && y < dimy ; ++y){ - for(x = 0 ; x < pp->cols && x < dimx; ++x){ - unsigned r, g, b; - channels_fg_rgb(pp->channels[pp->cols * y + x], &r, &g, &b); - unsigned br, bg, bb; - channels_bg_rgb(pp->channels[pp->cols * y + x], &br, &bg, &bb); - cell* c = &n->fb[dimx * y + x]; - if(!cell_fg_default_p(c)){ - r = r * iter / pp->maxsteps; - g = g * iter / pp->maxsteps; - b = b * iter / pp->maxsteps; - cell_set_fg_rgb(c, r, g, b); - } - if(!cell_bg_default_p(c)){ - br = br * iter / pp->maxsteps; - bg = bg * iter / pp->maxsteps; - bb = bb * iter / pp->maxsteps; - cell_set_bg_rgb(c, br, bg, bb); - } - } - } - uint64_t nextwake = (iter + 1) * pp->nanosecs_step + pp->startns; - struct timespec sleepspec; - sleepspec.tv_sec = nextwake / NANOSECS_IN_SEC; - sleepspec.tv_nsec = nextwake % NANOSECS_IN_SEC; - if(fader){ - ret |= fader(n->nc, n, &sleepspec, curry); - }else{ - ret |= notcurses_render(n->nc); - // clock_nanosleep() has no love for CLOCK_MONOTONIC_RAW, at least as - // of Glibc 2.29 + Linux 5.3 (or FreeBSD 12) :/. - ret |= clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &sleepspec, NULL); - } - if(ret){ - break; + int r = ncplane_fadein_iteration(n, pp, iter, fader, curry); + if(r){ + return r; } + clock_gettime(CLOCK_MONOTONIC, ×); }while(true); - return ret; + return 0; } int ncplane_fadeout_iteration(ncplane* n, ncfadectx* nctx, int iter, @@ -213,36 +228,54 @@ int ncplane_fadeout_iteration(ncplane* n, ncfadectx* nctx, int iter, return ret; } -int ncplane_fadeout(ncplane* n, const struct timespec* ts, fadecb fader, void* curry){ - ncfadectx pp; +ncfadectx* ncfadectx_setup(ncplane* n, const struct timespec* ts){ if(!n->nc->tcache.RGBflag && !n->nc->tcache.CCCflag){ // terminal can't fade + return NULL; + } + ncfadectx* nctx = malloc(sizeof(*nctx)); + if(nctx){ + if(alloc_ncplane_palette(n, nctx, ts) == 0){ + return nctx; + } + free(nctx); + } + return NULL; +} + +void ncfadectx_free(ncfadectx* nctx){ + if(nctx){ + free(nctx->channels); + free(nctx); + } +} + +int ncplane_fadeout(ncplane* n, const struct timespec* ts, fadecb fader, void* curry){ + ncfadectx* pp = ncfadectx_setup(n, ts); + if(!pp){ return -1; } - if(alloc_ncplane_palette(n, &pp, ts)){ - return -1; - } - int ret = 0; struct timespec times; - ns_to_timespec(pp.startns, ×); + ns_to_timespec(pp->startns, ×); do{ uint64_t curns = times.tv_sec * NANOSECS_IN_SEC + times.tv_nsec; - int iter = (curns - pp.startns) / pp.nanosecs_step + 1; - if(iter > pp.maxsteps){ + int iter = (curns - pp->startns) / pp->nanosecs_step + 1; + if(iter > pp->maxsteps){ break; } - int r = ncplane_fadeout_iteration(n, &pp, iter, fader, curry); + int r = ncplane_fadeout_iteration(n, pp, iter, fader, curry); if(r){ + ncfadectx_free(pp); return r; } clock_gettime(CLOCK_MONOTONIC, ×); }while(true); - free(pp.channels); - return ret; + ncfadectx_free(pp); + return 0; } int ncplane_fadein(ncplane* n, const struct timespec* ts, fadecb fader, void* curry){ - ncfadectx pp; - if(!n->nc->tcache.RGBflag && !n->nc->tcache.CCCflag){ // terminal can't fade + ncfadectx* nctx = ncfadectx_setup(n, ts); + if(nctx == NULL){ struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if(fader){ @@ -252,11 +285,8 @@ int ncplane_fadein(ncplane* n, const struct timespec* ts, fadecb fader, void* cu } return -1; } - if(alloc_ncplane_palette(n, &pp, ts)){ - return -1; - } - int ret = ncplane_fadein_internal(n, fader, &pp, curry); - free(pp.channels); + int ret = ncplane_fadein_internal(n, fader, nctx, curry); + ncfadectx_free(nctx); return ret; } diff --git a/tests/fade.cpp b/tests/fade.cpp index 7330d332a..2b1753cec 100644 --- a/tests/fade.cpp +++ b/tests/fade.cpp @@ -78,6 +78,28 @@ TEST_CASE("Fade") { CHECK(0 < ncplane_pulse(n_, &ts, pulser, &pulsestart)); } + SUBCASE("FadeOutUntimed") { + auto nctx = ncfadectx_setup(n_, nullptr); + REQUIRE(nctx); + auto maxiter = ncfadectx_iterations(nctx); + CHECK(0 < maxiter); + for(int i = 0 ; i < maxiter ; ++i){ + CHECK(0 == ncplane_fadeout_iteration(n_, nctx, i, nullptr, nullptr)); + } + ncfadectx_free(nctx); + } + + SUBCASE("FadeInUntimed") { + auto nctx = ncfadectx_setup(n_, nullptr); + REQUIRE(nctx); + auto maxiter = ncfadectx_iterations(nctx); + CHECK(0 < maxiter); + for(int i = 0 ; i < maxiter ; ++i){ + CHECK(0 == ncplane_fadein_iteration(n_, nctx, i, nullptr, nullptr)); + } + ncfadectx_free(nctx); + } + CHECK(0 == notcurses_stop(nc_)); }