Graceful fallback among blitters #637

If we're in ASCII mode, no blitter except for NCBLIT_1x1 is going to
work. Whenever NCBLIT_DEFAULT is provided, select NCBLIT_1x1 if we're
in ASCII mode. Add NCVISUAL_OPTIONS_MAYDEGRADE and
NCPLOT_OPTIONS_MAYDEGRADE. Both serve to allow smooth degradation when a
blitter other than NCBLIT_DEFAULT has been provided. Closes #637.

Make calc_gradient_cell() static inline so our templated ncppplot
implementation can use it (ugh). When using NCBLIT_1x1 for plots in
ASCII mode, use space rather than full block, and invert colors.

Use NCBLIT_DEFAULT in the demo for the FPS plot.
This commit is contained in:
nick black 2020-05-29 01:24:34 -04:00
parent 33c0e7fa2c
commit f47bde1c4e
No known key found for this signature in database
GPG Key ID: 5F43400C21CBFACC
10 changed files with 206 additions and 141 deletions

View File

@ -28,7 +28,21 @@ typedef enum {
NCBLIT_SIXEL, // six rows, 1 column (RGB)
} ncblitter_e;
typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
#define NCVISUAL_OPTIONS_MAYDEGRADE 0x0001
struct ncvisual_options {
struct ncplane* n;
ncscale_e scaling;
int y, x;
int begy, begx; // origin of rendered section
int leny, lenx; // size of rendered section
ncblitter_e blitter; // glyph set to use (maps input to output cells)
uint64_t flags; // bitmask over NCVISUAL_OPTIONS_*
};
typedef int (*streamcb)(struct notcurses*, struct ncvisual*, void*);
```
**bool notcurses_canopen_images(const struct notcurses* nc);**

View File

@ -2142,6 +2142,8 @@ API nc_err_e ncvisual_rotate(struct ncvisual* n, double rads);
// transformation, unless the size is unchanged.
API nc_err_e ncvisual_resize(struct ncvisual* n, int rows, int cols);
#define NCVISUAL_OPTIONS_MAYDEGRADE 0x0001 // blitter can be worse than requested
struct ncvisual_options {
// if no ncplane is provided, one will be created using the exact size
// necessary to render the source with perfect fidelity (this might be
@ -2161,8 +2163,10 @@ struct ncvisual_options {
// these numbers are all in terms of ncvisual pixels.
int begy, begx; // origin of rendered section
int leny, lenx; // size of rendered section
// use NCBLIT_DEFAULT if you don't care, to use NCBLIT_2x2 (assuming
// UTF8) or NCBLIT_1x1 (in an ASCII environment)
ncblitter_e blitter; // glyph set to use (maps input to output cells)
uint64_t flags; // currently all zero
uint64_t flags; // bitmask over NCVISUAL_OPTIONS_*
};
// Render the decoded frame to the specified ncplane (if one is not provided,
@ -2737,12 +2741,15 @@ API int ncmenu_destroy(struct ncmenu* n);
#define NCPLOT_OPTIONS_LABELTICKSD 0x0001 // show labels for dependent axis
#define NCPLOT_OPTIONS_EXPONENTIALD 0x0002 // exponential dependent axis
#define NCPLOT_OPTIONS_VERTICALI 0x0004 // independent axis is vertical
#define NCPLOT_OPTIONS_MAYDEGRADE 0x0008 // blitter can be worse than requested
typedef struct ncplot_options {
// channels for the maximum and minimum levels. linear interpolation will be
// applied across the domain between these two.
uint64_t maxchannel;
uint64_t minchannel;
// if you don't care, pass NCBLIT_DEFAULT and get NCBLIT_8x1 (assuming
// UTF8) or NCBLIT_1x1 (in an ASCII environment)
ncblitter_e gridtype; // number of "pixels" per row x column
// independent variable can either be a contiguous range, or a finite set
// of keys. for a time range, say the previous hour sampled with second
@ -2756,7 +2763,7 @@ typedef struct ncplot_options {
// The plot will make free use of the entirety of the plane.
// for domain autodiscovery, set miny == maxy == 0.
API struct ncuplot* ncuplot_create(struct ncplane* n, const ncplot_options* opts,
uint64_t miny, uint64_t maxy);
uint64_t miny, uint64_t maxy);
API struct ncdplot* ncdplot_create(struct ncplane* n, const ncplot_options* opts,
double miny, double maxy);

View File

@ -549,7 +549,6 @@ int fpsgraph_init(struct notcurses* nc){
ncplot_options opts;
memset(&opts, 0, sizeof(opts));
opts.flags = NCPLOT_OPTIONS_LABELTICKSD | NCPLOT_OPTIONS_EXPONENTIALD;
opts.gridtype = NCBLIT_8x1;
channels_set_fg_rgb(&opts.minchannel, 0xff, 0x00, 0xff);
channels_set_bg(&opts.minchannel, 0x201020);
channels_set_bg_alpha(&opts.minchannel, CELL_ALPHA_BLEND);

View File

@ -218,7 +218,7 @@ int main(void){
ncpp::Plane pplane{PLOTHEIGHT, dimx, dimy - PLOTHEIGHT, 0, nullptr};
struct ncplot_options popts{};
// FIXME would be nice to switch over to exponential at some level
popts.flags = NCPLOT_OPTIONS_LABELTICKSD;
popts.flags = NCPLOT_OPTIONS_LABELTICKSD | NCPLOT_OPTIONS_MAYDEGRADE;
popts.minchannel = popts.maxchannel = 0;
channels_set_fg_rgb(&popts.minchannel, 0x40, 0x50, 0xb0);
channels_set_fg_rgb(&popts.maxchannel, 0x40, 0xff, 0xd0);

View File

@ -283,7 +283,7 @@ braille_blit(ncplane* nc, int placey, int placex, int linesize,
}
// NCBLIT_DEFAULT is not included, as it has no defined properties. It ought
// be replaced with some real blitter implementation.
// be replaced with some real blitter implementation by the calling widget.
const struct blitset geomdata[] = {
{ .geom = NCBLIT_8x1, .width = 1, .height = 8, .egcs = L" ▁▂▃▄▅▆▇█",
.blit = NULL, .fill = false, },

View File

@ -1,8 +1,18 @@
#ifndef NOTCURSES_BLITSET
#define NOTCURSES_BLITSET
#include "notcurses/notcurses.h"
static inline const struct blitset*
lookup_blitset(ncblitter_e setid) {
lookup_blitset(const struct notcurses* nc, ncblitter_e setid, bool may_degrade) {
// the only viable blitter in ASCII is NCBLIT_1x1
if(!notcurses_canutf8(nc) && setid != NCBLIT_1x1){
if(may_degrade){
setid = NCBLIT_1x1;
}else{
return NULL;
}
}
const struct blitset* bset = geomdata;
while(bset->egcs){
if(bset->geom == setid){

View File

@ -67,99 +67,6 @@ int ncplane_polyfill_yx(ncplane* n, int y, int x, const cell* c){
return ret;
}
// Our gradient is a 2d lerp among the four corners of the region. We start
// with the observation that each corner ought be its exact specified corner,
// and the middle ought be the exact average of all four corners' components.
// Another observation is that if all four corners are the same, every cell
// ought be the exact same color. From this arises the observation that a
// perimeter element is not affected by the other three sides:
//
// a corner element is defined by itself
// a perimeter element is defined by the two points on its side
// an internal element is defined by all four points
//
// 2D equation of state: solve for each quadrant's contribution (min 2x2):
//
// X' = (xlen - 1) - X
// Y' = (ylen - 1) - Y
// TLC: X' * Y' * TL
// TRC: X * Y' * TR
// BLC: X' * Y * BL
// BRC: X * Y * BR
// steps: (xlen - 1) * (ylen - 1) [maximum steps away from origin]
//
// Then add TLC + TRC + BLC + BRC + steps / 2, and divide by steps (the
// steps / 2 is to work around truncate-towards-zero).
static int
calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br,
int y, int x, int ylen, int xlen){
assert(y >= 0);
assert(y < ylen);
assert(x >= 0);
assert(x < xlen);
const int avm = (ylen - 1) - y;
const int ahm = (xlen - 1) - x;
if(xlen < 2){
if(ylen < 2){
return tl;
}
return (tl * avm + bl * y) / (ylen - 1);
}
if(ylen < 2){
return (tl * ahm + tr * x) / (xlen - 1);
}
const int tlc = ahm * avm * tl;
const int blc = ahm * y * bl;
const int trc = x * avm * tr;
const int brc = y * x * br;
const int divisor = (ylen - 1) * (xlen - 1);
return ((tlc + blc + trc + brc) + divisor / 2) / divisor;
}
// calculate one of the channels of a gradient at a particular point.
static inline uint32_t
calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr,
int y, int x, int ylen, int xlen){
uint32_t chan = 0;
channel_set_rgb_clipped(&chan,
calc_gradient_component(channel_r(ul), channel_r(ur),
channel_r(ll), channel_r(lr),
y, x, ylen, xlen),
calc_gradient_component(channel_g(ul), channel_g(ur),
channel_g(ll), channel_g(lr),
y, x, ylen, xlen),
calc_gradient_component(channel_b(ul), channel_b(ur),
channel_b(ll), channel_b(lr),
y, x, ylen, xlen));
channel_set_alpha(&chan, channel_alpha(ul)); // precondition: all αs are equal
return chan;
}
// calculate both channels of a gradient at a particular point, storing them
// into `c`->channels. x and y ought be the location within the gradient.
static inline void
calc_gradient_channels(cell* c, uint64_t ul, uint64_t ur, uint64_t ll,
uint64_t lr, int y, int x, int ylen, int xlen){
if(!channels_fg_default_p(ul)){
cell_set_fchannel(c, calc_gradient_channel(channels_fchannel(ul),
channels_fchannel(ur),
channels_fchannel(ll),
channels_fchannel(lr),
y, x, ylen, xlen));
}else{
cell_set_fg_default(c);
}
if(!channels_bg_default_p(ul)){
cell_set_bchannel(c, calc_gradient_channel(channels_bchannel(ul),
channels_bchannel(ur),
channels_bchannel(ll),
channels_bchannel(lr),
y, x, ylen, xlen));
}else{
cell_set_bg_default(c);
}
}
static bool
check_gradient_channel_args(uint32_t ul, uint32_t ur, uint32_t bl, uint32_t br){
if(channel_default_p(ul) || channel_default_p(ur) ||
@ -307,7 +214,8 @@ int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword,
return -1;
}
targc->attrword = attrword;
calc_gradient_channels(targc, ul, ur, bl, br, y - yoff, x - xoff, ylen, xlen);
calc_gradient_channels(&targc->channels, ul, ur, bl, br,
y - yoff, x - xoff, ylen, xlen);
++total;
}
}
@ -340,7 +248,8 @@ int ncplane_stain(struct ncplane* n, int ystop, int xstop,
for(int y = yoff ; y <= ystop ; ++y){
for(int x = xoff ; x <= xstop ; ++x){
cell* targc = ncplane_cell_ref_yx(n, y, x);
calc_gradient_channels(targc, tl, tr, bl, br, y - yoff, x - xoff, ylen, xlen);
calc_gradient_channels(&targc->channels, tl, tr, bl, br,
y - yoff, x - xoff, ylen, xlen);
++total;
}
}

View File

@ -663,6 +663,102 @@ ncplane_center(const ncplane* n, int* RESTRICT y, int* RESTRICT x){
int ncvisual_bounding_box(const struct ncvisual* ncv, int* leny, int* lenx,
int* offy, int* offx);
// Our gradient is a 2d lerp among the four corners of the region. We start
// with the observation that each corner ought be its exact specified corner,
// and the middle ought be the exact average of all four corners' components.
// Another observation is that if all four corners are the same, every cell
// ought be the exact same color. From this arises the observation that a
// perimeter element is not affected by the other three sides:
//
// a corner element is defined by itself
// a perimeter element is defined by the two points on its side
// an internal element is defined by all four points
//
// 2D equation of state: solve for each quadrant's contribution (min 2x2):
//
// X' = (xlen - 1) - X
// Y' = (ylen - 1) - Y
// TLC: X' * Y' * TL
// TRC: X * Y' * TR
// BLC: X' * Y * BL
// BRC: X * Y * BR
// steps: (xlen - 1) * (ylen - 1) [maximum steps away from origin]
//
// Then add TLC + TRC + BLC + BRC + steps / 2, and divide by steps (the
// steps / 2 is to work around truncate-towards-zero).
static int
calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br,
int y, int x, int ylen, int xlen){
assert(y >= 0);
assert(y < ylen);
assert(x >= 0);
assert(x < xlen);
const int avm = (ylen - 1) - y;
const int ahm = (xlen - 1) - x;
if(xlen < 2){
if(ylen < 2){
return tl;
}
return (tl * avm + bl * y) / (ylen - 1);
}
if(ylen < 2){
return (tl * ahm + tr * x) / (xlen - 1);
}
const int tlc = ahm * avm * tl;
const int blc = ahm * y * bl;
const int trc = x * avm * tr;
const int brc = y * x * br;
const int divisor = (ylen - 1) * (xlen - 1);
return ((tlc + blc + trc + brc) + divisor / 2) / divisor;
}
// calculate one of the channels of a gradient at a particular point.
static inline uint32_t
calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr,
int y, int x, int ylen, int xlen){
uint32_t chan = 0;
channel_set_rgb_clipped(&chan,
calc_gradient_component(channel_r(ul), channel_r(ur),
channel_r(ll), channel_r(lr),
y, x, ylen, xlen),
calc_gradient_component(channel_g(ul), channel_g(ur),
channel_g(ll), channel_g(lr),
y, x, ylen, xlen),
calc_gradient_component(channel_b(ul), channel_b(ur),
channel_b(ll), channel_b(lr),
y, x, ylen, xlen));
channel_set_alpha(&chan, channel_alpha(ul)); // precondition: all αs are equal
return chan;
}
// calculate both channels of a gradient at a particular point, storing them
// into `channels'. x and y ought be the location within the gradient.
static inline void
calc_gradient_channels(uint64_t* channels, uint64_t ul, uint64_t ur,
uint64_t ll, uint64_t lr, int y, int x,
int ylen, int xlen){
if(!channels_fg_default_p(ul)){
channels_set_fchannel(channels,
calc_gradient_channel(channels_fchannel(ul),
channels_fchannel(ur),
channels_fchannel(ll),
channels_fchannel(lr),
y, x, ylen, xlen));
}else{
channels_set_fg_default(channels);
}
if(!channels_bg_default_p(ul)){
channels_set_bchannel(channels,
calc_gradient_channel(channels_bchannel(ul),
channels_bchannel(ur),
channels_bchannel(ll),
channels_bchannel(lr),
y, x, ylen, xlen));
}else{
channels_set_bg_default(channels);
}
}
#ifdef __cplusplus
}
#endif

