Files
vectorcraft/crates/render/src/addons/halftone.rs
T
bflatastic f4c245286e Add-ons: Color Effects — live Vector Halftone effect
Effect › Color Effects › Vector Halftone… (addon.color.halftone, also in
Object › Plug-ins › Color Effects) draws its object as the halftone dots
Object › Vector Halftone makes, live: mono (one dot colour, or each dot in
the art's colour under it) or CMYK screens, the five dot shapes,
frequency, angle, invert and clip to the art's outline.

- The screens, dots and caps move to vectorcraft_effects::addons::halftone,
  shared by the command (its behaviour and tests unchanged) and the effect.
- The renderer samples the art (vectorcraft_render::addons::halftone) and
  keeps the dots by the object's content and what it reads from the
  document, so unchanged art isn't sampled again; past 100000 cells per
  screen the object draws as it is.
- Expand Appearance turns the object into its dots, and the SVG, PDF, EPS
  and AI exporters write them as paths.
- Its dialog reuses the command's fields, without Keep Original.

Deferred: stochastic and radial patterns, a dot-gain curve, per-screen
separation settings, type or symbols as dots.
2026-10-30 22:13:33 +07:00

95 lines
4.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.
//! Color Effects' Vector Halftone: the art sampled as the dots read it ([`sample`], which Object ›
//! Vector Halftone uses too), and the art of an object with the live effect ([`art`]), which the
//! renderer draws, and the exporters and Expand Appearance write, in its place.
//!
//! The art is kept by the object's content and what it reads from the document (its symbols,
//! patterns, fonts and colour settings, as the live cache finds them): drawing, measuring or
//! exporting an object that hasn't changed takes the dots made before instead of sampling it again.
use std::sync::{Arc, Mutex, PoisonError};
use vectorcraft_doc::{Document, LayerColor, Node, NodeId};
use vectorcraft_effects::addons::halftone::{Halftone, Samples, art_outline, dot_nodes, split};
use vectorcraft_geom::Rect;
use crate::live::Found;
use crate::{RenderOptions, Renderer, region_pixels};
/// How many halftone arts are kept, the most recently used first.
const KEPT: usize = 8;
/// A halftone art kept: the object it was made for, what that read from the document, and the art
/// (`None`: the object draws as it is).
type Kept = (Node, Found, Option<Node>);
static CACHE: Mutex<Vec<Kept>> = Mutex::new(Vec::new());
#[cfg(test)]
thread_local! {
/// How many times this thread sampled art for the live effect.
pub(crate) static SAMPLED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// `nodes` from `doc` drawn over `region` at `scale` pixels per point, as the dots read them: on
/// white as RGBA for a mono screen, as CMYK ink amounts for CMYK screens (`mono` false). `None`
/// when the pixels couldn't be made.
pub fn sample(doc: &Document, nodes: Vec<Node>, region: Rect, scale: f64, mono: bool, invert: bool) -> Option<Samples> {
let mut tmp = doc.clone();
let mut layer = Node::layer(NodeId(u64::MAX), "halftone", LayerColor::Preset(0));
if let Some(ch) = layer.children_mut() {
*ch = nodes.into_iter().map(Arc::new).collect();
}
tmp.layers = vec![Arc::new(layer)];
let mut r = Renderer::new();
let (w, h) = region_pixels(region, scale);
let px = if mono {
r.render_region(&tmp, region, scale, true).pixels
} else {
r.render_region_inks(&tmp, region, scale, &RenderOptions { skip_templates: true, ..Default::default() })
};
Samples::new(px, (w as usize, h as usize), region, scale, mono, invert)
}
/// The art of `n`, an object with a visible Vector Halftone effect, in `doc`
/// ([`vectorcraft_effects::addons::halftone::halftone_art`]): `n`'s dots as the effect's last
/// visible settings make them from `n` drawn without the effect, over its painted bounds, in a
/// group that takes `n`'s place. `None` without the effect, or when the art has no area or would
/// take too many cells: `n` then draws as it is.
pub fn art(doc: &Document, n: &Node) -> Option<Node> {
let ht = Halftone::of(n)?;
let found = Renderer::new().live_found(doc, &Arc::new(n.clone()));
let mut cache = CACHE.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(at) = cache.iter().position(|(node, was, _)| was.0 == found.0 && node == n) {
let kept = cache.remove(at);
let art = kept.2.clone();
cache.insert(0, kept);
return art;
}
// Sampling draws other objects that may have the effect: not under the lock.
drop(cache);
let art = make(doc, n, &ht);
let mut cache = CACHE.lock().unwrap_or_else(PoisonError::into_inner);
cache.insert(0, (n.clone(), found, art.clone()));
cache.truncate(KEPT);
art
}
/// [`art`] made: `n` sampled and its dots built.
fn make(doc: &Document, n: &Node, ht: &Halftone) -> Option<Node> {
#[cfg(test)]
SAMPLED.with(|c| c.set(c.get() + 1));
let (object, mut group) = split(n);
let bounds = crate::drawn_bounds(doc, &object)?;
let (region, scale) = ht.plan(bounds)?;
let outline = if ht.clip { art_outline(std::slice::from_ref(&object)) } else { None };
let samples = sample(doc, vec![object], region, scale, ht.mono, ht.invert)?;
let (layers, _) = ht.layers(&samples, bounds, outline.as_ref());
if let Some(ch) = group.children_mut() {
*ch = dot_nodes(layers, ht.mono, &mut || n.id);
}
Some(group)
}
#[cfg(test)]
mod tests;