diff --git a/src/animations/Metaballs.zig b/src/animations/Metaballs.zig new file mode 100644 index 0000000..1633703 --- /dev/null +++ b/src/animations/Metaballs.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Animation = @import("../tui/Animation.zig"); +const Cell = @import("../tui/Cell.zig"); +const TerminalBuffer = @import("../tui/TerminalBuffer.zig"); + +const Metaballs = @This(); + +const math = std.math; +const Vec2 = @Vector(2, f32); + +const num_metaballs = 5; +const min_radius: f32 = 5.0; +const max_radius: f32 = 12.0; +const max_speed: f32 = 0.2; + +const threshold: f32 = 0.7; + +const Metaball = struct { + pos: Vec2, + vel: Vec2, + radius: f32, +}; + +allocator: Allocator, +terminal_buffer: *TerminalBuffer, +balls: [num_metaballs]Metaball, +palette: [5]Cell, + +pub fn init(allocator: Allocator, terminal_buffer: *TerminalBuffer) !Metaballs { + var self = Metaballs{ + .allocator = allocator, + .terminal_buffer = terminal_buffer, + .balls = undefined, + .palette = [_]Cell{ + Cell.init(' ', 0x2c0000, 0x4f0000), + Cell.init(0x2591, 0x8b0000, 0xae0000), + Cell.init(0x2592, 0xff4500, 0xff6347), + Cell.init(0x2593, 0xffa500, 0xffd700), + Cell.init(0x2588, 0xffff00, 0xffffe0), + }, + }; + + self.initBalls(); + return self; +} + +fn initBalls(self: *Metaballs) void { + const width_f = @as(f32, @floatFromInt(self.terminal_buffer.width)); + const height_f = @as(f32, @floatFromInt(self.terminal_buffer.height)); + const rand = self.terminal_buffer.random; + + for (&self.balls) |*ball| { + ball.* = .{ + .pos = .{ + rand.float(f32) * width_f, + rand.float(f32) * height_f, + }, + .vel = .{ + (rand.float(f32) - 0.5) * 2.0 * max_speed, + (rand.float(f32) - 0.5) * 2.0 * max_speed, + }, + .radius = min_radius + (rand.float(f32) * (max_radius - min_radius)), + }; + } +} + +pub fn animation(self: *Metaballs) Animation { + return Animation.init(self, deinit, realloc, draw); +} + +fn deinit(_: *Metaballs) void {} + +fn realloc(self: *Metaballs) anyerror!void { + self.initBalls(); +} + +fn draw(self: *Metaballs) void { + const width = self.terminal_buffer.width; + const height = self.terminal_buffer.height; + const width_f = @as(f32, @floatFromInt(width)); + const height_f = @as(f32, @floatFromInt(height)); + + for (&self.balls) |*ball| { + ball.pos += ball.vel; + + if (ball.pos[0] < 0 or ball.pos[0] > width_f) { + ball.vel[0] *= -1.0; + ball.pos[0] = math.clamp(ball.pos[0], 0, width_f); + } + if (ball.pos[1] < 0 or ball.pos[1] > height_f) { + ball.vel[1] *= -1.0; + ball.pos[1] = math.clamp(ball.pos[1], 0, height_f); + } + } + + for (0..height) |y| { + for (0..width) |x| { + const cell_pos = Vec2{ + @as(f32, @floatFromInt(x)) + 0.5, + @as(f32, @floatFromInt(y)) + 0.5, + }; + + var sum_influence: f32 = 0.0; + for (self.balls) |ball| { + const dist_vec = cell_pos - ball.pos; + const dist_sq = (dist_vec[0] * dist_vec[0]) + (dist_vec[1] * dist_vec[1]); + + if (dist_sq == 0) { + sum_influence += 1000.0; + } else { + sum_influence += (ball.radius * ball.radius) / dist_sq; + } + } + + if (sum_influence > threshold * 1.1) { + self.palette[4].put(x, y); + } else if (sum_influence > threshold * 1.0) { + self.palette[3].put(x, y); + } else if (sum_influence > threshold * 0.9) { + self.palette[2].put(x, y); + } else if (sum_influence > threshold * 0.8) { + self.palette[1].put(x, y); + } else { + self.palette[0].put(x, y); + } + } + } +} diff --git a/src/enums.zig b/src/enums.zig index 82c478d..875de8d 100644 --- a/src/enums.zig +++ b/src/enums.zig @@ -4,6 +4,7 @@ pub const Animation = enum { matrix, colormix, gameoflife, + metaballs }; pub const DisplayServer = enum { diff --git a/src/main.zig b/src/main.zig index 2abce0b..3ee921c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,6 +11,7 @@ const interop = @import("interop.zig"); const ColorMix = @import("animations/ColorMix.zig"); const Doom = @import("animations/Doom.zig"); const Dummy = @import("animations/Dummy.zig"); +const Metaballs = @import("animations/Metaballs.zig"); const Matrix = @import("animations/Matrix.zig"); const GameOfLife = @import("animations/GameOfLife.zig"); const Animation = @import("tui/Animation.zig"); @@ -456,6 +457,10 @@ pub fn main() !void { var game_of_life = try GameOfLife.init(allocator, &buffer, config.gameoflife_fg, config.gameoflife_entropy_interval, config.gameoflife_frame_delay, config.gameoflife_initial_density); animation = game_of_life.animation(); }, + .metaballs => { + var metaballs = try Metaballs.init(allocator, &buffer); + animation = metaballs.animation(); + }, } defer animation.deinit();