mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
Character: Anti-aliasing Method sets how a type object's edges are drawn as pixels
The reference app's Character panel has an anti-aliasing method for type, with the options None, Sharp, Crisp and Strong. VectorCraft had no such control or attribute. The Character panel has an Anti-aliasing Method dropdown under Character Rotation, and text.setFormat takes antiAlias. The method belongs to the whole type object and to every frame of its thread. It sets how the edges of the characters are drawn as pixels: on the canvas, in raster exports and when the type is rasterized. VectorCraft defines the methods. Sharp, the default, draws the edges as before. None paints a pixel only where the type covers at least half of it. Crisp shows a pixel that the type covers by a share c at 2c - 0.5, and Strong shows it at 2c, each kept between 0 and 1. The opacity of the type's paints is kept. In an export that supersamples its art, the type is drawn at the export's own resolution, and the averaging keeps its edges as the method draws them. Type is drawn as Sharp when it holds inline graphics, when its own fills or strokes include a brush stroke or a blend mode other than Normal, when it overprints while Overprint Preview is on, and on the canvas while it shows a substitution highlight. SVG, PDF and EPS export are unchanged. The method is saved only when it is not Sharp. Reset Panel sets Sharp, and it is now one undo step (it was two). Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -1150,6 +1150,44 @@ fn reverse_from_end(path: &mut PathData) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The anti-aliasing method of a type object (Character panel › Anti-aliasing Method): how the
|
||||
/// edges of its characters are drawn when the type is drawn as pixels (the canvas, raster exports,
|
||||
/// rasterizing). SVG, PDF and EPS export ignore it.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TypeAntiAlias {
|
||||
/// Hard edges: a pixel takes the type's color where the type covers at least half of it, and
|
||||
/// nothing elsewhere.
|
||||
None,
|
||||
/// Each edge pixel shows the share of it the type covers.
|
||||
#[default]
|
||||
Sharp,
|
||||
/// An edge pixel covered by a share `c` shows `2c - 0.5`, between 0 and 1: edges about half as
|
||||
/// wide as Sharp's.
|
||||
Crisp,
|
||||
/// An edge pixel covered by a share `c` shows `2c`, at most 1: heavier type.
|
||||
Strong,
|
||||
}
|
||||
|
||||
impl TypeAntiAlias {
|
||||
pub const ALL: [Self; 4] = [Self::None, Self::Sharp, Self::Crisp, Self::Strong];
|
||||
|
||||
/// The id used in command params and files (`none`, `sharp`, `crisp`, `strong`).
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Sharp => "sharp",
|
||||
Self::Crisp => "crisp",
|
||||
Self::Strong => "strong",
|
||||
}
|
||||
}
|
||||
|
||||
/// The method an id names.
|
||||
pub fn from_id(id: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|a| a.id() == id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A text object. `runs` split into paragraphs at `\n`.
|
||||
///
|
||||
/// Paragraph attributes: `para` is paragraph 0's (and, while `paras` is empty, every
|
||||
@@ -1193,6 +1231,11 @@ pub struct TextObject {
|
||||
/// for new type and for documents from before it, and saved only when on.
|
||||
#[serde(default, rename = "opticalMarginAlignment", skip_serializing_if = "crate::skip::is_default")]
|
||||
pub optical_margin: bool,
|
||||
/// Character panel › Anti-aliasing Method: how the type's edges are drawn as pixels. Every
|
||||
/// frame of a thread has the same method. Sharp for new type and for documents from before it,
|
||||
/// and saved only when it is another method.
|
||||
#[serde(default, rename = "antiAlias", skip_serializing_if = "crate::skip::is_default")]
|
||||
pub anti_alias: TypeAntiAlias,
|
||||
/// Cached layout bounds in text space, filled in by the layout engine (not serialized).
|
||||
#[serde(skip)]
|
||||
pub cached_bounds: Option<Rect>,
|
||||
@@ -1259,6 +1302,7 @@ impl TextObject {
|
||||
path_spacing: 0.0,
|
||||
wrap: Vec::new(),
|
||||
optical_margin: false,
|
||||
anti_alias: TypeAntiAlias::Sharp,
|
||||
cached_bounds: None,
|
||||
cached_baselines: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -276,6 +276,8 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
if !doc.text_threads.is_empty() && !restored {
|
||||
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);
|
||||
} else if !doc.text_threads.is_empty() {
|
||||
// Each frame also takes its thread's first frame's anti-aliasing method, as `reflow` gives it.
|
||||
crate::cmd::threads::share_anti_alias(&mut doc);
|
||||
crate::cmd::threads::share_optical_margin(&mut doc);
|
||||
}
|
||||
if let Some(mode) = opts.color_mode.filter(|m| *m != doc.color_mode) {
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), antiAlias?: \"none\"|\"sharp\"|\"crisp\"|\"strong\" (Character panel › Anti-aliasing Method, how the type's edges are drawn as pixels; it sets the whole type object and every frame of its thread, with or without a range; new type: \"sharp\"), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt (±1296), hyphenate?: bool, wordsLongerThan?: letters 1..25 (5), afterFirst?, beforeLast?: letters 1..15 (3), hyphenLimit?: lines 0..25 (0: no limit), hyphenationZone?: pt 0..1296 (0; Single-line Composer only), hyphenateCapitalized?: bool (true) (Paragraph panel menu › Hyphenation…: only words longer than wordsLongerThan break, keeping afterFirst letters before the hyphen and beforeLast after it; at most hyphenLimit lines in a row end with a hyphen; a line whose text already ends within hyphenationZone of its right edge breaks before the next word instead of hyphenating it; the counts are rounded, and every value is clamped to its range), mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), kinsoku?: \"none\"|\"hard\"|\"soft\" (Kinsoku Set: Soft lets 々, ー and small kana start a line), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\" (where characters smaller than the largest on their line line up with it), proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font, as text.setRangeStyle: each axis named set in its units and clamped to its range, null back to the style's own setting), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\"), romanHangingPunctuation?: bool (Paragraph panel menu › Roman Hanging Punctuation: quotation marks, hyphens, full stops and commas starting or ending a line hang outside the margin, asterisks, tildes, ellipses, dashes, colons and semicolons by half; off by default), composer?: \"singleLine\"|\"everyLine\" (line breaking, Every-line by default), start?: byte, end?: byte} (baselineShift is in document points and hScale includes the object transform; groups include their text descendants; with a range: the character attributes style that range and the paragraph attributes apply to the paragraphs it touches; without: all the text)",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -257,6 +257,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"allCaps",
|
||||
"smallCaps",
|
||||
"position",
|
||||
"antiAlias",
|
||||
"leftIndent",
|
||||
"rightIndent",
|
||||
"firstLineIndent",
|
||||
@@ -284,6 +285,15 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !keys.iter().any(|k| p.get(*k).is_some()) {
|
||||
return Err(bad(C, "nothing to change"));
|
||||
}
|
||||
// The anti-aliasing method belongs to the whole type object and to every frame of its thread.
|
||||
let anti_alias = match p.get("antiAlias") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(
|
||||
v.as_str()
|
||||
.and_then(vectorcraft_doc::text::TypeAntiAlias::from_id)
|
||||
.ok_or_else(|| bad(C, "`antiAlias` must be \"none\", \"sharp\", \"crisp\" or \"strong\""))?,
|
||||
),
|
||||
};
|
||||
let composer = match p.get("composer") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(match v.as_str() {
|
||||
@@ -460,6 +470,13 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let scale = num("hScale").map(|v| v.clamp(1.0, 10000.0));
|
||||
super::graph::capture_label_styles(d, &ids, &|st| chars(st, shift, scale));
|
||||
}
|
||||
if let Some(v) = anti_alias {
|
||||
for id in super::threads::with_threads(d, &ids) {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
t.anti_alias = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
|
||||
@@ -839,6 +839,7 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
path_spacing: 0.0,
|
||||
wrap: Vec::new(),
|
||||
optical_margin: false,
|
||||
anti_alias: Default::default(),
|
||||
cached_bounds: None,
|
||||
cached_baselines: Vec::new(),
|
||||
};
|
||||
@@ -1059,6 +1060,7 @@ fn headline_tracking(t: &TextObject, target: f64) -> Option<f64> {
|
||||
path_spacing: 0.0,
|
||||
wrap: Vec::new(),
|
||||
optical_margin: false,
|
||||
anti_alias: Default::default(),
|
||||
cached_bounds: None,
|
||||
cached_baselines: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -157,12 +157,45 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
}
|
||||
}
|
||||
if live.len() >= 2 {
|
||||
if let Some(a) = anti_alias(before, doc, &live) {
|
||||
give_anti_alias(doc, &live, a);
|
||||
}
|
||||
kept.push(live);
|
||||
}
|
||||
}
|
||||
doc.text_threads = kept;
|
||||
}
|
||||
|
||||
/// The anti-aliasing method that every frame of a thread with the frames `live` takes: that of the
|
||||
/// first frame whose method changed between `before` and `doc`, else the first frame's.
|
||||
fn anti_alias(before: &Document, doc: &Document, live: &[NodeId]) -> Option<vectorcraft_doc::text::TypeAntiAlias> {
|
||||
let now = |id: &NodeId| text_of(doc, *id).map(|t| t.anti_alias);
|
||||
let changed = live.iter().find(|id| text_of(before, **id).is_some_and(|t| Some(t.anti_alias) != now(id)));
|
||||
changed.or(live.first()).and_then(now)
|
||||
}
|
||||
|
||||
/// Give each of the frames `live` that has another anti-aliasing method the method `a`.
|
||||
fn give_anti_alias(doc: &mut Document, live: &[NodeId], a: vectorcraft_doc::text::TypeAntiAlias) {
|
||||
for id in live {
|
||||
if text_of(doc, *id).is_some_and(|t| t.anti_alias != a)
|
||||
&& let Some(NodeKind::Text(t)) = doc.node_mut(*id).map(|n| &mut n.kind)
|
||||
{
|
||||
t.anti_alias = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Give every frame of each thread the first frame's anti-aliasing method, without re-flowing the
|
||||
/// story: for a document restored from a file as it was saved.
|
||||
pub(crate) fn share_anti_alias(doc: &mut Document) {
|
||||
for thread in doc.text_threads.clone() {
|
||||
let live: Vec<NodeId> = thread.into_iter().filter(|id| text_of(doc, *id).is_some()).collect();
|
||||
if let Some(a) = anti_alias(doc, doc, &live) {
|
||||
give_anti_alias(doc, &live, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Optical Margin Alignment setting that every frame of a thread with the frames `live` takes:
|
||||
/// that of the first frame whose setting changed between `before` and `doc`, else the first frame's.
|
||||
fn optical_margin(before: &Document, doc: &Document, live: &[NodeId]) -> Option<bool> {
|
||||
@@ -735,6 +768,55 @@ mod tests {
|
||||
assert_eq!([a, b, c].map(|id| optical(&s, id).0), [true; 3]);
|
||||
}
|
||||
|
||||
/// The anti-aliasing method (`text.setFormat {antiAlias}`) belongs to the whole thread and is
|
||||
/// saved only when not Sharp: set in one frame, with or without a range, it is set in every
|
||||
/// frame in one undo step, a frame threaded on takes the thread's method, and a file whose
|
||||
/// frames of one thread differ opens with the first frame's method in all of them.
|
||||
#[test]
|
||||
fn the_anti_aliasing_method_is_set_for_every_frame_of_a_thread() {
|
||||
use vectorcraft_doc::text::TypeAntiAlias::{self, Crisp, None as Hard, Sharp, Strong};
|
||||
let method = |s: &Session, id: u64| text_of(&s.doc().unwrap().doc, NodeId(id)).map(|t| t.anti_alias);
|
||||
let saved =
|
||||
|s: &Session, id: u64| serde_json::to_value(text_of(&s.doc().unwrap().doc, NodeId(id)).unwrap()).unwrap().get("antiAlias").cloned();
|
||||
let (mut s, a, b) = quoted_thread();
|
||||
assert_eq!((method(&s, a), method(&s, b)), (Some(Sharp), Some(Sharp)));
|
||||
assert_eq!(saved(&s, a), None, "Sharp is not saved");
|
||||
s.execute("select.set", &json!({"ids": [b]})).unwrap();
|
||||
let err = s.execute("text.setFormat", &json!({"antiAlias": "smooth"})).unwrap_err().to_string();
|
||||
assert!(err.contains("`antiAlias` must be"), "{err}");
|
||||
s.execute("text.setFormat", &json!({"antiAlias": "crisp"})).unwrap();
|
||||
assert_eq!((method(&s, a), method(&s, b)), (Some(Crisp), Some(Crisp)), "the first frame takes it too");
|
||||
assert_eq!(saved(&s, a), Some(json!("crisp")));
|
||||
// A range of characters sets the whole thread as well.
|
||||
s.execute("text.setFormat", &json!({"ids": [a], "start": 0, "end": 1, "antiAlias": "none"})).unwrap();
|
||||
assert_eq!((method(&s, a), method(&s, b)), (Some(Hard), Some(Hard)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((method(&s, a), method(&s, b)), (Some(Crisp), Some(Crisp)), "one undo step");
|
||||
// A frame threaded on from the last one takes the thread's method.
|
||||
s.execute("text.setFormat", &json!({"ids": [b], "antiAlias": "strong"})).unwrap();
|
||||
let c = new_id(s.execute("text.thread.add", &json!({"from": b, "at": [400, 10]})).unwrap());
|
||||
assert_eq!([a, b, c].map(|id| method(&s, id)), [Some(Strong); 3]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let first: [TypeAntiAlias; 2] = [Crisp, Hard];
|
||||
for (one, name) in [(first, "old.vectorcraft"), ([Hard, Crisp], "old.vectorcraft"), (first, "old.eps"), ([Hard, Crisp], "old.eps")] {
|
||||
for (id, m) in [(a, one[0]), (b, one[1])] {
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let Some(NodeKind::Text(t)) = doc.node_mut(NodeId(id)).map(|n| &mut n.kind) else { panic!("not text") };
|
||||
t.anti_alias = m;
|
||||
}
|
||||
let bytes = if name.ends_with(".eps") {
|
||||
let r = s.execute("document.exportEps", &json!({})).unwrap();
|
||||
vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()
|
||||
} else {
|
||||
vectorcraft_format::save(&s.doc().unwrap().doc, false)
|
||||
};
|
||||
let r = s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
|
||||
assert_eq!(r["restored"], name.ends_with(".eps"), "{r}");
|
||||
assert_eq!(threads(&s), [[a, b]], "{name}");
|
||||
assert_eq!((method(&s, a), method(&s, b)), (Some(one[0]), Some(one[0])), "{name}, first frame {:?}", one[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A file saved with the frames of one thread set differently opens with the first frame's
|
||||
/// setting in every frame: a native file, and an EPS file restored from the document it carries.
|
||||
#[test]
|
||||
|
||||
@@ -160,7 +160,8 @@ fn rasterize_bounds(symbols: &SymbolBoxes, n: &Node, rfx: &[RasterFx]) -> Option
|
||||
|
||||
/// `f` with the content moved by (`dx`, `dy`) document units (a shadow's offset).
|
||||
fn shifted<'a>(f: &Frame<'a>, dx: f64, dy: f64) -> Frame<'a> {
|
||||
Frame { view: f.view * Affine::translate((dx, dy)), visible: f.visible - Vec2::new(dx, dy), ..*f }
|
||||
let moved = Affine::translate((dx, dy));
|
||||
Frame { view: f.view * moved, visible: f.visible - Vec2::new(dx, dy), output: f.output * moved, ..*f }
|
||||
}
|
||||
|
||||
fn node_bezpath(n: &Node) -> Option<BezPath> {
|
||||
@@ -557,7 +558,7 @@ fn pcolor(ink: Ink, c: &vectorcraft_doc::color::Color) -> peniko::Color {
|
||||
|
||||
/// Most pixels the raster of [`Renderer::pixel_content`] holds (a 4K screen's): larger content is
|
||||
/// cropped to the view when its effects only look nearby, else rendered at a lower resolution.
|
||||
const PIXEL_BUDGET: f64 = 8.4e6;
|
||||
pub(crate) const PIXEL_BUDGET: f64 = 8.4e6;
|
||||
|
||||
/// Most Rasterize effects of one stack that [`Renderer::raster_fx`] draws (the last ones): each
|
||||
/// draws the content again inside the one after it.
|
||||
@@ -2051,7 +2052,8 @@ impl Renderer {
|
||||
(true, false) => crate::AntiAlias::Art,
|
||||
};
|
||||
let opts = crate::RenderOptions { anti_alias, ..f.opts.clone() };
|
||||
let frame = Frame { mt: false, view, visible: rect, px: cell, opts: &opts, ..*f };
|
||||
// The image's pixels are the output's.
|
||||
let frame = Frame { mt: false, view, visible: rect, px: cell, opts: &opts, output: view, ..*f };
|
||||
let mut off = frame.offscreen_context(w, h);
|
||||
self.inside_layer(|s| paint(s, &mut off, &frame));
|
||||
off.flush();
|
||||
|
||||
+190
-9
@@ -24,6 +24,7 @@ pub mod proof;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::text::TypeAntiAlias;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, SymbolBoxes, TextObject, TraceView};
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, Rect, Shape};
|
||||
use vello_cpu::kurbo;
|
||||
@@ -580,11 +581,16 @@ struct Frame<'a> {
|
||||
view: Affine,
|
||||
/// Visible region in document coordinates (for culling).
|
||||
visible: Rect,
|
||||
/// Size of one output pixel in document units.
|
||||
/// Size of one of this frame's pixels in document units.
|
||||
px: f64,
|
||||
opts: &'a RenderOptions,
|
||||
/// What colours paint as: screen colours, or one ink plane of a CMYK document (see [`ink`]).
|
||||
ink: Ink,
|
||||
/// Document → pixels of the output being drawn: `view` itself, except while an export draws
|
||||
/// its art at several times its size (the export's own pixels) and in offscreen layers (the
|
||||
/// pixels they are placed back on). Type with an anti-aliasing method other than Sharp is
|
||||
/// drawn on these pixels ([`Renderer::draw_type_edges`]).
|
||||
output: Affine,
|
||||
}
|
||||
|
||||
impl Frame<'_> {
|
||||
@@ -652,18 +658,21 @@ impl Renderer {
|
||||
|
||||
/// Render `doc` into a `width`×`height` image using `view` (document → pixel transform).
|
||||
pub fn render(&mut self, doc: &Document, width: u32, height: u32, view: Affine, opts: &RenderOptions) -> Rendered {
|
||||
self.render_as(doc, width, height, view, opts, false)
|
||||
self.render_as(doc, width, height, view, opts, false, 1)
|
||||
}
|
||||
|
||||
/// [`Self::render`], or with `inks` the ink amounts in the working CMYK space instead of
|
||||
/// screen colours (4 bytes a pixel, see [`Self::render_region_inks`]).
|
||||
fn render_as(&mut self, doc: &Document, width: u32, height: u32, view: Affine, opts: &RenderOptions, inks: bool) -> Rendered {
|
||||
/// screen colours (4 bytes a pixel, see [`Self::render_region_inks`]). `cell` is the side of a
|
||||
/// pixel of the output in pixels of this render: the supersampling factor while an export
|
||||
/// draws its art at several times its size, else 1 ([`Frame::output`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_as(&mut self, doc: &Document, width: u32, height: u32, view: Affine, opts: &RenderOptions, inks: bool, cell: u32) -> Rendered {
|
||||
let start = now();
|
||||
if opts.supersample && opts.anti_alias == AntiAlias::Art && opts.mask_view.is_none() {
|
||||
let (w, h) = (width.clamp(1, u16::MAX as u32), height.clamp(1, u16::MAX as u32));
|
||||
if let Some(k) = supersample_factor(w, h) {
|
||||
let hard = RenderOptions { anti_alias: AntiAlias::None, supersample: false, ..opts.clone() };
|
||||
let big = self.render_as(doc, w * k, h * k, Affine::scale(f64::from(k)) * view, &hard, inks);
|
||||
let big = self.render_as(doc, w * k, h * k, Affine::scale(f64::from(k)) * view, &hard, inks, k);
|
||||
let pixels = downsample(&big.pixels, big.width as usize, big.height as usize, k as usize);
|
||||
self.stats.micros = now().saturating_sub(start);
|
||||
return Rendered { width: w, height: h, pixels };
|
||||
@@ -685,7 +694,8 @@ impl Renderer {
|
||||
let px = 1.0 / view.determinant().abs().sqrt().max(1e-12);
|
||||
let screen = if opts.rich_black { Ink::RichBlack } else { Ink::Display };
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let frame = Frame { mt: threads > 0, doc, symbols: &symbols, view, visible, px, opts, ink: screen };
|
||||
let output = Affine::scale(1.0 / f64::from(cell.max(1))) * view;
|
||||
let frame = Frame { mt: threads > 0, doc, symbols: &symbols, view, visible, px, opts, ink: screen, output };
|
||||
self.begin_frame();
|
||||
// View Opacity Mask: the mask's coverage in greyscale instead of the artwork.
|
||||
if let Some(m) = opts.mask_view.and_then(|id| doc.node(id)).and_then(|n| n.extras().mask.as_deref()) {
|
||||
@@ -850,7 +860,7 @@ impl Renderer {
|
||||
pub fn render_region_inks(&mut self, doc: &Document, region: Rect, scale: f64, opts: &RenderOptions) -> Vec<u8> {
|
||||
let (w, h) = region_pixels(region, scale);
|
||||
let view = Affine::scale(scale) * Affine::translate((-region.x0, -region.y0));
|
||||
self.render_as(doc, w, h, view, opts, true).pixels
|
||||
self.render_as(doc, w, h, view, opts, true, 1).pixels
|
||||
}
|
||||
|
||||
/// Render a single node (thumbnails, previews) fitted into `size`×`size` pixels.
|
||||
@@ -868,7 +878,7 @@ impl Renderer {
|
||||
let mut ctx = single_threaded_context(w, h);
|
||||
let opts = RenderOptions::default();
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts: &opts, ink: Ink::Display };
|
||||
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible, px, opts: &opts, ink: Ink::Display, output: view };
|
||||
self.begin_frame();
|
||||
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
|
||||
self.clip_paths.clear();
|
||||
@@ -911,7 +921,8 @@ impl Renderer {
|
||||
ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, w as f64, w as f64));
|
||||
}
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
let frame = Frame { mt: false, doc, symbols: &symbols, view, visible: b.inflate(1.0, 1.0), px: 1.0 / s, opts, ink: Ink::Display };
|
||||
let frame =
|
||||
Frame { mt: false, doc, symbols: &symbols, view, visible: b.inflate(1.0, 1.0), px: 1.0 / s, opts, ink: Ink::Display, output: view };
|
||||
self.begin_frame();
|
||||
(self.knockout, self.nested, self.backdrop, self.picture) = (false, 0, None, None);
|
||||
self.clip_paths.clear();
|
||||
@@ -1800,6 +1811,9 @@ impl Renderer {
|
||||
self.draw_inlines(ctx, f, t, g);
|
||||
return;
|
||||
}
|
||||
if t.anti_alias != TypeAntiAlias::Sharp && self.draw_type_edges(ctx, f, n, t, g) {
|
||||
return;
|
||||
}
|
||||
let tb = t.xf.transform_rect_bbox(g.bounds);
|
||||
// Overprinting characters darken per draw, like overprinting fills and strokes
|
||||
// ([`proof`]); the object blend mode is Normal here or applied by an enclosing layer.
|
||||
@@ -1861,6 +1875,119 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw type `t` of node `n` (laid out in `g`) with its anti-aliasing method, None, Crisp or
|
||||
/// Strong, on the pixels of the output ([`Frame::output`]): the type is drawn offscreen on those
|
||||
/// pixels and placed back pixel for pixel. With None it is drawn with hard edges, so a pixel
|
||||
/// takes the type's color where the type covers at least half of it. With Crisp and Strong it
|
||||
/// is drawn as Sharp draws it, and again with the characters and the object's own fills and
|
||||
/// strokes in opaque black, which gives the share `c` of each pixel they cover; the pixel's
|
||||
/// color and opacity are then scaled by [`edge_coverage`] of `c` divided by `c`, so the opacity
|
||||
/// of the paints is kept. Where the output's pixels would number more than the frame's there
|
||||
/// and more than [`fx::PIXEL_BUDGET`] (a frame drawn coarser than the output), the frame's own
|
||||
/// pixels are used.
|
||||
///
|
||||
/// Returns false, having drawn nothing, for type that is drawn as Sharp draws it: type with
|
||||
/// inline graphics, type whose own fills or strokes include a brush stroke or a blend mode
|
||||
/// other than Normal, type that overprints while Overprint Preview is on, and type that shows a
|
||||
/// substitution highlight on screen. Drawn offscreen, those would blend with nothing or take
|
||||
/// the method on art that is not the characters'.
|
||||
fn draw_type_edges(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject, g: &TextGeom) -> bool {
|
||||
if proof::overprints(f.opts) && t.runs.iter().any(|r| r.style.overprint_fill || r.style.overprint_stroke) {
|
||||
return false;
|
||||
}
|
||||
let normal = vectorcraft_color::BlendMode::Normal;
|
||||
let brush = |i: &AppearanceItem| matches!(i, AppearanceItem::Stroke(st) if st.brush.is_some());
|
||||
if t.runs.iter().any(|r| r.inline.is_some()) || n.appearance.items.iter().any(|i| i.visible() && (i.blend() != normal || brush(i))) {
|
||||
return false;
|
||||
}
|
||||
let setup = &f.doc.setup;
|
||||
let highlights = [
|
||||
(setup.highlight_substituted_fonts, &g.substituted_fonts),
|
||||
(setup.highlight_substituted_glyphs, &g.substituted_glyphs),
|
||||
(f.opts.highlight_alternate_glyphs, &g.alternate_glyphs),
|
||||
];
|
||||
if f.opts.highlight_substitutions && highlights.iter().any(|(on, path)| *on && !path.elements().is_empty()) {
|
||||
return false;
|
||||
}
|
||||
// Where the type draws: its glyphs, grown by the farthest its strokes reach, besides its
|
||||
// bounds (an area type frame's, or a point type estimate before it is laid out).
|
||||
let glyphs = type_reach(n, t, g);
|
||||
let reach = fx::cull_bounds(f.symbols, n).map_or(glyphs, |b| b.union(glyphs));
|
||||
// This frame's pixels to the output's: a scale and an offset (a supersampled export's
|
||||
// factor, an offscreen layer's crop).
|
||||
let grid = f.output * f.view.inverse();
|
||||
let [sx, _, _, sy, _, _] = grid.as_coeffs();
|
||||
let reach = reach.inflate(2.0 * f.px / sx.abs().min(1.0), 2.0 * f.px / sy.abs().min(1.0));
|
||||
let on = f.view.transform_rect_bbox(reach).intersect(Rect::new(0.0, 0.0, f64::from(ctx.width()), f64::from(ctx.height())));
|
||||
if !on.is_finite() {
|
||||
return false;
|
||||
}
|
||||
if on.width() <= 0.0 || on.height() <= 0.0 {
|
||||
return true;
|
||||
}
|
||||
let grid = edge_grid(on, grid);
|
||||
let output = grid * f.view;
|
||||
let [sx, _, _, sy, _, _] = grid.as_coeffs();
|
||||
let on = grid.transform_rect_bbox(on);
|
||||
// Whole pixels of the output.
|
||||
let (x0, y0) = (on.x0.floor(), on.y0.floor());
|
||||
let (Ok(w), Ok(h)) = (u16::try_from((on.x1 - x0).ceil() as u64), u16::try_from((on.y1 - y0).ceil() as u64)) else { return false };
|
||||
let view = Affine::translate((-x0, -y0)) * output;
|
||||
let visible = view.inverse().transform_rect_bbox(Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
let px = 1.0 / view.determinant().abs().sqrt().max(1e-12);
|
||||
let frame = Frame { mt: false, view, visible, px, output: view, ..*f };
|
||||
let threshold = match t.anti_alias {
|
||||
TypeAntiAlias::None => AntiAlias::None.threshold(),
|
||||
// The frame's pixels are smaller than the output's in a supersampled export, whose art
|
||||
// is smooth on the output's pixels (Art Optimized).
|
||||
_ if sx.abs() < 0.999 || sy.abs() < 0.999 => None,
|
||||
_ => f.opts.anti_alias.threshold(),
|
||||
};
|
||||
let draw = |r: &mut Self, frame: &Frame, n: &Node, t: &TextObject| {
|
||||
let mut off = single_threaded_context(w, h);
|
||||
off.set_aliasing_threshold(threshold);
|
||||
r.inside_layer(|r| r.draw_text_geom(&mut off, frame, n, t, g));
|
||||
off.flush();
|
||||
let mut pm = Pixmap::new(w, h);
|
||||
off.render(&mut pm, &mut r.resources);
|
||||
pm
|
||||
};
|
||||
let sharp = TextObject { anti_alias: TypeAntiAlias::Sharp, ..t.clone() };
|
||||
let mut pm = draw(self, &frame, n, &sharp);
|
||||
if t.anti_alias != TypeAntiAlias::None {
|
||||
let black = |p: &mut vectorcraft_color::Paint| {
|
||||
if !p.is_none() {
|
||||
*p = vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK);
|
||||
}
|
||||
};
|
||||
let mut ink = sharp;
|
||||
for r in &mut ink.runs {
|
||||
black(&mut r.style.fill);
|
||||
black(&mut r.style.stroke);
|
||||
}
|
||||
let mut inked = n.clone();
|
||||
for item in &mut inked.appearance.items {
|
||||
black(item.paint_mut());
|
||||
match item {
|
||||
AppearanceItem::Fill(fl) => fl.opacity = 1.0,
|
||||
AppearanceItem::Stroke(st) => st.opacity = 1.0,
|
||||
}
|
||||
}
|
||||
let covered = draw(self, &frame, &inked, &ink);
|
||||
for (p, c) in pm.data_mut().iter_mut().zip(covered.data()) {
|
||||
*p = edge_pixel(*p, c.a, t.anti_alias);
|
||||
}
|
||||
}
|
||||
ctx.set_transform(grid.inverse() * Affine::translate((x0, y0)));
|
||||
// The pixmap maps onto the rect pixel for pixel, each one on a pixel of the output.
|
||||
ctx.reset_paint_transform();
|
||||
let sampler = peniko::ImageSampler { quality: peniko::ImageQuality::Low, ..Default::default() };
|
||||
ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(Arc::new(pm)), sampler });
|
||||
ctx.fill_rect(&Rect::new(0.0, 0.0, f64::from(w), f64::from(h)));
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
true
|
||||
}
|
||||
|
||||
/// Paint some of type `n`'s own fills and strokes (`items`) on its glyph outlines `all`
|
||||
/// (document space; `tb` their bounds). Fills composite with their own opacity and blend mode;
|
||||
/// strokes take every stroke option, as on paths.
|
||||
@@ -2115,6 +2242,58 @@ pub(crate) fn single_threaded_context(w: u16, h: u16) -> RenderContext {
|
||||
RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: 0, ..Default::default() })
|
||||
}
|
||||
|
||||
/// How much of a pixel (out of 255) type that covers `c` of it shows with anti-aliasing method
|
||||
/// `m`: `c` with Sharp; all of it from half coverage (127) up, else none, with None; `2c - 0.5`
|
||||
/// with Crisp; and `2c` with Strong; each kept between 0 and all.
|
||||
pub(crate) fn edge_coverage(m: TypeAntiAlias, c: u8) -> u8 {
|
||||
let c = i32::from(c);
|
||||
let shown = match m {
|
||||
TypeAntiAlias::None => {
|
||||
if c >= 127 {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
TypeAntiAlias::Sharp => c,
|
||||
TypeAntiAlias::Crisp => 2 * c - 128,
|
||||
TypeAntiAlias::Strong => 2 * c,
|
||||
};
|
||||
shown.clamp(0, 255) as u8
|
||||
}
|
||||
|
||||
/// Where (document space) type `t` of node `n`, laid out in `g`, paints: its glyphs grown by the
|
||||
/// farthest the characters' strokes reach in text space, transformed as the type is, then grown by
|
||||
/// the farthest the object's own strokes reach in document space. A stroke's reach takes in its
|
||||
/// miter spikes and square caps ([`StrokeLayer::reach`]).
|
||||
fn type_reach(n: &Node, t: &TextObject, g: &TextGeom) -> Rect {
|
||||
let chars = t.runs.iter().filter(|r| r.style.has_stroke()).map(|r| r.style.stroke_layer().reach()).fold(0.0, f64::max);
|
||||
let own = n.appearance.outset();
|
||||
t.xf.transform_rect_bbox(g.bounds.inflate(chars, chars)).inflate(own, own)
|
||||
}
|
||||
|
||||
/// The map from a frame's pixels to the pixels [`Renderer::draw_type_edges`] draws type on, for
|
||||
/// the region `on` of the frame's pixels: `grid`, which maps them to the output's, unless the
|
||||
/// output's pixels there would number more than the frame's and more than [`fx::PIXEL_BUDGET`],
|
||||
/// and then the frame's own pixels (the identity).
|
||||
fn edge_grid(on: Rect, grid: Affine) -> Affine {
|
||||
let area = |r: Rect| r.width() * r.height();
|
||||
if area(grid.transform_rect_bbox(on)) > area(on).max(fx::PIXEL_BUDGET) { Affine::IDENTITY } else { grid }
|
||||
}
|
||||
|
||||
/// Pixel `p` of type the anti-aliasing method `m` draws, where the type covers `c` (out of 255) of
|
||||
/// the pixel: its color and opacity scaled by [`edge_coverage`] of `c` divided by `c`. A pixel the
|
||||
/// type covers wholly or not at all is kept.
|
||||
fn edge_pixel(p: vello_cpu::color::PremulRgba8, c: u8, m: TypeAntiAlias) -> vello_cpu::color::PremulRgba8 {
|
||||
if c == 0 || c == 255 {
|
||||
return p;
|
||||
}
|
||||
let (to, c) = (u32::from(edge_coverage(m, c)), u32::from(c));
|
||||
let scale = |v: u8| ((u32::from(v) * to + c / 2) / c).min(255) as u8;
|
||||
let a = scale(p.a);
|
||||
vello_cpu::color::PremulRgba8 { r: scale(p.r).min(a), g: scale(p.g).min(a), b: scale(p.b).min(a), a }
|
||||
}
|
||||
|
||||
/// Exactly one visible painted item, a fill with its own Normal blend and full opacity (one
|
||||
/// paint operation, so an object blend mode can be applied per draw).
|
||||
fn single_plain_fill(n: &Node) -> bool {
|
||||
@@ -2459,3 +2638,5 @@ mod tests_setup;
|
||||
mod tests_strokegradient;
|
||||
#[cfg(test)]
|
||||
mod tests_tileedge;
|
||||
#[cfg(test)]
|
||||
mod tests_typeedges;
|
||||
|
||||
@@ -181,6 +181,7 @@ fn rasterize(r: &mut Renderer, doc: &Document, def: &PatternDef, region: Rect, w
|
||||
px: 1.0 / v.determinant().abs().sqrt().max(1e-12),
|
||||
opts: &opts,
|
||||
ink,
|
||||
output: v,
|
||||
};
|
||||
for a in &def.art {
|
||||
r.draw_arc(&mut ctx, &frame, a);
|
||||
@@ -311,7 +312,8 @@ impl Renderer {
|
||||
let comp = crate::group::Composite { opacity: dim, ..Default::default() };
|
||||
self.group(ctx, f, comp, &mut |r, c, fr| {
|
||||
for o in def.preview_offsets() {
|
||||
let frame = Frame { view: fr.view * Affine::translate(o), visible: fr.visible - o, ..*fr };
|
||||
let moved = Affine::translate(o);
|
||||
let frame = Frame { view: fr.view * moved, visible: fr.visible - o, output: fr.output * moved, ..*fr };
|
||||
for a in &children {
|
||||
r.draw_arc(c, &frame, a);
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ impl Renderer {
|
||||
// The art in its own coordinates, with its own resources.
|
||||
let symbols = e.doc.symbol_boxes().quick();
|
||||
let visible = m.inverse().transform_rect_bbox(f.visible);
|
||||
let inner = Frame { doc: &e.doc, symbols: &symbols, view: f.view * m, visible, px: f.px / k, ..*f };
|
||||
let inner = Frame { doc: &e.doc, symbols: &symbols, view: f.view * m, visible, px: f.px / k, output: f.output * m, ..*f };
|
||||
let _inside = Inside::enter();
|
||||
self.draw_arc(ctx, &inner, &e.art);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
//! Character panel › Anti-aliasing Method: type drawn as pixels with None, Sharp, Crisp or Strong
|
||||
//! edges.
|
||||
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun, TypeAntiAlias};
|
||||
use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeKind, TextKind};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
|
||||
use super::*;
|
||||
|
||||
const W: u32 = 260;
|
||||
const H: u32 = 90;
|
||||
|
||||
/// A page with "Hamburg" in 40 pt black type drawn with `method`, and `fill` as the type object's
|
||||
/// own fill (instead of the characters' fill) when given.
|
||||
fn page(method: TypeAntiAlias, fill: Option<FillLayer>) -> Document {
|
||||
let mut d = Document::new(f64::from(W), f64::from(H));
|
||||
let mut t = TextObject::point(Point::new(10.3, 60.4), "Hamburg", CharStyle { size: 40.0, ..Default::default() });
|
||||
t.anti_alias = method;
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
if let Some(fl) = fill {
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
t.runs[0].style.fill = Paint::None;
|
||||
}
|
||||
n.appearance.items.push(AppearanceItem::Fill(fl));
|
||||
}
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// The opacity of each pixel of `d` drawn with `opts` on a transparent page.
|
||||
fn alphas(d: &Document, opts: &RenderOptions) -> Vec<u8> {
|
||||
Renderer::new().render(d, W, H, Affine::IDENTITY, opts).pixels.chunks(4).map(|p| p[3]).collect()
|
||||
}
|
||||
|
||||
/// Pixels the type covers only in part.
|
||||
fn partial(a: &[u8]) -> usize {
|
||||
a.iter().filter(|v| **v > 0 && **v < 255).count()
|
||||
}
|
||||
|
||||
/// On the canvas each method gives every pixel the share of it that `edge_coverage` gives the
|
||||
/// share Sharp draws: None paints whole pixels from half coverage up, Crisp doubles the contrast
|
||||
/// around half coverage, and Strong doubles the coverage.
|
||||
#[test]
|
||||
fn each_method_draws_the_share_of_each_pixel_it_defines() {
|
||||
let opts = RenderOptions::default();
|
||||
let sharp = alphas(&page(TypeAntiAlias::Sharp, None), &opts);
|
||||
assert!(partial(&sharp) > 200, "Sharp draws smooth edges: {} partial pixels", partial(&sharp));
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
let drawn = alphas(&page(m, None), &opts);
|
||||
assert_ne!(drawn, sharp, "{m:?} changes the edges");
|
||||
let wrong: Vec<_> = sharp
|
||||
.iter()
|
||||
.zip(&drawn)
|
||||
.enumerate()
|
||||
.filter(|(_, (c, v))| {
|
||||
// One step of rounding either way in the coverage.
|
||||
let near = [c.saturating_sub(1), **c, c.saturating_add(1)].map(|c| edge_coverage(m, c));
|
||||
let (lo, hi) = (near.iter().min().copied().unwrap_or(0), near.iter().max().copied().unwrap_or(255));
|
||||
**v < lo.saturating_sub(1) || **v > hi.saturating_add(1)
|
||||
})
|
||||
.map(|(i, (c, v))| (i, *c, *v))
|
||||
.take(5)
|
||||
.collect();
|
||||
assert!(wrong.is_empty(), "{m:?}: (pixel, Sharp, drawn) {wrong:?}");
|
||||
}
|
||||
assert_eq!(partial(&alphas(&page(TypeAntiAlias::None, None), &opts)), 0, "None paints whole pixels");
|
||||
}
|
||||
|
||||
/// A raster export draws its art at several times its size and averages it down; type with None
|
||||
/// still has hard edges on the exported pixels, Crisp has fewer partly covered pixels than Sharp,
|
||||
/// and Strong draws heavier type.
|
||||
#[test]
|
||||
fn an_exports_pixels_take_the_method() {
|
||||
let opts = RenderOptions { supersample: true, precise: true, ..Default::default() };
|
||||
let sharp = alphas(&page(TypeAntiAlias::Sharp, None), &opts);
|
||||
let none = alphas(&page(TypeAntiAlias::None, None), &opts);
|
||||
let crisp = alphas(&page(TypeAntiAlias::Crisp, None), &opts);
|
||||
let strong = alphas(&page(TypeAntiAlias::Strong, None), &opts);
|
||||
assert!(partial(&sharp) > 200, "the export smooths Sharp type: {}", partial(&sharp));
|
||||
assert_eq!(partial(&none), 0, "None type has hard edges in the export");
|
||||
assert!(none.iter().filter(|v| **v == 255).count() > 1000);
|
||||
assert!(partial(&crisp) < partial(&sharp), "Crisp: {} partial pixels, Sharp {}", partial(&crisp), partial(&sharp));
|
||||
let ink = |a: &[u8]| a.iter().map(|v| u64::from(*v)).sum::<u64>();
|
||||
assert!(ink(&strong) > ink(&sharp), "Strong draws heavier type");
|
||||
}
|
||||
|
||||
/// A frame drawn with hard edges (Anti-aliased Artwork off, an export's Anti-aliasing None) draws
|
||||
/// the type of every method as it draws Sharp type.
|
||||
#[test]
|
||||
fn hard_edged_frames_draw_every_method_alike() {
|
||||
for opts in [
|
||||
RenderOptions { anti_alias: AntiAlias::None, ..Default::default() },
|
||||
RenderOptions { anti_alias: AntiAlias::None, precise: true, supersample: true, ..Default::default() },
|
||||
] {
|
||||
let sharp = alphas(&page(TypeAntiAlias::Sharp, None), &opts);
|
||||
assert_eq!(partial(&sharp), 0);
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
assert_eq!(alphas(&page(m, None), &opts), sharp, "{m:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Crisp and Strong reshape the edges of type that is drawn with a translucent fill, and keep
|
||||
/// its opacity inside the characters; None keeps it too.
|
||||
#[test]
|
||||
fn the_methods_keep_the_opacity_of_the_paint() {
|
||||
let half = FillLayer { opacity: 0.5, ..FillLayer::new(Paint::solid(Color::BLACK)) };
|
||||
let opts = RenderOptions::default();
|
||||
let sharp = alphas(&page(TypeAntiAlias::Sharp, Some(half.clone())), &opts);
|
||||
let most = |a: &[u8]| a.iter().copied().max().unwrap_or(0);
|
||||
assert!((126..=129).contains(&most(&sharp)), "half opaque: {}", most(&sharp));
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
let drawn = alphas(&page(m, Some(half.clone())), &opts);
|
||||
assert!((126..=129).contains(&most(&drawn)), "{m:?} keeps the fill's opacity: {}", most(&drawn));
|
||||
assert_ne!(drawn, sharp, "{m:?} changes the edges");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type whose own fill multiplies with the art below it is drawn as Sharp draws it, so the fill
|
||||
/// still multiplies with that art.
|
||||
#[test]
|
||||
fn type_with_a_blended_fill_is_drawn_as_sharp() {
|
||||
let multiply = FillLayer { blend: vectorcraft_color::BlendMode::Multiply, ..FillLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0))) };
|
||||
let on_orange = |m: TypeAntiAlias| {
|
||||
let mut d = page(m, Some(multiply.clone()));
|
||||
let orange = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.5, 0.0)), Paint::None, 0.0);
|
||||
let back = Node::path(d.alloc_id(), rectangle(Rect::new(0.0, 0.0, f64::from(W), f64::from(H))), orange);
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, back).unwrap();
|
||||
Renderer::new().render(&d, W, H, Affine::IDENTITY, &RenderOptions::default()).pixels
|
||||
};
|
||||
let sharp = on_orange(TypeAntiAlias::Sharp);
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
assert_eq!(on_orange(m), sharp, "{m:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type with an inline graphic is drawn as Sharp draws it, so the graphic, which multiplies with
|
||||
/// the art below it here, is drawn as before.
|
||||
#[test]
|
||||
fn type_with_an_inline_graphic_is_drawn_as_sharp() {
|
||||
let on_orange = |m: TypeAntiAlias| {
|
||||
let mut d = Document::new(f64::from(W), f64::from(H));
|
||||
let orange = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.5, 0.0)), Paint::None, 0.0);
|
||||
let back = Node::path(d.alloc_id(), rectangle(Rect::new(0.0, 0.0, f64::from(W), f64::from(H))), orange);
|
||||
let blue = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0);
|
||||
let mut art = Node::path(d.alloc_id(), rectangle(Rect::new(0.0, -30.0, 30.0, 0.0)), blue);
|
||||
art.blend = vectorcraft_color::BlendMode::Multiply;
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
let style = CharStyle { size: 40.0, ..Default::default() };
|
||||
let mut t = TextObject::point(Point::new(10.3, 60.4), "I", style.clone());
|
||||
t.runs.push(TextRun::inline(InlineArt::new("S"), style));
|
||||
t.anti_alias = m;
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, back).unwrap();
|
||||
d.insert(Some(l), 1, n).unwrap();
|
||||
d.resolve_inline_art();
|
||||
Renderer::new().render(&d, W, H, Affine::IDENTITY, &RenderOptions::default()).pixels
|
||||
};
|
||||
let sharp = on_orange(TypeAntiAlias::Sharp);
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
assert_eq!(on_orange(m), sharp, "{m:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// In a supersampled export, None type in a group drawn offscreen (it isolates art that blends)
|
||||
/// still has hard edges on the exported pixels, wherever the group's layer starts.
|
||||
#[test]
|
||||
fn type_in_an_offscreen_group_keeps_the_exports_pixels() {
|
||||
let opts = RenderOptions { supersample: true, precise: true, ..Default::default() };
|
||||
for x in [10.3, 10.55, 10.8, 11.05] {
|
||||
let mut d = Document::new(f64::from(W), f64::from(H));
|
||||
let mut t = TextObject::point(Point::new(x, 60.4), "Hamburg", CharStyle { size: 40.0, ..Default::default() });
|
||||
t.anti_alias = TypeAntiAlias::None;
|
||||
let text = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let black = Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0);
|
||||
let mut square = Node::path(d.alloc_id(), rectangle(Rect::new(220.0, 10.0, 250.0, 30.0)), black);
|
||||
square.blend = vectorcraft_color::BlendMode::Multiply;
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(square), Arc::new(text)]);
|
||||
group.isolate = true;
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
let a = alphas(&d, &opts);
|
||||
assert!(a.iter().filter(|v| **v == 255).count() > 1000, "x {x}");
|
||||
assert_eq!(partial(&a), 0, "x {x}: None type has hard edges");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type with a brush stroke of its own is drawn as Sharp draws it.
|
||||
#[test]
|
||||
fn type_with_a_brush_stroke_is_drawn_as_sharp() {
|
||||
let draw = |m: TypeAntiAlias| {
|
||||
let mut d = page(m, None);
|
||||
let id = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id).unwrap();
|
||||
let n = d.node_mut(id).unwrap();
|
||||
let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), 2.0);
|
||||
st.brush = Some("Charcoal".into());
|
||||
n.appearance.items.push(AppearanceItem::Stroke(st));
|
||||
alphas(&d, &RenderOptions::default())
|
||||
};
|
||||
let sharp = draw(TypeAntiAlias::Sharp);
|
||||
for m in [TypeAntiAlias::None, TypeAntiAlias::Crisp, TypeAntiAlias::Strong] {
|
||||
assert_eq!(draw(m), sharp, "{m:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// On screen, type in a font the system doesn't have draws as Sharp draws it while Document
|
||||
/// Setup highlights substituted fonts, and with its method when nothing is highlighted.
|
||||
#[test]
|
||||
fn type_with_a_substitution_highlight_is_drawn_as_sharp() {
|
||||
let draw = |m: TypeAntiAlias, highlight: bool| {
|
||||
let mut d = Document::new(f64::from(W), f64::from(H));
|
||||
d.setup.highlight_substituted_fonts = true;
|
||||
let style = CharStyle { size: 40.0, font_family: "No Such Family 7Q".into(), ..Default::default() };
|
||||
let mut t = TextObject::point(Point::new(10.3, 60.4), "Hamburg", style);
|
||||
t.anti_alias = m;
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
alphas(&d, &RenderOptions { highlight_substitutions: highlight, ..Default::default() })
|
||||
};
|
||||
assert_eq!(draw(TypeAntiAlias::Strong, true), draw(TypeAntiAlias::Sharp, true));
|
||||
assert_ne!(draw(TypeAntiAlias::Strong, false), draw(TypeAntiAlias::Sharp, false));
|
||||
}
|
||||
|
||||
/// The type is drawn on the output's pixels unless, in a frame coarser than the output, they
|
||||
/// would number more than the frame's there and more than the effects' pixel budget.
|
||||
#[test]
|
||||
fn type_edges_are_drawn_on_at_most_the_budgets_pixels() {
|
||||
let finer = Affine::scale(0.25);
|
||||
assert_eq!(edge_grid(Rect::new(0.0, 0.0, 4000.0, 4000.0), finer), finer, "a supersampled export's pixels");
|
||||
let coarser = Affine::scale(4.0);
|
||||
assert_eq!(edge_grid(Rect::new(0.0, 0.0, 100.0, 100.0), coarser), coarser, "160,000 pixels");
|
||||
assert_eq!(edge_grid(Rect::new(0.0, 0.0, 1000.0, 1000.0), coarser), Affine::IDENTITY, "16 million pixels");
|
||||
}
|
||||
|
||||
/// The offscreen drawing takes in the farthest a stroke reaches: a mitered character stroke's
|
||||
/// spikes up to its miter limit, transformed with the type (stretched along one axis too), and
|
||||
/// the object's own strokes.
|
||||
#[test]
|
||||
fn the_offscreen_drawing_takes_in_the_strokes_reach() {
|
||||
let style = CharStyle {
|
||||
stroke: Paint::solid(Color::BLACK),
|
||||
stroke_width: 4.0,
|
||||
stroke_join: vectorcraft_doc::LineJoin::Miter,
|
||||
stroke_miter_limit: 10.0,
|
||||
..Default::default()
|
||||
};
|
||||
let mut t = TextObject::point(Point::new(0.0, 0.0), "V", style);
|
||||
let n = |t: &TextObject| Node::new(NodeId(1), NodeKind::Text(Box::new(t.clone())));
|
||||
// How far the reach goes past the transformed glyph bounds on the left and on the top.
|
||||
let past = |n: &Node, t: &TextObject| {
|
||||
let g = text_geom(t);
|
||||
let (r, b) = (type_reach(n, t, &g), t.xf.transform_rect_bbox(g.bounds));
|
||||
(b.x0 - r.x0, b.y0 - r.y0)
|
||||
};
|
||||
let near = |(a, b): (f64, f64), (x, y): (f64, f64)| (a - x).abs() < 1e-6 && (b - y).abs() < 1e-6;
|
||||
assert!(near(past(&n(&t), &t), (20.0, 20.0)), "half the weight times the miter limit: {:?}", past(&n(&t), &t));
|
||||
t.xf = Affine::scale_non_uniform(10.0, 0.1);
|
||||
assert!(near(past(&n(&t), &t), (200.0, 2.0)), "stretched with the type: {:?}", past(&n(&t), &t));
|
||||
t.xf = Affine::scale(2.0);
|
||||
let mut own = n(&t);
|
||||
let mut st = vectorcraft_doc::StrokeLayer::new(Paint::solid(Color::BLACK), 10.0);
|
||||
st.miter_limit = 10.0;
|
||||
own.appearance.items.push(AppearanceItem::Stroke(st));
|
||||
assert!(near(past(&own, &t), (90.0, 90.0)), "the object's own stroke after the characters': {:?}", past(&own, &t));
|
||||
}
|
||||
|
||||
/// Area type draws its punctuation outside its frame with Optical Margin Alignment, and None type
|
||||
/// draws it there too.
|
||||
#[test]
|
||||
fn type_outside_its_frame_is_drawn() {
|
||||
let page = |m: TypeAntiAlias| {
|
||||
let mut d = Document::new(f64::from(W), f64::from(H));
|
||||
let mut t = TextObject::point(Point::new(0.0, 0.0), "“Quoted.”", CharStyle { size: 30.0, ..Default::default() });
|
||||
t.kind = TextKind::Area { frame: rectangle(Rect::new(40.0, 10.0, 240.0, 80.0)) };
|
||||
t.optical_margin = true;
|
||||
t.anti_alias = m;
|
||||
let n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
alphas(&d, &RenderOptions::default())
|
||||
};
|
||||
let outside = |a: &[u8]| a.iter().enumerate().filter(|(i, v)| (*i as u32 % W) < 39 && **v > 0).count();
|
||||
assert!(outside(&page(TypeAntiAlias::Sharp)) > 20, "the opening quotation mark hangs outside the frame");
|
||||
assert!(outside(&page(TypeAntiAlias::None)) > 20, "None type draws it there too");
|
||||
}
|
||||
@@ -171,6 +171,10 @@
|
||||
Anti-Alias Заглаждане
|
||||
Anti-alias Rasters Заглаждане на растери
|
||||
Anti-aliased Artwork Загладено произведение
|
||||
Anti-aliasing Method Метод на заглаждане
|
||||
Sharp Остро
|
||||
Crisp Четко
|
||||
Strong Силно
|
||||
Anti-aliasing: Заглаждане:
|
||||
Any Kind Всякакъв вид
|
||||
Appearance Външен вид
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2513,6 +2513,10 @@ lighting Light Světlo
|
||||
< 1 sec < 1 s
|
||||
[Unnamed] [Bez názvu]
|
||||
Amount: Míra:
|
||||
Anti-aliasing Method Metoda vyhlazování
|
||||
Sharp Ostré
|
||||
Crisp Jasné
|
||||
Strong Silné
|
||||
Anti-aliasing: Vyhlazování:
|
||||
Artboard: Pracovní plocha:
|
||||
Color Table Tabulka barev
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2333,6 +2333,10 @@ lighting Light Lys
|
||||
< 1 sec < 1 sek
|
||||
[Unnamed] [Unavngiven]
|
||||
Amount: Mængde:
|
||||
Anti-aliasing Method Antialiaseringsmetode
|
||||
Sharp Skarp
|
||||
Crisp Klar
|
||||
Strong Kraftig
|
||||
Anti-aliasing: Antialiasering:
|
||||
Artboard: Tegnebræt:
|
||||
Color Table Farvetabel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -167,6 +167,10 @@
|
||||
Anti-Alias Glätten
|
||||
Anti-alias Rasters Pixelbilder glätten
|
||||
Anti-aliased Artwork Geglättete Grafik
|
||||
Anti-aliasing Method Glättungsmethode
|
||||
Sharp Scharf
|
||||
Crisp Klar
|
||||
Strong Stark
|
||||
Anti-aliasing: Glättung:
|
||||
Any Kind Beliebige Art
|
||||
Appearance Aussehen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -165,6 +165,10 @@
|
||||
Anti-Alias Εξομάλυνση ακμών
|
||||
Anti-alias Rasters Εξομάλυνση ακμών εικόνων ράστερ
|
||||
Anti-aliased Artwork Γραφικά με εξομαλυμένες ακμές
|
||||
Anti-aliasing Method Μέθοδος εξομάλυνσης ακμών
|
||||
Sharp Οξεία
|
||||
Crisp Καθαρή
|
||||
Strong Έντονη
|
||||
Anti-aliasing: Εξομάλυνση ακμών:
|
||||
Any Kind Οποιοσδήποτε τύπος
|
||||
Appearance Εμφάνιση
|
||||
|
||||
|
@@ -185,6 +185,10 @@
|
||||
Anti-Alias Suavizado
|
||||
Anti-alias Rasters Suavizar imágenes ráster
|
||||
Anti-aliased Artwork Ilustración suavizada
|
||||
Anti-aliasing Method Método de suavizado
|
||||
Sharp Nítido
|
||||
Crisp Definido
|
||||
Strong Fuerte
|
||||
Anti-aliasing: Suavizado:
|
||||
Any Kind Cualquier tipo
|
||||
Appearance Apariencia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2468,6 +2468,10 @@ lighting Light Valo
|
||||
< 1 sec < 1 s
|
||||
[Unnamed] [Nimetön]
|
||||
Amount: Määrä:
|
||||
Anti-aliasing Method Reunanpehmennystapa
|
||||
Sharp Terävä
|
||||
Crisp Selkeä
|
||||
Strong Vahva
|
||||
Anti-aliasing: Reunanpehmennys:
|
||||
Artboard: Piirtoalusta:
|
||||
Color Table Väritaulukko
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -186,6 +186,10 @@
|
||||
Anti-Alias Lissage
|
||||
Anti-alias Rasters Lisser les images pixellisées
|
||||
Anti-aliased Artwork Illustration lissée
|
||||
Anti-aliasing Method Méthode de lissage
|
||||
Sharp Net
|
||||
Crisp Précis
|
||||
Strong Fort
|
||||
Anti-aliasing: Lissage :
|
||||
Any Kind Tout type
|
||||
Appearance Aspect
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -273,6 +273,10 @@
|
||||
Anti-Alias Élsimítás
|
||||
Anti-alias Rasters Raszterek élsimítása
|
||||
Anti-aliased Artwork Élsimított grafika
|
||||
Anti-aliasing Method Élsimítási módszer
|
||||
Sharp Éles
|
||||
Crisp Tiszta
|
||||
Strong Erős
|
||||
Anti-aliasing: Élsimítás:
|
||||
Any except extracting pages Bármi, kivéve az oldalak kinyerését
|
||||
Any Kind Bármilyen típus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2333,6 +2333,10 @@ lighting Light Cahaya
|
||||
< 1 sec < 1 detik
|
||||
[Unnamed] [Tanpa nama]
|
||||
Amount: Jumlah:
|
||||
Anti-aliasing Method Metode anti-aliasing
|
||||
Sharp Tajam
|
||||
Crisp Jelas
|
||||
Strong Kuat
|
||||
Anti-aliasing: Anti-aliasing:
|
||||
Artboard: Artboard:
|
||||
Color Table Tabel warna
|
||||
|
||||
|
@@ -185,6 +185,10 @@
|
||||
Anti-Alias Anti-aliasing
|
||||
Anti-alias Rasters Anti-aliasing per immagini raster
|
||||
Anti-aliased Artwork Grafica con anti-aliasing
|
||||
Anti-aliasing Method Metodo di anti-aliasing
|
||||
Sharp Nitido
|
||||
Crisp Definito
|
||||
Strong Forte
|
||||
Anti-aliasing: Anti-aliasing:
|
||||
Any Kind Qualsiasi tipo
|
||||
Appearance Aspetto
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -869,6 +869,10 @@
|
||||
Anti-alias アンチエイリアス
|
||||
Anti-alias Rasters ラスターをアンチエイリアス
|
||||
Anti-aliased Artwork アートワークをアンチエイリアス
|
||||
Anti-aliasing Method アンチエイリアスの方式
|
||||
Sharp シャープ
|
||||
Crisp くっきり
|
||||
Strong 太め
|
||||
Anti-aliasing: アンチエイリアス:
|
||||
Appearance Item アピアランス項目
|
||||
Appearance Panel アピアランスパネル
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -864,6 +864,10 @@
|
||||
Anti-alias 안티 앨리어싱
|
||||
Anti-alias Rasters 래스터 안티 앨리어싱
|
||||
Anti-aliased Artwork 안티 앨리어싱 아트워크
|
||||
Anti-aliasing Method 안티 앨리어싱 방식
|
||||
Sharp 날카롭게
|
||||
Crisp 또렷하게
|
||||
Strong 강하게
|
||||
Anti-aliasing: 안티 앨리어싱:
|
||||
Appearance Item 모양새 항목
|
||||
Appearance Panel 모양새 패널
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2347,6 +2347,10 @@ lighting Light Licht
|
||||
< 1 sec < 1 sec
|
||||
[Unnamed] [Naamloos]
|
||||
Amount: Hoeveelheid:
|
||||
Anti-aliasing Method Anti-aliasingmethode
|
||||
Sharp Scherp
|
||||
Crisp Helder
|
||||
Strong Krachtig
|
||||
Anti-aliasing: Anti-aliasing:
|
||||
Artboard: Tekenbord:
|
||||
Color Table Kleurentabel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2421,6 +2421,10 @@ lighting Light Światło
|
||||
< 1 sec < 1 s
|
||||
[Unnamed] [Bez nazwy]
|
||||
Amount: Wartość:
|
||||
Anti-aliasing Method Metoda wygładzania
|
||||
Sharp Ostre
|
||||
Crisp Wyraźne
|
||||
Strong Mocne
|
||||
Anti-aliasing: Wygładzanie:
|
||||
Artboard: Obszar kompozycji:
|
||||
Color Table Tablica kolorów
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -190,6 +190,10 @@
|
||||
Anti-alias Suavizar bordas
|
||||
Anti-alias Rasters Suavizar Rasters
|
||||
Anti-aliased Artwork Arte Suavizada
|
||||
Anti-aliasing Method Método de Suavização
|
||||
Sharp Nítido
|
||||
Crisp Definido
|
||||
Strong Forte
|
||||
Anti-aliasing: Suavização:
|
||||
Appearance Aparência
|
||||
Appearance Attribute Atributo de Aparência
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -183,6 +183,10 @@
|
||||
Anti-Alias Сглаживание
|
||||
Anti-alias Rasters Сглаживать растровые изображения
|
||||
Anti-aliased Artwork Сглаженный рисунок
|
||||
Anti-aliasing Method Метод сглаживания
|
||||
Sharp Резкое
|
||||
Crisp Чёткое
|
||||
Strong Сильное
|
||||
Anti-aliasing: Сглаживание:
|
||||
Any Kind Любой тип
|
||||
Appearance Оформление
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -173,6 +173,10 @@
|
||||
Anti-Alias Kantutjämning
|
||||
Anti-alias Rasters Kantutjämna rasterbilder
|
||||
Anti-aliased Artwork Kantutjämnad grafik
|
||||
Anti-aliasing Method Kantutjämningsmetod
|
||||
Sharp Skarp
|
||||
Crisp Klar
|
||||
Strong Kraftig
|
||||
Anti-aliasing: Kantutjämning:
|
||||
Any Kind Alla typer
|
||||
Appearance Utseende
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -179,6 +179,10 @@
|
||||
Anti-Alias Згладжування
|
||||
Anti-alias Rasters Згладжувати растрові зображення
|
||||
Anti-aliased Artwork Згладжений малюнок
|
||||
Anti-aliasing Method Метод згладжування
|
||||
Sharp Різке
|
||||
Crisp Чітке
|
||||
Strong Сильне
|
||||
Anti-aliasing: Згладжування:
|
||||
Any Kind Будь-який тип
|
||||
Appearance Оформлення
|
||||
|
||||
|
@@ -209,6 +209,10 @@
|
||||
Anti-Alias Khử răng cưa
|
||||
Anti-alias Rasters Khử răng cưa ảnh raster
|
||||
Anti-aliased Artwork Tác phẩm khử răng cưa
|
||||
Anti-aliasing Method Phương pháp khử răng cưa
|
||||
Sharp Sắc nét
|
||||
Crisp Rõ nét
|
||||
Strong Đậm
|
||||
Anti-aliasing: Khử răng cưa:
|
||||
Any Kind Bất kỳ loại
|
||||
Appearance Diện mạo
|
||||
|
||||
|
@@ -141,6 +141,10 @@
|
||||
Anti-Alias 消除锯齿
|
||||
Anti-alias Rasters 栅格消除锯齿
|
||||
Anti-aliased Artwork 图稿抗锯齿
|
||||
Anti-aliasing Method 消除锯齿方法
|
||||
Sharp 锐利
|
||||
Crisp 清晰
|
||||
Strong 强
|
||||
Anti-aliasing: 消除锯齿:
|
||||
Appearance 外观
|
||||
Appearance Attribute 外观属性
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -150,6 +150,10 @@
|
||||
Anti-Alias 消除鋸齒
|
||||
Anti-alias Rasters 點陣影像消除鋸齒
|
||||
Anti-aliased Artwork 消除鋸齒的圖稿
|
||||
Anti-aliasing Method 消除鋸齒方法
|
||||
Sharp 銳利
|
||||
Crisp 清晰
|
||||
Strong 強
|
||||
Anti-aliasing: 消除鋸齒:
|
||||
Any Kind 所有類型
|
||||
Appearance 外觀
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1,11 +1,12 @@
|
||||
//! Character panel: font family / style, size, leading, kerning, tracking, vertical and horizontal
|
||||
//! scale, baseline shift, character rotation and the All Caps, Small Caps, Superscript, Subscript,
|
||||
//! Underline and Strikethrough toggles.
|
||||
//! scale, baseline shift, character rotation, the anti-aliasing method and the All Caps, Small Caps,
|
||||
//! Superscript, Subscript, Underline and Strikethrough toggles.
|
||||
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use egui::{Key, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::text::TypeAntiAlias;
|
||||
use vectorcraft_doc::{CharStyle, NodeId, NodeKind};
|
||||
use vectorcraft_tools::{Mods, ToolKey};
|
||||
|
||||
@@ -452,9 +453,10 @@ fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The Character panel's grid of numeric fields, `w` wide: Font Size, Leading, Kerning and
|
||||
/// Tracking, then with `more` the scales, Baseline Shift and Character Rotation (`s`: the style
|
||||
/// shown). The Properties panel shows its first rows.
|
||||
/// The Character panel's grid of fields, `w` wide: Font Size, Leading, Kerning and Tracking,
|
||||
/// then with `more` the scales, Baseline Shift and Character Rotation, and a row with the
|
||||
/// Anti-aliasing Method dropdown under Character Rotation (`s`: the style shown). The Properties
|
||||
/// panel shows its first rows.
|
||||
pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &CharStyle, w: f32, more: bool) {
|
||||
let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0);
|
||||
let unit = app.session.type_unit();
|
||||
@@ -507,9 +509,61 @@ pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
// Anti-aliasing Method, under Character Rotation.
|
||||
ui.label("");
|
||||
cell(ui, "aa", tl!("Anti-aliasing Method"), |ui| anti_alias_method(app, ui, fw));
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// The Anti-aliasing Method dropdown, `width` wide: how the edges of the type objects the panel acts
|
||||
/// on are drawn as pixels ([`set_anti_alias`]), blank where their methods differ.
|
||||
fn anti_alias_method(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) {
|
||||
// The labels of [`TypeAntiAlias::ALL`], in its order.
|
||||
let labels = [tl!("None"), tl!("Sharp"), tl!("Crisp"), tl!("Strong")];
|
||||
let current = shared_anti_alias(app);
|
||||
let shown = TypeAntiAlias::ALL.iter().zip(labels).find(|(m, _)| Some(**m) == current).map_or("", |(_, l)| l);
|
||||
if let Some(m) = widgets::dropdown_names(ui, "ch-anti-alias", shown, &labels, width).and_then(|i| TypeAntiAlias::ALL.get(i)) {
|
||||
set_anti_alias(app, *m);
|
||||
}
|
||||
}
|
||||
|
||||
/// The anti-aliasing method of the type objects the panel acts on: the one the Type tool edits,
|
||||
/// else the selected type (groups' included). `None` where they differ, or with no type.
|
||||
fn shared_anti_alias(app: &VectorcraftApp) -> Option<TypeAntiAlias> {
|
||||
let st = app.session.active()?;
|
||||
let mut acc = Shared::None;
|
||||
match text_editing(app) {
|
||||
Some((id, _, _)) => {
|
||||
if let Some(NodeKind::Text(t)) = st.doc.node(id).map(|n| &n.kind) {
|
||||
acc.add(t.anti_alias);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
|
||||
n.walk(&mut |c| {
|
||||
if let NodeKind::Text(t) = &c.kind {
|
||||
acc.add(t.anti_alias);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.value()
|
||||
}
|
||||
|
||||
/// Set the anti-aliasing method `m` of the type objects the panel acts on, and of every frame of
|
||||
/// their threads (`text.setFormat {antiAlias}`): the one the Type tool edits, whatever part of its
|
||||
/// text is selected, else the selected type.
|
||||
fn set_anti_alias(app: &mut VectorcraftApp, m: TypeAntiAlias) {
|
||||
let mut p = json!({"antiAlias": m.id()});
|
||||
if let Some((id, _, _)) = text_editing(app) {
|
||||
end_typing(app);
|
||||
p["ids"] = json!([id.0]);
|
||||
}
|
||||
app.run("text.setFormat", p).ok();
|
||||
}
|
||||
|
||||
/// The Font Size combo (Character and Properties panels, Control bar): typed, stepped, scrubbed or
|
||||
/// picked from the presets, in the type unit, blank where the selected text's sizes differ.
|
||||
pub(crate) fn size_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width: f32) {
|
||||
@@ -805,12 +859,16 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset Panel: the characters the panel acts on ([`char_cmd`]) take tracking 0, Auto leading and
|
||||
/// Reset Panel, in one undo step: the type objects the panel acts on take the Sharp anti-aliasing
|
||||
/// method, and the characters the panel acts on ([`char_cmd`]) take tracking 0, Auto leading and
|
||||
/// kerning, no baseline shift, 100% scales, no rotation, no underline or strikethrough, All Caps
|
||||
/// and Small Caps off, the normal position (neither superscript nor subscript), Roman Baseline
|
||||
/// character alignment, Proportional Metrics off and the Default digits, whether or not Show
|
||||
/// East Asian Options and Show Indic Options are on.
|
||||
fn reset_panel(app: &mut VectorcraftApp) {
|
||||
// Typing in progress stays an undo step of its own.
|
||||
end_typing(app);
|
||||
app.session.begin_named_undo_group(Some("Reset Panel".into()));
|
||||
let d = CharStyle::default();
|
||||
style(app, json!({"tracking": 0, "leading": "auto"}));
|
||||
format(
|
||||
@@ -821,6 +879,8 @@ fn reset_panel(app: &mut VectorcraftApp) {
|
||||
"proportionalMetrics": d.proportional_metrics, "digits": d.digits
|
||||
}),
|
||||
);
|
||||
set_anti_alias(app, TypeAntiAlias::Sharp);
|
||||
app.session.end_undo_group(false);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1126,6 +1186,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Anti-aliasing Method dropdown under Character Rotation shows the method of the selected
|
||||
/// type, blank where the selected objects differ, and a pick sets it for the whole object, also
|
||||
/// while the Type tool has part of its text selected. Reset Panel sets Sharp.
|
||||
#[test]
|
||||
fn the_anti_aliasing_method_dropdown_shows_and_sets_the_types_method() {
|
||||
use super::super::tests_appearance::{click, frame_events, text_rect};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "Hamburg", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
let method = |app: &VectorcraftApp, id: u64| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.anti_alias,
|
||||
_ => panic!("text"),
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
let texts = |app: &mut VectorcraftApp| frame_events(&ctx, app, vec![], show);
|
||||
let shown = |app: &mut VectorcraftApp| {
|
||||
texts(app).into_iter().map(|(t, _)| t).filter(|t| ["None", "Sharp", "Crisp", "Strong"].contains(&t.as_str())).collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(shown(&mut app), ["Sharp"]);
|
||||
// Open the dropdown and pick Crisp.
|
||||
let at = text_rect(&texts(&mut app), "Sharp").center();
|
||||
click(&ctx, &mut app, at, show);
|
||||
let listed = texts(&mut app);
|
||||
let crisp = text_rect(&listed, "Crisp").center();
|
||||
click(&ctx, &mut app, crisp, show);
|
||||
assert_eq!(method(&app, a), TypeAntiAlias::Crisp);
|
||||
assert_eq!(shown(&mut app), ["Crisp"]);
|
||||
// Another object with Sharp selected as well: blank.
|
||||
let b = app.run("text.create", json!({"x": 20, "y": 120, "text": "Bremen", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
assert!(shown(&mut app).is_empty(), "the methods differ");
|
||||
// The Type tool with two letters selected: the whole object takes the method.
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
app.session.select_tool("type", Default::default()).unwrap();
|
||||
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
|
||||
app.session.set_tool_option("select", &json!({"start": 1, "end": 3}));
|
||||
assert!(text_editing(&app).is_some());
|
||||
set_anti_alias(&mut app, TypeAntiAlias::Strong);
|
||||
assert_eq!(method(&app, a), TypeAntiAlias::Strong);
|
||||
assert_eq!(shared_anti_alias(&app), Some(TypeAntiAlias::Strong));
|
||||
// Reset Panel sets Sharp, with the characters' attributes, in one undo step.
|
||||
range_style(&mut app, json!({"tracking": 50}));
|
||||
let tracking = |app: &VectorcraftApp| shared(app, |s| s.tracking);
|
||||
assert_eq!(tracking(&app), Some(50.0));
|
||||
reset_panel(&mut app);
|
||||
assert_eq!((method(&app, a), tracking(&app)), (TypeAntiAlias::Sharp, Some(0.0)));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!((method(&app, a), tracking(&app)), (TypeAntiAlias::Strong, Some(50.0)), "one undo step");
|
||||
}
|
||||
|
||||
/// Character Alignment and Proportional Metrics are in the panel menu with the East Asian
|
||||
/// options only (as Mojikumi Set and Top-to-Top Leading are in the Paragraph panel).
|
||||
#[test]
|
||||
|
||||
+29
@@ -3613,6 +3613,35 @@ Setting superscript on subscript characters replaces the subscript, and setting
|
||||
replaces the superscript. When `document.setup {superscript, subscript, smallCapsSize}` changes the proportions, the
|
||||
characters take the new ones. One undo step.
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"position":"superscript","smallCaps":false}}}
|
||||
|
||||
## Anti-aliasing method
|
||||
|
||||
`text.setFormat {antiAlias: "none"|"sharp"|"crisp"|"strong"}` (the selected type, or `ids`) sets the Character panel's
|
||||
Anti-aliasing Method, which decides how the edges of the characters are drawn as pixels: on the canvas, in raster exports
|
||||
(Export As PNG, JPEG and the other raster formats, Export for Screens, Save for Web) and when Object › Rasterize or
|
||||
Effect › Rasterize draws the type. The method belongs to the whole type object and to every frame of its thread: a
|
||||
`start`/`end` range sets the whole object, threading frames gives the thread its first frame's method, and a file
|
||||
whose frames of one thread differ opens with the first frame's method in all of them. VectorCraft defines what each
|
||||
method does, on the pixels of the picture being drawn (the canvas at its zoom, an export at its resolution):
|
||||
|
||||
- `sharp`, the default, draws each edge pixel with the share of it the type covers, as before.
|
||||
- `none` draws hard edges: a pixel takes the type's color where the type covers at least half of it, and nothing
|
||||
elsewhere.
|
||||
- `crisp` draws a pixel the type covers by a share c at `2c - 0.5`, kept between 0 and 1, so the edges are about half as
|
||||
wide.
|
||||
- `strong` draws a pixel the type covers by a share c at `2c`, at most 1, so the type looks heavier.
|
||||
|
||||
The opacity of the type's paints stays as it is inside the characters. Type is drawn as Sharp draws it when it holds
|
||||
inline graphics, when its own fills or strokes in the Appearance panel include a brush stroke or a blend mode other than
|
||||
Normal, when it overprints while Overprint Preview is on, and on the canvas while it shows a substitution highlight.
|
||||
With Preferences › General › Anti-aliased Artwork off, the canvas draws all its art with hard edges, type included, and
|
||||
so does an export whose own Anti-aliasing option is None. SVG, PDF and EPS export are unchanged and carry no setting.
|
||||
Reset Panel sets Sharp. Saved as `antiAlias` only when not `sharp`. One undo step.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"antiAlias":"crisp"}}}
|
||||
```
|
||||
|
||||
## Character Alignment
|
||||
|
||||
`text.setFormat {charAlign}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end, charAlign}` (a range)
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user