Skip to content

Commit 364428d

Browse files
committed
feat: migrate h256 to ffmpeg encoder
1 parent 66a5030 commit 364428d

4 files changed

Lines changed: 65 additions & 13 deletions

File tree

src/components/shape.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ impl Widget for Shape {
8585
};
8686
if let Some(shader) = shader {
8787
paint.set_shader(shader);
88+
paint.set_dither(true);
8889
}
8990
paint
9091
}

src/encode/video.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,7 @@ pub fn encode_with_ffmpeg(
936936
let mut cmd = std::process::Command::new("ffmpeg");
937937
cmd.args([
938938
"-y",
939+
"-loglevel", "error",
939940
"-f", "rawvideo",
940941
"-pixel_format", pix_fmt,
941942
"-video_size", &format!("{}x{}", width, height),
@@ -969,8 +970,8 @@ pub fn encode_with_ffmpeg(
969970
}
970971
}
971972
_ => {
972-
// h264
973-
cmd.args(["-c:v", "libx264", "-crf", &crf_val.to_string(), "-preset", "medium", "-pix_fmt", "yuv420p"]);
973+
// h264 — use 10-bit for smoother dark gradients
974+
cmd.args(["-c:v", "libx264", "-crf", &crf_val.to_string(), "-preset", "medium", "-profile:v", "high10", "-pix_fmt", "yuv420p10le"]);
974975
}
975976
}
976977

src/engine/render_v2.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,10 +1168,16 @@ fn draw_bg_gradient_shift(
11681168
return;
11691169
}
11701170

1171-
let colors: Vec<skia_safe::Color4f> = bg.colors.iter().map(|c| color4f_from_hex(c)).collect();
1171+
let base_colors: Vec<skia_safe::Color4f> = bg.colors.iter().map(|c| color4f_from_hex(c)).collect();
11721172
let angle = (bg.speed * time) % 360.0;
11731173
let rad = angle.to_radians();
11741174

1175+
// Interpolate in linear color space to reduce banding on dark gradients
1176+
let linear_cs = skia_safe::ColorSpace::new_srgb_linear();
1177+
1178+
// Subdivide color stops (16 intermediate steps between each pair) for smoother gradients
1179+
let (colors, positions) = subdivide_gradient_stops(&base_colors, 16);
1180+
11751181
let shader = match bg.gradient_type {
11761182
GradientType::Linear => {
11771183
let cx = width / 2.0;
@@ -1181,8 +1187,8 @@ fn draw_bg_gradient_shift(
11811187
let end = Point::new(cx + rad.cos() * half_diag, cy + rad.sin() * half_diag);
11821188
skia_safe::shader::Shader::linear_gradient(
11831189
(start, end),
1184-
GradientShaderColors::ColorsInSpace(&colors, Some(skia_safe::ColorSpace::new_srgb())),
1185-
None,
1190+
GradientShaderColors::ColorsInSpace(&colors, Some(linear_cs)),
1191+
Some(&positions[..]),
11861192
skia_safe::TileMode::Clamp,
11871193
None,
11881194
None,
@@ -1194,8 +1200,8 @@ fn draw_bg_gradient_shift(
11941200
skia_safe::shader::Shader::radial_gradient(
11951201
center,
11961202
radius,
1197-
GradientShaderColors::ColorsInSpace(&colors, Some(skia_safe::ColorSpace::new_srgb())),
1198-
None,
1203+
GradientShaderColors::ColorsInSpace(&colors, Some(linear_cs)),
1204+
Some(&positions[..]),
11991205
skia_safe::TileMode::Clamp,
12001206
None,
12011207
None,
@@ -1206,10 +1212,49 @@ fn draw_bg_gradient_shift(
12061212
if let Some(shader) = shader {
12071213
let mut paint = Paint::default();
12081214
paint.set_shader(shader);
1215+
paint.set_dither(true);
12091216
canvas.draw_rect(skia_safe::Rect::from_wh(width, height), &paint);
12101217
}
12111218
}
12121219

1220+
/// Subdivide gradient color stops by inserting intermediate interpolated colors.
1221+
/// Returns (colors, positions) with `subdivisions` extra stops between each original pair.
1222+
fn subdivide_gradient_stops(
1223+
colors: &[skia_safe::Color4f],
1224+
subdivisions: u32,
1225+
) -> (Vec<skia_safe::Color4f>, Vec<f32>) {
1226+
let n = colors.len();
1227+
if n < 2 {
1228+
return (colors.to_vec(), vec![0.0]);
1229+
}
1230+
let total = (n - 1) * subdivisions as usize + n;
1231+
let mut out_colors = Vec::with_capacity(total);
1232+
let mut out_pos = Vec::with_capacity(total);
1233+
let seg = (n - 1) as f32;
1234+
1235+
for i in 0..n - 1 {
1236+
let c0 = &colors[i];
1237+
let c1 = &colors[i + 1];
1238+
let steps = subdivisions + 1;
1239+
for s in 0..steps {
1240+
let t = s as f32 / steps as f32;
1241+
let global_t = (i as f32 + t) / seg;
1242+
out_colors.push(skia_safe::Color4f {
1243+
r: c0.r + (c1.r - c0.r) * t,
1244+
g: c0.g + (c1.g - c0.g) * t,
1245+
b: c0.b + (c1.b - c0.b) * t,
1246+
a: c0.a + (c1.a - c0.a) * t,
1247+
});
1248+
out_pos.push(global_t);
1249+
}
1250+
}
1251+
// Last color
1252+
out_colors.push(colors[n - 1]);
1253+
out_pos.push(1.0);
1254+
1255+
(out_colors, out_pos)
1256+
}
1257+
12131258
/// Soft colored glow zones (halo preset).
12141259
fn draw_bg_halo(
12151260
canvas: &Canvas,

src/main.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,12 +260,17 @@ fn cmd_render(
260260
encode::encode_raw_stdout(&scenario, false)?;
261261
}
262262
_ => {
263-
// Check if we need FFmpeg (for h265, vp9, prores, webm, mov)
264-
let use_ffmpeg = codec.as_deref().map_or(false, |c| c != "h264")
265-
|| matches!(fmt, "webm" | "mov")
266-
|| transparent;
267-
268-
if use_ffmpeg {
263+
// Try FFmpeg first (supports 10-bit H.264, all codecs)
264+
// Fall back to built-in openh264 encoder if FFmpeg is not available
265+
let ffmpeg_available = std::process::Command::new("ffmpeg")
266+
.arg("-version")
267+
.stdout(std::process::Stdio::null())
268+
.stderr(std::process::Stdio::null())
269+
.status()
270+
.map(|s| s.success())
271+
.unwrap_or(false);
272+
273+
if ffmpeg_available {
269274
encode::encode_with_ffmpeg(
270275
&scenario,
271276
output.to_str().unwrap(),

0 commit comments

Comments
 (0)