mirror of
https://codeberg.org/fairyglade/ly.git
synced 2026-08-21 21:14:20 +02:00
[Feature] Add support for .dur animations
This commit is contained in:
parent
3bfdc75a70
commit
dc1793d2ff
5 changed files with 381 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ allow_empty_password = true
|
|||
# matrix -> CMatrix
|
||||
# colormix -> Color mixing shader
|
||||
# gameoflife -> John Conway's Game of Life
|
||||
# dur_file -> .dur file format (https://github.com/cmang/durdraw/tree/master)
|
||||
animation = none
|
||||
|
||||
# Stop the animation after some time
|
||||
|
|
@ -163,6 +164,15 @@ doom_middle_color = 0x00C78F17
|
|||
# DOOM animation custom bottom color (high intensity flames)
|
||||
doom_bottom_color = 0x00FFFFFF
|
||||
|
||||
# Dur file path
|
||||
dur_file_path = $CONFIG_DIRECTORY/ly/example.dur
|
||||
|
||||
# Dur offset x direction
|
||||
dur_x_offset = 0
|
||||
|
||||
# Dur offset y direction
|
||||
dur_y_offset = 0
|
||||
|
||||
# Set margin to the edges of the DM (useful for curved monitors)
|
||||
edge_margin = 0
|
||||
|
||||
|
|
|
|||
362
src/animations/DurFile.zig
Normal file
362
src/animations/DurFile.zig
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
const std = @import("std");
|
||||
const Animation = @import("../tui/Animation.zig");
|
||||
const Cell = @import("../tui/Cell.zig");
|
||||
const TerminalBuffer = @import("../tui/TerminalBuffer.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Json = std.json;
|
||||
const eql = std.mem.eql;
|
||||
const flate = std.compress.flate; // compress.gzip is moved to flate with zig 15
|
||||
|
||||
const DurParseError = error{ FileNotFound, NotValidFile };
|
||||
|
||||
const Frame = struct {
|
||||
frameNumber: i32,
|
||||
delay: i32,
|
||||
contents: [][]u8,
|
||||
colorMap: [][][]i32,
|
||||
|
||||
// allocator must be outside of struct as it will fail the json parser
|
||||
pub fn deinit(self: *const Frame, allocator: Allocator) void {
|
||||
for (self.contents) |con| {
|
||||
allocator.free(con);
|
||||
}
|
||||
allocator.free(self.contents);
|
||||
|
||||
for (self.colorMap) |cm| {
|
||||
for (cm) |int2| {
|
||||
allocator.free(int2);
|
||||
}
|
||||
allocator.free(cm);
|
||||
}
|
||||
allocator.free(self.colorMap);
|
||||
}
|
||||
};
|
||||
|
||||
// https://github.com/cmang/durdraw/blob/0.29.0/durformat.md
|
||||
const DurFormat = struct {
|
||||
allocator: Allocator,
|
||||
formatVersion: ?i64 = null,
|
||||
colorFormat: ?[]const u8 = null,
|
||||
encoding: ?[]const u8 = null,
|
||||
framerate: ?f64 = null,
|
||||
columns: ?i64 = null,
|
||||
lines: ?i64 = null,
|
||||
frames: std.ArrayList(Frame) = undefined,
|
||||
|
||||
pub fn valid(self: *DurFormat) bool {
|
||||
if (self.formatVersion != null and
|
||||
self.colorFormat != null and
|
||||
self.encoding != null and
|
||||
self.framerate != null and
|
||||
self.columns != null and
|
||||
self.lines != null and
|
||||
self.frames.items.len >= 1) {
|
||||
|
||||
// Oldest example in dur repo was 5 so unsure if older changes json layout
|
||||
if (self.formatVersion.? < 5) return false;
|
||||
// v8 may have breaking changes like changing the colormap xy direction
|
||||
// (https://github.com/cmang/durdraw/issues/24)
|
||||
if (self.formatVersion.? > 7) return false;
|
||||
|
||||
// Code currently only supports 16 and 256 color format only
|
||||
if (!(eql(u8, "16", self.colorFormat.?) or eql(u8, "256", self.colorFormat.?)))
|
||||
return false;
|
||||
|
||||
// Code currently supports only utf-8 encoding
|
||||
if (!eql(u8, self.encoding.?, "utf-8")) return false;
|
||||
|
||||
// Sanity check on file
|
||||
if (self.columns.? <= 0) return false;
|
||||
if (self.lines.? <= 0) return false;
|
||||
if (self.framerate.? < 0) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn read_decompress_dur(allocator: Allocator, file_path: []const u8) ![]u8 {
|
||||
const file_buffer = std.fs.cwd().openFile(file_path, .{}) catch {
|
||||
return error.FileNotFound;
|
||||
};
|
||||
defer file_buffer.close();
|
||||
|
||||
var file_reader_buffer: [4096]u8 = undefined;
|
||||
var decompress_buffer: [flate.max_window_len]u8 = undefined;
|
||||
|
||||
var file_reader = file_buffer.reader(&file_reader_buffer);
|
||||
var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &decompress_buffer);
|
||||
|
||||
const file_decompressed = decompress.reader.allocRemaining(allocator, .unlimited) catch {
|
||||
return error.NotValidFile;
|
||||
};
|
||||
|
||||
return file_decompressed;
|
||||
}
|
||||
|
||||
fn parse_dur_from_json(self: *DurFormat, allocator: Allocator, dur_json_root: Json.Value) !void {
|
||||
var dur_movie = if (dur_json_root.object.get("DurMovie")) |dm| dm.object else { return error.NotValidFile; };
|
||||
|
||||
// Depending on the version, a dur file can have different json object names (ie: columns vs sizeX)
|
||||
self.formatVersion = if (dur_movie.get("formatVersion"))|x| x.integer else null;
|
||||
self.colorFormat = if (dur_movie.get("colorFormat")) |x| try allocator.dupe(u8, x.string) else null;
|
||||
self.encoding = if (dur_movie.get("encoding")) |x| try allocator.dupe(u8, x.string) else null;
|
||||
self.framerate = if (dur_movie.get("framerate")) |x| x.float else null;
|
||||
self.columns = if (dur_movie.get("columns")) |x| x.integer
|
||||
else if (dur_movie.get("sizeX")) |x| x.integer else null;
|
||||
|
||||
self.lines = if (dur_movie.get("lines")) |x| x.integer
|
||||
else if (dur_movie.get("sizeY")) |x| x.integer else null;
|
||||
|
||||
const frames = dur_movie.get("frames") orelse return error.NotValidFile;
|
||||
|
||||
self.frames = try .initCapacity(allocator, frames.array.items.len);
|
||||
|
||||
for (frames.array.items) |frame| {
|
||||
var parsed_frame = try Json.parseFromValue(Frame, allocator, frame, .{});
|
||||
defer parsed_frame.deinit();
|
||||
|
||||
const frame_val = parsed_frame.value;
|
||||
|
||||
// copy all fields to own the ptrs for deallocation, the parsed_frame has some other
|
||||
// allocated memory making it difficult to deallocate without leaks
|
||||
const tmp_frame: Frame = .{
|
||||
.frameNumber = frame_val.frameNumber,
|
||||
.delay = frame_val.delay,
|
||||
.contents = try allocator.alloc([]u8, frame_val.contents.len),
|
||||
.colorMap = try allocator.alloc([][]i32, frame_val.colorMap.len)
|
||||
};
|
||||
|
||||
for (0..tmp_frame.contents.len) |i| {
|
||||
tmp_frame.contents[i] = try allocator.dupe(u8, frame_val.contents[i]);
|
||||
}
|
||||
|
||||
for (0..tmp_frame.colorMap.len) |i| {
|
||||
tmp_frame.colorMap[i] = try allocator.alloc([]i32, frame_val.colorMap[i].len);
|
||||
for (0..tmp_frame.colorMap[i].len) |j| {
|
||||
tmp_frame.colorMap[i][j] = try allocator.alloc(i32, 2);
|
||||
tmp_frame.colorMap[i][j][0] = frame_val.colorMap[i][j][0];
|
||||
tmp_frame.colorMap[i][j][1] = frame_val.colorMap[i][j][1];
|
||||
}
|
||||
}
|
||||
|
||||
try self.frames.append(allocator, tmp_frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_from_file(self: *DurFormat, allocator: Allocator, file_path: [] const u8) !void {
|
||||
const file_decompressed = try read_decompress_dur(allocator, file_path);
|
||||
defer allocator.free(file_decompressed);
|
||||
|
||||
const parsed = try Json.parseFromSlice(Json.Value, allocator, file_decompressed, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
try parse_dur_from_json(self, allocator, parsed.value);
|
||||
|
||||
if (!self.valid()) { return error.NotValidFile; }
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator) DurFormat {
|
||||
return .{ .allocator = allocator };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DurFormat) void {
|
||||
if (self.colorFormat != null) self.allocator.free(self.colorFormat.?);
|
||||
if (self.encoding != null) self.allocator.free(self.encoding.?);
|
||||
|
||||
for (self.frames.items) |frame| {
|
||||
frame.deinit(self.allocator);
|
||||
}
|
||||
self.frames.deinit(self.allocator);
|
||||
}
|
||||
};
|
||||
|
||||
// Using bold for bright colors allows for all 16 colors to be rendered on tty term
|
||||
const color16 = [16]u32{
|
||||
0x00000000, // black
|
||||
0x00800000, // red
|
||||
0x00008000, // green
|
||||
0x00808000, // yellow
|
||||
0x00000080, // blue
|
||||
0x00800080, // magenta
|
||||
0x00008080, // cyan
|
||||
0x00C0C0C0, // light gray
|
||||
0x00808080, // gray
|
||||
0x01FF5555, // bright red
|
||||
0x0100FF00, // bright green
|
||||
0x01FFFF00, // bright yellow
|
||||
0x010000FF, // bright blue
|
||||
0x01FF00FF, // bright magenta
|
||||
0x0100FFFF, // bright cyan
|
||||
0x01FFFFFF, // bright white
|
||||
};
|
||||
|
||||
// Made this table from looking at colormapping in dur source, not sure whats going on with the mapping logic
|
||||
// Array indexes are dur colormappings which value maps to indexes in table above. Only needed for dur 16 color
|
||||
const durcolor_table_to_color16 = [17]u32{
|
||||
0, // 0 black
|
||||
0, // 1 nothing?? dur source did not say why 1 is unused
|
||||
4, // 2 blue
|
||||
2, // 3 green
|
||||
6, // 4 cyan
|
||||
1, // 5 red
|
||||
5, // 6 magenta
|
||||
3, // 7 yellow
|
||||
7, // 8 light gray
|
||||
8, // 9 gray
|
||||
12, // 10 bright blue
|
||||
10, // 11 bright green
|
||||
14, // 12 bright cyan
|
||||
9, // 13 bright red
|
||||
13, // 14 bright magenta
|
||||
11, // 15 bright yellow
|
||||
15, // 16 bright white
|
||||
};
|
||||
|
||||
// 256 to rgb math
|
||||
// For extended term range we subtract by 16 to get it in a 0..(6x6x6) cube range
|
||||
// divide by 36 gets the depth of the cube
|
||||
// divide by 6 gets the width of the cube
|
||||
// divide by 1 gets the height of the cube (divide 1 for clarity for what we are doing)
|
||||
//
|
||||
// Each channel can be 6 levels of brightness hence remander operation of 6
|
||||
//
|
||||
// 0x28 = 0xF0 / 6 # This gets us channel color in rgb, 6 equal steps
|
||||
// 0x37 = 0xFF - (0x28 * 5) # 0x37 is the remander scaler
|
||||
|
||||
fn sixcube_to_channel(sixcube: u32) u32 {
|
||||
return if (sixcube > 0) (sixcube * 0x28) + 0x37 else 0;
|
||||
}
|
||||
|
||||
fn convert_256_to_rgb(color_256: u32) u32 {
|
||||
var rgb_color: u32 = 0;
|
||||
|
||||
if (color_256 < 16) {
|
||||
rgb_color = color16[color_256];
|
||||
}
|
||||
else if (color_256 < 232) {
|
||||
rgb_color |= sixcube_to_channel(((color_256 - 16) / 36) % 6 ) << 16;
|
||||
rgb_color |= sixcube_to_channel(((color_256 - 16) / 6) % 6 ) << 8;
|
||||
rgb_color |= sixcube_to_channel(((color_256 - 16) / 1) % 6 );
|
||||
}
|
||||
else {
|
||||
const channel = 0x08 + 0x0a * (color_256 - 232);
|
||||
rgb_color = channel | (channel << 8) | (channel << 16);
|
||||
}
|
||||
|
||||
return rgb_color;
|
||||
}
|
||||
|
||||
|
||||
const DurFile = @This();
|
||||
|
||||
allocator: Allocator,
|
||||
terminal_buffer: *TerminalBuffer,
|
||||
frames: u64,
|
||||
time_previous: i64,
|
||||
x_offset: u32,
|
||||
y_offset: u32,
|
||||
dur_movie: DurFormat,
|
||||
|
||||
pub fn init(allocator: Allocator,
|
||||
terminal_buffer: *TerminalBuffer,
|
||||
log_writer: *std.io.Writer,
|
||||
file_path: []const u8,
|
||||
x_offset: u32,
|
||||
y_offset: u32) !DurFile {
|
||||
var dur_movie: DurFormat = .init(allocator);
|
||||
|
||||
// error state when DurParseError is recoverable and results in no background
|
||||
dur_movie.create_from_file(allocator, file_path) catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
try log_writer.print("DurFile was not found at {s}", .{file_path});
|
||||
},
|
||||
error.NotValidFile => {
|
||||
try log_writer.print("DurFile was invalid or not a dur file!", .{});
|
||||
},
|
||||
else => {
|
||||
return err;
|
||||
},
|
||||
};
|
||||
|
||||
const buf_width: u32 = @intCast(terminal_buffer.width);
|
||||
const buf_height: u32 = @intCast(terminal_buffer.height);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.terminal_buffer = terminal_buffer,
|
||||
.frames = 0,
|
||||
.time_previous = std.time.milliTimestamp(),
|
||||
.x_offset = std.math.clamp(x_offset, 0, buf_width - 1),
|
||||
.y_offset = std.math.clamp(y_offset, 0, buf_height - 1),
|
||||
.dur_movie = dur_movie,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn animation(self: *DurFile) Animation {
|
||||
return Animation.init(self, deinit, realloc, draw);
|
||||
}
|
||||
|
||||
fn deinit(self: *DurFile) void {
|
||||
self.dur_movie.deinit();
|
||||
}
|
||||
|
||||
fn realloc(_: *DurFile) anyerror!void {}
|
||||
|
||||
fn draw(self: *DurFile) void {
|
||||
// dur_movie will be invalid if DurParseError errored out in init
|
||||
if (!self.dur_movie.valid()) return;
|
||||
|
||||
const buf_width: u32 = @intCast(self.terminal_buffer.width);
|
||||
const buf_height: u32 = @intCast(self.terminal_buffer.height);
|
||||
|
||||
const movie_width:u32 = @intCast(self.dur_movie.columns.?);
|
||||
const movie_height:u32 = @intCast(self.dur_movie.lines.?);
|
||||
|
||||
// Ensure if user offsets and frame goes offscreen, it will not overflow draw
|
||||
const frame_width = if ((movie_width + self.x_offset) < buf_width) movie_width else buf_width - self.x_offset;
|
||||
const frame_height = if ((movie_height + self.y_offset) < buf_height) movie_height else buf_height - self.y_offset;
|
||||
|
||||
const current_frame = self.dur_movie.frames.items[self.frames];
|
||||
|
||||
for (0..frame_width) |x| {
|
||||
for (0..frame_height) |y| {
|
||||
var iter = std.unicode.Utf8View.initUnchecked(current_frame.contents[y]).iterator();
|
||||
|
||||
// Peak programming here, could not find a better way to get specific codepoints from an offset
|
||||
for (0..x) |_| {_ = iter.nextCodepoint().?;}
|
||||
const codepoint: u21 = iter.nextCodepoint().?;
|
||||
|
||||
var color_map_0: u32 = @intCast(current_frame.colorMap[x][y][0]);
|
||||
var color_map_1: u32 = @intCast(current_frame.colorMap[x][y][1]);
|
||||
|
||||
if (eql(u8, self.dur_movie.colorFormat.?, "16")) {
|
||||
color_map_0 = durcolor_table_to_color16[color_map_0];
|
||||
color_map_1 = durcolor_table_to_color16[color_map_1 + 1]; // Add 1, dur source stores it like this for some reason
|
||||
}
|
||||
|
||||
const cell = Cell {
|
||||
.ch = @intCast(codepoint),
|
||||
.fg = convert_256_to_rgb(color_map_0),
|
||||
.bg = convert_256_to_rgb(color_map_1)
|
||||
};
|
||||
|
||||
cell.put(x + self.x_offset, y + self.y_offset);
|
||||
}
|
||||
}
|
||||
|
||||
const time_current = std.time.milliTimestamp();
|
||||
const delta_time = time_current - self.time_previous;
|
||||
|
||||
// Convert fps to time in ms, and delay from sec to ms
|
||||
const frame_time: u32 = @intFromFloat(1000 / self.dur_movie.framerate.?);
|
||||
const delay_time: u32 = @intCast(current_frame.delay * 1000);
|
||||
|
||||
if (delta_time > (frame_time + delay_time)) {
|
||||
self.time_previous = time_current;
|
||||
|
||||
const frame_count = self.dur_movie.frames.items.len;
|
||||
self.frames = (self.frames + 1) % frame_count;
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,9 @@ doom_fire_spread: u8 = 2,
|
|||
doom_top_color: u32 = 0x00FF0000,
|
||||
doom_middle_color: u32 = 0x00FFFF00,
|
||||
doom_bottom_color: u32 = 0x00FFFFFF,
|
||||
dur_file_path: []const u8 = build_options.config_directory ++ "/ly/example.dur",
|
||||
dur_x_offset: u32 = 0,
|
||||
dur_y_offset: u32 = 0,
|
||||
edge_margin: u8 = 0,
|
||||
error_bg: u32 = 0x00000000,
|
||||
error_fg: u32 = 0x01FF0000,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ pub const Animation = enum {
|
|||
matrix,
|
||||
colormix,
|
||||
gameoflife,
|
||||
dur_file,
|
||||
};
|
||||
|
||||
pub const DisplayServer = enum {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const Doom = @import("animations/Doom.zig");
|
|||
const Dummy = @import("animations/Dummy.zig");
|
||||
const Matrix = @import("animations/Matrix.zig");
|
||||
const GameOfLife = @import("animations/GameOfLife.zig");
|
||||
const DurFile = @import("animations/DurFile.zig");
|
||||
const Animation = @import("tui/Animation.zig");
|
||||
const TerminalBuffer = @import("tui/TerminalBuffer.zig");
|
||||
const Session = @import("tui/components/Session.zig");
|
||||
|
|
@ -572,6 +573,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();
|
||||
},
|
||||
.dur_file => {
|
||||
var dur = try DurFile.init(allocator, &buffer, log_writer, config.dur_file_path, config.dur_x_offset, config.dur_y_offset);
|
||||
animation = dur.animation();
|
||||
},
|
||||
}
|
||||
defer animation.deinit();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue