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.
This commit is contained in:
Titanium Brain 2026-05-14 20:08:35 +01:00
commit 152ee0141c
4 changed files with 115 additions and 0 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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,

View file

@ -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();
},