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>
This commit is contained in:
parent
7fb2c47006
commit
c71af3079c
16
ROADMAP.md
16
ROADMAP.md
File diff suppressed because one or more lines are too long
|
|
@ -144,6 +144,7 @@ const WARP: &[&str] = &["Effect", "Warp"];
|
|||
const BLUR: &[&str] = &["Effect", "Blur"];
|
||||
const SHARPEN: &[&str] = &["Effect", "Sharpen"];
|
||||
const PIXELATE: &[&str] = &["Effect", "Pixelate"];
|
||||
const VIDEO: &[&str] = &["Effect", "Video"];
|
||||
const PATHFINDER: &[&str] = &["Effect", "Pathfinder"];
|
||||
const PLUGINS: &[&str] = &["Effect", "Plug-ins"];
|
||||
const ADJUST: &[&str] = &["Effect", "Color Adjustments"];
|
||||
|
|
@ -331,6 +332,20 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
|||
"{cellSize: pt 3..300 (5)} redraws the object as randomly placed dots of its colours on a white canvas",
|
||||
json!({"cellSize": 5.0}),
|
||||
),
|
||||
r(
|
||||
"video.deinterlace",
|
||||
"De-Interlace…",
|
||||
VIDEO,
|
||||
"{eliminate: \"odd\"|\"even\" (\"odd\"; which field lines are taken out), create: \"duplication\"|\"interpolation\" (\"duplication\"; a line taken out becomes a copy of the line above, or the average of the lines above and below)} the field lines are the rows of the document's raster grid (Document Raster Effects Settings › Resolution), numbered from 1 at the top of the page",
|
||||
json!({"eliminate": "odd", "create": "duplication"}),
|
||||
),
|
||||
r(
|
||||
"video.ntscColors",
|
||||
"NTSC Colors",
|
||||
VIDEO,
|
||||
"{} makes colours a television signal can't carry (luma plus chroma past 110 % of white, or luma minus chroma below −20 %) less saturated, keeping their brightness",
|
||||
json!({}),
|
||||
),
|
||||
];
|
||||
v.extend([
|
||||
g(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Pixelate › Color
|
||||
//! Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, and Stylize › Glowing
|
||||
//! Edges): filters over premultiplied RGBA8 pixels.
|
||||
//! Halftone, Crystallize, Mezzotint and Pointillize, Sharpen › Unsharp Mask, Stylize › Glowing
|
||||
//! Edges, and Video › De-Interlace and NTSC Colors): filters over premultiplied RGBA8 pixels.
|
||||
//!
|
||||
//! - Every distance is in document units (points) and becomes pixels through the raster's
|
||||
//! [`PixelSpace::px`], so an effect looks the same at any zoom and at any Document Raster Effects
|
||||
|
|
@ -13,6 +13,7 @@ mod blur;
|
|||
mod edges;
|
||||
mod pixelate;
|
||||
mod sharpen;
|
||||
mod video;
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_geom::{Affine, Point, Rect};
|
||||
|
|
@ -22,7 +23,7 @@ use crate::util::{num, text};
|
|||
pub use pixelate::{MEZZOTINT_TYPES, Mezzotint};
|
||||
|
||||
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
|
||||
pub const PIXEL_EFFECTS: [&str; 8] = [
|
||||
pub const PIXEL_EFFECTS: [&str; 10] = [
|
||||
"blur.radial",
|
||||
"blur.smart",
|
||||
"pixelate.colorHalftone",
|
||||
|
|
@ -31,6 +32,8 @@ pub const PIXEL_EFFECTS: [&str; 8] = [
|
|||
"pixelate.pointillize",
|
||||
"sharpen.unsharpMask",
|
||||
"stylize.glowingEdges",
|
||||
"video.deinterlace",
|
||||
"video.ntscColors",
|
||||
];
|
||||
|
||||
/// A Photoshop-style raster effect with its parameters read and clamped to their ranges. Lengths
|
||||
|
|
@ -59,6 +62,11 @@ pub enum PixelFx {
|
|||
/// Pixelate › Pointillize: random dots about `cell` across of the object's colours on a white
|
||||
/// canvas.
|
||||
Pointillize { cell: f64 },
|
||||
/// Video › De-Interlace: the odd (or `even`) field lines made again from the others, by
|
||||
/// duplication or, with `interpolate`, by averaging.
|
||||
DeInterlace { even: bool, interpolate: bool },
|
||||
/// Video › NTSC Colors: colours a television signal can't carry made less saturated.
|
||||
NtscColors,
|
||||
}
|
||||
|
||||
/// What a raster's colour channels hold, for the filters that treat them apart (Color Halftone
|
||||
|
|
@ -89,6 +97,9 @@ pub struct PixelSpace {
|
|||
pub center: Point,
|
||||
/// What the colour channels hold.
|
||||
pub channels: Channels,
|
||||
/// The height of a line of the document's raster grid (document units: 72 / Document Raster
|
||||
/// Effects Settings › Resolution), the fields De-Interlace works on.
|
||||
pub line: f64,
|
||||
}
|
||||
|
||||
/// Effect `id`'s parameters `p` (defaults merged in) as a [`PixelFx`]; `None` for other effects.
|
||||
|
|
@ -126,6 +137,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
|
|||
"pixelate.crystallize" => PixelFx::Crystallize { cell: num(p, "cellSize", 10.0).clamp(3.0, 300.0) },
|
||||
"pixelate.mezzotint" => PixelFx::Mezzotint { kind: Mezzotint::parse(text(p, "type", "fineDots")) },
|
||||
"pixelate.pointillize" => PixelFx::Pointillize { cell: num(p, "cellSize", 5.0).clamp(3.0, 300.0) },
|
||||
"video.deinterlace" => PixelFx::DeInterlace {
|
||||
even: text(p, "eliminate", "odd").eq_ignore_ascii_case("even"),
|
||||
interpolate: text(p, "create", "duplication").eq_ignore_ascii_case("interpolation"),
|
||||
},
|
||||
"video.ntscColors" => PixelFx::NtscColors,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
@ -143,7 +159,7 @@ impl PixelFx {
|
|||
PixelFx::SmartBlur { radius, .. } => radius,
|
||||
PixelFx::UnsharpMask { .. } => 0.0,
|
||||
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
|
||||
PixelFx::ColorHalftone { .. } | PixelFx::Mezzotint { .. } => 0.0,
|
||||
PixelFx::ColorHalftone { .. } | PixelFx::Mezzotint { .. } | PixelFx::DeInterlace { .. } | PixelFx::NtscColors => 0.0,
|
||||
// A crystal's or dot's point inside the object reaches out by up to its cell.
|
||||
PixelFx::Crystallize { cell } | PixelFx::Pointillize { cell } => 1.5 * cell,
|
||||
}
|
||||
|
|
@ -163,6 +179,9 @@ impl PixelFx {
|
|||
PixelFx::Mezzotint { .. } => Some(0.0),
|
||||
// The dots' points, and the softened colour around them.
|
||||
PixelFx::Pointillize { cell } => Some(2.5 * cell),
|
||||
// The lines above and below.
|
||||
PixelFx::DeInterlace { .. } => Some(video::MAX_LINE),
|
||||
PixelFx::NtscColors => Some(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +199,8 @@ impl PixelFx {
|
|||
PixelFx::Crystallize { cell } => pixelate::crystallize(px, w, h, space, cell),
|
||||
PixelFx::Mezzotint { kind } => pixelate::mezzotint(px, w, h, space, kind),
|
||||
PixelFx::Pointillize { cell } => pixelate::pointillize(px, w, h, space, cell),
|
||||
PixelFx::DeInterlace { even, interpolate } => video::deinterlace(px, w, h, space, even, interpolate),
|
||||
PixelFx::NtscColors => video::ntsc(px),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::{RasterFx, raster_effects};
|
|||
|
||||
/// A `w` × `h` raster whose pixels are document points (origin top-left).
|
||||
fn space(w: usize, h: usize) -> PixelSpace {
|
||||
PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(w as f64 / 2.0, h as f64 / 2.0), channels: Channels::Rgb }
|
||||
PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(w as f64 / 2.0, h as f64 / 2.0), channels: Channels::Rgb, line: 1.0 }
|
||||
}
|
||||
|
||||
fn image(w: usize, h: usize, f: impl Fn(usize, usize) -> [u8; 4]) -> Vec<u8> {
|
||||
|
|
@ -224,7 +224,8 @@ fn lengths_are_document_units() {
|
|||
let edge = |scale: usize| image(40 * scale, 4, move |x, _| grey(if x < 20 * scale { 100 } else { 150 }));
|
||||
let run = |scale: usize| {
|
||||
let mut d = edge(scale);
|
||||
let s = PixelSpace { to_doc: Affine::scale(1.0 / scale as f64), px: 1.0 / scale as f64, center: Point::ZERO, channels: Channels::Rgb };
|
||||
let s =
|
||||
PixelSpace { to_doc: Affine::scale(1.0 / scale as f64), px: 1.0 / scale as f64, center: Point::ZERO, channels: Channels::Rgb, line: 1.0 };
|
||||
fx("sharpen.unsharpMask", json!({"amount": 100, "radius": 2})).apply(&mut d, 40 * scale, 4, &s);
|
||||
(0..40 * scale).filter(|x| at(&d, 40 * scale, *x, 2)[0] < 99).count()
|
||||
};
|
||||
|
|
@ -253,7 +254,7 @@ fn gaussian_and_plane_blurs_keep_their_mass() {
|
|||
fn pixel_timings() {
|
||||
let (w, h) = (2000, 2000);
|
||||
let src = image(w, h, |x, y| [(x % 256) as u8, (y % 256) as u8, ((x ^ y) % 256) as u8, 255]);
|
||||
let s = PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(1000.0, 1000.0), channels: Channels::Rgb };
|
||||
let s = PixelSpace { to_doc: Affine::IDENTITY, px: 1.0, center: Point::new(1000.0, 1000.0), channels: Channels::Rgb, line: 1.0 };
|
||||
let cases = [
|
||||
("blur.radial", json!({"quality": "draft"})),
|
||||
("blur.radial", json!({"quality": "good"})),
|
||||
|
|
@ -477,3 +478,50 @@ fn pixelate_patterns_follow_the_object_at_any_resolution() {
|
|||
assert_eq!(shifted, one, "{id}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Video › De-Interlace: lines 1, 3, 5… (or 2, 4, 6…) are made again from the others, by copying
|
||||
/// the line above or by averaging the lines above and below; the lines are the document's raster
|
||||
/// rows, whatever the raster's own resolution.
|
||||
#[test]
|
||||
fn deinterlace_remakes_one_field_from_the_other() {
|
||||
let (w, h) = (2, 6);
|
||||
// Line n (from 1) is grey 40 n.
|
||||
let src = image(w, h, |_, y| grey(40 * (y as u8 + 1)));
|
||||
let run = |p: serde_json::Value, s: &PixelSpace| {
|
||||
let mut d = src.clone();
|
||||
fx("video.deinterlace", p).apply(&mut d, w, h, s);
|
||||
(0..h).map(|y| at(&d, w, 1, y)[0]).collect::<Vec<_>>()
|
||||
};
|
||||
let s = space(w, h);
|
||||
// Odd lines (1, 3, 5) copy the line above; line 1 has none, so it takes line 2.
|
||||
assert_eq!(run(json!({}), &s), [80, 80, 80, 160, 160, 240]);
|
||||
assert_eq!(run(json!({"eliminate": "even"}), &s), [40, 40, 120, 120, 200, 200]);
|
||||
// Interpolated: the average of the lines around (one side at the edges).
|
||||
assert_eq!(run(json!({"eliminate": "even", "create": "interpolation"}), &s), [40, 80, 120, 160, 200, 200]);
|
||||
// Lines two pixels tall: lines 1 and 3 (pixels 0–1 and 4–5) copy their neighbour, row by row.
|
||||
let coarse = PixelSpace { line: 2.0, ..space(w, h) };
|
||||
assert_eq!(run(json!({}), &coarse), [120, 160, 120, 160, 120, 160]);
|
||||
// A damaged space changes nothing.
|
||||
assert_eq!(run(json!({}), &PixelSpace { line: f64::NAN, ..space(w, h) }), [40, 80, 120, 160, 200, 240]);
|
||||
}
|
||||
|
||||
/// Video › NTSC Colors: saturated yellow and cyan lose saturation (their composite signal is too
|
||||
/// strong) but keep their brightness; greys, dark colours and transparency stay as they are.
|
||||
#[test]
|
||||
fn ntsc_colors_tame_only_colours_too_strong_for_the_signal() {
|
||||
let (w, h) = (5, 1);
|
||||
let px = [[255, 255, 0, 255], [0, 255, 255, 255], [128, 128, 128, 255], [100, 20, 20, 255], [0, 0, 0, 0]];
|
||||
let mut d: Vec<u8> = px.concat();
|
||||
fx("video.ntscColors", json!({})).apply(&mut d, w, h, &space(w, h));
|
||||
let luma = |p: [u8; 4]| 0.299 * f64::from(p[0]) + 0.587 * f64::from(p[1]) + 0.114 * f64::from(p[2]);
|
||||
for (x, &a) in px.iter().enumerate() {
|
||||
let b = at(&d, w, x, 0);
|
||||
if x >= 2 {
|
||||
assert_eq!(b, a, "{x}: unchanged");
|
||||
continue;
|
||||
}
|
||||
let spread = |p: [u8; 4]| p[..3].iter().max().unwrap() - p[..3].iter().min().unwrap();
|
||||
assert!(spread(b) < spread(a), "{x}: less saturated {b:?}");
|
||||
assert!((luma(a) - luma(b)).abs() < 3.0, "{x}: as bright {a:?} {b:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
//! 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]];
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,10 @@ const EFFECTS: [(&str, &str); 3] = [
|
|||
const PIXELATE: [(&str, &str); 4] =
|
||||
[("pixelate.colorHalftone", "{}"), ("pixelate.crystallize", "{}"), ("pixelate.mezzotint", "{}"), ("pixelate.pointillize", "{}")];
|
||||
|
||||
/// Effect › Video (NTSC Colors leaves the striped art's legal colours as they are: see
|
||||
/// `ntsc_colors_draws_saturated_yellow_safer`).
|
||||
const VIDEO: [(&str, &str); 1] = [("video.deinterlace", r#"{"create": "interpolation"}"#)];
|
||||
|
||||
/// A group of a red square and a darker red stripe across it; returns the group's id.
|
||||
fn striped() -> (Session, u64) {
|
||||
let mut s = Session::new();
|
||||
|
|
@ -57,9 +61,11 @@ fn listed_applied_edited_and_undone() {
|
|||
let menus: Vec<Value> =
|
||||
EFFECTS.iter().map(|(id, _)| list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == *id).unwrap()["menu"].clone()).collect();
|
||||
assert_eq!(menus, [json!(["Effect", "Blur"]), json!(["Effect", "Blur"]), json!(["Effect", "Sharpen"])]);
|
||||
for (id, _) in PIXELATE {
|
||||
let e = list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == id).unwrap_or_else(|| panic!("{id} listed"));
|
||||
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", "Pixelate"])), "{id}");
|
||||
for (group, menu) in [(PIXELATE.as_slice(), "Pixelate"), (VIDEO.as_slice(), "Video"), ([("video.ntscColors", "{}")].as_slice(), "Video")] {
|
||||
for (id, _) in group {
|
||||
let e = list["catalog"].as_array().unwrap().iter().find(|e| e["id"] == *id).unwrap_or_else(|| panic!("{id} listed"));
|
||||
assert_eq!((&e["raster"], &e["menu"]), (&json!(true), &json!(["Effect", menu])), "{id}");
|
||||
}
|
||||
}
|
||||
// Junk parameters are stored as given and read clamped; the canvas still draws.
|
||||
for (id, _) in EFFECTS {
|
||||
|
|
@ -80,7 +86,7 @@ fn listed_applied_edited_and_undone() {
|
|||
|
||||
#[test]
|
||||
fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE) {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(VIDEO) {
|
||||
let (mut s, g) = striped();
|
||||
let plain = render(&s);
|
||||
let params: Value = serde_json::from_str(params).unwrap();
|
||||
|
|
@ -109,7 +115,7 @@ fn spin_blur_reaches_past_the_corners() {
|
|||
|
||||
#[test]
|
||||
fn pdf_and_svg_write_the_effected_object_as_an_image() {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE) {
|
||||
for (id, params) in EFFECTS.into_iter().chain(PIXELATE).chain(VIDEO) {
|
||||
let (mut s, g) = striped();
|
||||
let params: Value = serde_json::from_str(params).unwrap();
|
||||
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": params})).unwrap();
|
||||
|
|
@ -140,3 +146,22 @@ fn expand_appearance_makes_an_image() {
|
|||
s.execute("effect.expandAppearance", &json!({"ids": [g]})).unwrap();
|
||||
assert!(matches!(s.doc().unwrap().doc.node(NodeId(g)).unwrap().kind, NodeKind::Image(_)));
|
||||
}
|
||||
|
||||
/// Video › NTSC Colors on the canvas: a saturated yellow, too strong for a television signal, draws
|
||||
/// less saturated; the effect is listed and applies without options.
|
||||
#[test]
|
||||
fn ntsc_colors_draws_saturated_yellow_safer() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
let id = s.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setFill", &json!({"color": "#ffff00", "ids": [id]})).unwrap();
|
||||
s.execute("paint.setStroke", &json!({"none": true, "ids": [id]})).unwrap();
|
||||
let before = render(&s);
|
||||
s.execute("effect.apply", &json!({"effect": "video.ntscColors", "ids": [id]})).unwrap();
|
||||
let after = render(&s);
|
||||
let px = |r: &Rendered| r.pixel(100, 100);
|
||||
let (a, b) = (px(&before), px(&after));
|
||||
assert_eq!(&a[..3], &[255, 255, 0]);
|
||||
assert!(b[2] > 20 && b[0] < 255 && b[3] == 255, "less saturated: {b:?}");
|
||||
assert_eq!(after.pixels, render(&s).pixels, "deterministic");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,6 +282,8 @@ fn structured_junk() {
|
|||
("effect.apply", json!({"effect": "pixelate.crystallize", "params": {"cellSize": -1e308}})),
|
||||
("effect.apply", json!({"effect": "pixelate.mezzotint", "params": {"type": 5}})),
|
||||
("effect.apply", json!({"effect": "pixelate.pointillize", "params": {"cellSize": "NaN"}})),
|
||||
("effect.apply", json!({"effect": "video.deinterlace", "params": {"eliminate": 7, "create": ["x"]}})),
|
||||
("effect.apply", json!({"effect": "video.ntscColors", "params": {"junk": 1e308}})),
|
||||
("effect.apply", json!({"effect": "no.such.effect"})),
|
||||
("effect.remove", json!({"index": 99})),
|
||||
("effect.setParams", json!({"index": 0, "params": null})),
|
||||
|
|
|
|||
|
|
@ -601,7 +601,9 @@ impl Renderer {
|
|||
Ink::Display if f.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk => effects::pixel::Channels::Cmyk,
|
||||
Ink::Display => effects::pixel::Channels::Rgb,
|
||||
};
|
||||
let space = effects::PixelSpace { to_doc, px, center: content.reach.center(), channels };
|
||||
// De-Interlace's field lines: the rows of the document's raster grid (1 to 2400 ppi).
|
||||
let ppi = Some(f.doc.raster_effects_ppi).filter(|v| v.is_finite()).unwrap_or(72.0).clamp(1.0, 2400.0);
|
||||
let space = effects::PixelSpace { to_doc, px, center: content.reach.center(), channels, line: 72.0 / ppi };
|
||||
let (wz, hz) = (wu as usize, hu as usize);
|
||||
for fx in chain {
|
||||
match fx {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ fn choices(effect: &str, key: &str) -> Option<form::Choices> {
|
|||
("blur.radial", "quality") => Some(&[("Draft", "draft"), ("Good", "good"), ("Best", "best")]),
|
||||
("blur.smart", "quality") => Some(&[("Low", "low"), ("Medium", "medium"), ("High", "high")]),
|
||||
("pixelate.mezzotint", "type") => Some(&vectorcraft_effects::pixel::MEZZOTINT_TYPES),
|
||||
("video.deinterlace", "eliminate") => Some(&[("Odd Fields", "odd"), ("Even Fields", "even")]),
|
||||
("video.deinterlace", "create") => Some(&[("Duplication", "duplication"), ("Interpolation", "interpolation")]),
|
||||
("path.offsetPath", "joins") => Some(&[("Miter", "miter"), ("Round", "round"), ("Bevel", "bevel")]),
|
||||
("distort.roughen" | "distort.zigZag", "points") => Some(&[("Smooth", "smooth"), ("Corner", "corner")]),
|
||||
_ => None,
|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ pub(super) fn humanize(k: &str) -> String {
|
|||
"Channel4" => "Channel 4:".into(),
|
||||
"Include Cmy Blacks" => "Include Blacks with CMY:".into(),
|
||||
"Align To Path" => "Align to Path:".into(),
|
||||
"Create" => "Create New Fields by:".into(),
|
||||
_ => format!("{s}:"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -657,3 +657,6 @@
|
|||
Crystallize… Krystalizace…
|
||||
Mezzotint… Mezzotinta…
|
||||
Pointillize… Pointilismus…
|
||||
Video Video
|
||||
De-Interlace… Odstranit prokládání…
|
||||
NTSC Colors Barvy NTSC
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3871,3 +3871,13 @@ axis Both Ambos
|
|||
@msg the PSD file is cut short el archivo PSD está incompleto
|
||||
@msg the PSD file's header is damaged el encabezado del archivo PSD está dañado
|
||||
@msg the PSD file's image data can't be read: {e} no se pueden leer los datos de imagen del archivo PSD: {e}
|
||||
Video Video
|
||||
De-Interlace… Desentrelazar…
|
||||
De-Interlace Desentrelazar
|
||||
NTSC Colors Colores NTSC
|
||||
Eliminate Eliminar
|
||||
Create New Fields by Crear campos nuevos por
|
||||
Odd Fields Campos impares
|
||||
Even Fields Campos pares
|
||||
Duplication Duplicación
|
||||
Interpolation Interpolación
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3882,3 +3882,13 @@ axis Both Les deux
|
|||
@msg the PSD file is cut short le fichier PSD est tronqué
|
||||
@msg the PSD file's header is damaged l'en-tête du fichier PSD est endommagé
|
||||
@msg the PSD file's image data can't be read: {e} les données d'image du fichier PSD ne peuvent pas être lues : {e}
|
||||
Video Vidéo
|
||||
De-Interlace… Désentrelacement…
|
||||
De-Interlace Désentrelacement
|
||||
NTSC Colors Couleurs NTSC
|
||||
Eliminate Supprimer
|
||||
Create New Fields by Créer des trames par
|
||||
Odd Fields Trames impaires
|
||||
Even Fields Trames paires
|
||||
Duplication Duplication
|
||||
Interpolation Interpolation
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3871,3 +3871,13 @@ axis Both Entrambi
|
|||
@msg the PSD file is cut short il file PSD è troncato
|
||||
@msg the PSD file's header is damaged l'intestazione del file PSD è danneggiata
|
||||
@msg the PSD file's image data can't be read: {e} i dati immagine del file PSD non possono essere letti: {e}
|
||||
Video Video
|
||||
De-Interlace… Deinterlaccia…
|
||||
De-Interlace Deinterlaccia
|
||||
NTSC Colors Colori NTSC
|
||||
Eliminate Elimina
|
||||
Create New Fields by Crea nuovi campi per
|
||||
Odd Fields Campi dispari
|
||||
Even Fields Campi pari
|
||||
Duplication Duplicazione
|
||||
Interpolation Interpolazione
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3863,3 +3863,13 @@ theme Appearance 外観
|
|||
@msg the PSD file is cut short PSD ファイルが途中で切れています
|
||||
@msg the PSD file's header is damaged PSD ファイルのヘッダーが破損しています
|
||||
@msg the PSD file's image data can't be read: {e} PSD ファイルの画像データを読み込めません:{e}
|
||||
Video ビデオ
|
||||
De-Interlace… インターレース解除…
|
||||
De-Interlace インターレース解除
|
||||
NTSC Colors NTSC カラー
|
||||
Eliminate 削除
|
||||
Create New Fields by 新規フィールドの作成
|
||||
Odd Fields 奇数フィールド
|
||||
Even Fields 偶数フィールド
|
||||
Duplication 複製
|
||||
Interpolation 補間
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2718,3 +2718,6 @@
|
|||
Crystallize… Cristalizar…
|
||||
Mezzotint… Meia-Tinta…
|
||||
Pointillize… Pontilhar…
|
||||
Video Vídeo
|
||||
De-Interlace… Desentrelaçar…
|
||||
NTSC Colors Cores NTSC
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3869,3 +3869,13 @@ axis Both Оба
|
|||
@msg the PSD file is cut short файл PSD обрезан
|
||||
@msg the PSD file's header is damaged заголовок файла PSD повреждён
|
||||
@msg the PSD file's image data can't be read: {e} не удаётся прочитать данные изображения файла PSD: {e}
|
||||
Video Видео
|
||||
De-Interlace… Устранение чересстрочности…
|
||||
De-Interlace Устранение чересстрочности
|
||||
NTSC Colors Цвета NTSC
|
||||
Eliminate Удалить
|
||||
Create New Fields by Создать новые поля
|
||||
Odd Fields Нечётные поля
|
||||
Even Fields Чётные поля
|
||||
Duplication Дублированием
|
||||
Interpolation Интерполяцией
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3854,3 +3854,13 @@ axis Both Обидві
|
|||
@msg the PSD file is cut short файл PSD обрізаний
|
||||
@msg the PSD file's header is damaged заголовок файлу PSD пошкоджено
|
||||
@msg the PSD file's image data can't be read: {e} не вдається прочитати дані зображення файлу PSD: {e}
|
||||
Video Відео
|
||||
De-Interlace… Усунення черезрядковості…
|
||||
De-Interlace Усунення черезрядковості
|
||||
NTSC Colors Кольори NTSC
|
||||
Eliminate Вилучити
|
||||
Create New Fields by Створити нові поля
|
||||
Odd Fields Непарні поля
|
||||
Even Fields Парні поля
|
||||
Duplication Дублюванням
|
||||
Interpolation Інтерполяцією
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -3504,3 +3504,13 @@ axis Both 两者
|
|||
Draft 草稿
|
||||
Good 良好
|
||||
Best 最佳
|
||||
Video 视频
|
||||
De-Interlace… 逐行…
|
||||
De-Interlace 逐行
|
||||
NTSC Colors NTSC 颜色
|
||||
Eliminate 消除
|
||||
Create New Fields by 创建新场方式
|
||||
Odd Fields 奇数场
|
||||
Even Fields 偶数场
|
||||
Duplication 复制
|
||||
Interpolation 插值
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -3282,3 +3282,13 @@ axis Both 兩者
|
|||
Draft 草稿
|
||||
Good 良好
|
||||
Best 最佳
|
||||
Video 視訊
|
||||
De-Interlace… 反交錯…
|
||||
De-Interlace 反交錯
|
||||
NTSC Colors NTSC 色彩
|
||||
Eliminate 消除
|
||||
Create New Fields by 建立新圖場以
|
||||
Odd Fields 奇數圖場
|
||||
Even Fields 偶數圖場
|
||||
Duplication 複製
|
||||
Interpolation 內插
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3305,8 +3305,8 @@ fn font_items(english_names: bool) -> Vec<Item> {
|
|||
|
||||
/// The raster effects' submenus of the Effect menu (the Photoshop-style effects, below the
|
||||
/// vector effects): (submenu, the catalogue's menu path of its effects).
|
||||
const RASTER_MENUS: [(&str, &[&str]); 3] =
|
||||
[("Blur", &["Effect", "Blur"]), ("Pixelate", &["Effect", "Pixelate"]), ("Sharpen", &["Effect", "Sharpen"])];
|
||||
const RASTER_MENUS: [(&str, &[&str]); 4] =
|
||||
[("Blur", &["Effect", "Blur"]), ("Pixelate", &["Effect", "Pixelate"]), ("Sharpen", &["Effect", "Sharpen"]), ("Video", &["Effect", "Video"])];
|
||||
|
||||
/// The Effect menu, built from the effects catalogue (vector effects), plus raster effects.
|
||||
fn effect_menu() -> Vec<Item> {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,20 @@ fn effect_menu_lists_the_pixelate_filters_between_blur_and_sharpen() {
|
|||
assert!(at("Blur") < at("Pixelate") && at("Pixelate") < at("Sharpen"));
|
||||
}
|
||||
|
||||
/// Effect › Video, after Sharpen: De-Interlace opens its dialog, NTSC Colors (no options) applies.
|
||||
#[test]
|
||||
fn effect_menu_lists_the_video_filters_last() {
|
||||
let app = app();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let video: Vec<_> = entries.iter().filter(|e| e.path == ["Effect", "Video"]).collect();
|
||||
let items: Vec<(&str, Option<&str>)> = video.iter().map(|e| (e.label.as_str(), e.command.as_deref())).collect();
|
||||
assert_eq!(items, [("De-Interlace…", Some("effect.dialog")), ("NTSC Colors", Some("effect.apply"))]);
|
||||
let raster: Vec<&str> =
|
||||
entries.iter().filter_map(|e| e.path.get(1).filter(|_| e.path.len() == 2 && e.path[0] == "Effect")).map(String::as_str).collect();
|
||||
let at = |name: &str| raster.iter().position(|s| *s == name).unwrap();
|
||||
assert!(at("Sharpen") < at("Video"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_submenus_are_disabled_placeholders() {
|
||||
let app = app();
|
||||
|
|
|
|||
Loading…
Reference in New Issue