diff --git a/README.md b/README.md index 2f05e898f..9055ea30d 100644 --- a/README.md +++ b/README.md @@ -2657,6 +2657,11 @@ Using the "default color" as only one of the foreground or background requires emitting the `op` escape followed by the appropriate escape for changing the fore- or background (since `op` changes both at once). +Certain EGCs are understood to be all-foreground or all-background. U+2588 +FULL BLOCK is all foreground. U+0020 SPACE is all background. When such a +character is used, notcurses will emit whichever character can take advantage +of the current color. + ## Included tools Five binaries are built as part of notcurses: diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index 2a94ea1c6..4cb5f22b7 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -875,7 +875,16 @@ API int ncplane_polyfill_yx(struct ncplane* n, int y, int x, const cell* c); // are composed into foreground and background gradients. To do a vertical // gradient, 'ul' ought equal 'ur' and 'll' ought equal 'lr'. To do a // horizontal gradient, 'ul' ought equal 'll' and 'ur' ought equal 'ul'. To -// color everything the same, all four channels should be equivalent. +// color everything the same, all four channels should be equivalent. The +// resulting alpha values are equal to incoming alpha values. +// +// Preconditions for gradient operations (error otherwise): +// +// all: only RGB colors (no defaults, no palette-indexed) +// all: all alpha values must be the same +// 1x1: all four colors must be the same +// 1xN: both top and both bottom colors must be the same (vertical gradient) +// Nx1: both left and both right colors must be the same (horizontal gradient) API int ncplane_gradient(struct ncplane* n, const char* egc, uint32_t attrword, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr, int ystop, int xstop); @@ -886,6 +895,9 @@ static inline int ncplane_gradient_sized(struct ncplane* n, const char* egc, uint32_t attrword, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr, int ylen, int xlen){ + if(ylen < 1 || xlen < 1){ + return -1; + } int y, x; ncplane_cursor_yx(n, &y, &x); return ncplane_gradient(n, egc, attrword, ul, ur, ll, lr, y + ylen - 1, x + xlen - 1); diff --git a/src/demo/intro.c b/src/demo/intro.c index dca842919..e4e0b4ebd 100644 --- a/src/demo/intro.c +++ b/src/demo/intro.c @@ -24,10 +24,10 @@ int intro(struct notcurses* nc){ struct ncplane* ncp = notcurses_stddim_yx(nc, &rows, &cols); uint64_t cul, cur, cll, clr; cul = cur = cll = clr = 0; - channels_set_fg_rgb(&cul, 0, 0xd0, 0); - channels_set_fg_rgb(&cur, 0xff, 0, 0); - channels_set_fg_rgb(&cll, 0x88, 0, 0xcc); - channels_set_fg_rgb(&clr, 0, 0, 0); + channels_set_fg_rgb(&cul, 0, 0xd0, 0); channels_set_bg(&cul, 0); + channels_set_fg_rgb(&cur, 0xff, 0, 0); channels_set_bg(&cur, 0); + channels_set_fg_rgb(&cll, 0x88, 0, 0xcc); channels_set_bg(&cll, 0); + channels_set_fg_rgb(&clr, 0, 0, 0); channels_set_bg(&clr, 0); // we use full block rather+fg than space+bg to conflict less with the menu if(ncplane_cursor_move_yx(ncp, 0, 0)){ return -1; diff --git a/src/demo/sliding.c b/src/demo/sliding.c index 047cd7ba5..2335f198c 100644 --- a/src/demo/sliding.c +++ b/src/demo/sliding.c @@ -105,10 +105,10 @@ fill_chunk(struct ncplane* n, int idx){ channels_set_fg_rgb(&channels, r, g, b); uint64_t ul, ur, ll, lr; ul = ur = ll = lr = 0; - channels_set_fg_rgb(&ul, r, g, b); - channels_set_fg_rgb(&ur, g, b, r); - channels_set_fg_rgb(&ll, b, r, g); - channels_set_fg_rgb(&lr, r, g, b); + channels_set_fg_rgb(&ul, r, g, b); channels_set_bg(&ul, 0); + channels_set_fg_rgb(&lr, r, g, b); channels_set_bg(&lr, 0); + channels_set_fg_rgb(&ur, g, b, r); channels_set_bg(&ur, 0); + channels_set_fg_rgb(&ll, b, r, g); channels_set_bg(&ll, 0); if(ncplane_gradient_sized(n, "█", 0, ul, ur, ll, lr, maxy, maxx)){ return -1; } diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index a9e7b76f8..9f06fc389 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -1932,45 +1932,55 @@ 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 ul, unsigned ur, unsigned ll, unsigned lr, +calc_gradient_component(unsigned tl, unsigned tr, unsigned bl, unsigned br, int y, int x, int ylen, int xlen){ -/*fprintf(stderr, "%08x %08x %08x %08x %d %d %d %d -> %08x\n", - ul, ur, ll, lr, y, x, ylen, xlen, - (((xlen - x) * ul / xlen + x * ur / xlen) * (ylen - y)) / ylen + - (((xlen - x) * ll / xlen + x * lr / xlen) * (y)) / ylen);*/ - /*unsigned lchuk = xlen * !!((xlen - x) % xlen); - unsigned rchuk = xlen * !!(x % (xlen));*/ - - const unsigned xchuk = xlen / 2; - - unsigned upchuk = ylen * !!(((((xlen - x) * ul + xchuk) / xlen + (x * ur + xchuk) / xlen) * (ylen - y)) % ylen); - unsigned downchuk = ylen * !!(((((xlen - x) * ll + xchuk) / xlen + (x * lr + xchuk) / xlen) * y) % ylen); - - upchuk = downchuk = ylen / 2; - //upchuk = 0; - //downchuk = 0; - - //fprintf(stderr, "uc: %u dc: %u lc: %u rc: %u\n", upchuk, downchuk, xchuk, xchuk); - - int up = ((((xlen - x) * ul + xchuk) / xlen + (x * ur + xchuk) / xlen) * (ylen - y) + upchuk) / ylen; - int down = ((((xlen - x) * ll + xchuk) / xlen + (x * lr + xchuk) / xlen) * y + downchuk) / ylen; - /* - fprintf(stderr, "UL: (xlen - x) * ul / xlen (%d - %d) * %u / %d (%g)\n", xlen, x, ul, xlen, (((float)xlen - x) * ul + xchuk) / xlen); - fprintf(stderr, "UR: x * ur / xlen %d * %u / %d (%g)\n", x, ur, xlen, ((float)x * ur + xchuk) / xlen); - fprintf(stderr, "ul: %u ur: %u u: %u\n", (xlen - x) * ul / xlen, - x * ur / xlen, up); -fprintf(stderr, "%u %u %u %u top: %u bottom: %u -> %u\n", - ul, ur, ll, lr, up, down, up + down);*/ - return up + down; + assert(xlen >= 2); + assert(ylen >= 2); + assert(y >= 0); + assert(y < ylen); + assert(x >= 0); + assert(x < xlen); + const int avm = (ylen - 1) - y; + const int ahm = (xlen - 1) - x; + 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 uint32_t +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(&chan, calc_gradient_component(channel_r(ul), channel_r(ur), + 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), @@ -1984,7 +1994,7 @@ calc_gradient_channel(uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr, // 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 void +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){ cell_set_fchannel(c, calc_gradient_channel(channels_fchannel(ul), @@ -2002,6 +2012,13 @@ calc_gradient_channels(cell* c, uint64_t ul, uint64_t ur, uint64_t ll, int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword, uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr, int ystop, int xstop){ + // Can't use default or palette-indexed colors in a gradient + if(channel_default_p(ul) || channel_default_p(ll) || + channel_default_p(lr) || channel_default_p(ur) || + channel_palindex_p(ul) || channel_palindex_p(ll) || + channel_palindex_p(lr) || channel_palindex_p(ur)){ + return -1; + } int yoff, xoff, ymax, xmax; ncplane_cursor_yx(n, &yoff, &xoff); // must be at least 1x1, with its upper-left corner at the current cursor @@ -2018,8 +2035,19 @@ int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword, } const int xlen = xstop - xoff + 1; const int ylen = ystop - yoff + 1; - for(int y = yoff ; y < ystop + 1 ; ++y){ - for(int x = xoff ; x < xstop + 1 ; ++x){ + if(ylen == 1){ + if(xlen == 1){ + // single cell FIXME + }else{ + // horizontal 1d FIXME + } + return 0; + }else if(xlen == 1){ + // vertical 1d FIXME + return 0; + } + for(int y = yoff ; y <= ystop ; ++y){ + for(int x = xoff ; x <= xstop ; ++x){ cell* targc = ncplane_cell_ref_yx(n, y, x); targc->channels = 0; if(cell_load(n, targc, egc) < 0){ @@ -2033,7 +2061,14 @@ int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword, } int ncplane_stain(struct ncplane* n, int ystop, int xstop, - uint64_t ul, uint64_t ur, uint64_t ll, uint64_t lr){ + uint64_t tl, uint64_t tr, uint64_t bl, uint64_t br){ + // Can't use default or palette-indexed colors in a gradient + if(channel_default_p(tl) || channel_default_p(bl) || + channel_default_p(br) || channel_default_p(tr) || + channel_palindex_p(tl) || channel_palindex_p(bl) || + channel_palindex_p(br) || channel_palindex_p(tr)){ + return -1; + } int yoff, xoff, ymax, xmax; ncplane_cursor_yx(n, &yoff, &xoff); // must be at least 1x1, with its upper-left corner at the current cursor @@ -2050,10 +2085,10 @@ int ncplane_stain(struct ncplane* n, int ystop, int xstop, } const int xlen = xstop - xoff + 1; const int ylen = ystop - yoff + 1; - for(int y = yoff ; y < ystop + 1 ; ++y){ - for(int x = xoff ; x < xstop + 1 ; ++x){ + 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, ul, ur, ll, lr, y - yoff, x - xoff, ylen, xlen); + calc_gradient_channels(targc, tl, tr, bl, br, y - yoff, x - xoff, ylen, xlen); } } return 0; diff --git a/src/lib/render.c b/src/lib/render.c index ad100c4ba..8de6b0469 100644 --- a/src/lib/render.c +++ b/src/lib/render.c @@ -419,6 +419,9 @@ notcurses_render_internal(notcurses* nc, struct crender* rvec){ struct crender* crender = &rvec[fbcellidx(y, dimx, x)]; lock_in_highcontrast(targc, crender); cell* prevcell = &nc->lastframe[fbcellidx(y, dimx, x)]; + if(targc->gcluster == 0){ + targc->gcluster = ' '; + } if(cellcmp_and_dupfar(&nc->pool, prevcell, crender->p, targc)){ crender->damaged = true; } diff --git a/tests/fills.cpp b/tests/fills.cpp index aeaaed09a..b30b66b58 100644 --- a/tests/fills.cpp +++ b/tests/fills.cpp @@ -65,30 +65,23 @@ TEST_CASE("Fills") { } SUBCASE("GradientMonochromatic") { - uint64_t ul, ur, ll, lr; - ul = ur = ll = lr = 0; - channels_set_fg(&ul, 0x40f040); - channels_set_bg(&ul, 0x40f040); - channels_set_fg(&ur, 0x40f040); - channels_set_bg(&ur, 0x40f040); - channels_set_fg(&ll, 0x40f040); - channels_set_bg(&ll, 0x40f040); - channels_set_fg(&lr, 0x40f040); - channels_set_bg(&lr, 0x40f040); + uint64_t c = 0; + channels_set_fg(&c, 0x40f040); + channels_set_bg(&c, 0x40f040); int dimy, dimx; ncplane_dim_yx(n_, &dimy, &dimx); - REQUIRE(0 == ncplane_gradient_sized(n_, "M", 0, ul, ur, ll, lr, dimy, dimx)); - cell c = CELL_TRIVIAL_INITIALIZER; + REQUIRE(0 == ncplane_gradient_sized(n_, "M", 0, c, c, c, c, dimy, dimx)); + cell cl = CELL_TRIVIAL_INITIALIZER; uint64_t channels = 0; channels_set_fg(&channels, 0x40f040); channels_set_bg(&channels, 0x40f040); // check all squares for(int y = 0 ; y < dimy ; ++y){ for(int x = 0 ; x < dimx ; ++x){ - REQUIRE(0 <= ncplane_at_yx(n_, y, x, &c)); - CHECK('M' == c.gcluster); - CHECK(0 == c.attrword); - CHECK(channels == c.channels); + REQUIRE(0 <= ncplane_at_yx(n_, y, x, &cl)); + CHECK('M' == cl.gcluster); + CHECK(0 == cl.attrword); + CHECK(channels == cl.channels); } } CHECK(0 == notcurses_render(nc_)); @@ -127,8 +120,7 @@ TEST_CASE("Fills") { lastyrgb = c.channels; CHECK(ul == c.channels); }else if(y == dimy - 1){ -/*fprintf(stderr, "HJAVE %016lx, WANT %016lx %d %d\n", c.channels, ll, y, x); - CHECK(ll == c.channels);*/ + CHECK(ll == c.channels); } lastxrgb = c.channels; }else{ @@ -138,8 +130,7 @@ TEST_CASE("Fills") { if(y == 0){ CHECK(ur == c.channels); }else if(y == dimy - 1){ -/*fprintf(stderr, "LR %016lx, WANT %016lx %d %d\n", c.channels, lr, y, x); - CHECK(lr == c.channels);*/ + CHECK(lr == c.channels); } } } @@ -201,6 +192,51 @@ TEST_CASE("Fills") { CHECK(0 == notcurses_render(nc_)); } + SUBCASE("Ncplane_Format") { + int sbytes; + CHECK(0 == ncplane_set_fg(n_, 0x444444)); + CHECK(1 == ncplane_putegc(n_, "A", &sbytes)); + CHECK(0 == ncplane_set_fg(n_, 0x888888)); + CHECK(1 == ncplane_putegc(n_, "B", &sbytes)); + CHECK(0 == notcurses_render(nc_)); + // attr should change, but not the EGC/color + CHECK(0 == ncplane_cursor_move_yx(n_, 0, 0)); + cell c = CELL_TRIVIAL_INITIALIZER; + cell_styles_on(&c, NCSTYLE_BOLD); + CHECK(0 == ncplane_format(n_, 0, 0, c.attrword)); + cell d = CELL_TRIVIAL_INITIALIZER; + CHECK(1 == ncplane_at_yx(n_, 0, 0, &d)); + CHECK(d.attrword == c.attrword); + CHECK(0x444444 == cell_fg(&d)); + } + + SUBCASE("Ncplane_Stain") { + int sbytes; + CHECK(0 == ncplane_set_fg(n_, 0x444444)); + for(int y = 0 ; y < 8 ; ++y){ + for(int x = 0 ; x < 8 ; ++x){ + CHECK(1 == ncplane_putegc_yx(n_, y, x, "A", &sbytes)); + } + } + CHECK(0 == notcurses_render(nc_)); + // attr should change, but not the EGC/color + CHECK(0 == ncplane_cursor_move_yx(n_, 0, 0)); + uint64_t channels = 0; + channels_set_fg_rgb(&channels, 0x88, 0x99, 0x77); + channels_set_bg(&channels, 0); + REQUIRE(0 == ncplane_stain(n_, 7, 7, channels, channels, channels, channels)); + CHECK(0 == notcurses_render(nc_)); + cell d = CELL_TRIVIAL_INITIALIZER; + for(int y = 0 ; y < 8 ; ++y){ + for(int x = 0 ; x < 8 ; ++x){ + CHECK(1 == ncplane_at_yx(n_, y, x, &d)); + CHECK(channels == d.channels); + REQUIRE(cell_simple_p(&d)); + CHECK('A' == d.gcluster); + } + } + } + CHECK(0 == notcurses_stop(nc_)); CHECK(0 == fclose(outfp_)); diff --git a/tests/ncplane.cpp b/tests/ncplane.cpp index f24994a0c..925dd4682 100644 --- a/tests/ncplane.cpp +++ b/tests/ncplane.cpp @@ -968,24 +968,6 @@ TEST_CASE("NCPlane") { CHECK(0 == notcurses_render(nc_)); } - SUBCASE("Ncplane_Format") { - int sbytes; - CHECK(0 == ncplane_set_fg(n_, 0x444444)); - CHECK(1 == ncplane_putegc(n_, "A", &sbytes)); - CHECK(0 == ncplane_set_fg(n_, 0x888888)); - CHECK(1 == ncplane_putegc(n_, "B", &sbytes)); - CHECK(0 == notcurses_render(nc_)); - // attr should change, but not the EGC/color - CHECK(0 == ncplane_cursor_move_yx(n_, 0, 0)); - cell c = CELL_TRIVIAL_INITIALIZER; - cell_styles_on(&c, NCSTYLE_BOLD); - CHECK(0 == ncplane_format(n_, 0, 0, c.attrword)); - cell d = CELL_TRIVIAL_INITIALIZER; - CHECK(1 == ncplane_at_yx(n_, 0, 0, &d)); - CHECK(d.attrword == c.attrword); - CHECK(0x444444 == cell_fg(&d)); - } - SUBCASE("EGCStainable") { cell c = CELL_TRIVIAL_INITIALIZER; int sbytes;