notcurses/src/lib/fill.c
2020-05-19 19:39:16 -04:00

689 lines
21 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "internal.h"
void ncplane_greyscale(ncplane *n){
for(int y = 0 ; y < n->leny ; ++y){
for(int x = 0 ; x < n->lenx ; ++x){
cell* c = &n->fb[nfbcellidx(n, y, x)];
unsigned r, g, b;
cell_fg_rgb(c, &r, &g, &b);
int gy = rgb_greyscale(r, g, b);
cell_set_fg_rgb(c, gy, gy, gy);
cell_bg_rgb(c, &r, &g, &b);
gy = rgb_greyscale(r, g, b);
cell_set_bg_rgb(c, gy, gy, gy);
}
}
}
// if this is not polyfillable cell, we return 0. if it is, we attempt to fill
// it, then recurse out. return -1 on error, or number of cells filled on
// success. so a return of 0 means there's no work to be done here, and N means
// we did some work here, filling everything we could reach. out-of-plane is 0.
static int
ncplane_polyfill_recurse(ncplane* n, int y, int x, const cell* c){
if(y >= n->leny || x >= n->lenx){
return 0; // not fillable
}
if(y < 0 || x < 0){
return 0; // not fillable
}
cell* cur = &n->fb[nfbcellidx(n, y, x)];
if(cur->gcluster){
return 0; // glyph, not polyfillable
}
if(cell_duplicate(n, cur, c) < 0){
return -1;
}
int r, ret = 1;
if((r = ncplane_polyfill_recurse(n, y - 1, x, c)) < 0){
return -1;
}
ret += r;
if((r = ncplane_polyfill_recurse(n, y + 1, x, c)) < 0){
return -1;
}
ret += r;
if((r = ncplane_polyfill_recurse(n, y, x - 1, c)) < 0){
return -1;
}
ret += r;
if((r = ncplane_polyfill_recurse(n, y, x + 1, c)) < 0){
return -1;
}
ret += r;
return ret;
}
// at the initial step only, invalid y, x is an error, so explicitly check.
int ncplane_polyfill_yx(ncplane* n, int y, int x, const cell* c){
int ret = -1;
if(c->gcluster){ // can't polyfill with a null EGC
if(y < n->leny && x < n->lenx){
if(y >= 0 && x >= 0){
ret = ncplane_polyfill_recurse(n, y, x, 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) ||
channel_default_p(bl) || channel_default_p(br)){
if(!(channel_default_p(ul) && channel_default_p(ur) &&
channel_default_p(bl) && channel_default_p(br))){
return true;
}
}
if(channel_alpha(ul) != channel_alpha(ur) ||
channel_alpha(ur) != channel_alpha(bl) ||
channel_alpha(bl) != channel_alpha(br)){
return true;
}
if(channel_palindex_p(ul) || channel_palindex_p(bl) ||
channel_palindex_p(br) || channel_palindex_p(ur)){
return true;
}
return false;
}
// Given the four channels arguments, verify that:
//
// - if any is default foreground, all are default foreground
// - if any is default background, all are default background
// - all foregrounds must have the same alpha
// - all backgrounds must have the same alpha
// - palette-indexed color must not be used
static bool
check_gradient_args(uint64_t ul, uint64_t ur, uint64_t bl, uint64_t br){
if(check_gradient_channel_args(channels_fchannel(ul), channels_fchannel(ur),
channels_fchannel(bl), channels_fchannel(br))){
return true;
}
if(check_gradient_channel_args(channels_bchannel(ul), channels_bchannel(ur),
channels_bchannel(bl), channels_bchannel(br))){
return true;
}
return false;
}
// calculate both channels of a gradient at a particular point, knowing that
// we're using double halfblocks, into `c`->channels.
static inline void
calc_highgradient(cell* c, uint64_t ul, uint64_t ur, uint64_t ll,
uint64_t lr, int y, int x, int ylen, int xlen){
if(!channel_default_p(ul)){
cell_set_fchannel(c, calc_gradient_channel(ul, ur, ll, lr,
y * 2, x, ylen, xlen));
cell_set_bchannel(c, calc_gradient_channel(ul, ur, ll, lr,
y * 2 + 1, x, ylen, xlen));
}else{
cell_set_fg_default(c);
cell_set_bg_default(c);
}
}
int ncplane_highgradient(ncplane* n, uint32_t ul, uint32_t ur,
uint32_t ll, uint32_t lr, int ystop, int xstop){
if(check_gradient_channel_args(ul, ur, ll, lr)){
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
if(ystop < yoff){
return -1;
}
if(xstop < xoff){
return -1;
}
ncplane_dim_yx(n, &ymax, &xmax);
// must be within the ncplane
if(xstop >= xmax || ystop >= ymax){
return -1;
}
const int xlen = xstop - xoff + 1;
const int ylen = (ystop - yoff + 1) * 2;
if(xlen == 1){
if(ul != ur || ll != lr){
return -1;
}
}
int total = 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, "") < 0){
return -1;
}
calc_highgradient(targc, ul, ur, ll, lr, y - yoff, x - xoff, ylen, xlen);
++total;
}
}
return total;
}
int ncplane_gradient(ncplane* n, const char* egc, uint32_t attrword,
uint64_t ul, uint64_t ur, uint64_t bl, uint64_t br,
int ystop, int xstop){
if(check_gradient_args(ul, ur, bl, br)){
return -1;
}
if(egc == NULL){
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
if(ystop < yoff){
return -1;
}
if(xstop < xoff){
return -1;
}
ncplane_dim_yx(n, &ymax, &xmax);
// must be within the ncplane
if(xstop >= xmax || ystop >= ymax){
return -1;
}
const int xlen = xstop - xoff + 1;
const int ylen = ystop - yoff + 1;
if(ylen == 1){
if(xlen == 1){
if(ul != ur || ur != br || br != bl){
return -1;
}
}else{
if(ul != bl || ur != br){
return -1;
}
}
}else if(xlen == 1){
if(ul != ur || bl != br){
return -1;
}
}
int total = 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){
return -1;
}
targc->attrword = attrword;
calc_gradient_channels(targc, ul, ur, bl, br, y - yoff, x - xoff, ylen, xlen);
++total;
}
}
return total;
}
int ncplane_stain(struct ncplane* n, int ystop, int xstop,
uint64_t tl, uint64_t tr, uint64_t bl, uint64_t br){
// Can't use default or palette-indexed colors in a gradient
if(check_gradient_args(tl, tr, bl, br)){
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
if(ystop < yoff){
return -1;
}
if(xstop < xoff){
return -1;
}
ncplane_dim_yx(n, &ymax, &xmax);
// must be within the ncplane
if(xstop >= xmax || ystop >= ymax){
return -1;
}
const int xlen = xstop - xoff + 1;
const int ylen = ystop - yoff + 1;
int total = 0;
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);
++total;
}
}
return total;
}
int ncplane_format(struct ncplane* n, int ystop, int xstop, uint32_t attrword){
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
if(ystop < yoff){
return -1;
}
if(xstop < xoff){
return -1;
}
ncplane_dim_yx(n, &ymax, &xmax);
// must be within the ncplane
if(xstop >= xmax || ystop >= ymax){
return -1;
}
int total = 0;
for(int y = yoff ; y < ystop + 1 ; ++y){
for(int x = xoff ; x < xstop + 1 ; ++x){
cell* targc = ncplane_cell_ref_yx(n, y, x);
targc->attrword = attrword;
++total;
}
}
return total;
}
// if we're a lower block, reverse the channels. if we're a space, set both to
// the background. if we're a full block, set both to the foreground.
static void
rotate_channels(ncplane* src, const cell* c, uint32_t* fchan, uint32_t* bchan){
if(cell_simple_p(c)){
if(!isgraph(c->gcluster)){
*fchan = *bchan;
}
return;
}
const char* origc = cell_extended_gcluster(src, c);
if(strcmp(origc, "") == 0){
uint32_t tmp = *fchan;
*fchan = *bchan;
*bchan = tmp;
return;
}else if(strcmp(origc, "") == 0){
*bchan = *fchan;
return;
}
}
static int
rotate_output(ncplane* dst, uint32_t tchan, uint32_t bchan){
dst->channels = channels_combine(tchan, bchan);
if(tchan != bchan){
return ncplane_putegc(dst, "", NULL);
}
if(channel_default_p(tchan) && channel_default_p(bchan)){
return ncplane_putegc(dst, "", NULL);
}else if(channel_default_p(tchan)){
return ncplane_putegc(dst, " ", NULL);
}
return ncplane_putegc(dst, "", NULL);
}
// rotation works at two levels:
// 1) each 1x2 block is rotated into a 1x2 block ala
// ab cw ca ccw ab ccw bd ccw dc ccw ca ccw ab
// cd --> db --> cd --> ac --> ba --> db --> cd
// 2) each 1x2 block is rotated into its new location
//
// Characters which can be rotated must be RGB, to differentiate full blocks,
// spaces, and nuls. For clockwise rotations:
//
// nul: converts to two half defaults
// space: converts to two half backgrounds
// full: converts to two half foregrounds
// upper: converts to half background + half foreground
// lower: converts to half foreground + half background
//
// Fore/background carry full channel, including transparency.
//
// Ideally, rotation through 360 degrees will restore the original 2x1 squre.
// Unfortunately, the case where a half block occupies a cell having the same
// fore- and background will see it rotated into a single full block. In
// addition, lower blocks eventually become upper blocks with their channels
// reversed. In general:
//
// if a "row" (the bottom or top halves) are the same forechannel, merge to a
// single full block of that color (what is its background?).
// if a "row" is two different channels, they become a upper block (why not
// lower?) having the two channels as fore- and background.
static int
rotate_2x1_cw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){
cell c1 = CELL_TRIVIAL_INITIALIZER;
cell c2 = CELL_TRIVIAL_INITIALIZER;
if(ncplane_at_yx_cell(src, srcy, srcx, &c1) < 0){
return -1;
}
if(ncplane_at_yx_cell(src, srcy, srcx + 1, &c2) < 0){
cell_release(src, &c1);
return -1;
}
// there can be at most 4 colors and 4 transparencies:
// - c1fg, c1bg, c2fg, c2bg, c1ftrans, c2ftrans, c1btrans, c2btrans
// but not all are necessarily used:
// - topleft gets lowerleft. if lowerleft is foreground, c1fg c1ftrans.
// otherwise, c1bg c1btrans
// - topright gets upperleft. if upperleft is foreground, c1fg c1ftrans.
// otherwise, c1bg c1btrans
// - botleft get botright. if botright is foreground, c2fg c2ftrans.
// otherwise, c2bg c2btrans
// - botright gets topright. if topright is foreground, c2fg c2ftrans.
// otherwise, c2bg c2btrans
uint32_t c1b = cell_bchannel(&c1);
uint32_t c2b = cell_bchannel(&c2);
uint32_t c1t = cell_fchannel(&c1);
uint32_t c2t = cell_fchannel(&c2);
rotate_channels(src, &c1, &c1t, &c1b);
rotate_channels(src, &c2, &c2t, &c2b);
// right char comes from two tops. left char comes from two bottoms. if
// they're the same channel, they become a:
//
// nul if the channel is default
// space if the fore is default
// full if the back is default
ncplane_cursor_move_yx(dst, dsty, dstx);
rotate_output(dst, c1b, c2b);
rotate_output(dst, c1t, c2t);
return 0;
}
static int
rotate_2x1_ccw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){
cell c1 = CELL_TRIVIAL_INITIALIZER;
cell c2 = CELL_TRIVIAL_INITIALIZER;
if(ncplane_at_yx_cell(src, srcy, srcx, &c1) < 0){
return -1;
}
if(ncplane_at_yx_cell(src, srcy, srcx + 1, &c2) < 0){
cell_release(src, &c1);
return -1;
}
uint32_t c1b = cell_bchannel(&c1);
unsigned c2b = cell_bchannel(&c2);
unsigned c1t = cell_fchannel(&c1);
unsigned c2t = cell_fchannel(&c2);
rotate_channels(src, &c1, &c1t, &c1b);
rotate_channels(src, &c2, &c2t, &c2b);
ncplane_cursor_move_yx(dst, dsty, dstx);
rotate_output(dst, c1t, c2t);
rotate_output(dst, c1b, c2b);
return 0;
}
// copy 'newp' into 'n' after resizing 'n' to match 'newp'
static int
rotate_merge(ncplane* n, ncplane* newp){
int dimy, dimx;
ncplane_dim_yx(newp, &dimy, &dimx);
int ret = ncplane_resize(n, 0, 0, 0, 0, 0, 0, dimy, dimx);
if(ret == 0){
for(int y = 0 ; y < dimy ; ++y){
for(int x = 0 ; x < dimx ; ++x){
const cell* src = &newp->fb[fbcellidx(y, dimx, x)];
cell* targ = &n->fb[fbcellidx(y, dimx, x)];
if(cell_duplicate_far(&n->pool, targ, newp, src) < 0){
return -1;
}
}
}
}
return ret;
}
// generate a temporary plane that can hold the contents of n, rotated 90°
static ncplane*
rotate_plane(const ncplane* n){
int absy, absx;
ncplane_yx(n, &absy, &absx);
int dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
if(dimx % 2 != 0){
return NULL;
}
const int newy = dimx / 2;
const int newx = dimy * 2;
ncplane* newp = ncplane_new(n->nc, newy, newx, absy, absx, n->userptr);
return newp;
}
int ncplane_rotate_cw(ncplane* n){
ncplane* newp = rotate_plane(n);
if(newp == NULL){
return -1;
}
int dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
int centy, centx;
ncplane_center_abs(n, &centy, &centx);
// the topmost row consists of the leftmost two columns. the rightmost column
// of the topmost row consists of the top half of the top two leftmost cells.
// the penultimate column of the topmost row consists of the bottom half of
// the top two leftmost cells. work from the bottom up on the source, so we
// can copy to the top row from the left to the right.
int targx, targy = 0;
for(int x = 0 ; x < dimx ; x += 2){
targx = 0;
for(int y = dimy - 1 ; y >= 0 ; --y){
if(rotate_2x1_cw(n, newp, y, x, targy, targx)){
ncplane_destroy(newp);
return -1;
}
targx += 2;
}
++targy;
}
int ret = rotate_merge(n, newp);
ret |= ncplane_destroy(newp);
return ret;
}
int ncplane_rotate_ccw(ncplane* n){
ncplane* newp = rotate_plane(n);
if(newp == NULL){
return -1;
}
int dimy, dimx, targdimy, targdimx;
ncplane_dim_yx(n, &dimy, &dimx);
ncplane_dim_yx(newp, &targdimy, &targdimx);
int x = dimx - 2, y;
// Each row of the target plane is taken from a column of the source plane.
// As the target row grows (down), the source column shrinks (moves left).
for(int targy = 0 ; targy < targdimy ; ++targy){
y = 0;
for(int targx = 0 ; targx < targdimx ; targx += 2){
if(rotate_2x1_ccw(n, newp, y, x, targy, targx)){
ncplane_destroy(newp);
return -1;
}
++y;
}
x -= 2;
}
int ret = rotate_merge(n, newp);
ret |= ncplane_destroy(newp);
return ret;
}
#ifdef USE_QRCODEGEN
#include <qrcodegen/qrcodegen.h>
#define QR_BASE_SIZE 17
#define PER_QR_VERSION 4
static inline int
qrcode_rows(int version){
return QR_BASE_SIZE + (version * PER_QR_VERSION / 2);
}
static inline int
qrcode_cols(int version){
return QR_BASE_SIZE + (version * PER_QR_VERSION);
}
int ncplane_qrcode(ncplane* n, int maxversion, const void* data, size_t len){
const int MAX_QR_VERSION = 40; // QR library only supports up to 40
if(maxversion < 0){
return -1;
}
if(len == 0){
return -1;
}
const int starty = n->y;
const int startx = n->x;
const int availx = n->lenx - startx;
const int availy = n->leny - starty;
if(availy < qrcode_rows(1)){
return -1;
}
if(availx < qrcode_cols(1)){
return -1;
}
const int availsquare = availy * 2 < availx ? availy * 2 : availx;
const int roomforver = (availsquare - QR_BASE_SIZE) / 4;
if(maxversion == 0){
maxversion = roomforver;
}else if(maxversion > roomforver){
maxversion = roomforver;
}
if(maxversion > MAX_QR_VERSION){
maxversion = MAX_QR_VERSION;
}
const size_t bsize = qrcodegen_BUFFER_LEN_FOR_VERSION(maxversion);
if(bsize < len){
return -1;
}
uint8_t* src = malloc(bsize);
uint8_t* dst = malloc(bsize);
if(src == NULL || dst == NULL){
free(src);
free(dst);
return -1;
}
memcpy(src, data, len);
int ret = -1;
if(qrcodegen_encodeBinary(src, len, dst, qrcodegen_Ecc_HIGH, 1, maxversion, qrcodegen_Mask_AUTO, true)){
ret = qrcodegen_getSize(dst);
for(int y = starty ; y < starty + (ret + 1) / 2 ; ++y){
for(int x = startx ; x < startx + ret ; ++x){
const bool top = qrcodegen_getModule(dst, x, y);
const bool bot = qrcodegen_getModule(dst, x, y + 1);
const char* egc;
if(top && bot){
egc = "";
}else if(top){
egc = "";
}else if(bot){
egc = "";
}else{
egc = " ";
}
int sbytes;
if(ncplane_putegc_yx(n, y, x, egc, &sbytes) <= 0){
ret = -1;
break;
}
}
}
}
free(src);
free(dst);
return ret < 0 ? ret : (ret - QR_BASE_SIZE) / PER_QR_VERSION;
}
#else
int ncplane_qrcode(ncplane* n, int maxversion, const void* data, size_t len){
(void)n;
(void)maxversion;
(void)data;
(void)len;
return -1;
}
#endif