fade: finish out proposed API, basic unit tests #659

pull/665/head
nick black 4 years ago committed by Nick Black
parent 74e8e9c3d9
commit 3deeecdf2e

@ -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.

@ -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);
```
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);
```
Finally, a raw stream of RGBA or BGRx data can be blitted directly to an ncplane:
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

@ -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

@ -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, &times);
@ -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, &times);
}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 -1;
return NULL;
}
if(alloc_ncplane_palette(n, &pp, ts)){
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;
}
int ret = 0;
struct timespec times;
ns_to_timespec(pp.startns, &times);
ns_to_timespec(pp->startns, &times);
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, &times);
}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;
}

@ -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_));
}

Loading…
Cancel
Save