toaruos/apps/color-picker.c

469 lines
12 KiB
C

/**
* @file apps/color-picker.c
* @brief Color picker
*
* Color Picker widget demo, eventually maybe a paint app again...
*
* @copyright
* This file is part of ToaruOS and is released under the terms
* of the NCSA / University of Illinois License - see LICENSE.md
* Copyright (C) 2021 K. Lange
*/
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <wait.h>
#include <sched.h>
#include <signal.h>
#include <toaru/yutani.h>
#include <toaru/graphics.h>
#include <toaru/decorations.h>
#include <toaru/spinlock.h>
#include <toaru/menu.h>
#include <toaru/text.h>
#define dist(a,b,c,d) sqrt((double)(((a) - (c)) * ((a) - (c)) + ((b) - (d)) * ((b) - (d))))
static yutani_t * yctx;
static yutani_window_t * wina;
static int should_exit = 0;
uint16_t win_width;
uint16_t win_height;
uint16_t off_x;
uint16_t off_y;
static int needs_redraw = 0;
gfx_context_t * ctx;
static struct TT_Font * tt_font_thin = NULL;
void redraw_borders() {
render_decorations(wina, ctx, "Color Picker");
}
double fmin(double a, double b) {
return a < b ? a : b;
}
double fmax(double a, double b) {
return a > b ? a : b;
}
uint32_t hsv_to_rgb(float h, float s, float v) {
float c = v * s;
float hp = fmod(h, 2 * M_PI);
float x = c * (1.0 - fabs(fmod(hp / 1.0472, 2) - 1.0));
float m = v - c;
float rp, gp, bp;
if (hp <= 1.0472) { rp = c; gp = x; bp = 0; }
else if (hp <= 2.0944) { rp = x; gp = c; bp = 0; }
else if (hp <= 3.1416) { rp = 0; gp = c; bp = x; }
else if (hp <= 4.1888) { rp = 0; gp = x; bp = c; }
else if (hp <= 5.2360) { rp = x; gp = 0; bp = c; }
else { rp = c; gp = 0; bp = x; }
return rgb((rp + m) * 255, (gp + m) * 255, (bp + m) * 255);
}
void rgb_to_hsv(uint32_t c, double *h, double *s, double *v) {
float r = _RED(c) / 255.0;
float g = _GRE(c) / 255.0;
float b = _BLU(c) / 255.0;
float c_max = fmax(r,fmax(g,b));
float c_min = fmin(r,fmin(g,b));
float delta = c_max - c_min;
if (!delta) {
*h = 0;
} else if (c_max == r) {
*h = 1.0471975512 * fmod((g - b) / delta, 6.0);
} else if (c_max == g) {
*h = 1.0471975512 * ((b - r) / delta + 2.0);
} else {
*h = 1.0471975512 * ((r - g) / delta + 4.0);
}
if (c_max == 0) {
*s = 0;
} else {
*s = delta / c_max;
}
*v = c_max;
}
struct Picker {
int x;
int y;
int radius;
struct gfx_point red;
struct gfx_point white;
struct gfx_point black;
double dp;
double hue;
};
static double pt_sign(const struct gfx_point *p1, const struct gfx_point *p2, const struct gfx_point *p3) {
return (p1->x - p3->x) * (p2->y - p3->y) - (p2->x - p3->x) * (p1->y - p3->y);
}
static int in_triangle(const struct gfx_point * pt, const struct gfx_point * v1, const struct gfx_point * v2, const struct gfx_point * v3, double *proximity) {
double d1 = pt_sign(pt,v1,v2);
double d2 = pt_sign(pt,v2,v3);
double d3 = pt_sign(pt,v3,v1);
int neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
int pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
*proximity = 1.0;
*proximity = fmin(*proximity, gfx_line_distance(pt, v1, v2));
*proximity = fmin(*proximity, gfx_line_distance(pt, v2, v3));
*proximity = fmin(*proximity, gfx_line_distance(pt, v3, v1));
return !(neg && pos);
}
static uint32_t gfx_fill_magic(int32_t x, int32_t y, double alpha, void * extra) {
if (alpha > 1.0) alpha = 1.0;
if (alpha < 0.0) alpha = 0.0;
struct Picker * picker = extra;
/* Picker center */
int _x = picker->x + picker->radius;
int _y = picker->y + picker->radius;
/* hole in the middle */
double r = dist(x,y,_x,_y);
int inner_radius = picker->radius * 0.8;
uint32_t c;
if (r < inner_radius) {
struct gfx_point p = {(float)x,(float)y};
/* Are we in the triangle? */
double proximity;
if (!in_triangle(&p,&picker->red,&picker->white,&picker->black,&proximity)) {
return rgba(0,0,0,0);
}
alpha = proximity;
double h = picker->hue;
double v = 1.0 - (gfx_line_distance(&p, &picker->red, &picker->white) / picker->dp);
double _l = gfx_line_distance(&p, &picker->black, &picker->white);
double _h = gfx_line_distance(&p, &picker->black, &picker->red);
double s = _l + _h > 0.0 ? (_l / (_l+_h)) : 1.0;
c = hsv_to_rgb(h,s,v);
} else {
double angle = atan2(y-_y,_x-x) + M_PI;
if (r < inner_radius + 1) {
alpha *= r - inner_radius;
}
c = hsv_to_rgb(angle, 1.0, 1.0);
}
return premultiply(rgba(_RED(c),_GRE(c),_BLU(c),(int)(255 * alpha)));
}
static void fill_picker(struct Picker *picker) {
/* Triangle stuff */
picker->red.x = 0.8 * picker->radius * cos(-picker->hue) + picker->x + picker->radius;
picker->red.y = 0.8 * picker->radius * sin(-picker->hue) + picker->y + picker->radius;
picker->white.x = 0.8 * picker->radius * cos(-picker->hue + 2.09439510239) + picker->x + picker->radius;
picker->white.y = 0.8 * picker->radius * sin(-picker->hue + 2.09439510239) + picker->y + picker->radius;
picker->black.x = 0.8 * picker->radius * cos(-picker->hue + 4.18879020479) + picker->x + picker->radius;
picker->black.y = 0.8 * picker->radius * sin(-picker->hue + 4.18879020479) + picker->y + picker->radius;
/* Midpoint */
struct gfx_point midpoint = {(picker->white.x + picker->black.x) / 2.0, (picker->white.y + picker->black.y) / 2.0};
picker->dp = gfx_point_distance(&picker->red, &midpoint);
}
static double _hue = 0;
static double _sat = 1.0;
static double _val = 1.0;
static uint32_t my_color = 0xFFFF0000;
static void draw_ring(gfx_context_t * ctx, double x, double y, double radius, double thickness, uint32_t c) {
struct gfx_point p = {x,y};
for (int _y = y - radius - thickness; _y <= y + radius + thickness; ++_y) {
if (_y < 0) continue;
if (_y >= ctx->height) break;
for (int _x = x - radius - thickness; _x <= x + radius + thickness; ++_x) {
if (_x < 0) continue;
if (_x >= ctx->width) break;
struct gfx_point pt = {_x,_y};
double dist = gfx_point_distance(&p,&pt);
if (dist > radius - thickness && dist < radius + thickness) {
double alpha = fmin(1.0,thickness - fabs(radius - dist));
GFX(ctx,_x,_y) = alpha_blend_rgba(GFX(ctx,_x,_y), premultiply(rgba(_RED(c),_GRE(c),_BLU(c),_ALP(c)*alpha)));
}
}
}
}
static void redraw_everything(void) {
draw_fill(ctx, rgb(200,200,200));
struct Picker picker = {off_x, off_y, win_width / 2, {0,0}, {0,0}, {0,0}, 0, _hue};
fill_picker(&picker);
draw_rounded_rectangle_pattern(ctx,picker.x,picker.y,picker.radius*2,picker.radius*2,picker.radius,gfx_fill_magic, &picker);
/* Now figure out where the s + v goes */
double x = picker.white.x * (1.0 - _sat) + picker.red.x * (_sat);
double y = picker.white.y * (1.0 - _sat) + picker.red.y * (_sat);
x = x * (_val) + picker.black.x * (1.0 - _val);
y = y * (_val) + picker.black.y * (1.0 - _val);
draw_ring(ctx, x, y, 5, 1.5, _val < 0.5 ? rgb(255,255,255) : rgb(0,0,0));
draw_rounded_rectangle(ctx,off_x + 5, off_y + picker.radius * 2 + 5, 15, 15, 5, my_color);
char colorName[11];
sprintf(colorName,"#%02x%02x%02x", _RED(my_color), _GRE(my_color), _BLU(my_color));
tt_set_size(tt_font_thin, 13);
tt_draw_string(ctx, tt_font_thin, off_x + 25, off_y + picker.radius * 2 + 18, colorName, rgb(0,0,0));
redraw_borders();
flip(ctx);
yutani_flip(yctx, wina);
}
static double clamp_to_line(struct gfx_point *p, const struct gfx_point *v, const struct gfx_point *w, struct gfx_point *v_t) {
float lengthlength = gfx_point_distance_squared(v,w);
struct gfx_point p_v = gfx_point_sub(p,v);
struct gfx_point w_v = gfx_point_sub(w,v);
float tmp = gfx_point_dot(&p_v,&w_v) / lengthlength;
tmp = fmin(1.0,tmp);
float t = fmax(0.0, tmp);
w_v.x *= t;
w_v.y *= t;
*v_t= gfx_point_add(v, &w_v);
return gfx_point_distance(p, v_t);
}
static int inside_circle = -1;
static void handle_mouse(struct yutani_msg_window_mouse_event * me) {
if (me->command != YUTANI_MOUSE_EVENT_DOWN &&
me->command != YUTANI_MOUSE_EVENT_DRAG) return;
/* Can we figure out a hue? */
int32_t x = me->new_x;
int32_t y = me->new_y;
struct Picker _picker = {off_x, off_y, win_width / 2, {0,0}, {0,0}, {0,0}, 0, _hue};
struct Picker * picker = &_picker;
fill_picker(&_picker);
/* Picker center */
int _x = picker->x + picker->radius;
int _y = picker->y + picker->radius;
/* hole in the middle */
double r = dist(x,y,_x,_y);
int inner_radius = picker->radius * 0.8;
struct gfx_point p = {(float)x,(float)y};
if (me->command == YUTANI_MOUSE_EVENT_DOWN) {
if (r > picker->radius) {
inside_circle = -1;
return;
}
inside_circle = r < inner_radius;
if (inside_circle) {
double proximity;
if (!in_triangle(&p,&picker->red,&picker->white,&picker->black,&proximity)) {
inside_circle = -1;
return;
}
}
}
if (inside_circle == 1) {
double proximity;
if (!in_triangle(&p,&picker->red,&picker->white,&picker->black,&proximity)) {
struct gfx_point a;
struct gfx_point b;
struct gfx_point c;
double a_d = clamp_to_line(&p,&picker->red,&picker->white,&a);
double b_d = clamp_to_line(&p,&picker->black,&picker->white,&b);
double c_d = clamp_to_line(&p,&picker->black,&picker->red,&c);
if (a_d <= b_d && a_d <= c_d) {
p = a;
} else if (b_d <= a_d && b_d <= c_d) {
p = b;
} else if (c_d <= a_d && c_d <= b_d) {
p = c;
}
}
double v = 1.0 - (gfx_line_distance(&p, &picker->red, &picker->white) / picker->dp);
double _l = gfx_line_distance(&p, &picker->black, &picker->white);
double _h = gfx_line_distance(&p, &picker->black, &picker->red);
double s = _l + _h > 0.0 ? (_l / (_l+_h)) : 1.0;
_sat = s;
_val = v;
} else if (inside_circle == 0) {
_hue = atan2(y-_y,_x-x) + M_PI;
} else {
return;
}
my_color = hsv_to_rgb(_hue,_sat,_val);
needs_redraw = 1;
}
void resize_finish(int w, int h) {
yutani_window_resize_accept(yctx, wina, w, h);
reinit_graphics_yutani(ctx, wina);
struct decor_bounds bounds;
decor_get_bounds(wina, &bounds);
win_width = w - bounds.width;
win_height = h - bounds.height;
off_x = bounds.left_width;
off_y = bounds.top_height;
redraw_everything();
yutani_window_resize_done(yctx, wina);
}
static uint32_t parseColor(const char * c) {
if (*c != '#' || strlen(c) != 7) return rgba(0,0,0,255);
char r[3] = {c[1],c[2],'\0'};
char g[3] = {c[3],c[4],'\0'};
char b[3] = {c[5],c[6],'\0'};
return rgba(strtoul(r,NULL,16),strtoul(g,NULL,16),strtoul(b,NULL,16),255);
}
int main (int argc, char ** argv) {
yctx = yutani_init();
if (!yctx) {
fprintf(stderr, "%s: failed to connect to compositor\n", argv[0]);
return 1;
}
if (argc > 1) {
my_color = parseColor(argv[1]);
rgb_to_hsv(my_color, &_hue, &_sat, &_val);
}
win_width = 160;
win_height = 200;
tt_font_thin = tt_font_from_shm("sans-serif");
init_decorations();
struct decor_bounds bounds;
decor_get_bounds(NULL, &bounds);
/* Do something with a window */
wina = yutani_window_create(yctx, win_width + bounds.width, win_height + bounds.height);
yutani_window_move(yctx, wina, 300, 300);
decor_get_bounds(wina, &bounds);
off_x = bounds.left_width;
off_y = bounds.top_height;
win_width = wina->width - bounds.width;
win_height = wina->height - bounds.height;
ctx = init_graphics_yutani_double_buffer(wina);
redraw_everything();
yutani_window_advertise_icon(yctx, wina, "Color Picker", "art");
while (!should_exit) {
yutani_msg_t * m = yutani_poll(yctx);
while (m) {
menu_process_event(yctx, m);
switch (m->type) {
case YUTANI_MSG_KEY_EVENT:
{
struct yutani_msg_key_event * ke = (void*)m->data;
if (ke->event.action == KEY_ACTION_DOWN && ke->event.keycode == 'q') {
should_exit = 1;
}
}
break;
case YUTANI_MSG_WINDOW_FOCUS_CHANGE:
{
struct yutani_msg_window_focus_change * wf = (void*)m->data;
yutani_window_t * win = hashmap_get(yctx->windows, (void*)(uintptr_t)wf->wid);
if (win && win == wina) {
win->focused = wf->focused;
needs_redraw = 1;
}
}
break;
case YUTANI_MSG_WINDOW_CLOSE:
case YUTANI_MSG_SESSION_END:
should_exit = 1;
break;
case YUTANI_MSG_RESIZE_OFFER:
{
struct yutani_msg_window_resize * wr = (void*)m->data;
if (wr->wid == wina->wid) {
resize_finish(wr->width, wr->height);
}
}
break;
case YUTANI_MSG_WINDOW_MOUSE_EVENT:
{
struct yutani_msg_window_mouse_event * me = (void*)m->data;
switch (decor_handle_event(yctx, m)) {
case DECOR_CLOSE:
should_exit = 1;
break;
case DECOR_RIGHT:
decor_show_default_menu(wina, wina->x + me->new_x, wina->y + me->new_y);
break;
}
if (me->wid == wina->wid) {
handle_mouse(me);
}
}
break;
default:
break;
}
free(m);
m = yutani_poll_async(yctx);
}
if (needs_redraw) {
redraw_everything();
needs_redraw = 0;
}
}
wait(NULL);
yutani_close(yctx, wina);
return 0;
}