From 152ee0141cbe064308c180bd1dc1e44c463ec09c Mon Sep 17 00:00:00 2001 From: Titanium Brain Date: Thu, 14 May 2026 20:08:35 +0100 Subject: [PATCH] feat(doom): add hue shifting to fire colors Adds the ability to shift the hue of the DOOM fire animation by a configurable amount and rate. --- res/config.ini | 6 +++ src/animations/Doom.zig | 105 ++++++++++++++++++++++++++++++++++++++++ src/config/Config.zig | 2 + src/main.zig | 2 + 4 files changed, 115 insertions(+) diff --git a/res/config.ini b/res/config.ini index a290773..458a77e 100644 --- a/res/config.ini +++ b/res/config.ini @@ -174,6 +174,12 @@ doom_fire_spread = 2 # DOOM animation custom top color (low intensity flames) doom_top_color = 0x009F2707 +# DOOM animation delay before shifting hue in miliseconds +doom_hue_shift_delay = 100 + +# DOOM animation amount of hue shift in degrees [0.0, 360.0] +doom_hue_shift_amount = 5 + # DOOM animation custom middle color (medium intensity flames) doom_middle_color = 0x00C78F17 diff --git a/src/animations/Doom.zig b/src/animations/Doom.zig index 9b0abae..45050ca 100644 --- a/src/animations/Doom.zig +++ b/src/animations/Doom.zig @@ -27,6 +27,9 @@ buffer: []u8, height: u8, spread: u8, fire: [STEPS + 1]Cell, +hue_shift_amount: u16, +hue_shift_delay: usize, +hue_shift_time: TimeOfDay, pub fn init( allocator: Allocator, @@ -39,6 +42,8 @@ pub fn init( animate: *bool, timeout_sec: u12, frame_delay: u16, + hue_shift_amount: u16, + hue_shift_delay: usize, ) !Doom { const buffer = try allocator.alloc(u8, terminal_buffer.width * terminal_buffer.height); initBuffer(buffer, terminal_buffer.width); @@ -72,6 +77,96 @@ pub fn init( .height = @min(HEIGHT_MAX, fire_height), .spread = @min(SPREAD_MAX, fire_spread), .fire = levels, + .hue_shift_amount = hue_shift_amount, + .hue_shift_delay = hue_shift_delay, + .hue_shift_time = try interop.getTimeOfDay(), + }; +} + +/// dupdate the fire character array +fn update_fire(self: *Doom, top_color: u32, middle_color: u32, bottom_color: u32) void { + self.fire = .{ + Cell.init(' ', self.terminal_buffer.bg, self.terminal_buffer.bg), + Cell.init(0x2591, top_color, self.terminal_buffer.bg), + Cell.init(0x2592, top_color, self.terminal_buffer.bg), + Cell.init(0x2593, top_color, self.terminal_buffer.bg), + Cell.init(0x2588, top_color, self.terminal_buffer.bg), + Cell.init(0x2591, middle_color, top_color), + Cell.init(0x2592, middle_color, top_color), + Cell.init(0x2593, middle_color, top_color), + Cell.init(0x2588, middle_color, top_color), + Cell.init(0x2591, bottom_color, middle_color), + Cell.init(0x2592, bottom_color, middle_color), + Cell.init(0x2593, bottom_color, middle_color), + Cell.init(0x2588, bottom_color, middle_color), + }; +} + +/// Hue shift the fire colours +fn shift_hue(self: *Doom) void { + var colors = [_]u32{ self.fire[1].fg, self.fire[5].fg, self.fire[9].fg }; + for (colors, 0..) |color, i| { + const r, const g, const b = .{ + (color & 0x00ff0000) >> 16, + (color & 0x0000ff00) >> 8, + (color & 0x000000ff), + }; + + var hue, const saturation, const lightness = rgb_to_hsl(r, g, b); + + hue = @mod(hue + self.hue_shift_amount + 360, 360.0); + + const red, const green, const blue = hsl_to_rgb(hue, saturation, lightness); + + colors[i] = (red << 16 | green << 8 | blue); + } + self.update_fire(colors[0], colors[1], colors[2]); +} + +fn rgb_to_hsl(red: u32, green: u32, blue: u32) struct { f64, f64, f64 } { + // Normalise to [0, 1] + const r, const g, const b = .{ @as(f64, red) / 255, @as(f64, green) / 255, @as(f64, blue) / 255 }; + + const c_max: f64 = @max(r, g, b); + const c_min: f64 = @min(r, g, b); + const chroma = c_max - c_min; + + const hue = if (chroma == 0) + 0 + else if (c_max == r) + 60.0 * (((g - b) / chroma) + @as(f64, if (g < b) 6 else 0)) + else if (c_max == g) + 60.0 * (((b - r) / chroma) + 2) + else + 60.0 * (((r - g) / chroma) + 4); + + const lightness = (c_max + c_min) / 2; + const saturation = if (chroma == 0) 0 else chroma / (1 - @abs(2 * lightness - 1)); + + return .{ hue, saturation, lightness }; +} +fn hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) struct { u32, u32, u32 } { + const c = (1 - @abs(2 * lightness - 1)) * saturation; + const x = c * (1 - @abs(@mod((hue / 60.0), 2.0) - 1)); + const m = lightness - c / 2; + + const red: f64, const green: f64, const blue: f64 = if (hue < 60) + .{ c, x, 0.0 } + else if (hue < 120) + .{ x, c, 0.0 } + else if (hue < 180) + .{ 0.0, c, x } + else if (hue < 240) + .{ 0.0, x, c } + else if (hue < 300) + .{ x, 0.0, c } + else + .{ c, 0.0, x }; + + return .{ + @trunc((red + m) * 255), + @trunc((green + m) * 255), + @trunc((blue + m) * 255), }; } @@ -156,6 +251,16 @@ fn update(self: *Doom, _: *anyopaque) !void { if (self.timeout_sec > 0 and time.seconds - self.start_time.seconds > self.timeout_sec) { self.animate.* = false; } + + const dt_us = + (time.seconds - self.hue_shift_time.seconds) * 1_000_000 + + (time.microseconds - self.hue_shift_time.microseconds); + if (self.hue_shift_delay > 0 and self.hue_shift_amount > 0) { + if (dt_us > self.hue_shift_delay * 1000) { + self.shift_hue(); + self.hue_shift_time = time; + } + } } fn calculateTimeout(self: *Doom, _: *anyopaque) !?usize { diff --git a/src/config/Config.zig b/src/config/Config.zig index a592faa..bcad47e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -47,6 +47,8 @@ doom_fire_spread: u8 = 2, doom_top_color: u32 = 0x00FF0000, doom_middle_color: u32 = 0x00FFFF00, doom_bottom_color: u32 = 0x00FFFFFF, +doom_hue_shift_delay: usize = 0, +doom_hue_shift_amount: u16 = 0, dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur", dur_offset_alignment: DurOffsetAlignment = .center, dur_x_offset: i32 = 0, diff --git a/src/main.zig b/src/main.zig index 6413935..ecbd95d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1038,6 +1038,8 @@ pub fn main(init: std.process.Init) !void { &state.animate, state.config.animation_timeout_sec, state.config.animation_frame_delay, + state.config.doom_hue_shift_amount, + state.config.doom_hue_shift_delay, ); animation = doom.widget(); },