You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/blit.c

884 lines
36 KiB
C

#include <stddef.h>
#include "internal.h"
static const uint32_t zeroes32;
static const unsigned char zeroes[] = "\x00\x00\x00\x00";
// linearly interpolate a 24-bit RGB value along each 8-bit channel
static inline uint32_t
lerp(uint32_t c0, uint32_t c1){
uint32_t ret = 0;
unsigned r0, g0, b0, r1, g1, b1;
channel_rgb8(c0, &r0, &g0, &b0);
channel_rgb8(c1, &r1, &g1, &b1);
channel_set_rgb8(&ret, (r0 + r1 + 1) / 2,
(g0 + g1 + 1) / 2,
(b0 + b1 + 1) / 2);
return ret;
}
// linearly interpolate a 24-bit RGB value along each 8-bit channel
static inline uint32_t
trilerp(uint32_t c0, uint32_t c1, uint32_t c2){
uint32_t ret = 0;
unsigned r0, g0, b0, r1, g1, b1, r2, g2, b2;
channel_rgb8(c0, &r0, &g0, &b0);
channel_rgb8(c1, &r1, &g1, &b1);
channel_rgb8(c2, &r2, &g2, &b2);
channel_set_rgb8(&ret, (r0 + r1 + r2 + 2) / 3,
(g0 + g1 + g2 + 2) / 3,
(b0 + b1 + b2 + 2) / 3);
return ret;
}
// alpha comes to us 0--255, but we have only 3 alpha values to map them to.
// settled on experimentally.
static inline bool
ffmpeg_trans_p(unsigned char alpha){
if(alpha < 192){
//fprintf(stderr, "TRANSPARENT!\n");
return true;
}
return false;
}
// Retarded RGBA blitter (ASCII only).
static inline int
tria_blit_ascii(ncplane* nc, int placey, int placex, int linesize,
const void* data, int begy, int begx,
int leny, int lenx, bool blendcolors){
//fprintf(stderr, "ASCII %d X %d @ %d X %d (%p) place: %d X %d\n", leny, lenx, begy, begx, data, placey, placex);
const int bpp = 32;
int dimy, dimx, x, y;
int total = 0; // number of cells written
ncplane_dim_yx(nc, &dimy, &dimx);
// FIXME not going to necessarily be safe on all architectures hrmmm
const unsigned char* dat = data;
int visy = begy;
for(y = placey ; visy < (begy + leny) && y < dimy ; ++y, ++visy){
if(ncplane_cursor_move_yx(nc, y, placex)){
return -1;
}
int visx = begx;
for(x = placex ; visx < (begx + lenx) && x < dimx ; ++x, ++visx){
const unsigned char* rgbbase_up = dat + (linesize * visy) + (visx * bpp / CHAR_BIT);
//fprintf(stderr, "[%04d/%04d] bpp: %d lsize: %d %02x %02x %02x %02x\n", y, x, bpp, linesize, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2], rgbbase_up[3]);
nccell* c = ncplane_cell_ref_yx(nc, y, x);
// use the default for the background, as that's the only way it's
// effective in that case anyway
c->channels = 0;
c->stylemask = 0;
if(blendcolors){
cell_set_bg_alpha(c, CELL_ALPHA_BLEND);
cell_set_fg_alpha(c, CELL_ALPHA_BLEND);
}
if(ffmpeg_trans_p(rgbbase_up[3])){
cell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
cell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
}else{
cell_set_fg_rgb8(c, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2]);
cell_set_bg_rgb8(c, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2]);
if(pool_load_direct(&nc->pool, c, " ", 1, 1) <= 0){
return -1;
}
++total;
}
}
}
return total;
}
// RGBA half-block blitter. Best for most images/videos. Full fidelity
// combined with 1:1 pixel aspect ratio.
static inline int
tria_blit(ncplane* nc, int placey, int placex, int linesize,
const void* data, int begy, int begx,
int leny, int lenx, bool blendcolors){
//fprintf(stderr, "HALF %d X %d @ %d X %d (%p) place: %d X %d\n", leny, lenx, begy, begx, data, placey, placex);
const int bpp = 32;
int dimy, dimx, x, y;
int total = 0; // number of cells written
ncplane_dim_yx(nc, &dimy, &dimx);
// FIXME not going to necessarily be safe on all architectures hrmmm
const unsigned char* dat = data;
int visy = begy;
for(y = placey ; visy < (begy + leny) && y < dimy ; ++y, visy += 2){
if(ncplane_cursor_move_yx(nc, y, placex)){
return -1;
}
int visx = begx;
for(x = placex ; visx < (begx + lenx) && x < dimx ; ++x, ++visx){
const unsigned char* rgbbase_up = dat + (linesize * visy) + (visx * bpp / CHAR_BIT);
const unsigned char* rgbbase_down = zeroes;
if(visy < begy + leny - 1){
rgbbase_down = dat + (linesize * (visy + 1)) + (visx * bpp / CHAR_BIT);
}
//fprintf(stderr, "[%04d/%04d] bpp: %d lsize: %d %02x %02x %02x %02x\n", y, x, bpp, linesize, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2], rgbbase_up[3]);
nccell* c = ncplane_cell_ref_yx(nc, y, x);
// use the default for the background, as that's the only way it's
// effective in that case anyway
c->channels = 0;
c->stylemask = 0;
if(blendcolors){
cell_set_bg_alpha(c, CELL_ALPHA_BLEND);
cell_set_fg_alpha(c, CELL_ALPHA_BLEND);
}
if(ffmpeg_trans_p(rgbbase_up[3]) || ffmpeg_trans_p(rgbbase_down[3])){
cell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
if(ffmpeg_trans_p(rgbbase_up[3]) && ffmpeg_trans_p(rgbbase_down[3])){
cell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
}else if(ffmpeg_trans_p(rgbbase_up[3])){ // down has the color
if(cell_load(nc, c, "\u2584") <= 0){ // lower half block
return -1;
}
cell_set_fg_rgb8(c, rgbbase_down[0], rgbbase_down[1], rgbbase_down[2]);
++total;
}else{ // up has the color
if(cell_load(nc, c, "\u2580") <= 0){ // upper half block
return -1;
}
cell_set_fg_rgb8(c, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2]);
++total;
}
}else{
if(memcmp(rgbbase_up, rgbbase_down, 3) == 0){
cell_set_fg_rgb8(c, rgbbase_down[0], rgbbase_down[1], rgbbase_down[2]);
cell_set_bg_rgb8(c, rgbbase_down[0], rgbbase_down[1], rgbbase_down[2]);
if(cell_load(nc, c, " ") <= 0){ // only need the background
return -1;
}
}else{
cell_set_fg_rgb8(c, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2]);
cell_set_bg_rgb8(c, rgbbase_down[0], rgbbase_down[1], rgbbase_down[2]);
if(cell_load(nc, c, "\u2580") <= 0){ // upper half block
return -1;
}
}
++total;
}
}
}
return total;
}
// get a non-negative "distance" between two rgb values
static inline uint32_t
rgb_diff(unsigned r1, unsigned g1, unsigned b1, unsigned r2, unsigned g2, unsigned b2){
uint32_t distance = 0;
distance += r1 > r2 ? r1 - r2 : r2 - r1;
distance += g1 > g2 ? g1 - g2 : g2 - g1;
distance += b1 > b2 ? b1 - b2 : b2 - b1;
return distance;
}
// once we find the closest pair of colors, we need look at the other two
// colors, and determine whether either belongs with us rather with them.
// if so, take the closer, and trilerp it in with us. otherwise, lerp the
// two excluded pixels (and retain our original lerp).
static const struct qdriver {
int pair[2]; // indices of contributing pair
int others[2]; // indices of excluded pair
const char* egc; // EGC corresponding to contributing pair
const char* oth0egc; // EGC upon absorbing others[0]
const char* oth1egc; // EGC upon absorbing others[1]
} quadrant_drivers[6] = {
{ .pair = { 0, 1 }, .others = { 2, 3 }, .egc = "", .oth0egc = "", .oth1egc = "", },
{ .pair = { 0, 2 }, .others = { 1, 3 }, .egc = "", .oth0egc = "", .oth1egc = "", },
{ .pair = { 0, 3 }, .others = { 1, 2 }, .egc = "", .oth0egc = "", .oth1egc = "", },
{ .pair = { 1, 2 }, .others = { 0, 3 }, .egc = "", .oth0egc = "", .oth1egc = "", },
{ .pair = { 1, 3 }, .others = { 0, 2 }, .egc = "", .oth0egc = "", .oth1egc = "", },
{ .pair = { 2, 3 }, .others = { 0, 1 }, .egc = "", .oth0egc = "", .oth1egc = "", },
};
// get the six distances between four colors. diffs must be an array of
// at least 6 uint32_t values.
static void
rgb_4diff(uint32_t* diffs, uint32_t tl, uint32_t tr, uint32_t bl, uint32_t br){
struct rgb {
unsigned r, g, b;
} colors[4];
channel_rgb8(tl, &colors[0].r, &colors[0].g, &colors[0].b);
channel_rgb8(tr, &colors[1].r, &colors[1].g, &colors[1].b);
channel_rgb8(bl, &colors[2].r, &colors[2].g, &colors[2].b);
channel_rgb8(br, &colors[3].r, &colors[3].g, &colors[3].b);
for(size_t idx = 0 ; idx < sizeof(quadrant_drivers) / sizeof(*quadrant_drivers) ; ++idx){
const struct qdriver* qd = quadrant_drivers + idx;
const struct rgb* rgb0 = colors + qd->pair[0];
const struct rgb* rgb1 = colors + qd->pair[1];
diffs[idx] = rgb_diff(rgb0->r, rgb0->g, rgb0->b,
rgb1->r, rgb1->g, rgb1->b);
}
}
// solve for the EGC and two colors to best represent four colors at top
// left, top right, bot left, bot right
static inline const char*
quadrant_solver(uint32_t tl, uint32_t tr, uint32_t bl, uint32_t br,
uint32_t* fore, uint32_t* back){
const uint32_t colors[4] = { tl, tr, bl, br };
//fprintf(stderr, "%08x/%08x/%08x/%08x\n", tl, tr, bl, br);
uint32_t diffs[sizeof(quadrant_drivers) / sizeof(*quadrant_drivers)];
rgb_4diff(diffs, tl, tr, bl, br);
// compiler can't verify that we'll always be less than 769 somewhere,
// so fuck it, just go ahead and initialize to 0 / diffs[0]
size_t mindiffidx = 0;
unsigned mindiff = diffs[0]; // 3 * 256 + 1; // max distance is 256 * 3
// if all diffs are 0, emit a space
bool allzerodiffs = (mindiff == 0);
for(size_t idx = 1 ; idx < sizeof(diffs) / sizeof(*diffs) ; ++idx){
if(diffs[idx] < mindiff){
mindiffidx = idx;
mindiff = diffs[idx];
}
if(diffs[idx]){
allzerodiffs = false;
}
}
if(allzerodiffs){
*fore = *back = tl;
return " ";
}
// at this point, 0 <= mindiffidx <= 5. foreground color will be the
// lerp of this nearest pair. we then check the other two. if they are
// closer to one another than either is to our lerp, lerp between them.
// otherwise, bring the closer one into our lerped fold.
const struct qdriver* qd = &quadrant_drivers[mindiffidx];
// the diff of the excluded pair is conveniently located at the inverse
// location within diffs[] viz mindiffidx.
// const uint32_t otherdiff = diffs[5 - mindiffidx];
*fore = lerp(colors[qd->pair[0]], colors[qd->pair[1]]);
*back = lerp(colors[qd->others[0]], colors[qd->others[1]]);
//fprintf(stderr, "mindiff: %u[%zu] fore: %08x back: %08x %d+%d/%d+%d\n", mindiff, mindiffidx, *fore, *back, qd->pair[0], qd->pair[1], qd->others[0], qd->others[1]);
const char* egc = qd->egc;
// break down the excluded pair and lerp
unsigned r0, r1, g0, g1, b0, b1;
unsigned roth, goth, both, rlerp, glerp, blerp;
channel_rgb8(colors[qd->others[0]], &r0, &g0, &b0);
channel_rgb8(colors[qd->others[1]], &r1, &g1, &b1);
channel_rgb8(*fore, &rlerp, &glerp, &blerp);
channel_rgb8(*back, &roth, &goth, &both);
//fprintf(stderr, "rgbs: %02x %02x %02x / %02x %02x %02x\n", r0, g0, b0, r1, g1, b1);
diffs[0] = rgb_diff(r0, g0, b0, rlerp, glerp, blerp);
diffs[1] = rgb_diff(r0, g0, b0, roth, goth, both);
diffs[2] = rgb_diff(r1, g1, b1, rlerp, glerp, blerp);
diffs[3] = rgb_diff(r1, g1, b1, roth, goth, both);
//fprintf(stderr, "diffs: %08x %08x %08x %08x\n", diffs[0], diffs[1], diffs[2], diffs[3]);
if(diffs[0] < diffs[1] && diffs[0] < diffs[2]){
egc = qd->oth0egc;
*back = colors[qd->others[1]];
*fore = trilerp(colors[qd->pair[0]], colors[qd->pair[1]], colors[qd->others[0]]);
//fprintf(stderr, "swap 1 %08x %08x\n", *fore, *back);
}else if(diffs[2] < diffs[3]){
egc = qd->oth1egc;
*back = colors[qd->others[0]];
*fore = trilerp(colors[qd->pair[0]], colors[qd->pair[1]], colors[qd->others[1]]);
//fprintf(stderr, "swap 2 %08x %08x\n", *fore, *back);
}
return egc;
}
// quadrant check for transparency. returns an EGC if we found transparent
// quads and have solved for colors (this EGC ought then be loaded into the
// cell). returns NULL otherwise. transparency trumps everything else in terms
// of priority -- if even one quadrant is transparent, we will have a
// transparent background, and lerp the rest together for foreground. we thus
// have a 16-way conditional tree in which each EGC must show up exactly once.
// FIXME we ought be able to just build up a bitstring and use it as an index!
// FIXME pass in rgbas as array of uint32_t ala sexblitter
static inline const char*
qtrans_check(nccell* c, bool blendcolors,
const unsigned char* rgbbase_tl, const unsigned char* rgbbase_tr,
const unsigned char* rgbbase_bl, const unsigned char* rgbbase_br){
uint32_t tl = 0, tr = 0, bl = 0, br = 0;
channel_set_rgb8(&tl, rgbbase_tl[0], rgbbase_tl[1], rgbbase_tl[2]);
channel_set_rgb8(&tr, rgbbase_tr[0], rgbbase_tr[1], rgbbase_tr[2]);
channel_set_rgb8(&bl, rgbbase_bl[0], rgbbase_bl[1], rgbbase_bl[2]);
channel_set_rgb8(&br, rgbbase_br[0], rgbbase_br[1], rgbbase_br[2]);
const char* egc = NULL;
if(ffmpeg_trans_p(rgbbase_tl[3])){
// top left is transparent
if(ffmpeg_trans_p(rgbbase_tr[3])){
// all of top is transparent
if(ffmpeg_trans_p(rgbbase_bl[3])){
// top and left are transparent
if(ffmpeg_trans_p(rgbbase_br[3])){
// entirety is transparent, load with nul (but not NULL)
cell_set_fg_default(c);
egc = "";
}else{
cell_set_fg_rgb8(c, rgbbase_br[0], rgbbase_br[1], rgbbase_br[2]);
egc = "";
}
}else{
if(ffmpeg_trans_p(rgbbase_br[3])){
cell_set_fg_rgb8(c, rgbbase_bl[0], rgbbase_bl[1], rgbbase_bl[2]);
egc = "";
}else{
cell_set_fchannel(c, lerp(bl, br));
egc = "";
}
}
}else{ // top right is foreground, top left is transparent
if(ffmpeg_trans_p(rgbbase_bl[3])){
if(ffmpeg_trans_p(rgbbase_br[3])){ // entire bottom is transparent
cell_set_fg_rgb8(c, rgbbase_tr[0], rgbbase_tr[1], rgbbase_tr[2]);
egc = "";
}else{
cell_set_fchannel(c, lerp(tr, br));
egc = "";
}
}else if(ffmpeg_trans_p(rgbbase_br[3])){ // only br is transparent
cell_set_fchannel(c, lerp(tr, bl));
egc = "";
}else{
cell_set_fchannel(c, trilerp(tr, bl, br));
egc = "";
}
}
}else{ // topleft is foreground for all here
if(ffmpeg_trans_p(rgbbase_tr[3])){
if(ffmpeg_trans_p(rgbbase_bl[3])){
if(ffmpeg_trans_p(rgbbase_br[3])){
cell_set_fg_rgb8(c, rgbbase_tl[0], rgbbase_tl[1], rgbbase_tl[2]);
egc = "";
}else{
cell_set_fchannel(c, lerp(tl, br));
egc = "";
}
}else if(ffmpeg_trans_p(rgbbase_br[3])){
cell_set_fchannel(c, lerp(tl, bl));
egc = "";
}else{
cell_set_fchannel(c, trilerp(tl, bl, br));
egc = "";
}
}else if(ffmpeg_trans_p(rgbbase_bl[3])){
if(ffmpeg_trans_p(rgbbase_br[3])){ // entire bottom is transparent
cell_set_fchannel(c, lerp(tl, tr));
egc = "";
}else{ // only bl is transparent
cell_set_fchannel(c, trilerp(tl, tr, br));
egc = "";
}
}else if(ffmpeg_trans_p(rgbbase_br[3])){ // only br is transparent
cell_set_fchannel(c, trilerp(tl, tr, bl));
egc = "";
}else{
return NULL; // no transparency
}
}
assert(egc);
cell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
if(*egc == '\0'){
cell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
}else if(blendcolors){
cell_set_fg_alpha(c, CELL_ALPHA_BLEND);
}
return egc;
}
// quadrant blitter. maps 2x2 to each cell. since we only have two colors at
// our disposal (foreground and background), we lose some fidelity.
static inline int
quadrant_blit(ncplane* nc, int placey, int placex, int linesize,
const void* data, int begy, int begx,
int leny, int lenx, bool blendcolors){
const int bpp = 32;
int dimy, dimx, x, y;
int total = 0; // number of cells written
ncplane_dim_yx(nc, &dimy, &dimx);
//fprintf(stderr, "quadblitter %dx%d -> %d/%d+%d/%d\n", leny, lenx, dimy, dimx, placey, placex);
// FIXME not going to necessarily be safe on all architectures hrmmm
const unsigned char* dat = data;
int visy = begy;
for(y = placey ; visy < (begy + leny) && y < dimy ; ++y, visy += 2){
if(ncplane_cursor_move_yx(nc, y, placex)){
return -1;
}
int visx = begx;
for(x = placex ; visx < (begx + lenx) && x < dimx ; ++x, visx += 2){
const unsigned char* rgbbase_tl = dat + (linesize * visy) + (visx * bpp / CHAR_BIT);
const unsigned char* rgbbase_tr = zeroes;
const unsigned char* rgbbase_bl = zeroes;
const unsigned char* rgbbase_br = zeroes;
if(visx < begx + lenx - 1){
rgbbase_tr = dat + (linesize * visy) + ((visx + 1) * bpp / CHAR_BIT);
if(visy < begy + leny - 1){
rgbbase_br = dat + (linesize * (visy + 1)) + ((visx + 1) * bpp / CHAR_BIT);
}
}
if(visy < begy + leny - 1){
rgbbase_bl = dat + (linesize * (visy + 1)) + (visx * bpp / CHAR_BIT);
}
//fprintf(stderr, "[%04d/%04d] bpp: %d lsize: %d %02x %02x %02x %02x\n", y, x, bpp, linesize, rgbbase_tl[0], rgbbase_tr[1], rgbbase_bl[2], rgbbase_br[3]);
nccell* c = ncplane_cell_ref_yx(nc, y, x);
c->channels = 0;
c->stylemask = 0;
const char* egc = qtrans_check(c, blendcolors, rgbbase_tl, rgbbase_tr, rgbbase_bl, rgbbase_br);
if(egc == NULL){
uint32_t tl = 0, tr = 0, bl = 0, br = 0;
channel_set_rgb8(&tl, rgbbase_tl[0], rgbbase_tl[1], rgbbase_tl[2]);
channel_set_rgb8(&tr, rgbbase_tr[0], rgbbase_tr[1], rgbbase_tr[2]);
channel_set_rgb8(&bl, rgbbase_bl[0], rgbbase_bl[1], rgbbase_bl[2]);
channel_set_rgb8(&br, rgbbase_br[0], rgbbase_br[1], rgbbase_br[2]);
uint32_t bg, fg;
//fprintf(stderr, "qtrans check: %d/%d\n%08x %08x\n%08x %08x\n", y, x, *(const uint32_t*)rgbbase_tl, *(const uint32_t*)rgbbase_tr, *(const uint32_t*)rgbbase_bl, *(const uint32_t*)rgbbase_br);
egc = quadrant_solver(tl, tr, bl, br, &fg, &bg);
assert(egc);
//fprintf(stderr, "%d/%d %08x/%08x\n", y, x, fg, bg);
cell_set_fchannel(c, fg);
cell_set_bchannel(c, bg);
if(blendcolors){
cell_set_bg_alpha(c, CELL_ALPHA_BLEND);
cell_set_fg_alpha(c, CELL_ALPHA_BLEND);
}
}
if(*egc){
if(pool_blit_direct(&nc->pool, c, egc, strlen(egc), 1) <= 0){
return -1;
}
++total;
}
}
}
return total;
}
// take a sum over channels, and the sample count, write back lerped channel
static inline uint32_t
generalerp(unsigned rsum, unsigned gsum, unsigned bsum, int count){
if(count == 0){
assert(0 == rsum);
assert(0 == gsum);
assert(0 == bsum);
return 0;
}
return CHANNEL_RGB_INITIALIZER((rsum + (count - 1)) / count,
(gsum + (count - 1)) / count,
(bsum + (count - 1)) / count);
}
// Solve for the cell rendered by this 3x2 sample. None of the input pixels may
// be transparent (that ought already have been handled). We use exhaustive
// search, which might be quite computationally intensive for the worst case
// (all six pixels are different colors). We want to solve for the 2-partition
// of pixels that minimizes total source distance from the resulting lerps.
static const char*
sex_solver(const uint32_t rgbas[6], uint64_t* channels, bool blendcolors){
static const char* sex[64] = {
" ", "🬀", "🬁", "🬃", "🬇", "🬏", "🬞", "🬂",
"🬄", "🬈", "🬐", "🬟", "🬅", "🬉", "🬑", "🬠",
"🬋", "🬓", "🬢", "🬖", "🬦", "🬭", "🬆", "🬊",
"🬒", "🬡", "🬌", "", "🬣", "🬗", "🬧", "🬍",
"", "🬻", "🬺", "🬸", "🬴", "🬬", "🬝", "🬹",
"🬷", "🬳", "🬫", "🬜", "🬶", "🬲", "🬪", "🬛",
"🬰", "🬨", "🬙", "🬥", "🬕", "🬎", "🬵", "🬱",
"🬩", "🬚", "🬯", "", "🬘", "🬤", "🬔", "🬮",
};
// each element within the set of 64 has an inverse element within the set,
// for which we will calculate the same total differences, so just handle the
// first 32, and then assign fg to whichever cluster is larger.
static const unsigned partitions[32] = {
0, // 1 way to arrange 0
1, 2, 4, 8, 16, 32, // 6 ways to arrange 1
3, 5, 9, 17, 33, 6, 10, 18, 34, 12, 20, 36, 24, 40, 48, // 15 ways for 2
// 16 ways to arrange 3, *but* six of them are inverses, so 10
7, 11, 19, 35, 13, 21, 37, 25, 41, 14 // 10 + 15 + 6 + 1 == 32
};
// we loop over the bitstrings, dividing the pixels into two sets, and then
// taking a general lerp over each set. we then compute the sum of absolute
// differences, and see if it's the new minimum.
int best = -1;
uint32_t mindiff = UINT_MAX;
//fprintf(stderr, "%06x %06x\n%06x %06x\n%06x %06x\n", rgbas[0], rgbas[1], rgbas[2], rgbas[3], rgbas[4], rgbas[5]);
for(size_t glyph = 0 ; glyph < sizeof(partitions) / sizeof(*partitions) ; ++glyph){
unsigned rsum0 = 0, rsum1 = 0;
unsigned gsum0 = 0, gsum1 = 0;
unsigned bsum0 = 0, bsum1 = 0;
int insum = 0;
for(unsigned mask = 0 ; mask < 6 ; ++mask){
if(partitions[glyph] & (1u << mask)){
rsum0 += ncpixel_r(rgbas[mask]);
gsum0 += ncpixel_g(rgbas[mask]);
bsum0 += ncpixel_b(rgbas[mask]);
++insum;
}else{
rsum1 += ncpixel_r(rgbas[mask]);
gsum1 += ncpixel_g(rgbas[mask]);
bsum1 += ncpixel_b(rgbas[mask]);
}
}
//fprintf(stderr, "sum0: %u/%u/%u sum1: %u/%u/%u insum: %d\n", rsum0, gsum0, bsum0, rsum1, gsum1, bsum1, insum);
uint32_t l0 = generalerp(rsum0, gsum0, bsum0, insum);
uint32_t l1 = generalerp(rsum1, gsum1, bsum1, 6 - insum);
uint32_t totaldiff = 0;
for(unsigned mask = 0 ; mask < 6 ; ++mask){
unsigned r, g, b;
if(partitions[glyph] & (1u << mask)){
channel_rgb8(l0, &r, &g, &b);
}else{
channel_rgb8(l1, &r, &g, &b);
}
totaldiff += rgb_diff(ncpixel_r(rgbas[mask]), ncpixel_g(rgbas[mask]),
ncpixel_b(rgbas[mask]), r, g, b);
//fprintf(stderr, "mask: %u totaldiff: %u insum: %d (%08x / %08x)\n", mask, totaldiff, insum, l0, l1);
}
//fprintf(stderr, "bits: %u %zu totaldiff: %u best: %u (%d)\n", partitions[glyph], glyph, totaldiff, mindiff, best);
if(totaldiff < mindiff){
mindiff = totaldiff;
best = glyph;
channels_set_fchannel(channels, l0);
channels_set_bchannel(channels, l1);
}
if(totaldiff == 0){ // can't beat that!
break;
}
}
//fprintf(stderr, "solved for best: %d (%u)\n", best, mindiff);
assert(best >= 0 && best < 64);
if(blendcolors){
channels_set_fg_alpha(channels, CELL_ALPHA_BLEND);
channels_set_bg_alpha(channels, CELL_ALPHA_BLEND);
}
return sex[best];
}
static const char*
sex_trans_check(const uint32_t rgbas[6], uint64_t* channels, bool blendcolors){
static const char* sex[64] = {
"", "🬻", "🬺", "🬹", "🬸", "🬷", "🬶", "🬵", "🬴", "🬳", "🬲", // 10
"🬱", "🬰", "🬯", "🬮", "🬭", "🬬", "🬫", "🬪", "🬩", "🬨", "", // 21
"🬧", "🬦", "🬥", "🬤", "🬣", "🬢", "🬡", "🬠", "🬟", // 30
"🬞", "🬝", "🬜", "🬛", "🬚", "🬙", "🬘", "🬗", "🬖", "🬕", // 40
"🬔", "", "🬓", "🬒", "🬑", "🬐", "🬏", "🬎", "🬍", "🬌", // 50
"🬋", "🬊", "🬉", "🬈", "🬇", "🬆", "🬅", "🬄", "🬃", "🬂", // 60
"🬁", "🬀", " ",
};
unsigned transstring = 0;
unsigned r = 0, g = 0, b = 0;
unsigned div = 0;
for(unsigned mask = 0 ; mask < 6 ; ++mask){
if(ffmpeg_trans_p(ncpixel_a(rgbas[mask]))){
transstring |= (1u << mask);
}else{
r += ncpixel_r(rgbas[mask]);
g += ncpixel_g(rgbas[mask]);
b += ncpixel_b(rgbas[mask]);
++div;
}
}
if(transstring == 0){
return NULL;
}
channels_set_bg_alpha(channels, CELL_ALPHA_TRANSPARENT);
// there were some transparent pixels. since they get priority, the foreground
// is just a general lerp across non-transparent pixels.
const char* egc = sex[transstring];
channels_set_bg_alpha(channels, CELL_ALPHA_TRANSPARENT);
//fprintf(stderr, "transtring: %u egc: %s\n", transtring, egc);
if(*egc == ' '){ // entirely transparent
channels_set_fg_alpha(channels, CELL_ALPHA_TRANSPARENT);
return "";
}else{ // partially transparent, thus div >= 1
//fprintf(stderr, "div: %u r: %u g: %u b: %u\n", div, r, g, b);
channels_set_fchannel(channels, generalerp(r, g, b, div));
if(blendcolors){
channels_set_fg_alpha(channels, CELL_ALPHA_BLEND);
}
}
return egc;
}
// sextant blitter. maps 3x2 to each cell. since we only have two colors at
// our disposal (foreground and background), we lose some fidelity.
static inline int
sextant_blit(ncplane* nc, int placey, int placex, int linesize,
const void* data, int begy, int begx,
int leny, int lenx, bool blendcolors){
const int bpp = 32;
int dimy, dimx, x, y;
int total = 0; // number of cells written
ncplane_dim_yx(nc, &dimy, &dimx);
//fprintf(stderr, "sexblitter %dx%d -> %d/%d+%d/%d\n", leny, lenx, dimy, dimx, placey, placex);
const unsigned char* dat = data;
int visy = begy;
for(y = placey ; visy < (begy + leny) && y < dimy ; ++y, visy += 3){
if(ncplane_cursor_move_yx(nc, y, placex)){
return -1;
}
int visx = begx;
for(x = placex ; visx < (begx + lenx) && x < dimx ; ++x, visx += 2){
uint32_t rgbas[6] = { 0, 0, 0, 0, 0, 0 };
memcpy(&rgbas[0], (dat + (linesize * visy) + (visx * bpp / CHAR_BIT)), sizeof(*rgbas));
if(visx < begx + lenx - 1){
memcpy(&rgbas[1], (dat + (linesize * visy) + ((visx + 1) * bpp / CHAR_BIT)), sizeof(*rgbas));
if(visy < begy + leny - 1){
memcpy(&rgbas[3], (dat + (linesize * (visy + 1)) + ((visx + 1) * bpp / CHAR_BIT)), sizeof(*rgbas));
if(visy < begy + leny - 2){
memcpy(&rgbas[5], (dat + (linesize * (visy + 2)) + ((visx + 1) * bpp / CHAR_BIT)), sizeof(*rgbas));
}
}
}
if(visy < begy + leny - 1){
memcpy(&rgbas[2], (dat + (linesize * (visy + 1)) + (visx * bpp / CHAR_BIT)), sizeof(*rgbas));
if(visy < begy + leny - 2){
memcpy(&rgbas[4], (dat + (linesize * (visy + 2)) + (visx * bpp / CHAR_BIT)), sizeof(*rgbas));
}
}
nccell* c = ncplane_cell_ref_yx(nc, y, x);
c->channels = 0;
c->stylemask = 0;
const char* egc = sex_trans_check(rgbas, &c->channels, blendcolors);
if(egc == NULL){
egc = sex_solver(rgbas, &c->channels, blendcolors);
}
//fprintf(stderr, "sex EGC: %s channels: %016lx\n", egc, c->channels);
if(*egc){
if(pool_blit_direct(&nc->pool, c, egc, strlen(egc), 1) <= 0){
return -1;
}
++total;
}
}
}
return total;
}
// fold the r, g, and b components of the pixel into *r, *g, and *b, and
// increment *foldcount
static inline void
fold_rgb8(unsigned* restrict r, unsigned* restrict g, unsigned* restrict b,
const uint32_t* pixel, unsigned* foldcount){
*r += ncpixel_r(*pixel);
*g += ncpixel_g(*pixel);
*b += ncpixel_b(*pixel);
++*foldcount;
}
// Braille blitter. maps 4x2 to each cell. since we only have one color at
// our disposal (foreground), we lose some fidelity. this is optimal for
// visuals with only two colors in a given area, as it packs lots of
// resolution. always transparent background.
static inline int
braille_blit(ncplane* nc, int placey, int placex, int linesize,
const void* data, int begy, int begx,
int leny, int lenx, bool blendcolors){
const int bpp = 32;
int dimy, dimx, x, y;
int total = 0; // number of cells written
ncplane_dim_yx(nc, &dimy, &dimx);
// FIXME not going to necessarily be safe on all architectures hrmmm
const unsigned char* dat = data;
int visy = begy;
for(y = placey ; visy < (begy + leny) && y < dimy ; ++y, visy += 4){
if(ncplane_cursor_move_yx(nc, y, placex)){
return -1;
}
int visx = begx;
for(x = placex ; visx < (begx + lenx) && x < dimx ; ++x, visx += 2){
const uint32_t* rgbbase_l0 = (const uint32_t*)(dat + (linesize * visy) + (visx * bpp / CHAR_BIT));
const uint32_t* rgbbase_r0 = &zeroes32;
const uint32_t* rgbbase_l1 = &zeroes32;
const uint32_t* rgbbase_r1 = &zeroes32;
const uint32_t* rgbbase_l2 = &zeroes32;
const uint32_t* rgbbase_r2 = &zeroes32;
const uint32_t* rgbbase_l3 = &zeroes32;
const uint32_t* rgbbase_r3 = &zeroes32;
unsigned r = 0, g = 0, b = 0;
unsigned blends = 0;
unsigned egcidx = 0;
if(visx < begx + lenx - 1){
rgbbase_r0 = (const uint32_t*)(dat + (linesize * visy) + ((visx + 1) * bpp / CHAR_BIT));
if(visy < begy + leny - 1){
rgbbase_r1 = (const uint32_t*)(dat + (linesize * (visy + 1)) + ((visx + 1) * bpp / CHAR_BIT));
if(visy < begy + leny - 2){
rgbbase_r2 = (const uint32_t*)(dat + (linesize * (visy + 2)) + ((visx + 1) * bpp / CHAR_BIT));
if(visy < begy + leny - 3){
rgbbase_r3 = (const uint32_t*)(dat + (linesize * (visy + 3)) + ((visx + 1) * bpp / CHAR_BIT));
}
}
}
}
if(visy < begy + leny - 1){
rgbbase_l1 = (const uint32_t*)(dat + (linesize * (visy + 1)) + (visx * bpp / CHAR_BIT));
if(visy < begy + leny - 2){
rgbbase_l2 = (const uint32_t*)(dat + (linesize * (visy + 2)) + (visx * bpp / CHAR_BIT));
if(visy < begy + leny - 3){
rgbbase_l3 = (const uint32_t*)(dat + (linesize * (visy + 3)) + (visx * bpp / CHAR_BIT));
}
}
}
// FIXME fold this into the above?
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_l0))){
egcidx |= 1u;
fold_rgb8(&r, &g, &b, rgbbase_l0, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_l1))){
egcidx |= 2u;
fold_rgb8(&r, &g, &b, rgbbase_l1, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_l2))){
egcidx |= 4u;
fold_rgb8(&r, &g, &b, rgbbase_l2, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_r0))){
egcidx |= 8u;
fold_rgb8(&r, &g, &b, rgbbase_r0, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_r1))){
egcidx |= 16u;
fold_rgb8(&r, &g, &b, rgbbase_r1, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_r2))){
egcidx |= 32u;
fold_rgb8(&r, &g, &b, rgbbase_r2, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_l3))){
egcidx |= 64u;
fold_rgb8(&r, &g, &b, rgbbase_l3, &blends);
}
if(!ffmpeg_trans_p(ncpixel_a(*rgbbase_r3))){
egcidx |= 128u;
fold_rgb8(&r, &g, &b, rgbbase_r3, &blends);
}
//fprintf(stderr, "[%04d/%04d] bpp: %d lsize: %d %02x %02x %02x %02x\n", y, x, bpp, linesize, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2], rgbbase_up[3]);
nccell* c = ncplane_cell_ref_yx(nc, y, x);
// use the default for the background, as that's the only way it's
// effective in that case anyway
c->channels = 0;
c->stylemask = 0;
if(blendcolors){
cell_set_fg_alpha(c, CELL_ALPHA_BLEND);
}
// FIXME for now, we just sample, color-wise, and always draw crap.
// more complicated to do optimally than quadrants, for sure. ideally,
// we only get one color in an area.
cell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
if(!egcidx){
cell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
// FIXME else look for pairs of transparency!
}else{
if(blends){
cell_set_fg_rgb8(c, r / blends, g / blends, b / blends);
}
// UTF-8 encodings of the Braille Patterns are always 0xe2 0xaX 0xCC,
// where 0 <= X <= 3 and 0x80 <= CC <= 0xbf (4 groups of 64).
char egc[4] = { 0xe2, 0xa0, 0x80, 0x00 };
egc[2] += egcidx % 64;
egc[1] += egcidx / 64;
if(pool_blit_direct(&nc->pool, c, egc, strlen(egc), 1) <= 0){
return -1;
}
}
++total;
}
}
return total;
}
// NCBLIT_DEFAULT is not included, as it has no defined properties. It ought
// be replaced with some real blitter implementation by the calling widget.
const struct blitset notcurses_blitters[] = {
{ .geom = NCBLIT_8x1, .width = 1, .height = 8, .egcs = L" ▁▂▃▄▅▆▇█",
.blit = tria_blit, .name = "eightstep", .fill = false, },
{ .geom = NCBLIT_1x1, .width = 1, .height = 1, .egcs = L"",
.blit = tria_blit_ascii,.name = "ascii", .fill = false, },
{ .geom = NCBLIT_2x1, .width = 1, .height = 2, .egcs = L" ▄█",
.blit = tria_blit, .name = "halfblock", .fill = false, },
{ .geom = NCBLIT_2x2, .width = 2, .height = 2, .egcs = L" ▗▐▖▄▟▌▙█",
.blit = quadrant_blit, .name = "quadblitter", .fill = false, },
{ .geom = NCBLIT_3x2, .width = 2, .height = 3, .egcs = L" 🬞🬦▐🬏🬭🬵🬷🬓🬱🬹🬻▌🬲🬺█",
.blit = sextant_blit, .name = "sexblitter", .fill = false, },
{ .geom = NCBLIT_4x1, .width = 1, .height = 4, .egcs = L" ▂▄▆█",
.blit = tria_blit, .name = "fourstep", .fill = false, },
{ .geom = NCBLIT_BRAILLE, .width = 2, .height = 4, .egcs = L"⠀⢀⢠⢰⢸⡀⣀⣠⣰⣸⡄⣄⣤⣴⣼⡆⣆⣦⣶⣾⡇⣇⣧⣷⣿",
.blit = braille_blit, .name = "braille", .fill = true, },
{ .geom = NCBLIT_SIXEL, .width = 1, .height = 6, .egcs = L"",
.blit = NULL, .name = "sixel", .fill = true, }, // FIXME
{ .geom = 0, .width = 0, .height = 0, .egcs = NULL,
.blit = NULL, .name = NULL, .fill = false, },
};
int notcurses_lex_blitter(const char* op, ncblitter_e* blitter){
const struct blitset* bset = notcurses_blitters;
while(bset->name){
if(strcasecmp(bset->name, op) == 0){
*blitter = bset->geom;
return 0;
}
++bset;
}
if(strcasecmp("default", op) == 0){
*blitter = NCBLIT_DEFAULT;
return 0;
}
return -1;
}
const char* notcurses_str_blitter(ncblitter_e blitter){
if(blitter == NCBLIT_DEFAULT){
return "default";
}
const struct blitset* bset = notcurses_blitters;
while(bset->name){
if(bset->geom == blitter){
return bset->name;
}
++bset;
}
return NULL;
}
int ncblit_bgrx(const void* data, int linesize, const struct ncvisual_options* vopts){
if(vopts->leny <= 0 || vopts->lenx <= 0){
return -1;
}
void* rdata = bgra_to_rgba(data, vopts->leny, linesize, vopts->lenx);
if(rdata == NULL){
return -1;
}
int r = ncblit_rgba(rdata, linesize, vopts);
free(rdata);
return r;
}
int ncblit_rgba(const void* data, int linesize, const struct ncvisual_options* vopts){
if(vopts->flags > NCVISUAL_OPTION_BLEND){
fprintf(stderr, "Warning: unknown ncvisual options %016jx\n", (uintmax_t)vopts->flags);
}
if(linesize <= 0 || (size_t)linesize < vopts->lenx * sizeof(uint32_t)){
return -1;
}
struct ncplane* nc = vopts->n;
if(nc == NULL){
return -1;
}
int lenx = vopts->lenx;
int leny = vopts->leny;
int begy = vopts->begy;
int begx = vopts->begx;
//fprintf(stderr, "render %dx%d+%dx%d %p\n", begy, begx, leny, lenx, ncv->data);
if(begy < 0 || begx < 0 || lenx < -1 || leny < -1){
return -1;
}
ncblitter_e blitter;
if(!vopts || vopts->blitter == NCBLIT_DEFAULT){
blitter = ncvisual_media_defblitter(ncplane_notcurses(nc), NCSCALE_NONE);
}else{
blitter = vopts->blitter;
}
const bool degrade = !(vopts->flags & NCVISUAL_OPTION_NODEGRADE);
const struct blitset* bset = lookup_blitset(notcurses_canutf8(ncplane_notcurses(nc)),
blitter, degrade);
if(bset == NULL){
return -1;
}
const bool blend = (vopts->flags & NCVISUAL_OPTION_BLEND);
return bset->blit(nc, vopts->y, vopts->x, linesize, data, begy, begx,
leny, lenx, blend);
}
ncblitter_e ncvisual_media_defblitter(const notcurses* nc, ncscale_e scale){
return rgba_blitter_default(nc->utf8, scale, nc->tcache.sextants);
}