Files
vectorcraft/crates/effects/src/pixel/video.rs
T
bflatasticandClaude Opus 5.5 c71af3079c Part of gap 2: Effect › Video (De-Interlace, NTSC Colors)
Adds the Video group to the raster effects, after Sharpen:

- De-Interlace…: Eliminate Odd or Even Fields, Create New Fields by
  Duplication or Interpolation. The fields are the rows of the document's
  raster grid (Document Raster Effects Settings › Resolution, carried to
  the filters as PixelSpace::line) counted from the top of the page, so
  the result is the same at any zoom and moves with the page. A line
  taken out copies the line above (the one below at the top), or takes
  the average of the two.
- NTSC Colors (no options, applied at once): colours a composite
  television signal can't carry, with luma plus chroma past 110 % of white
  or luma minus chroma below -20 % in YIQ, have their chroma cut to fit,
  keeping their luma. Saturated yellow, cyan and pure red lose some
  saturation; legal colours stay as they are.

Catalogue rows with parameter docs, dialog dropdowns, menu, translations,
tests (effects, canvas, command sweep junk, menu) and ROADMAP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:05:56 +07:00

75 lines
3.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Effect › Video: De-Interlace and NTSC Colors.
use vectorcraft_geom::Point;
use super::PixelSpace;
/// The tallest field line De-Interlace reads across (document units): one pixel at the lowest
/// document raster resolution, 1 ppi.
pub(super) const MAX_LINE: f64 = 72.0;
/// De-Interlace: the odd (or, with `even`, the even) fields of the image are made again from the
/// others. The fields are the lines of the document's raster grid (each [`PixelSpace::line`]
/// tall, numbered from 1 at the top of the page): a line taken out becomes a copy of the line
/// above it (the one below at the top), or with `interpolate` the average of the two.
pub(super) fn deinterlace(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpace, even: bool, interpolate: bool) {
let line = space.line;
let to_px = space.to_doc.inverse();
if !(line.is_finite() && line > 0.0 && to_px.is_finite() && space.to_doc.is_finite()) {
return;
}
let src = px.to_vec();
// The pixel holding document point `q`.
let at = |q: Point| {
let p = to_px * q;
(p.x >= 0.0 && p.y >= 0.0 && p.x < w as f64 && p.y < h as f64).then(|| src.get(p.y as usize * w + p.x as usize).copied()).flatten()
};
for (i, out) in px.iter_mut().enumerate().take(w.saturating_mul(h)) {
let q = space.to_doc * Point::new((i % w) as f64 + 0.5, (i / w) as f64 + 0.5);
let k = (q.y / line).floor();
// Line k + 1: odd lines have an even k.
if !k.is_finite() || ((k as i64).rem_euclid(2) == 1) != even {
continue;
}
let (above, below) = (at(Point::new(q.x, q.y - line)), at(Point::new(q.x, q.y + line)));
let made = match (interpolate, above, below) {
(true, Some(a), Some(b)) => Some([0, 1, 2, 3].map(|c| (u16::from(a[c]) + u16::from(b[c])).div_ceil(2) as u8)),
(_, a, b) => a.or(b),
};
if let Some(v) = made {
*out = v;
}
}
}
/// NTSC Colors: colours a television's composite signal can't carry are made less saturated,
/// keeping their brightness. In YIQ, the luma plus the chroma's amplitude must stay within 110 %
/// of white and the luma minus it above −20 %; a colour past either has its chroma cut to fit.
pub(super) fn ntsc(px: &mut [[u8; 4]]) {
for p in px.iter_mut() {
let a = f32::from(p[3]) / 255.0;
if a <= 0.0 {
continue;
}
// Straight colour (the pixels are premultiplied).
let [r, g, b] = [p[0], p[1], p[2]].map(|v| (f32::from(v) / 255.0 / a).min(1.0));
let y = 0.299 * r + 0.587 * g + 0.114 * b;
let (i, q) = (0.596 * r - 0.274 * g - 0.322 * b, 0.211 * r - 0.523 * g + 0.312 * b);
let c = i.hypot(q);
let mut keep = 1.0f32;
if y + c > 1.1 {
keep = keep.min((1.1 - y) / c);
}
if y - c < -0.2 {
keep = keep.min((y + 0.2) / c);
}
if keep >= 1.0 || !keep.is_finite() {
continue;
}
let (i, q) = (i * keep.max(0.0), q * keep.max(0.0));
let rgb = [y + 0.956 * i + 0.621 * q, y - 0.272 * i - 0.647 * q, y - 1.106 * i + 1.703 * q];
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * a * 255.0).round() as u8);
*p = [r, g, b, p[3]];
}
}