render: implement coordinate-based anchor positioning

Not to be confused with Minecraft coordinates.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifdb90fc92a1565ba1d30b85c91d6e1ab6a6a6964
This commit is contained in:
raf 2026-04-15 11:54:30 +03:00
commit dadba853e8
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 168 additions and 85 deletions

8
include/chroma.h vendored
View file

@ -104,6 +104,8 @@ typedef struct {
chroma_scale_mode_t scale_mode; chroma_scale_mode_t scale_mode;
chroma_filter_quality_t filter_quality; chroma_filter_quality_t filter_quality;
chroma_anchor_t anchor; chroma_anchor_t anchor;
float anchor_x; // custom X offset (0-100, 50=center)
float anchor_y; // custom Y offset (0-100, 50=center)
bool config_loaded; bool config_loaded;
// OpenGL resource cache // OpenGL resource cache
@ -123,6 +125,8 @@ typedef struct {
chroma_scale_mode_t scale_mode; chroma_scale_mode_t scale_mode;
chroma_filter_quality_t filter_quality; chroma_filter_quality_t filter_quality;
chroma_anchor_t anchor; chroma_anchor_t anchor;
float anchor_x; // custom X offset (0-100, 50=center, 0=left, 100=right)
float anchor_y; // custom Y offset (0-100, 50=center, 0=top, 100=bottom)
} chroma_config_mapping_t; } chroma_config_mapping_t;
// Application configuration // Application configuration
@ -136,6 +140,8 @@ typedef struct {
chroma_scale_mode_t default_scale_mode; chroma_scale_mode_t default_scale_mode;
chroma_filter_quality_t default_filter_quality; chroma_filter_quality_t default_filter_quality;
chroma_anchor_t default_anchor; chroma_anchor_t default_anchor;
float default_anchor_x; // custom anchor X offset (0-100, 50=center)
float default_anchor_y; // custom anchor Y offset (0-100, 50=center)
// Image downsampling settings // Image downsampling settings
bool enable_downsampling; // enable automatic downsampling bool enable_downsampling; // enable automatic downsampling
@ -248,7 +254,7 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
int chroma_config_get_mapping_for_output( int chroma_config_get_mapping_for_output(
chroma_config_t *config, const char *output_name, chroma_config_t *config, const char *output_name,
chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality, chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality,
chroma_anchor_t *anchor); chroma_anchor_t *anchor, float *anchor_x, float *anchor_y);
void chroma_config_print(const chroma_config_t *config); void chroma_config_print(const chroma_config_t *config);

View file

@ -184,7 +184,8 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path, const char *image_path,
chroma_scale_mode_t scale_mode, chroma_scale_mode_t scale_mode,
chroma_filter_quality_t filter_quality, chroma_filter_quality_t filter_quality,
chroma_anchor_t anchor) { chroma_anchor_t anchor, float anchor_x,
float anchor_y) {
if (!config || !output_name || !image_path) { if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
} }
@ -218,13 +219,17 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
mapping->scale_mode = scale_mode; mapping->scale_mode = scale_mode;
mapping->filter_quality = filter_quality; mapping->filter_quality = filter_quality;
mapping->anchor = anchor; mapping->anchor = anchor;
mapping->anchor_x = anchor_x;
mapping->anchor_y = anchor_y;
config->mapping_count++; config->mapping_count++;
chroma_log( chroma_log(
"DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s)", "DEBUG",
"Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s @ %.1f,%.1f)",
output_name, image_path, scale_mode_to_string(scale_mode), output_name, image_path, scale_mode_to_string(scale_mode),
filter_quality_to_string(filter_quality), anchor_to_string(anchor)); filter_quality_to_string(filter_quality), anchor_to_string(anchor),
anchor_x, anchor_y);
chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)", chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
config->mapping_count, output_name, image_path, path_len); config->mapping_count, output_name, image_path, path_len);
return CHROMA_OK; return CHROMA_OK;
@ -244,6 +249,8 @@ static void init_default_config(chroma_config_t *config) {
config->default_scale_mode = CHROMA_SCALE_FILL; config->default_scale_mode = CHROMA_SCALE_FILL;
config->default_filter_quality = CHROMA_FILTER_LINEAR; config->default_filter_quality = CHROMA_FILTER_LINEAR;
config->default_anchor = CHROMA_ANCHOR_CENTER; config->default_anchor = CHROMA_ANCHOR_CENTER;
config->default_anchor_x = 50.0f; // center
config->default_anchor_y = 50.0f; // center
// Set default downsampling settings // Set default downsampling settings
config->enable_downsampling = true; // enable by default, performance etc. config->enable_downsampling = true; // enable by default, performance etc.
@ -351,6 +358,34 @@ static int parse_config_line(chroma_config_t *config, char *line,
config->default_anchor = parse_anchor(value); config->default_anchor = parse_anchor(value);
chroma_log("DEBUG", "Set default anchor: %s", chroma_log("DEBUG", "Set default anchor: %s",
anchor_to_string(config->default_anchor)); anchor_to_string(config->default_anchor));
} else if (strcasecmp(key, "anchor_x") == 0) {
char *endptr = NULL;
float ax = strtof(value, &endptr);
if (endptr == value || *endptr != '\0') {
chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)",
value);
config->default_anchor_x = 50.0f;
} else if (ax < 0.0f || ax > 100.0f) {
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value);
config->default_anchor_x = 50.0f;
} else {
config->default_anchor_x = ax;
chroma_log("DEBUG", "Set default anchor_x: %.1f", ax);
}
} else if (strcasecmp(key, "anchor_y") == 0) {
char *endptr = NULL;
float ay = strtof(value, &endptr);
if (endptr == value || *endptr != '\0') {
chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)",
value);
config->default_anchor_y = 50.0f;
} else if (ay < 0.0f || ay > 100.0f) {
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value);
config->default_anchor_y = 50.0f;
} else {
config->default_anchor_y = ay;
chroma_log("DEBUG", "Set default anchor_y: %.1f", ay);
}
} else if (strcasecmp(key, "max_output_width") == 0) { } else if (strcasecmp(key, "max_output_width") == 0) {
int width = atoi(value); int width = atoi(value);
if (width > 0 && width <= 16384) { // Reasonable limits if (width > 0 && width <= 16384) { // Reasonable limits
@ -418,8 +453,70 @@ static int parse_config_line(chroma_config_t *config, char *line,
filter_quality_to_string(mapping->filter_quality)); filter_quality_to_string(mapping->filter_quality));
} else if (strcasecmp(property, "anchor") == 0) { } else if (strcasecmp(property, "anchor") == 0) {
mapping->anchor = parse_anchor(value); mapping->anchor = parse_anchor(value);
chroma_log("DEBUG", "Set anchor for output %s: %s", output_name, // Set anchor_x/anchor_y based on named anchor
anchor_to_string(mapping->anchor)); switch (mapping->anchor) {
case CHROMA_ANCHOR_TOP:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_BOTTOM:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 100.0f;
break;
case CHROMA_ANCHOR_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 50.0f;
break;
case CHROMA_ANCHOR_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 50.0f;
break;
case CHROMA_ANCHOR_TOP_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_TOP_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_BOTTOM_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 100.0f;
break;
case CHROMA_ANCHOR_BOTTOM_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 100.0f;
break;
default:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 50.0f;
break;
}
chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",
output_name, anchor_to_string(mapping->anchor),
mapping->anchor_x, mapping->anchor_y);
} else if (strcasecmp(property, "anchor_x") == 0) {
float ax = atof(value);
if (ax >= 0.0f && ax <= 100.0f) {
mapping->anchor_x = ax;
chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name,
ax);
} else {
mapping->anchor_x = 50.0f;
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)",
value);
}
} else if (strcasecmp(property, "anchor_y") == 0) {
float ay = atof(value);
if (ay >= 0.0f && ay <= 100.0f) {
mapping->anchor_y = ay;
chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name,
ay);
} else {
mapping->anchor_y = 50.0f;
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",
value);
}
} else { } else {
chroma_log("WARN", "Unknown output property: %s (line %d)", property, chroma_log("WARN", "Unknown output property: %s (line %d)", property,
line_number); line_number);
@ -442,10 +539,10 @@ static int parse_config_line(chroma_config_t *config, char *line,
return CHROMA_OK; return CHROMA_OK;
} }
if (add_output_mapping(config, output_name, path_to_validate, if (add_output_mapping(
config->default_scale_mode, config, output_name, path_to_validate, config->default_scale_mode,
config->default_filter_quality, config->default_filter_quality, config->default_anchor,
config->default_anchor) != CHROMA_OK) { config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) {
chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name, chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
path_to_validate); path_to_validate);
if (expanded_path) { if (expanded_path) {
@ -589,12 +686,13 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
} }
// Get configuration mapping for output, including scale mode, filter // Get configuration mapping for output, including scale mode, filter
// quality, and anchor // quality, anchor, and custom anchor coordinates
int chroma_config_get_mapping_for_output( int chroma_config_get_mapping_for_output(
chroma_config_t *config, const char *output_name, chroma_config_t *config, const char *output_name,
chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality, chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality,
chroma_anchor_t *anchor) { chroma_anchor_t *anchor, float *anchor_x, float *anchor_y) {
if (!config || !output_name || !scale_mode || !filter_quality || !anchor) { if (!config || !output_name || !scale_mode || !filter_quality || !anchor ||
!anchor_x || !anchor_y) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
} }
@ -604,12 +702,14 @@ int chroma_config_get_mapping_for_output(
*scale_mode = config->mappings[i].scale_mode; *scale_mode = config->mappings[i].scale_mode;
*filter_quality = config->mappings[i].filter_quality; *filter_quality = config->mappings[i].filter_quality;
*anchor = config->mappings[i].anchor; *anchor = config->mappings[i].anchor;
*anchor_x = config->mappings[i].anchor_x;
*anchor_y = config->mappings[i].anchor_y;
chroma_log("DEBUG", chroma_log("DEBUG",
"Found specific mapping for output %s: scale=%s, filter=%s, " "Found specific mapping for output %s: scale=%s, filter=%s, "
"anchor=%s", "anchor=%s @ %.1f,%.1f",
output_name, scale_mode_to_string(*scale_mode), output_name, scale_mode_to_string(*scale_mode),
filter_quality_to_string(*filter_quality), filter_quality_to_string(*filter_quality),
anchor_to_string(*anchor)); anchor_to_string(*anchor), *anchor_x, *anchor_y);
return CHROMA_OK; return CHROMA_OK;
} }
} }
@ -618,10 +718,14 @@ int chroma_config_get_mapping_for_output(
*scale_mode = config->default_scale_mode; *scale_mode = config->default_scale_mode;
*filter_quality = config->default_filter_quality; *filter_quality = config->default_filter_quality;
*anchor = config->default_anchor; *anchor = config->default_anchor;
chroma_log( *anchor_x = config->default_anchor_x;
"DEBUG", "Using defaults for output %s: scale=%s, filter=%s, anchor=%s", *anchor_y = config->default_anchor_y;
chroma_log("DEBUG",
"Using defaults for output %s: scale=%s, filter=%s, anchor=%s @ "
"%.1f,%.1f",
output_name, scale_mode_to_string(*scale_mode), output_name, scale_mode_to_string(*scale_mode),
filter_quality_to_string(*filter_quality), anchor_to_string(*anchor)); filter_quality_to_string(*filter_quality),
anchor_to_string(*anchor), *anchor_x, *anchor_y);
return CHROMA_OK; return CHROMA_OK;
} }

View file

@ -113,20 +113,29 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
chroma_scale_mode_t old_scale_mode = output->scale_mode; chroma_scale_mode_t old_scale_mode = output->scale_mode;
chroma_filter_quality_t old_filter_quality = output->filter_quality; chroma_filter_quality_t old_filter_quality = output->filter_quality;
chroma_anchor_t old_anchor = output->anchor; chroma_anchor_t old_anchor = output->anchor;
float old_anchor_x = output->anchor_x;
float old_anchor_y = output->anchor_y;
bool had_config = output->config_loaded; bool had_config = output->config_loaded;
// Load configuration for this output (scale mode, filter quality, anchor) // Load configuration for this output (scale mode, filter quality, anchor,
// anchor coords)
if (chroma_config_get_mapping_for_output( if (chroma_config_get_mapping_for_output(
&state->config, output->name ? output->name : "unknown", &state->config, output->name ? output->name : "unknown",
&output->scale_mode, &output->filter_quality, &output->anchor) == CHROMA_OK) { &output->scale_mode, &output->filter_quality, &output->anchor,
&output->anchor_x, &output->anchor_y) == CHROMA_OK) {
output->config_loaded = true; output->config_loaded = true;
chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d, anchor=%d", chroma_log("DEBUG",
output->id, output->scale_mode, output->filter_quality, output->anchor); "Loaded config for output %u: scale=%d, filter=%d, anchor=%d @ "
"%.1f,%.1f",
output->id, output->scale_mode, output->filter_quality,
output->anchor, output->anchor_x, output->anchor_y);
// Check if configuration changed and invalidate texture if needed // Check if configuration changed and invalidate texture if needed
if (had_config && (old_scale_mode != output->scale_mode || if (had_config &&
(old_scale_mode != output->scale_mode ||
old_filter_quality != output->filter_quality || old_filter_quality != output->filter_quality ||
old_anchor != output->anchor)) { old_anchor != output->anchor || old_anchor_x != output->anchor_x ||
old_anchor_y != output->anchor_y)) {
chroma_output_invalidate_texture(output); chroma_output_invalidate_texture(output);
output->vbo_dirty = true; // VBO needs update for new scale mode output->vbo_dirty = true; // VBO needs update for new scale mode
chroma_log("DEBUG", chroma_log("DEBUG",
@ -380,14 +389,16 @@ void chroma_log(const char *level, const char *format, ...) {
gettimeofday(&tv, NULL); gettimeofday(&tv, NULL);
tm_info = localtime(&tv.tv_sec); tm_info = localtime(&tv.tv_sec);
truncation_check = snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d.%03d", truncation_check =
tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday, snprintf(timestamp, sizeof(timestamp),
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec, "%04d-%02d-%02d %02d:%02d:%02d.%03d", tm_info->tm_year + 1900,
(int)(tv.tv_usec / 1000)); tm_info->tm_mon + 1, tm_info->tm_mday, tm_info->tm_hour,
tm_info->tm_min, tm_info->tm_sec, (int)(tv.tv_usec / 1000));
if (truncation_check > 32 || truncation_check < 0) { if (truncation_check > 32 || truncation_check < 0) {
// Something went seriously wrong with the snprintf, this is a fairly serious error as // Something went seriously wrong with the snprintf, this is a fairly
// the timestamp may be incomplete or corrupted, so print a warning // serious error as the timestamp may be incomplete or corrupted, so print a
// warning
printf("Following timestamp may be incomplete, truncated or corrupted!\n"); printf("Following timestamp may be incomplete, truncated or corrupted!\n");
} }
printf("[%s] %s: ", timestamp, level); printf("[%s] %s: ", timestamp, level);

View file

@ -43,7 +43,7 @@ static void get_gl_filter_params(chroma_filter_quality_t quality,
// Calculate texture coordinates based on scaling mode and anchor position // Calculate texture coordinates based on scaling mode and anchor position
static void calculate_texture_coords(chroma_scale_mode_t scale_mode, static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
chroma_anchor_t anchor, float anchor_x, float anchor_y,
int image_width, int image_height, int image_width, int image_height,
int output_width, int output_height, int output_width, int output_height,
float tex_coords[8]) { float tex_coords[8]) {
@ -143,53 +143,13 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
break; break;
} }
// Apply anchor-based offset by shifting the visible crop region // Apply anchor-based offset using anchor_x and anchor_y (0-100, 50=center)
// For fill: shift which part of image is shown // anchor_x: 0=left edge, 50=center, 100=right edge
// For fit/center: shift where the image is positioned // anchor_y: 0=top edge, 50=center, 100=bottom edge
// // u_shift: negative moves view left (shows more right side of image)
// Positive shift reveals left/bottom portion // v_shift: negative moves view up (shows more top of image)
// Negative shift reveals right/top portion float u_shift = (anchor_x - 50.0f) / 50.0f; // -1 to 1
float u_shift = 0.0f; float v_shift = (50.0f - anchor_y) / 50.0f; // -1 to 1 (inverted for top-down)
float v_shift = 0.0f;
switch (anchor) {
case CHROMA_ANCHOR_CENTER:
u_shift = 0.0f;
v_shift = 0.0f;
break;
case CHROMA_ANCHOR_TOP:
u_shift = 0.0f;
v_shift = -0.5f; // shift up to show top of image
break;
case CHROMA_ANCHOR_BOTTOM:
u_shift = 0.0f;
v_shift = 0.5f; // shift down to show bottom of image
break;
case CHROMA_ANCHOR_LEFT:
u_shift = -0.5f; // shift left to show left of image
v_shift = 0.0f;
break;
case CHROMA_ANCHOR_RIGHT:
u_shift = 0.5f; // shift right to show right of image
v_shift = 0.0f;
break;
case CHROMA_ANCHOR_TOP_LEFT:
u_shift = -0.5f;
v_shift = -0.5f;
break;
case CHROMA_ANCHOR_TOP_RIGHT:
u_shift = 0.5f;
v_shift = -0.5f;
break;
case CHROMA_ANCHOR_BOTTOM_LEFT:
u_shift = -0.5f;
v_shift = 0.5f;
break;
case CHROMA_ANCHOR_BOTTOM_RIGHT:
u_shift = 0.5f;
v_shift = 0.5f;
break;
}
// Calculate the shift amount based on crop/border space available // Calculate the shift amount based on crop/border space available
float u_crop = 1.0f - (u2 - u1); float u_crop = 1.0f - (u2 - u1);
@ -755,11 +715,13 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
// Update VBO only if needed. E.g, image changed, scale mode changed, or first // Update VBO only if needed. E.g, image changed, scale mode changed, or first
// render // render
if (output->vbo_dirty) { if (output->vbo_dirty) {
// Calculate texture coordinates based on scaling mode and anchor // Calculate texture coordinates based on scaling mode, anchor, and anchor
// coords
float tex_coords[8]; float tex_coords[8];
calculate_texture_coords(output->scale_mode, output->anchor, calculate_texture_coords(output->scale_mode, output->anchor_x,
output->image->width, output->image->height, output->anchor_y, output->image->width,
output->width, output->height, tex_coords); output->image->height, output->width,
output->height, tex_coords);
// Create dynamic vertex data with calculated texture coordinates // Create dynamic vertex data with calculated texture coordinates
float dynamic_vertices[] = { float dynamic_vertices[] = {