View File

@ -24,7 +24,19 @@ class ncppplot {
if(maxy < miny){
return false;
}
auto bset = lookup_blitset(opts && opts->gridtype ? opts->gridtype : NCBLIT_8x1);
ncblitter_e blitter = opts ? opts->gridtype : NCBLIT_DEFAULT;
if(blitter == NCBLIT_DEFAULT){
if(notcurses_canutf8(ncplane_notcurses(n))){
blitter = NCBLIT_8x1;
}else{
blitter = NCBLIT_1x1;
}
}
bool degrade_blitter = true;
if(opts && !(opts->flags & NCPLOT_OPTIONS_MAYDEGRADE)){
degrade_blitter = false;
}
auto bset = lookup_blitset(ncplane_notcurses(n), blitter, degrade_blitter);
if(bset == nullptr){
return false;
}
@ -76,38 +88,11 @@ class ncppplot {
return false;
}
// Add to or set the value corresponding to this x. If x is beyond the current
// x window, the x window is advanced to include x, and values passing beyond
// the window are lost. The first call will place the initial window. The plot
// will be redrawn, but notcurses_render() is not called.
int add_sample(uint64_t x, T y){
if(window_slide(x)){
return -1;
}
update_sample(x, y, false);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
int set_sample(uint64_t x, T y){
if(window_slide(x)){
return -1;
}
update_sample(x, y, true);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
void destroy(){
free(slots);
}
// FIXME everything below here ought be private, but it busts unit tests
int redraw_plot(){
int redraw_plot() {
ncplane_erase(ncp);
const int scale = bset->width;
int dimy, dimx;
@ -122,7 +107,7 @@ class ncppplot {
if(exponentiali){
if(maxy > miny){
interval = pow(maxy - miny, (double)1 / (dimy * states));
//fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states);
//fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states);
}else{
interval = 0;
}
@ -158,6 +143,8 @@ class ncppplot {
#define MAXWIDTH 2
int idx = slotstart; // idx holds the real slot index; we move backwards
for(int x = finalx ; x >= startx ; --x){
// a single column might correspond to more than 1 ('scale', up to
// MAXWIDTH) slot's worth of samples. prepare the working gval set.
T gvals[MAXWIDTH];
// load it retaining the same ordering we have in the actual array
for(int i = scale - 1 ; i >= 0 ; --i){
@ -179,6 +166,10 @@ class ncppplot {
double intervalbase = miny;
const wchar_t* egc = bset->egcs;
for(int y = 0 ; y < dimy ; ++y){
uint64_t channels = 0;
calc_gradient_channels(&channels, maxchannel, maxchannel,
minchannel, minchannel, y, x, dimy, dimx);
ncplane_set_channels(ncp, channels);
size_t egcidx = 0, sumidx = 0;
// if we've got at least one interval's worth on the number of positions
// times the number of intervals per position plus the starting offset,
@ -206,10 +197,26 @@ class ncppplot {
egcidx = 0;
}
}
if(sumidx){
// if we're not UTF8, we can only arrive here via NCBLIT_1x1 (otherwise
// we would have errored out during construction). even then, however,
// we need handle ASCII differently, since it can't print full block.
// in ASCII mode, egcidx != means swap colors and use space.
if(notcurses_canutf8(ncplane_notcurses(ncp)) || !sumidx){
if(ncplane_putwc_yx(ncp, dimy - y - 1, x, egc[sumidx]) <= 0){
return -1;
}
}else{
const uint64_t swapbg = channels_bchannel(channels);
const uint64_t swapfg = channels_fchannel(channels);
channels_set_bchannel(&channels, swapfg);
channels_set_fchannel(&channels, swapbg);
ncplane_set_channels(ncp, channels);
if(ncplane_putsimple_yx(ncp, dimy - y - 1, x, ' ') <= 0){
return -1;
}
channels_set_bchannel(&channels, swapbg);
channels_set_fchannel(&channels, swapfg);
ncplane_set_channels(ncp, channels);
}
if(done){
break;
@ -224,11 +231,33 @@ class ncppplot {
if(ncplane_cursor_move_yx(ncp, 0, 0)){
return -1;
}
if(ncplane_stain(ncp, dimy - 1, dimx - 1, maxchannel, maxchannel,
minchannel, minchannel) <= 0){
return 0;
}
// Add to or set the value corresponding to this x. If x is beyond the current
// x window, the x window is advanced to include x, and values passing beyond
// the window are lost. The first call will place the initial window. The plot
// will be redrawn, but notcurses_render() is not called.
int add_sample(uint64_t x, T y) {
if(window_slide(x)){
return -1;
}
return 0;
update_sample(x, y, false);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
int set_sample(uint64_t x, T y) {
if(window_slide(x)){
return -1;
}
update_sample(x, y, true);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
// if we're doing domain detection, update the domain to reflect the value we

View File

@ -41,7 +41,7 @@ auto ncvisual_geom(const notcurses* nc, const ncvisual* n, ncblitter_e blitter,
if(blitter == NCBLIT_DEFAULT){
blitter = ncvisual_default_blitter(nc);
}
const struct blitset* bset = lookup_blitset(blitter);
const struct blitset* bset = lookup_blitset(nc, blitter, false);
if(!bset){
return -1;
}
@ -65,10 +65,11 @@ auto ncvisual_geom(const notcurses* nc, const ncvisual* n, ncblitter_e blitter,
static const struct blitset*
rgba_blitter(const notcurses* nc, const struct ncvisual_options* opts){
const struct blitset* bset;
if(opts && opts->blitter){
bset = lookup_blitset(opts->blitter);
const bool maydegrade = !opts || (opts->flags & NCVISUAL_OPTIONS_MAYDEGRADE);
if(opts && opts->blitter != NCBLIT_DEFAULT){
bset = lookup_blitset(nc, opts->blitter, maydegrade);
}else{
bset = lookup_blitset(ncvisual_default_blitter(nc));
bset = lookup_blitset(nc, ncvisual_default_blitter(nc), maydegrade);
}
if(bset && !bset->blit){ // FIXME remove this once all blitters are enabled
bset = nullptr;
@ -376,7 +377,7 @@ auto ncvisual_from_bgra(const void* bgra, int rows, int rowstride,
auto ncvisual_render(notcurses* nc, ncvisual* ncv,
const struct ncvisual_options* vopts) -> ncplane* {
if(vopts && vopts->flags){
if(vopts && vopts->flags > NCVISUAL_OPTIONS_MAYDEGRADE){
return nullptr;
}
int lenx = vopts ? vopts->lenx : 0;