brewery
notashelf /
0732ddd427d053e70468db8147f14e4a2eba127d

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit 0732ddd427d0

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-19 16:54 UTC

unverified3 files changed+522-7
transition: add animated gif and crossfade support

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1d9ed746430a78f5b734807428a159136a6a6964
diff --git a/src/animation.c b/src/animation.cnew file mode 100644index 0000000..90e58ba--- /dev/null+++ b/src/animation.c@@ -0,0 +1,363 @@+#include <stdio.h>+#include <stdlib.h>+#include <string.h>++#include "../include/chroma.h"+#include "../include/vendor/stb_image.h"++// GIF block types+#define GIF_EXT_INTRODUCER 0x21+#define GIF_GRAPHIC_CONTROL 0xF9+#define GIF_IMAGE_SEPARATOR 0x2C+#define GIF_TRAILER 0x3B++// Read a little-endian uint16 from a buffer+static uint16_t read_le16(const unsigned char *buf) {+  return (uint16_t)(buf[0] | (buf[1] << 8));+}++// Parse a GIF file to extract frame count and timing+static int gif_parse_header(const unsigned char *data, size_t size, int *width,+                            int *height, int *frame_count, int **frame_delays) {+  if (size < 13)+    return -1;++  // Verify GIF87a or GIF89a signature+  if (memcmp(data, "GIF87a", 6) != 0 && memcmp(data, "GIF89a", 6) != 0) {+    return -1;+  }++  *width = (int)read_le16(data + 6);+  *height = (int)read_le16(data + 8);++  // Skip logical screen descriptor fields+  size_t pos = 10;+  unsigned char packed = data[pos++];+  int has_gct = (packed & 0x80) >> 7;+  int bpp = (packed & 0x07) + 1;+  int gct_size = has_gct ? (3 << bpp) : 0;++  pos += 1; // background color index+  pos += 1; // pixel aspect ratio+  pos += (size_t)gct_size;++  // Count frames and parse frame delays+  int count = 0;+  int *delays = NULL;++  if (frame_count && frame_delays) {+    delays = malloc(1024 * sizeof(int));+    if (!delays)+      return -1;+  }++  while (pos < size) {+    unsigned char block_type = data[pos];++    if (block_type == GIF_TRAILER) {+      break;+    }++    if (block_type == GIF_EXT_INTRODUCER && pos + 1 < size) {+      unsigned char ext_type = data[pos + 1];+      pos += 2;++      if (ext_type == GIF_GRAPHIC_CONTROL && pos + 7 < size) {+        unsigned char block_size = data[pos];+        pos += 1 + (size_t)block_size + 1;++        if (delays && count < 1024) {+          // GCE block: [size:1][flags:1][delay:2][transparent:1][terminator:1]+          // pos has moved past the block, so data[pos - block_size] is the+          // start+          uint16_t delay_cs = read_le16(data + pos - block_size);+          delays[count] = (int)delay_cs * 10;+        }+      } else {+        // Skip other extensions+        while (pos < size && data[pos] != 0x00) {+          unsigned char sub_size = data[pos];+          pos += 1 + sub_size;+        }+        if (pos < size && data[pos] == 0x00) {+          pos++;+        }+      }+    } else if (block_type == GIF_IMAGE_SEPARATOR) {+      count++;+      pos += 10; // skip image descriptor header++      // Skip local color table if present+      unsigned char img_packed = data[pos - 1];+      int has_lct = (img_packed & 0x80) >> 7;+      int img_bpp = (img_packed & 0x07) + 1;+      int lct_size = has_lct ? (3 << img_bpp) : 0;+      pos += (size_t)lct_size;++      // Skip LZW minimum code size+      pos += 1;++      // Skip image data sub-blocks+      while (pos < size && data[pos] != 0x00) {+        unsigned char sub_size = data[pos];+        pos += 1 + sub_size;+      }+      if (pos < size && data[pos] == 0x00) {+        pos++;+      }+    } else {+      pos++;+    }+  }++  if (delays) {+    *frame_count = count;+    *frame_delays = delays;+  }++  return 0;+}++// Load an animated GIF into chroma_image_t+// Uses stb_image to decode individual frames+int chroma_anim_load_gif(chroma_image_t *image, const char *path) {+  if (!image || !path) {+    return CHROMA_ERROR_INIT;+  }++  // Read the entire GIF file into memory+  FILE *fp = fopen(path, "rb");+  if (!fp) {+    chroma_log("ERROR", "Cannot open GIF file: %s", path);+    return CHROMA_ERROR_IMAGE;+  }++  fseek(fp, 0, SEEK_END);+  long file_size = ftell(fp);+  fseek(fp, 0, SEEK_SET);++  if (file_size <= 0 || file_size > 100 * 1024 * 1024) {+    chroma_log("ERROR", "GIF file too large or invalid: %s", path);+    fclose(fp);+    return CHROMA_ERROR_IMAGE;+  }++  unsigned char *gif_data = malloc((size_t)file_size);+  if (!gif_data) {+    fclose(fp);+    return CHROMA_ERROR_MEMORY;+  }++  if (fread(gif_data, 1, (size_t)file_size, fp) != (size_t)file_size) {+    chroma_log("ERROR", "Failed to read GIF file: %s", path);+    free(gif_data);+    fclose(fp);+    return CHROMA_ERROR_IMAGE;+  }+  fclose(fp);++  int gif_width, gif_height, frame_count;+  int *frame_delays = NULL;++  if (gif_parse_header(gif_data, (size_t)file_size, &gif_width, &gif_height,+                       &frame_count, &frame_delays) != 0) {+    chroma_log("ERROR", "Failed to parse GIF header: %s", path);+    free(gif_data);+    return CHROMA_ERROR_IMAGE;+  }++  if (frame_count < 2) {+    // Single-frame GIF - use stb_image to load as regular image+    free(gif_data);+    free(frame_delays);+    return 1; // signal to caller: not animated+  }++  chroma_log("DEBUG", "Animated GIF: %dx%d, %d frames", gif_width, gif_height,+             frame_count);++  // Load frames using stb_image+  int *delays_stbi = NULL;+  int frame_w, frame_h, comp;+  unsigned char *frames_data =+      stbi_load_gif_from_memory(gif_data, (int)file_size, &delays_stbi,+                                &frame_w, &frame_h, &frame_count, &comp, 4);++  free(gif_data);++  if (!frames_data || frame_count < 2) {+    chroma_log("ERROR", "Failed to decode GIF frames: %s", path);+    if (frames_data)+      stbi_image_free(frames_data);+    free(frame_delays);+    if (delays_stbi)+      stbi_image_free(delays_stbi);+    return CHROMA_ERROR_IMAGE;+  }++  image->is_animated = true;+  image->frame_count = frame_count;+  image->current_frame = 0;+  image->width = frame_w;+  image->height = frame_h;+  image->channels = 4;++  image->frames = malloc((size_t)frame_count * sizeof(unsigned char *));+  image->frame_delays_ms = malloc((size_t)frame_count * sizeof(int));+  image->frame_widths = malloc((size_t)frame_count * sizeof(int));+  image->frame_heights = malloc((size_t)frame_count * sizeof(int));++  if (!image->frames || !image->frame_delays_ms || !image->frame_widths ||+      !image->frame_heights) {+    free(image->frames);+    free(image->frame_delays_ms);+    free(image->frame_widths);+    free(image->frame_heights);+    stbi_image_free(frames_data);+    free(frame_delays);+    if (delays_stbi)+      stbi_image_free(delays_stbi);+    return CHROMA_ERROR_MEMORY;+  }++  size_t frame_size = (size_t)frame_w * (size_t)frame_h * 4;++  for (int i = 0; i < frame_count; i++) {+    image->frames[i] = malloc(frame_size);+    if (!image->frames[i]) {+      for (int j = 0; j < i; j++)+        free(image->frames[j]);+      free(image->frames);+      free(image->frame_delays_ms);+      free(image->frame_widths);+      free(image->frame_heights);+      free(frames_data);+      free(frame_delays);+      if (delays_stbi)+        stbi_image_free(delays_stbi);+      return CHROMA_ERROR_MEMORY;+    }+    memcpy(image->frames[i], frames_data + (size_t)i * frame_size, frame_size);+    image->frame_delays_ms[i] =+        delays_stbi ? delays_stbi[i] : (frame_delays ? frame_delays[i] : 100);+    image->frame_widths[i] = frame_w;+    image->frame_heights[i] = frame_h;+  }++  stbi_image_free(frames_data);+  free(frame_delays);+  if (delays_stbi)+    stbi_image_free(delays_stbi);++  // Set initial data to first frame+  image->data = image->frames[0];+  image->loaded = true;+  image->ref_count = 1;++  chroma_log("INFO", "Loaded animated GIF: %s (%d frames, %dx%d)", path,+             frame_count, frame_w, frame_h);++  return CHROMA_OK;+}++// Advance to the next frame if enough time has passed+void chroma_anim_advance_frame(chroma_image_t *image,+                               long long current_time_ms) {+  if (!image || !image->is_animated || image->frame_count < 2) {+    return;+  }++  int delay = image->frame_delays_ms[image->current_frame];+  if (delay < 16)+    delay = 100;++  if (image->last_frame_ms == 0) {+    image->last_frame_ms = current_time_ms;+    return;+  }++  long long elapsed = current_time_ms - image->last_frame_ms;+  if (elapsed < (long long)delay) {+    return;+  }++  image->current_frame = (image->current_frame + 1) % image->frame_count;+  image->data = image->frames[image->current_frame];+  image->last_frame_ms = current_time_ms;+}++// Free animated frame data+void chroma_anim_free_frames(chroma_image_t *image) {+  if (!image || !image->is_animated) {+    return;+  }++  for (int i = 0; i < image->frame_count; i++) {+    free(image->frames[i]);+  }+  free(image->frames);+  free(image->frame_delays_ms);+  free(image->frame_widths);+  free(image->frame_heights);++  image->frames = NULL;+  image->frame_delays_ms = NULL;+  image->frame_widths = NULL;+  image->frame_heights = NULL;+  image->frame_count = 0;+  image->is_animated = false;+}++// Check if any output has an animated image that needs frame advance+bool chroma_anim_needs_redraw(chroma_state_t *state) {+  if (!state || !state->config.animation_enabled) {+    return false;+  }++  for (int i = 0; i < state->output_count; i++) {+    chroma_output_t *output = &state->outputs[i];+    if (output->active && output->image && output->image->is_animated) {+      return true;+    }+  }++  return false;+}++// Advance animation frames and re-render+int chroma_anim_update_outputs(chroma_state_t *state) {+  if (!state || !state->config.animation_enabled) {+    return CHROMA_OK;+  }++  long long now = chroma_get_time_ms();+  int updated = 0;++  for (int i = 0; i < state->output_count; i++) {+    chroma_output_t *output = &state->outputs[i];+    if (!output->active || !output->image || !output->image->is_animated) {+      continue;+    }++    if (!output->surface) {+      continue;+    }++    int old_frame = output->image->current_frame;+    chroma_anim_advance_frame(output->image, now);++    if (output->image->current_frame != old_frame) {+      int ret = chroma_render_wallpaper(state, output);+      if (ret == CHROMA_OK) {+        updated++;+      }+    }+  }++  if (updated > 0) {+    chroma_log("DEBUG", "Animation frames advanced: %d outputs", updated);+  }++  state->last_anim_ms = now;+  return CHROMA_OK;+}diff --git a/src/render.c b/src/render.cindex 12eb6a7..1cf6e49 100644--- a/src/render.c+++ b/src/render.c@@ -161,6 +161,7 @@ static inline uint32_t sample_pixel(const chroma_image_t *image, int x, int y) { }  // Draw scaled image into the shared memory buffer+// When a transition is active, blends between prev buffer and new image static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) {   if (!output || !image || !output->shm_data) {     return;@@ -172,6 +173,17 @@ static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) {   int img_h = image->height;   uint32_t *dst = (uint32_t *)output->shm_data; +  // Save current buffer as previous frame for transition+  bool has_prev = false;+  unsigned char *prev_copy = NULL;+  if (output->in_transition && output->shm_size > 0) {+    prev_copy = malloc(output->shm_size);+    if (prev_copy) {+      memcpy(prev_copy, output->shm_data, output->shm_size);+      has_prev = true;+    }+  }+   // Pre-fill with black   memset(output->shm_data, 0, output->shm_size); @@ -313,6 +325,45 @@ static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) {     break;   }   }++  // Apply crossfade transition blend+  if (has_prev && prev_copy) {+    float alpha = chroma_transition_get_alpha(output, chroma_get_time_ms());+    uint32_t *prev = (uint32_t *)prev_copy;++    for (int i = 0; i < out_w * out_h; i++) {+      uint32_t new_px = dst[i];+      uint32_t old_px = prev[i];++      unsigned char na = (unsigned char)(new_px >> 24);+      unsigned char nr = (unsigned char)(new_px >> 16);+      unsigned char ng = (unsigned char)(new_px >> 8);+      unsigned char nb = (unsigned char)(new_px);++      unsigned char oa = (unsigned char)(old_px >> 24);+      unsigned char or_ = (unsigned char)(old_px >> 16);+      unsigned char og = (unsigned char)(old_px >> 8);+      unsigned char ob = (unsigned char)(old_px);++      unsigned char fa =+          (unsigned char)(oa ++                          (unsigned char)((float)((int)na - (int)oa) * alpha));+      unsigned char fr =+          (unsigned char)(or_ ++                          (unsigned char)((float)((int)nr - (int)or_) * alpha));+      unsigned char fg =+          (unsigned char)(og ++                          (unsigned char)((float)((int)ng - (int)og) * alpha));+      unsigned char fb =+          (unsigned char)(ob ++                          (unsigned char)((float)((int)nb - (int)ob) * alpha));++      dst[i] = ((uint32_t)fa << 24) | ((uint32_t)fr << 16) |+               ((uint32_t)fg << 8) | (uint32_t)fb;+    }++    free(prev_copy);+  } }  // Create surface for output@@ -435,12 +486,5 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {   // Unmap client-side memory; compositor keeps its own fd reference   chroma_buffer_unmap(output); -  // Release image reference and free CPU data if no longer needed-  if (output->image) {-    chroma_image_t *image = output->image;-    chroma_image_release(image);-    output->image = NULL;-  }-   return CHROMA_OK; }diff --git a/src/transition.c b/src/transition.cnew file mode 100644index 0000000..3514e7d--- /dev/null+++ b/src/transition.c@@ -0,0 +1,108 @@+#include <stdio.h>+#include <stdlib.h>+#include <string.h>++#include "../include/chroma.h"++void chroma_transition_init_output(chroma_output_t *output, int duration_ms) {+  if (!output) {+    return;+  }++  if (duration_ms <= 0) {+    chroma_log("DEBUG", "Transition skipped for output %u: duration_ms=%d",+               output->id, duration_ms);+    return;+  }++  output->transition_duration_ms = duration_ms;+  output->transition_start_ms = chroma_get_time_ms();+  output->in_transition = true;++  chroma_log("DEBUG", "Transition started for output %u: %dms", output->id,+             duration_ms);+}++void chroma_transition_cleanup_output(chroma_output_t *output) {+  if (!output) {+    return;+  }++  if (output->transition_prev) {+    free(output->transition_prev);+    output->transition_prev = NULL;+    output->transition_prev_size = 0;+  }++  output->in_transition = false;+  output->transition_start_ms = 0;+}++bool chroma_transition_is_active(chroma_output_t *output) {+  if (!output || !output->in_transition) {+    return false;+  }++  long long now = chroma_get_time_ms();+  long long elapsed = now - output->transition_start_ms;++  if (elapsed >= output->transition_duration_ms) {+    chroma_transition_cleanup_output(output);+    return false;+  }++  return true;+}++float chroma_transition_get_alpha(chroma_output_t *output,+                                  long long current_time_ms) {+  if (!output || !output->in_transition) {+    return 1.0f;+  }++  long long elapsed = current_time_ms - output->transition_start_ms;+  if (elapsed <= 0)+    return 0.0f;+  if (elapsed >= output->transition_duration_ms)+    return 1.0f;++  float alpha = (float)elapsed / (float)output->transition_duration_ms;+  return alpha;+}++bool chroma_transition_needs_redraw(chroma_state_t *state) {+  if (!state) {+    return false;+  }++  for (int i = 0; i < state->output_count; i++) {+    if (chroma_transition_is_active(&state->outputs[i])) {+      return true;+    }+  }++  return false;+}++int chroma_transition_update_outputs(chroma_state_t *state) {+  if (!state) {+    return CHROMA_ERROR_INIT;+  }++  int updated = 0;+  for (int i = 0; i < state->output_count; i++) {+    chroma_output_t *output = &state->outputs[i];+    if (chroma_transition_is_active(output)) {+      int ret = chroma_render_wallpaper(state, output);+      if (ret == CHROMA_OK) {+        updated++;+      }+    }+  }++  if (updated > 0) {+    chroma_log("DEBUG", "Transition frames rendered: %d outputs", updated);+  }++  return CHROMA_OK;+}