commit 309c94dec63e38036b25c3acc7edcd2085c5cee9 Author: Michael Mikovsky <77305074+Astatin3@users.noreply.github.com> Date: Wed Jul 30 14:02:34 2025 -0600 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2c02292 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "luavid" +version = "0.1.0" +edition = "2024" + +[dependencies] +mlua = { version = "0.11.1", features = ["lua54"] } +ndarray = "0.16.1" +nshare = { version = "0.10.0", default-features = false, features = [ + "ndarray", + "image", +] } +# yuv = "0.8.6" +# rustyline = "16.0.0" +ab_glyph = "0.2.31" +clap = { version = "4.5.41", features = ["derive"] } +gstreamer = "0.24.0" +gstreamer-app = "0.24.0" +gstreamer-video = "0.24.0" +image = "0.25.6" +imageproc = "0.25.0" +indicatif = "0.18.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30808e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Michael Mikovsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dec57c5 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# luavid + Lua based video editing diff --git a/examples/cube.lua b/examples/cube.lua new file mode 100644 index 0000000..e678af2 --- /dev/null +++ b/examples/cube.lua @@ -0,0 +1,103 @@ + +local screen_size = {1920, 1080}; +local scale = 300; + +local length_seconds = 10; +local updates = 100; +local spf = length_seconds / updates; + +config:set_size(screen_size[1], screen_size[2]) +config:set_background_color(Color(20, 20, 30)) +config:set_fps(60) +config:set_num_layers(1) + +local cubeCorners = { + { -1.0, -1.0, -1.0 }, -- Point 0 + { 1.0, -1.0, -1.0 }, -- Point 1 + { -1.0, -1.0, 1.0 }, -- Point 2 + { 1.0, -1.0, 1.0 }, -- Point 3 + { -1.0, 1.0, -1.0 }, -- Point 4 + { 1.0, 1.0, -1.0 }, -- Point 5 + { -1.0, 1.0, 1.0 }, -- Point 6 + { 1.0, 1.0, 1.0 } -- Point 7 +}; + +local cubeLines = { + { 0, 1 }, + { 0, 2 }, + { 1, 3 }, + { 2, 3 }, + { 0, 4 }, + { 1, 5 }, + { 2, 6 }, + { 3, 7 }, + { 4, 5 }, + { 4, 6 }, + { 5, 7 }, + { 6, 7 }, +}; + +function rotatePoint(arr, rot) + local x = arr[1]; + local y = arr[2]; + local z = arr[3]; + + -- Rotate around x axis: + local y1 = (y * math.cos(rot[1])) - (z * math.sin(rot[1])); + local z1 = (z * math.cos(rot[1])) + (y * math.sin(rot[1])); + + -- Rotate around y axis: + local x2 = (z1 * math.sin(rot[2])) + (x * math.cos(rot[2])); + local z2 = (z1 * math.cos(rot[2])) - (x * math.sin(rot[2])); + + -- Rotate around z axis: + local x3 = (x2 * math.cos(rot[3])) - (y1 * math.sin(rot[3])); + local y3 = (x2 * math.sin(rot[3])) + (y1 * math.cos(rot[3])); + + return { x3, y3, z2 }; +end + +function renderLine(pos, line) + local p1 = cubeCorners[line[1] + 1]; + local p2 = cubeCorners[line[2] + 1]; + local p1r = rotatePoint(p1, { pos[1], pos[2], pos[3] }); + local p2r = rotatePoint(p2, { pos[1], pos[2], pos[3] }); + + local p1s = Point((scale * p1r[1]) + (screen_size[1] / 2), (scale * p1r[2]) + (screen_size[2]/ 2)) + local p2s = Point((scale * p2r[1]) + (screen_size[1] / 2), (scale * p2r[2]) + (screen_size[2] / 2)) + + return Line(p1s, p2s, Color(255, 255, 255), 2.0) +end + +layer(0) + +local speeds = { 0.2, 0.4, 0.6 } + +local pos = { math.pi / 2, math.pi / 3, math.pi / 4 } + +for i = 0, updates, 1 do + local nextpos = { pos[1] + speeds[1], pos[2] + speeds[2], pos[3] + speeds[3] }; + + for j, line in ipairs(cubeLines) do + animate(Animation( + renderLine(pos, line), + renderLine(nextpos, line), + spf, EaseInOut() + )) + end + + pos = nextpos; + + print(pos[1]) + + render(spf) +end + +-- function Block(x, y, i) +-- animate(Animation( +-- Rect(pos, size, Color(clr_start, clr_start, clr_start)), +-- Rect(pos, size, Color(clr_end, clr_end, clr_end)), +-- 1.0, +-- Linear() +-- )) +-- end diff --git a/examples/perlin.lua b/examples/perlin.lua new file mode 100644 index 0000000..82cb6b5 --- /dev/null +++ b/examples/perlin.lua @@ -0,0 +1,281 @@ +--[[ + https://gist.github.com/kymckay/25758d37f8e3872e1636d90ad41fe2ed + Implemented as described here: + http://flafla2.github.io/2014/08/09/perlinnoise.html +]] -- + +bit32 = {}; +local N = 32; +local P = 2 ^ N; + +bit32.bnot = function(x) + x = x % P; + return (P - 1) - x; +end; + +bit32.band = function(x, y) + if (y == 255) then return x % 256; end + if (y == 65535) then return x % 65536; end + if (y == 4294967295) then return x % 4294967296; end + x, y = x % P, y % P; + local r = 0; + local p = 1; + for i = 1, N do + local a, b = x % 2, y % 2; + x, y = math.floor(x / 2), math.floor(y / 2); + if ((a + b) == 2) then r = r + p; end + p = 2 * p; + end + return r; +end; + +bit32.bor = function(x, y) + if (y == 255) then return (x - (x % 256)) + 255; end + if (y == 65535) then return (x - (x % 65536)) + 65535; end + if (y == 4294967295) then return 4294967295; end + x, y = x % P, y % P; + local r = 0; + local p = 1; + for i = 1, N do + local a, b = x % 2, y % 2; + x, y = math.floor(x / 2), math.floor(y / 2); + if ((a + b) >= 1) then r = r + p; end + p = 2 * p; + end + return r; +end; + +bit32.bxor = function(x, y) + x, y = x % P, y % P; + local r = 0; + local p = 1; + for i = 1, N do + local a, b = x % 2, y % 2; + x, y = math.floor(x / 2), math.floor(y / 2); + if ((a + b) == 1) then r = r + p; end + p = 2 * p; + end + return r; +end; + +bit32.lshift = function(x, s_amount) + if (math.abs(s_amount) >= N) then return 0; end + x = x % P; + if (s_amount < 0) then + return math.floor(x * (2 ^ s_amount)); + else + return (x * (2 ^ s_amount)) % P; + end +end; + +bit32.rshift = function(x, s_amount) + if (math.abs(s_amount) >= N) then return 0; end + x = x % P; + if (s_amount > 0) then + return math.floor(x * (2 ^ -s_amount)); + else + return (x * (2 ^ -s_amount)) % P; + end +end; + +bit32.arshift = function(x, s_amount) + if (math.abs(s_amount) >= N) then return 0; end + x = x % P; + if (s_amount > 0) then + local add = 0; + if (x >= (P / 2)) then + add = P - (2 ^ (N - s_amount)); + end + return math.floor(x * (2 ^ -s_amount)) + add; + else + return (x * (2 ^ -s_amount)) % P; + end +end; + +bit32.extract = function(n, field, width) + width = width or 1; + return (n >> field) & ((1 << width) - 1); +end; + +bit32.replace = function(n, v, field, width) + width = width or 1; + local mask = ((1 << width) - 1) << field; + return (n & ~mask) | ((v << field) & mask); +end; + +bit32.btest = function(...) + return bit32.band(...) ~= 0; +end; + + + +--[[ + Implemented as described here: + http://flafla2.github.io/2014/08/09/perlinnoise.html +]] -- + +perlin = {} +perlin.p = {} + +-- Hash lookup table as defined by Ken Perlin +-- This is a randomly arranged array of all numbers from 0-255 inclusive +local permutation = { 151, 160, 137, 91, 90, 15, + 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, + 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, + 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, + 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, + 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, + 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, + 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, + 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, + 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, + 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, + 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 +} + +-- p is used to hash unit cube coordinates to [0, 255] +for i = 0, 255 do + -- Convert to 0 based index table + perlin.p[i] = permutation[i + 1] + -- Repeat the array to avoid buffer overflow in hash function + perlin.p[i + 256] = permutation[i + 1] +end + +-- Return range: [-1, 1] +function perlin:noise(x, y, z) + y = y or 0 + z = z or 0 + + -- Calculate the "unit cube" that the point asked will be located in + local xi = bit32.band(math.floor(x), 255) + local yi = bit32.band(math.floor(y), 255) + local zi = bit32.band(math.floor(z), 255) + + -- Next we calculate the location (from 0 to 1) in that cube + x = x - math.floor(x) + y = y - math.floor(y) + z = z - math.floor(z) + + -- We also fade the location to smooth the result + local u = self.fade(x) + local v = self.fade(y) + local w = self.fade(z) + + -- Hash all 8 unit cube coordinates surrounding input coordinate + local p = self.p + local A, AA, AB, AAA, ABA, AAB, ABB, B, BA, BB, BAA, BBA, BAB, BBB + A = p[xi] + yi + AA = p[A] + zi + AB = p[A + 1] + zi + AAA = p[AA] + ABA = p[AB] + AAB = p[AA + 1] + ABB = p[AB + 1] + + B = p[xi + 1] + yi + BA = p[B] + zi + BB = p[B + 1] + zi + BAA = p[BA] + BBA = p[BB] + BAB = p[BA + 1] + BBB = p[BB + 1] + + -- Take the weighted average between all 8 unit cube coordinates + return self.lerp(w, + self.lerp(v, + self.lerp(u, + self:grad(AAA, x, y, z), + self:grad(BAA, x - 1, y, z) + ), + self.lerp(u, + self:grad(ABA, x, y - 1, z), + self:grad(BBA, x - 1, y - 1, z) + ) + ), + self.lerp(v, + self.lerp(u, + self:grad(AAB, x, y, z - 1), self:grad(BAB, x - 1, y, z - 1) + ), + self.lerp(u, + self:grad(ABB, x, y - 1, z - 1), self:grad(BBB, x - 1, y - 1, z - 1) + ) + ) + ) +end + +-- Gradient function finds dot product between pseudorandom gradient vector +-- and the vector from input coordinate to a unit cube vertex +perlin.dot_product = { + [0x0] = function(x, y, z) return x + y end, + [0x1] = function(x, y, z) return -x + y end, + [0x2] = function(x, y, z) return x - y end, + [0x3] = function(x, y, z) return -x - y end, + [0x4] = function(x, y, z) return x + z end, + [0x5] = function(x, y, z) return -x + z end, + [0x6] = function(x, y, z) return x - z end, + [0x7] = function(x, y, z) return -x - z end, + [0x8] = function(x, y, z) return y + z end, + [0x9] = function(x, y, z) return -y + z end, + [0xA] = function(x, y, z) return y - z end, + [0xB] = function(x, y, z) return -y - z end, + [0xC] = function(x, y, z) return y + x end, + [0xD] = function(x, y, z) return -y + z end, + [0xE] = function(x, y, z) return y - x end, + [0xF] = function(x, y, z) return -y - z end +} +function perlin:grad(hash, x, y, z) + return self.dot_product[bit32.band(hash, 0xF)](x, y, z) +end + +-- Fade function is used to smooth final output +function perlin.fade(t) + return t * t * t * (t * (t * 6 - 15) + 10) +end + +function perlin.lerp(t, a, b) + return a + t * (b - a) +end + +local iterations = 10; +local length = 10; + +local screen_size = 800; +local block_count = 10; + +local block_size = screen_size / block_count; + +config:set_size(screen_size, screen_size) +config:set_background_color(Color(20, 20, 30)) +config:set_fps(60) +config:set_num_layers(1) + +layer(0) + +function Block(x, y, i) + local pos = Point(x * block_size, y * block_size); + local size = Size(block_size, block_size); + + local xi = 2 * (x / block_count) - 1; + local yi = 2 * (y / block_count) - 1; + + local clr_start = perlin:noise(xi, yi, (i) / iterations); + local clr_end = perlin:noise(xi, yi, (i + 1) / iterations); + -- local clr_end = clr_start; + + animate(Animation( + Rect(pos, size, Color(clr_start, clr_start, clr_start)), + Rect(pos, size, Color(clr_end, clr_end, clr_end)), + 1.0, + Linear() + )) +end + +for i = 0, iterations do + for x = 0, block_count - 1 do + for y = 0, block_count - 1 do + Block(x, y, i) + end + end + render(1) +end diff --git a/examples/simple.lua b/examples/simple.lua new file mode 100644 index 0000000..cf187f8 --- /dev/null +++ b/examples/simple.lua @@ -0,0 +1,43 @@ +config:set_size(1920, 1080) +config:set_background_color(Color(20, 20, 30)) +config:set_fps(60) +config:set_num_layers(5) + +layer(0) +draw(Rect(Point(100, 100), Size(200, 150), Color(255, 100, 100, 255))) +layer(1) +draw(Circle(Point(400, 300), 50, Color(100, 255, 100, 255))) +draw(Line(Point(50, 50), Point(750, 550), Color(255, 255, 100, 255), 5.0)) +render(5) +layer(0) + + + +animate(Animation( + Rect(Point(100, 100), Size(200, 150), Color(255, 100, 100, 255), true, 0.0), + Rect(Point(500, 350), Size(100, 100), Color(100, 100, 255, 255), true, 0.0), + 3.0, + EaseInOut() +)) + + +layer(2) + + +animate(Animation( + CircleOutline( + Point(400.0, 300.0), + 50.0, + Color(100, 255, 100, 255), + 10.0 + ), + CircleOutline( + Point(200.0, 150.0), + 100.0, + Color(255, 150, 50, 255), + 10. + ), + 3.0, + Bounce() +)) +render(3.0) diff --git a/output.mp4 b/output.mp4 new file mode 100644 index 0000000..884a64c Binary files /dev/null and b/output.mp4 differ diff --git a/src/lua/color.rs b/src/lua/color.rs new file mode 100644 index 0000000..ae89f39 --- /dev/null +++ b/src/lua/color.rs @@ -0,0 +1,64 @@ +use image::Rgba; +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Color { r, g, b, a } + } + + pub fn to_rgba(&self) -> Rgba { + Rgba([self.r, self.g, self.b, self.a]) + } +} + +impl FromLua for Color { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Color { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_r", |_, color, x: u8| { + color.r = x; + Ok(()) + }); + methods.add_method_mut("set_g", |_, color, x: u8| { + color.g = x; + Ok(()) + }); + methods.add_method_mut("set_b", |_, color, x: u8| { + color.b = x; + Ok(()) + }); + methods.add_method_mut("set_a", |_, color, x: u8| { + color.a = x; + Ok(()) + }); + + methods.add_method("get_r", |_, color, ()| Ok(color.r)); + methods.add_method("get_g", |_, color, ()| Ok(color.g)); + methods.add_method("get_b", |_, color, ()| Ok(color.b)); + methods.add_method("get_a", |_, color, ()| Ok(color.a)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Color", |_, (r, g, b)| Ok(Color::new(r, g, b, 255)))?; + lua.func("ColorRGB", |_, (r, g, b)| Ok(Color::new(r, g, b, 255)))?; + lua.func("ColorRGBA", |_, (r, g, b, a)| Ok(Color::new(r, g, b, a)))?; + Ok(()) +} diff --git a/src/lua/decoder.rs b/src/lua/decoder.rs new file mode 100644 index 0000000..625d22f --- /dev/null +++ b/src/lua/decoder.rs @@ -0,0 +1,102 @@ +use std::{ + fs::File, + sync::{Arc, Mutex}, +}; + +use mlua::prelude::*; + +use crate::renderer::{ + prelude::Config, + types::{ + Animation, Element, Task, animation, circle_element, color, config, easing, line_element, + point, rectangle_element, scene, size, + }, +}; + +use super::lua_env::LuaEnv; + +// #[derive(Copy, Clone)] +pub struct LuaDecoder { + // config: Table, + tasks: Vec, +} + +// impl LuaRenderer { +// fn new(lua: &Lua) -> Result { +// Ok(Self { +// config: Config::default().to_table(lua)?, +// blocks: lua.create_table()?, +// }) +// } +// } + +impl Default for LuaDecoder { + fn default() -> Self { + Self { tasks: Vec::new() } + } +} + +impl LuaDecoder { + pub fn run(file: &File) -> crate::Result<(Vec, Config)> { + let lua = LuaEnv::new(); + + let renderer = Arc::new(Mutex::new(Self::default())); + + circle_element::add_constructor(&lua)?; + line_element::add_constructor(&lua)?; + rectangle_element::add_constructor(&lua)?; + animation::add_constructor(&lua)?; + color::add_constructor(&lua)?; + easing::add_constructor(&lua)?; + point::add_constructor(&lua)?; + size::add_constructor(&lua)?; + + lua.lua.globals().set("config", Config::default())?; + + Self::add_func(&lua, &renderer, "layer", Self::move_to_layer)?; + Self::add_func(&lua, &renderer, "draw", Self::draw)?; + Self::add_func(&lua, &renderer, "animate", Self::animate)?; + Self::add_func(&lua, &renderer, "render", Self::render)?; + + lua.exec(std::io::read_to_string(file)?)?; + + Ok(( + renderer.lock().unwrap().tasks.clone(), + lua.lua.globals().get("config")?, + )) + } + + fn add_func( + lua: &LuaEnv, + this: &Arc>, + name: &str, + func: F, + ) -> crate::Result<()> + where + F: Fn(&Arc>, A) -> R + 'static, + A: FromLuaMulti, + R: IntoLuaMulti, + { + let renderer = Arc::clone(this); + + lua.func(name, move |_, args| Ok(func(&renderer, args)))?; + + Ok(()) + } + + fn move_to_layer(this: &Arc>, layer: usize) { + this.lock().unwrap().tasks.push(Task::MoveToLayer(layer)); + } + + fn draw(this: &Arc>, element: Element) { + this.lock().unwrap().tasks.push(Task::Draw(element)); + } + + fn animate(this: &Arc>, animation: Animation) { + this.lock().unwrap().tasks.push(Task::Animate(animation)); + } + + fn render(this: &Arc>, duration: f64) { + this.lock().unwrap().tasks.push(Task::RenderScene(duration)); + } +} diff --git a/src/lua/lua_env.rs b/src/lua/lua_env.rs new file mode 100644 index 0000000..7726ac9 --- /dev/null +++ b/src/lua/lua_env.rs @@ -0,0 +1,29 @@ +use mlua::prelude::*; + +pub struct LuaEnv { + pub lua: Lua, +} + +impl LuaEnv { + pub fn new() -> Self { + LuaEnv { lua: Lua::new() } + } + + pub fn func(&self, name: &str, func: F) -> crate::Result<()> + where + F: Fn(&Lua, A) -> mlua::Result + mlua::MaybeSend + 'static, + A: FromLuaMulti, + R: IntoLuaMulti, + { + self.lua + .globals() + .set(name, self.lua.create_function(func)?)?; + + Ok(()) + } + + pub fn exec(&self, script: String) -> crate::Result<()> { + self.lua.load(script).exec()?; + Ok(()) + } +} diff --git a/src/lua/mod.rs b/src/lua/mod.rs new file mode 100644 index 0000000..fd49387 --- /dev/null +++ b/src/lua/mod.rs @@ -0,0 +1,5 @@ +mod decoder; +mod lua_env; + +pub use self::decoder::LuaDecoder; +pub use self::lua_env::LuaEnv; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dca9be1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +#![allow(dead_code)] + +use clap::Parser; + +// use crate::renderer::LuaRenderer; + +mod lua; +mod renderer; + +use lua::LuaDecoder; + +use crate::renderer::prelude::VideoRenderer; + +// mod tasks; + +pub type Error = Box; +pub type Result = std::result::Result; + +/// A fictional versioning CLI +#[derive(Debug, Parser)] +#[command(name = "luavid")] +#[command(about = "Tool for scanning Minecraft servers", long_about = None)] +struct Args { + file: String, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + if args.file.is_empty() { + return Err("File path cannot be empty".into()); + } + + if !std::path::Path::new(&args.file).exists() { + return Err(format!("File '{}' does not exist", args.file).into()); + } + + let file = std::fs::File::open(&args.file)?; + + let (tasks, config) = LuaDecoder::run(&file)?; + + // println!("config: {:?}", config); + println!("tasks: {}", tasks.len()); + + let mut renderer = VideoRenderer::new(config); + + renderer.process_tasks(tasks); + + // let info = renderer.export_info(); + // println!("Video Info: {:?}", info); + + // Render the complete video + renderer.render_to_video("output.mp4")?; + + Ok(()) +} + +// use mlua::{Error, Lua, MultiValue}; +// use rustyline::DefaultEditor; + +// fn main() { +// let lua = Lua::new(); +// let mut editor = DefaultEditor::new().expect("Failed to create editor"); + +// loop { +// let mut prompt = "> "; +// let mut line = String::new(); + +// loop { +// match editor.readline(prompt) { +// Ok(input) => line.push_str(&input), +// Err(_) => return, +// } + +// match lua.load(&line).eval::() { +// Ok(values) => { +// editor.add_history_entry(line).unwrap(); +// println!( +// "{}", +// values +// .iter() +// .map(|value| format!("{:#?}", value)) +// .collect::>() +// .join("\t") +// ); +// break; +// } +// Err(Error::SyntaxError { +// incomplete_input: true, +// .. +// }) => { +// // continue reading input and append it to `line` +// line.push_str("\n"); // separate input lines +// prompt = ">> "; +// } +// Err(e) => { +// eprintln!("error: {}", e); +// break; +// } +// } +// } +// } +// } diff --git a/src/main_old2.rs b/src/main_old2.rs new file mode 100644 index 0000000..35c7e59 --- /dev/null +++ b/src/main_old2.rs @@ -0,0 +1,112 @@ +mod renderer; + +use renderer::prelude::*; + +// Add these to Cargo.toml: +// [dependencies] +// image = "0.24" +// imageproc = "0.23" +// gstreamer = "0.21" +// gstreamer-app = "0.21" + +// Configuration struct for rendering settings + +// Color representation + +// Easing functions for animations + +// Animation structure + +// Drawable item that exists on a layer + +// Scene represents a collection of drawables and animations for a duration + +pub fn create_example_video() -> Vec { + vec![ + Task::SetConfig(Config { + width: 800, + height: 600, + background_color: Color::new(20, 20, 30, 255), + fps: 30.0, + num_layers: 5, + }), + Task::MoveToLayer(0), + Task::Draw(Element::Rectangle(RectangleElement::new( + Point::new(100.0, 100.0), + Size::new(200.0, 150.0), + Color::new(255, 100, 100, 255), + true, + 0.0, + ))), + Task::MoveToLayer(1), + Task::Draw(Element::Circle(CircleElement::new( + Point::new(400.0, 300.0), + 50.0, + Color::new(100, 255, 100, 255), + false, + 3.0, + ))), + Task::Draw(Element::Line(LineElement::new( + Point::new(50.0, 50.0), + Point::new(750.0, 550.0), + Color::new(255, 255, 100, 255), + 5.0, + ))), + Task::RenderScene(5.0), + Task::MoveToLayer(0), + Task::Animate(Animation::new( + Element::Rectangle(RectangleElement::new( + Point::new(100.0, 100.0), + Size::new(200.0, 150.0), + Color::new(255, 100, 100, 255), + true, + 0.0, + )), + Element::Rectangle(RectangleElement::new( + Point::new(500.0, 350.0), + Size::new(100.0, 100.0), + Color::new(100, 100, 255, 255), + true, + 0.0, + )), + 3.0, + EasingFunction::EaseInOut, + )), + Task::MoveToLayer(2), + Task::Animate(Animation::new( + Element::Circle(CircleElement::new( + Point::new(400.0, 300.0), + 50.0, + Color::new(100, 255, 100, 255), + false, + 3.0, + )), + Element::Circle(CircleElement::new( + Point::new(200.0, 150.0), + 100.0, + Color::new(255, 150, 50, 255), + true, + 0.0, + )), + 3.0, + EasingFunction::Bounce, + )), + Task::RenderScene(3.0), + ] +} + +fn main() -> Result<(), Box> { + // Example usage + let mut renderer = VideoRenderer::new(); + let tasks = create_example_video(); + + renderer.process_tasks(tasks); + + let info = renderer.export_info(); + println!("Video Info: {:?}", info); + + // Render the complete video + renderer.render_to_video("output.mp4")?; + + Ok(()) +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..4855494 --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,9 @@ +pub mod types; +pub mod video_encoder; +pub mod video_renderer; + +#[allow(unused_imports)] +pub mod prelude { + pub use super::types::*; + pub use super::video_renderer::VideoRenderer; +} diff --git a/src/renderer/types/animation.rs b/src/renderer/types/animation.rs new file mode 100644 index 0000000..62cc676 --- /dev/null +++ b/src/renderer/types/animation.rs @@ -0,0 +1,113 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; +pub use crate::renderer::prelude::*; + +#[derive(Debug, Clone, Copy)] +pub struct Animation { + pub start_element: Element, + pub end_element: Element, + pub duration: f64, + pub easing: EasingFunction, +} + +impl Animation { + pub fn new( + start_element: Element, + end_element: Element, + duration: f64, + easing: EasingFunction, + ) -> Self { + Animation { + start_element, + end_element, + duration, + easing, + } + } + + pub fn interpolate_at(&self, time: f64) -> Element { + let progress = (time / self.duration).clamp(0.0, 1.0); + let eased_progress = self.easing.apply(progress); + + match (&self.start_element, &self.end_element) { + (Element::Line(start), Element::Line(end)) => Element::Line(LineElement { + start: Point::new( + start.start.x + (end.start.x - start.start.x) * eased_progress, + start.start.y + (end.start.y - start.start.y) * eased_progress, + ), + end: Point::new( + start.end.x + (end.end.x - start.end.x) * eased_progress, + start.end.y + (end.end.y - start.end.y) * eased_progress, + ), + color: interpolate_color(start.color, end.color, eased_progress), + thickness: start.thickness + (end.thickness - start.thickness) * eased_progress, + }), + (Element::Rectangle(start), Element::Rectangle(end)) => { + Element::Rectangle(RectangleElement { + position: Point::new( + start.position.x + (end.position.x - start.position.x) * eased_progress, + start.position.y + (end.position.y - start.position.y) * eased_progress, + ), + size: Size::new( + start.size.width + (end.size.width - start.size.width) * eased_progress, + start.size.height + (end.size.height - start.size.height) * eased_progress, + ), + color: interpolate_color(start.color, end.color, eased_progress), + filled: start.filled, + border_thickness: start.border_thickness + + (end.border_thickness - start.border_thickness) * eased_progress, + }) + } + (Element::Circle(start), Element::Circle(end)) => Element::Circle(CircleElement { + center: Point::new( + start.center.x + (end.center.x - start.center.x) * eased_progress, + start.center.y + (end.center.y - start.center.y) * eased_progress, + ), + radius: start.radius + (end.radius - start.radius) * eased_progress, + color: interpolate_color(start.color, end.color, eased_progress), + filled: start.filled, + border_thickness: start.border_thickness + + (end.border_thickness - start.border_thickness) * eased_progress, + }), + _ => self.start_element.clone(), // Fallback for mismatched types + } + } +} + +fn interpolate_color(start: Color, end: Color, progress: f64) -> Color { + Color::new( + (start.r as f64 + (end.r as f64 - start.r as f64) * progress) as u8, + (start.g as f64 + (end.g as f64 - start.g as f64) * progress) as u8, + (start.b as f64 + (end.b as f64 - start.b as f64) * progress) as u8, + (start.a as f64 + (end.a as f64 - start.a as f64) * progress) as u8, + ) +} + +impl FromLua for Animation { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Animation { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("duration", |_, this| Ok(this.duration)); + fields.add_field_method_get("start_element", |_, this| Ok(this.start_element)); + fields.add_field_method_get("end_element", |_, this| Ok(this.end_element)); + fields.add_field_method_get("easing", |_, this| Ok(this.easing)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func( + "Animation", + |_, (start_element, end_element, duration, easing)| { + Ok(Animation::new(start_element, end_element, duration, easing)) + }, + )?; + Ok(()) +} diff --git a/src/renderer/types/color.rs b/src/renderer/types/color.rs new file mode 100644 index 0000000..ae89f39 --- /dev/null +++ b/src/renderer/types/color.rs @@ -0,0 +1,64 @@ +use image::Rgba; +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Color { r, g, b, a } + } + + pub fn to_rgba(&self) -> Rgba { + Rgba([self.r, self.g, self.b, self.a]) + } +} + +impl FromLua for Color { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Color { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_r", |_, color, x: u8| { + color.r = x; + Ok(()) + }); + methods.add_method_mut("set_g", |_, color, x: u8| { + color.g = x; + Ok(()) + }); + methods.add_method_mut("set_b", |_, color, x: u8| { + color.b = x; + Ok(()) + }); + methods.add_method_mut("set_a", |_, color, x: u8| { + color.a = x; + Ok(()) + }); + + methods.add_method("get_r", |_, color, ()| Ok(color.r)); + methods.add_method("get_g", |_, color, ()| Ok(color.g)); + methods.add_method("get_b", |_, color, ()| Ok(color.b)); + methods.add_method("get_a", |_, color, ()| Ok(color.a)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Color", |_, (r, g, b)| Ok(Color::new(r, g, b, 255)))?; + lua.func("ColorRGB", |_, (r, g, b)| Ok(Color::new(r, g, b, 255)))?; + lua.func("ColorRGBA", |_, (r, g, b, a)| Ok(Color::new(r, g, b, a)))?; + Ok(()) +} diff --git a/src/renderer/types/config.rs b/src/renderer/types/config.rs new file mode 100644 index 0000000..9a38cab --- /dev/null +++ b/src/renderer/types/config.rs @@ -0,0 +1,62 @@ +// use mlua::prelude::*; +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::renderer::prelude::*; + +#[derive(Debug, Clone, Copy)] +pub struct Config { + pub width: u32, + pub height: u32, + pub background_color: Color, + pub fps: f64, + pub num_layers: usize, +} + +impl Default for Config { + fn default() -> Self { + Config { + width: 1920, + height: 1080, + background_color: Color::new(0, 0, 0, 255), + fps: 30.0, + num_layers: 10, + } + } +} + +impl FromLua for Config { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Config { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_size", |_, config, (width, height): (u32, u32)| { + config.width = width; + config.height = height; + Ok(()) + }); + + methods.add_method_mut("set_background_color", |_, config, color: Color| { + config.background_color = color; + Ok(()) + }); + methods.add_method_mut("set_fps", |_, config, fps: f64| { + config.fps = fps; + Ok(()) + }); + methods.add_method_mut("set_num_layers", |_, config, num_layers: usize| { + config.num_layers = num_layers; + Ok(()) + }); + } +} + +// pub fn add_constructor(lua: &Lua) -> mlua::Result<()> { +// lua.globals().set("Config", Config::default())?; +// Ok(()) +// } diff --git a/src/renderer/types/easing.rs b/src/renderer/types/easing.rs new file mode 100644 index 0000000..ba66ced --- /dev/null +++ b/src/renderer/types/easing.rs @@ -0,0 +1,80 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; + +#[derive(Debug, Clone, Copy)] +pub enum EasingFunction { + Linear, + EaseIn, + EaseOut, + EaseInOut, + Bounce, + Elastic, +} + +impl EasingFunction { + pub fn apply(&self, t: f64) -> f64 { + let t = t.clamp(0.0, 1.0); + match self { + EasingFunction::Linear => t, + EasingFunction::EaseIn => t * t, + EasingFunction::EaseOut => 1.0 - (1.0 - t) * (1.0 - t), + EasingFunction::EaseInOut => { + if t < 0.5 { + 2.0 * t * t + } else { + 1.0 - 2.0 * (1.0 - t) * (1.0 - t) + } + } + EasingFunction::Bounce => { + let n1 = 7.5625; + let d1 = 2.75; + if t < 1.0 / d1 { + n1 * t * t + } else if t < 2.0 / d1 { + let t = t - 1.5 / d1; + n1 * t * t + 0.75 + } else if t < 2.5 / d1 { + let t = t - 2.25 / d1; + n1 * t * t + 0.9375 + } else { + let t = t - 2.625 / d1; + n1 * t * t + 0.984375 + } + } + EasingFunction::Elastic => { + if t == 0.0 { + 0.0 + } else if t == 1.0 { + 1.0 + } else { + let c4 = (2.0 * std::f64::consts::PI) / 3.0; + -(2.0_f64.powf(10.0 * t - 10.0)) * ((t * 10.0 - 10.75) * c4).sin() + } + } + } + } +} + +impl FromLua for EasingFunction { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for EasingFunction { + fn add_methods>(_: &mut M) {} +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Linear", |_, ()| Ok(EasingFunction::Linear))?; + lua.func("EaseIn", |_, ()| Ok(EasingFunction::EaseIn))?; + lua.func("EaseOut", |_, ()| Ok(EasingFunction::EaseOut))?; + lua.func("EaseInOut", |_, ()| Ok(EasingFunction::EaseInOut))?; + lua.func("Bounce", |_, ()| Ok(EasingFunction::Bounce))?; + lua.func("Elastic", |_, ()| Ok(EasingFunction::Elastic))?; + Ok(()) +} diff --git a/src/renderer/types/elements/circle_element.rs b/src/renderer/types/elements/circle_element.rs new file mode 100644 index 0000000..45bff23 --- /dev/null +++ b/src/renderer/types/elements/circle_element.rs @@ -0,0 +1,97 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; +pub use crate::renderer::prelude::*; + +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct CircleElement { + pub center: Point, + pub radius: f64, + pub color: Color, + pub filled: bool, + pub border_thickness: f64, +} + +impl CircleElement { + pub fn new( + center: Point, + radius: f64, + color: Color, + filled: bool, + border_thickness: f64, + ) -> Self { + CircleElement { + center, + radius, + color, + filled, + border_thickness, + } + } +} + +impl FromLua for CircleElement { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for CircleElement { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_center", |_, circle, point: Point| { + circle.center = point; + Ok(()) + }); + methods.add_method_mut("set_radius", |_, circle, radius: f64| { + circle.radius = radius; + Ok(()) + }); + methods.add_method_mut("set_color", |_, circle, color: Color| { + circle.color = color; + Ok(()) + }); + methods.add_method_mut("set_filled", |_, circle, filled: bool| { + circle.filled = filled; + Ok(()) + }); + methods.add_method_mut( + "set_border_thickness", + |_, circle, border_thickness: f64| { + circle.border_thickness = border_thickness; + Ok(()) + }, + ); + + methods.add_method("get_center", |_, circle, ()| Ok(circle.center)); + methods.add_method("get_radius", |_, circle, ()| Ok(circle.radius)); + methods.add_method("get_color", |_, circle, ()| Ok(circle.color)); + methods.add_method("get_filled", |_, circle, ()| Ok(circle.filled)); + methods.add_method("get_border_thickness", |_, circle, ()| { + Ok(circle.border_thickness) + }); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Circle", |_, (center, radius, color)| { + Ok(Element::Circle(CircleElement::new( + center, radius, color, true, 0., + ))) + })?; + lua.func( + "CircleOutline", + |_, (center, radius, color, border_thickness)| { + Ok(Element::Circle(CircleElement::new( + center, + radius, + color, + false, + border_thickness, + ))) + }, + )?; + Ok(()) +} diff --git a/src/renderer/types/elements/line_element.rs b/src/renderer/types/elements/line_element.rs new file mode 100644 index 0000000..bdd8684 --- /dev/null +++ b/src/renderer/types/elements/line_element.rs @@ -0,0 +1,67 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; +pub use crate::renderer::prelude::*; + +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct LineElement { + pub start: Point, + pub end: Point, + pub color: Color, + pub thickness: f64, +} + +impl LineElement { + pub fn new(start: Point, end: Point, color: Color, thickness: f64) -> Self { + LineElement { + start, + end, + color, + thickness, + } + } +} + +impl FromLua for LineElement { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for LineElement { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_p1", |_, line, point: Point| { + line.start = point; + Ok(()) + }); + methods.add_method_mut("set_p2", |_, line, point: Point| { + line.end = point; + Ok(()) + }); + methods.add_method_mut("set_color", |_, line, color: Color| { + line.color = color; + Ok(()) + }); + methods.add_method_mut("set_thickness", |_, line, thickness: f64| { + line.thickness = thickness; + Ok(()) + }); + + methods.add_method("get_p1", |_, line, ()| Ok(line.start)); + methods.add_method("get_p2", |_, line, ()| Ok(line.end)); + methods.add_method("get_color", |_, line, ()| Ok(line.color)); + methods.add_method("get_thickness", |_, line, ()| Ok(line.thickness)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Line", |_, (start, end, color, thickness)| { + Ok(Element::Line(LineElement::new( + start, end, color, thickness, + ))) + })?; + Ok(()) +} diff --git a/src/renderer/types/elements/mod.rs b/src/renderer/types/elements/mod.rs new file mode 100644 index 0000000..433d6a7 --- /dev/null +++ b/src/renderer/types/elements/mod.rs @@ -0,0 +1,40 @@ +pub mod circle_element; +pub mod line_element; +pub mod rectangle_element; + +pub use circle_element::CircleElement; +pub use line_element::LineElement; +use mlua::{FromLua, Lua, UserData, UserDataMethods}; +pub use rectangle_element::RectangleElement; + +#[derive(Debug, Clone)] +pub struct Drawable { + pub element: Element, + pub layer: usize, +} + +impl Drawable { + pub fn new(element: Element, layer: usize) -> Self { + Drawable { element, layer } + } +} + +#[derive(Debug, Clone, PartialEq, Copy)] +pub enum Element { + Line(LineElement), + Rectangle(RectangleElement), + Circle(CircleElement), +} + +impl FromLua for Element { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Element { + fn add_methods>(methods: &mut M) {} +} diff --git a/src/renderer/types/elements/rectangle_element.rs b/src/renderer/types/elements/rectangle_element.rs new file mode 100644 index 0000000..9aab840 --- /dev/null +++ b/src/renderer/types/elements/rectangle_element.rs @@ -0,0 +1,84 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; +pub use crate::renderer::prelude::*; + +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct RectangleElement { + pub position: Point, + pub size: Size, + pub color: Color, + pub filled: bool, + pub border_thickness: f64, +} + +impl RectangleElement { + pub fn new( + position: Point, + size: Size, + color: Color, + filled: bool, + border_thickness: f64, + ) -> Self { + RectangleElement { + position, + size, + color, + filled, + border_thickness, + } + } +} + +impl FromLua for RectangleElement { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for RectangleElement { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_point", |_, rect, point: Point| { + rect.position = point; + Ok(()) + }); + methods.add_method_mut("set_size", |_, rect, size: Size| { + rect.size = size; + Ok(()) + }); + methods.add_method_mut("set_color", |_, rect, color: Color| { + rect.color = color; + Ok(()) + }); + methods.add_method_mut("set_thickness", |_, rect, thickness: f64| { + rect.border_thickness = thickness; + Ok(()) + }); + methods.add_method_mut("set_filled", |_, rect, filled: bool| { + rect.filled = filled; + Ok(()) + }); + + methods.add_method("get_point", |_, rect, ()| Ok(rect.position)); + methods.add_method("get_color", |_, rect, ()| Ok(rect.color)); + methods.add_method("get_thickness", |_, rect, ()| Ok(rect.border_thickness)); + methods.add_method("get_filled", |_, rect, ()| Ok(rect.filled)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Rect", |_, (position, size, color)| { + Ok(Element::Rectangle(RectangleElement::new( + position, size, color, true, 0., + ))) + })?; + lua.func("RectOutline", |_, (position, size, color, thickness)| { + Ok(Element::Rectangle(RectangleElement::new( + position, size, color, false, thickness, + ))) + })?; + Ok(()) +} diff --git a/src/renderer/types/mod.rs b/src/renderer/types/mod.rs new file mode 100644 index 0000000..691a099 --- /dev/null +++ b/src/renderer/types/mod.rs @@ -0,0 +1,19 @@ +pub mod animation; +pub mod color; +pub mod config; +pub mod easing; +pub mod elements; +pub mod point; +pub mod scene; +pub mod size; +pub mod task; + +pub use animation::Animation; +pub use color::Color; +pub use config::Config; +pub use easing::EasingFunction; +pub use elements::*; +pub use point::Point; +pub use scene::Scene; +pub use size::Size; +pub use task::Task; diff --git a/src/renderer/types/point.rs b/src/renderer/types/point.rs new file mode 100644 index 0000000..7cc39b3 --- /dev/null +++ b/src/renderer/types/point.rs @@ -0,0 +1,59 @@ +use mlua::{FromLua, Lua, MetaMethod, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; + +// Position and size structures +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Point { + pub x: f64, + pub y: f64, +} + +impl Point { + pub fn new(x: f64, y: f64) -> Self { + Point { x, y } + } +} + +impl FromLua for Point { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Point { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set", |_, point, (x, y): (f64, f64)| { + point.x = x; + point.y = y; + Ok(()) + }); + + // methods.add_meta_function(MetaMethod::Call, |_, (x, y)| Ok(Point::new(x, y))); + // methods.add_meta_function(MetaMethod::ToString, |_, (x, y): (f64, f64)| { + // Ok(format!("({}, {})", x, y)) + // }); + } + fn add_fields>(fields: &mut F) { + fields.add_field_method_set("x", |_, this, x| { + this.x = x; + Ok(()) + }); + fields.add_field_method_set("y", |_, point, y: f64| { + point.y = y; + Ok(()) + }); + + fields.add_field_method_get("x", |_, point| Ok(point.x)); + fields.add_field_method_get("y", |_, point| Ok(point.y)); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Point", |_, (x, y)| Ok(Point::new(x, y)))?; + + Ok(()) +} diff --git a/src/renderer/types/scene.rs b/src/renderer/types/scene.rs new file mode 100644 index 0000000..e004dc1 --- /dev/null +++ b/src/renderer/types/scene.rs @@ -0,0 +1,40 @@ +use crate::renderer::prelude::*; + +#[derive(Debug, Clone)] +pub struct Scene { + pub drawables: Vec, + pub animations: Vec<(Animation, usize)>, // Animation with layer + pub duration: f64, +} + +impl Scene { + pub fn new(duration: f64) -> Self { + Scene { + drawables: Vec::new(), + animations: Vec::new(), + duration, + } + } + + pub fn add_drawable(&mut self, drawable: Drawable) { + self.drawables.push(drawable); + } + + pub fn add_animation(&mut self, animation: Animation, layer: usize) { + self.animations.push((animation, layer)); + } + + pub fn get_frame_at(&self, time: f64) -> Vec { + let mut frame_drawables = self.drawables.clone(); + + // Add animated elements + for (animation, layer) in &self.animations { + let interpolated_element = animation.interpolate_at(time); + frame_drawables.push(Drawable::new(interpolated_element, *layer)); + } + + // Sort by layer + frame_drawables.sort_by_key(|d| d.layer); + frame_drawables + } +} diff --git a/src/renderer/types/size.rs b/src/renderer/types/size.rs new file mode 100644 index 0000000..8908c99 --- /dev/null +++ b/src/renderer/types/size.rs @@ -0,0 +1,54 @@ +use mlua::{FromLua, Lua, UserData, UserDataMethods}; + +use crate::lua::LuaEnv; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Size { + pub width: f64, + pub height: f64, +} + +impl Size { + pub fn new(width: f64, height: f64) -> Self { + Size { width, height } + } +} + +impl FromLua for Size { + fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result { + match value { + mlua::Value::UserData(ud) => Ok(*ud.borrow::()?), + _ => unreachable!(), + } + } +} + +impl UserData for Size { + fn add_methods>(methods: &mut M) { + methods.add_method_mut("set_width", |_, size, width: f64| { + size.width = width; + Ok(()) + }); + methods.add_method_mut("set_height", |_, size, height: f64| { + size.height = height; + Ok(()) + }); + + methods.add_method_mut("set_size", |_, size, (width, height): (f64, f64)| { + size.width = width; + size.height = height; + Ok(()) + }); + + methods.add_method("get_width", |_, size, ()| Ok(size.width)); + methods.add_method("get_height", |_, size, ()| Ok(size.height)); + methods.add_method("get_size", |_, size, ()| Ok((size.width, size.height))); + } +} + +pub fn add_constructor(lua: &LuaEnv) -> crate::Result<()> { + lua.func("Size", |_, (width, height): (f64, f64)| { + Ok(Size::new(width, height)) + })?; + Ok(()) +} diff --git a/src/renderer/types/task.rs b/src/renderer/types/task.rs new file mode 100644 index 0000000..d8bfae2 --- /dev/null +++ b/src/renderer/types/task.rs @@ -0,0 +1,10 @@ +use crate::renderer::prelude::*; + +#[derive(Debug, Clone)] +pub enum Task { + // SetConfig(Config), + MoveToLayer(usize), + Draw(Element), + Animate(Animation), + RenderScene(f64), +} diff --git a/src/renderer/video_encoder.rs b/src/renderer/video_encoder.rs new file mode 100644 index 0000000..a9b2df1 --- /dev/null +++ b/src/renderer/video_encoder.rs @@ -0,0 +1,136 @@ +pub use gstreamer as gst; +use gstreamer::{ + glib::object::Cast, + prelude::{ElementExt, GstBinExtManual}, +}; +pub use gstreamer_app as gst_app; +pub use gstreamer_video as gst_video; +use image::RgbaImage; + +use crate::renderer::types::Config; + +// GStreamer video encoder +pub struct VideoEncoder { + video_info: gst_video::VideoInfo, + pipeline: gst::Pipeline, + appsrc: gst_app::AppSrc, + config: Config, +} + +impl VideoEncoder { + pub fn new(config: &Config, output_path: &str) -> Result> { + gst::init()?; + + let pipeline = gst::Pipeline::new(); + + let video_info = gst_video::VideoInfo::builder( + gst_video::VideoFormat::Rgba, + config.width as u32, + config.height as u32, + ) + .fps(gst::Fraction::new(config.fps as i32, 1)) + .build() + .expect("Failed to create video info"); + + // Configure appsrc + let appsrc = gst_app::AppSrc::builder() + .caps(&video_info.to_caps()?) + .property("stream-type", gst_app::AppStreamType::Stream) + .property("format", gst::Format::Time) + .build(); + + // Create pipeline elements + let videoconvert = gst::ElementFactory::make("videoconvert").build()?; + + // Configure encoder + let x264enc = gst::ElementFactory::make("x264enc") + .property("bitrate", 2000u32) // 2Mbps + .property_from_str("speed-preset", "fast") + .build()?; + + let mp4mux = gst::ElementFactory::make("mp4mux").build()?; + let filesink = gst::ElementFactory::make("filesink") + .property("location", output_path) + .build()?; + + // Add elements to pipeline + pipeline.add_many(&[ + appsrc.upcast_ref(), + &videoconvert, + &x264enc, + &mp4mux, + &filesink, + ])?; + + // Link elements + gst::Element::link_many(&[ + appsrc.upcast_ref(), + &videoconvert, + &x264enc, + &mp4mux, + &filesink, + ])?; + + Ok(VideoEncoder { + video_info, + pipeline, + appsrc, + config: config.clone(), + }) + } + + pub fn start(&self) -> Result<(), gst::StateChangeError> { + self.pipeline.set_state(gst::State::Playing)?; + Ok(()) + } + + pub fn push_frame( + &self, + image: &RgbaImage, + millis: u64, + ) -> Result<(), Box> { + let data = image.as_raw().clone(); + let buffer_size = data.len(); + + let mut buffer = gst::Buffer::with_size(buffer_size)?; + { + buffer + .get_mut() + .unwrap() + .set_pts(millis * gst::ClockTime::MSECOND); + + let buffer_ref = buffer.get_mut().unwrap(); + let mut buffer_map = buffer_ref.map_writable()?; + buffer_map.copy_from_slice(&data); + } + + self.appsrc.push_buffer(buffer)?; + Ok(()) + } + + pub fn finish(&self) -> Result<(), Box> { + self.appsrc.end_of_stream()?; + + // Wait for EOS + let bus = self.pipeline.bus().unwrap(); + for msg in bus.iter_timed(gst::ClockTime::NONE) { + match msg.view() { + gst::MessageView::Eos(..) => break, + gst::MessageView::Error(err) => { + self.pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); + // return err.message().into(); + return Err(format!("Error: {}", err.error()).into()); + } + _ => (), + } + } + + self.pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); + + Ok(()) + } +} diff --git a/src/renderer/video_renderer.rs b/src/renderer/video_renderer.rs new file mode 100644 index 0000000..c7f25e1 --- /dev/null +++ b/src/renderer/video_renderer.rs @@ -0,0 +1,247 @@ +use image::{ImageBuffer, RgbaImage}; +use imageproc::{ + drawing::{ + draw_filled_circle_mut, draw_filled_rect_mut, draw_hollow_circle_mut, draw_hollow_rect_mut, + draw_line_segment_mut, + }, + rect::Rect, +}; +use indicatif::{ProgressBar, ProgressStyle}; + +use crate::renderer::{prelude::*, video_encoder::VideoEncoder}; + +#[derive(Debug)] +pub struct VideoInfo { + pub width: u32, + pub height: u32, + pub fps: f64, + pub duration: f64, + pub total_frames: u32, + pub num_scenes: usize, +} + +// Main renderer state +#[derive(Debug)] +pub struct VideoRenderer { + pub config: Config, + pub current_layer: usize, + pub scenes: Vec, + pub current_scene_drawables: Vec, + pub current_scene_animations: Vec<(Animation, usize)>, +} + +impl VideoRenderer { + pub fn new(config: Config) -> Self { + VideoRenderer { + config, + current_layer: 0, + scenes: Vec::new(), + current_scene_drawables: Vec::new(), + current_scene_animations: Vec::new(), + } + } + + pub fn process_tasks(&mut self, tasks: Vec) { + for task in tasks { + self.process_task(task); + } + } + + pub fn process_task(&mut self, task: Task) { + match task { + // Task::SetConfig(config) => { + // self.config = config; + // } + Task::MoveToLayer(layer) => { + if layer < self.config.num_layers { + self.current_layer = layer; + } + } + Task::Draw(element) => { + let drawable = Drawable::new(element, self.current_layer); + self.current_scene_drawables.push(drawable); + } + Task::Animate(animation) => { + self.current_scene_animations + .push((animation, self.current_layer)); + } + Task::RenderScene(duration) => { + let mut scene = Scene::new(duration); + scene.drawables = std::mem::take(&mut self.current_scene_drawables); + scene.animations = std::mem::take(&mut self.current_scene_animations); + self.scenes.push(scene); + } + } + } + + pub fn get_total_duration(&self) -> f64 { + self.scenes.iter().map(|s| s.duration).sum() + } + + pub fn get_frame_at_time(&self, time: f64) -> Option> { + let mut current_time = 0.0; + + for scene in &self.scenes { + if time >= current_time && time < current_time + scene.duration { + let scene_time = time - current_time; + return Some(scene.get_frame_at(scene_time)); + } + current_time += scene.duration; + } + + None + } + + pub fn get_frame_number(&self, frame: u32) -> Option> { + let time = frame as f64 / self.config.fps; + self.get_frame_at_time(time) + } + + pub fn render_frame_to_image(&self, drawables: &[Drawable]) -> RgbaImage { + let mut image = ImageBuffer::new(self.config.width, self.config.height); + + // Fill with background color + for pixel in image.pixels_mut() { + *pixel = self.config.background_color.to_rgba(); + } + + // Render each drawable using imageproc + for drawable in drawables { + self.render_element_to_image(&mut image, &drawable.element); + } + + image + } + + fn render_element_to_image(&self, image: &mut RgbaImage, element: &Element) { + match element { + Element::Line(line) => { + let start = (line.start.x as f32, line.start.y as f32); + let end = (line.end.x as f32, line.end.y as f32); + + // Draw multiple lines for thickness + let thickness = line.thickness.max(1.0) as i32; + for i in -(thickness / 2)..=(thickness / 2) { + for j in -(thickness / 2)..=(thickness / 2) { + let offset_start = (start.0 + i as f32, start.1 + j as f32); + let offset_end = (end.0 + i as f32, end.1 + j as f32); + + if offset_start.0 >= 0.0 + && offset_start.1 >= 0.0 + && offset_end.0 >= 0.0 + && offset_end.1 >= 0.0 + && offset_start.0 < self.config.width as f32 + && offset_start.1 < self.config.height as f32 + && offset_end.0 < self.config.width as f32 + && offset_end.1 < self.config.height as f32 + { + draw_line_segment_mut( + image, + offset_start, + offset_end, + line.color.to_rgba(), + ); + } + } + } + } + Element::Rectangle(rect) => { + let x = rect.position.x as i32; + let y = rect.position.y as i32; + let w = rect.size.width as u32; + let h = rect.size.height as u32; + + if x >= 0 + && y >= 0 + && x + w as i32 <= self.config.width as i32 + && y + h as i32 <= self.config.height as i32 + { + let rectangle = Rect::at(x, y).of_size(w, h); + + if rect.filled { + draw_filled_rect_mut(image, rectangle, rect.color.to_rgba()); + } else { + draw_hollow_rect_mut(image, rectangle, rect.color.to_rgba()); + } + } + } + Element::Circle(circle) => { + let center = (circle.center.x as i32, circle.center.y as i32); + let radius = circle.radius as i32; + + if center.0 - radius >= 0 + && center.1 - radius >= 0 + && center.0 + radius < self.config.width as i32 + && center.1 + radius < self.config.height as i32 + { + if circle.filled { + draw_filled_circle_mut(image, center, radius, circle.color.to_rgba()); + } else { + draw_hollow_circle_mut(image, center, radius, circle.color.to_rgba()); + } + } + } + } + } + + pub fn export_info(&self) -> VideoInfo { + VideoInfo { + width: self.config.width, + height: self.config.height, + fps: self.config.fps, + duration: self.get_total_duration(), + total_frames: (self.get_total_duration() * self.config.fps) as u32, + num_scenes: self.scenes.len(), + } + } + + pub fn render_to_video(&self, output_path: &str) -> Result<(), Box> { + let encoder = VideoEncoder::new(&self.config, output_path)?; + encoder.start()?; + + let total_frames = (self.get_total_duration() * self.config.fps) as u32; + + println!("Rendering {} frames to {}", total_frames, output_path); + + let pb = ProgressBar::new(total_frames as u64); + + pb.set_style( + ProgressStyle::with_template( + "{msg} [{elapsed_precise}>{duration_precise}] {wide_bar} {pos:>4}/{len:4} ({percent_precise}%)", + ) + .unwrap(), + ); + pb.set_message(output_path.to_string()); + + pb.inc(1); + + for frame_num in 0..total_frames { + if let Some(drawables) = self.get_frame_number(frame_num) { + let image = self.render_frame_to_image(&drawables); + encoder.push_frame( + &image, + ((frame_num as f64 / self.config.fps) * 1000.) as u64, + )?; + pb.inc(1); + } + } + + encoder.finish()?; + pb.finish(); + println!("Video rendering complete!"); + Ok(()) + } + + pub fn save_frame( + &self, + frame_num: u32, + output_path: &str, + ) -> Result<(), Box> { + if let Some(drawables) = self.get_frame_number(frame_num) { + let image = self.render_frame_to_image(&drawables); + image.save(output_path)?; + println!("Frame {} saved to {}", frame_num, output_path); + } + Ok(()) + } +} diff --git a/src/renderer2.rs b/src/renderer2.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/renderer2.rs @@ -0,0 +1 @@ +