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:
dotCooCoo
2026-10-17 17:30:41 -07:00
parent e6aec0664c
commit 983df1a9d1
35 changed files with 872 additions and 21 deletions
+44
View File
@@ -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(),
}
+2
View File
@@ -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) {
+18 -1
View File
@@ -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<_>>() }))
+2
View File
@@ -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(),
};
+82
View File
@@ -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]
+5 -3
View File
@@ -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
View File
@@ -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;
+3 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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);
}
+291
View File
@@ -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");
}
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -165,6 +165,10 @@
Anti-Alias Εξομάλυνση ακμών
Anti-alias Rasters Εξομάλυνση ακμών εικόνων ράστερ
Anti-aliased Artwork Γραφικά με εξομαλυμένες ακμές
Anti-aliasing Method Μέθοδος εξομάλυνσης ακμών
Sharp Οξεία
Crisp Καθαρή
Strong Έντονη
Anti-aliasing: Εξομάλυνση ακμών:
Any Kind Οποιοσδήποτε τύπος
Appearance Εμφάνιση
1 # Ελληνικά (el) — VectorCraft
165
166
167
168
169
170
171
172
173
174
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -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.
+4
View File
@@ -179,6 +179,10 @@
Anti-Alias Згладжування
Anti-alias Rasters Згладжувати растрові зображення
Anti-aliased Artwork Згладжений малюнок
Anti-aliasing Method Метод згладжування
Sharp Різке
Crisp Чітке
Strong Сильне
Anti-aliasing: Згладжування:
Any Kind Будь-який тип
Appearance Оформлення
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
179
180
181
182
183
184
185
186
187
188
+4
View File
@@ -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
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
209
210
211
212
213
214
215
216
217
218
+4
View File
@@ -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.
+4
View File
@@ -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.
+116 -6
View File
@@ -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
View File
@@ -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