Initial commit

This commit is contained in:
Michael Mikovsky
2025-07-30 14:02:34 -06:00
commit 309c94dec6
32 changed files with 2085 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+14
View File
@@ -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
+22
View File
@@ -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"
+21
View File
@@ -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.
+2
View File
@@ -0,0 +1,2 @@
# luavid
Lua based video editing
+103
View File
@@ -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
+281
View File
@@ -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
+43
View File
@@ -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)
BIN
View File
Binary file not shown.
+64
View File
@@ -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<u8> {
Rgba([self.r, self.g, self.b, self.a])
}
}
impl FromLua for Color {
fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Color {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
+102
View File
@@ -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<Task>,
}
// impl LuaRenderer {
// fn new(lua: &Lua) -> Result<Self, LuaError> {
// 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<Task>, 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<F, A, R>(
lua: &LuaEnv,
this: &Arc<Mutex<Self>>,
name: &str,
func: F,
) -> crate::Result<()>
where
F: Fn(&Arc<Mutex<Self>>, 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<Mutex<Self>>, layer: usize) {
this.lock().unwrap().tasks.push(Task::MoveToLayer(layer));
}
fn draw(this: &Arc<Mutex<Self>>, element: Element) {
this.lock().unwrap().tasks.push(Task::Draw(element));
}
fn animate(this: &Arc<Mutex<Self>>, animation: Animation) {
this.lock().unwrap().tasks.push(Task::Animate(animation));
}
fn render(this: &Arc<Mutex<Self>>, duration: f64) {
this.lock().unwrap().tasks.push(Task::RenderScene(duration));
}
}
+29
View File
@@ -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<F, A, R>(&self, name: &str, func: F) -> crate::Result<()>
where
F: Fn(&Lua, A) -> mlua::Result<R> + 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(())
}
}
+5
View File
@@ -0,0 +1,5 @@
mod decoder;
mod lua_env;
pub use self::decoder::LuaDecoder;
pub use self::lua_env::LuaEnv;
+103
View File
@@ -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<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
/// 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::<MultiValue>() {
// Ok(values) => {
// editor.add_history_entry(line).unwrap();
// println!(
// "{}",
// values
// .iter()
// .map(|value| format!("{:#?}", value))
// .collect::<Vec<_>>()
// .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;
// }
// }
// }
// }
// }
+112
View File
@@ -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<Task> {
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<dyn std::error::Error>> {
// 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(())
}
+9
View File
@@ -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;
}
+113
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Animation {
fn add_fields<F: mlua::UserDataFields<Self>>(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(())
}
+64
View File
@@ -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<u8> {
Rgba([self.r, self.g, self.b, self.a])
}
}
impl FromLua for Color {
fn from_lua(value: mlua::Value, _: &Lua) -> mlua::Result<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Color {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
+62
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Config {
fn add_methods<M: UserDataMethods<Self>>(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(())
// }
+80
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for EasingFunction {
fn add_methods<M: UserDataMethods<Self>>(_: &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(())
}
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for CircleElement {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for LineElement {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
+40
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Element {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {}
}
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for RectangleElement {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
+19
View File
@@ -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;
+59
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Point {
fn add_methods<M: UserDataMethods<Self>>(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<F: mlua::UserDataFields<Self>>(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(())
}
+40
View File
@@ -0,0 +1,40 @@
use crate::renderer::prelude::*;
#[derive(Debug, Clone)]
pub struct Scene {
pub drawables: Vec<Drawable>,
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<Drawable> {
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
}
}
+54
View File
@@ -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<Self> {
match value {
mlua::Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Size {
fn add_methods<M: UserDataMethods<Self>>(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(())
}
+10
View File
@@ -0,0 +1,10 @@
use crate::renderer::prelude::*;
#[derive(Debug, Clone)]
pub enum Task {
// SetConfig(Config),
MoveToLayer(usize),
Draw(Element),
Animate(Animation),
RenderScene(f64),
}
+136
View File
@@ -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<Self, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}
}
+247
View File
@@ -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<Scene>,
pub current_scene_drawables: Vec<Drawable>,
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<Task>) {
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<Vec<Drawable>> {
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<Vec<Drawable>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}
}
+1
View File
@@ -0,0 +1 @@