mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
feat(text): gradient swatches on text, laid across the frame as in InDesign
LYH Studio episode 6 (8:28) gives the "1969" headline the "New Gradient Swatch" (magenta → cyan) from the Swatches panel. InDesign runs a gradient on text across its text frame's box. DesignCraft resolved a character fill to one solid colour, so a gradient swatch drew no text at all. - render: a character fill or stroke that names a gradient swatch is painted with that gradient over the frame's box in text space (type on a path: the path's box). - pdf: text runs take a gradient paint: the shading is laid out in the frame's text space and mapped into each run's origin, so skew and horizontal scale don't bend it; outlined runs and type on a path too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e862e0a613
commit
ac54c8faf7
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-13** · Text takes gradient swatches: a gradient fill or stroke on characters spans the text frame's box (on a path: the path's), on screen and in PDF. No readiness percentage change.
|
||||
- **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change.
|
||||
- **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
|
||||
- **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change.
|
||||
|
||||
@@ -2048,3 +2048,58 @@ mod multi_click_tests {
|
||||
assert_eq!(para_bounds(s, s.len()), (s.len(), s.len()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradient_text_tests {
|
||||
use serde_json::json;
|
||||
|
||||
/// Mean red and blue of the ink (pixels whose green is low: red → blue has none) in columns
|
||||
/// `x0..x1`.
|
||||
fn ink(img: &designcraft_render::Rendered, x0: u32, x1: u32) -> (f64, f64) {
|
||||
let (mut r, mut b, mut n) = (0.0, 0.0, 0.0);
|
||||
for x in x0..x1 {
|
||||
for y in 0..img.height {
|
||||
let p = img.pixel(x, y);
|
||||
if p[1] < 100 {
|
||||
(r, b, n) = (r + p[0] as f64, b + p[2] as f64, n + 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(n > 50.0, "no ink in {x0}..{x1}");
|
||||
(r / n, b / n)
|
||||
}
|
||||
|
||||
/// Red ink at the frame's left, blue at its right.
|
||||
fn check(img: &designcraft_render::Rendered, what: &str) {
|
||||
let (lr, lb) = ink(img, 0, 60);
|
||||
let (rr, rb) = ink(img, 200, 300);
|
||||
assert!(lr > lb + 80.0, "{what}: the left ink is red ({lr:.0}, {lb:.0})");
|
||||
assert!(rb > rr + 80.0, "{what}: the right ink is blue ({rr:.0}, {rb:.0})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_on_text_spans_its_frame_on_screen_and_in_pdf() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({"width": 300, "height": 120, "facingPages": false, "margins": 0})).unwrap();
|
||||
// A gradient swatch (an off-page object's unnamed one), red → blue.
|
||||
let shape = s.execute("frame.create", &json!({"rect": [-60, 0, -50, 10], "content": "unassigned"})).unwrap()["id"].clone();
|
||||
s.execute("object.gradient", &json!({"ids": [shape], "stops": [{"location": 0, "color": "#ff0000"}, {"location": 100, "color": "#0000ff"}]}))
|
||||
.unwrap();
|
||||
let id = designcraft_doc::ItemId(shape.as_u64().unwrap());
|
||||
let swatch = s.doc().unwrap().doc.item(id).unwrap().fill.swatch.clone();
|
||||
let r = s.execute("frame.create", &json!({"rect": [0, 0, 300, 120], "content": "text", "text": "IIIIIIIIIIIIIIIIIIIIIIIIIIIIII"})).unwrap();
|
||||
s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 30})).unwrap();
|
||||
s.execute("type.char", &json!({"size": 60, "tracking": 0, "fill": swatch})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
check(&rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap(), "screen");
|
||||
// The PDF, placed back and drawn: the same gradient.
|
||||
let pdf = designcraft_pdf::export_pdf(&d, &s.cache, &Default::default()).unwrap();
|
||||
let mut p = crate::Session::new();
|
||||
p.execute("file.new", &json!({"width": 300, "height": 120, "facingPages": false})).unwrap();
|
||||
p.execute("file.place", &json!({"base64": super::super::file::base64_encode(&pdf), "name": "text.pdf", "x": 0, "y": 0, "width": 300}))
|
||||
.unwrap();
|
||||
let d = p.doc().unwrap().doc.clone();
|
||||
check(&rr.render_page(&d, &p.cache, 0, 1.0, false, &Default::default()).unwrap(), "pdf");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ pub fn export_pdf_with_report(doc: &Document, cache: &Cache, opts: &PdfOptions)
|
||||
tags: Vec::new(),
|
||||
story_tags: HashMap::new(),
|
||||
tag_story: None,
|
||||
text_area: None,
|
||||
para_tags: HashMap::new(),
|
||||
};
|
||||
if ex.rgb_only {
|
||||
@@ -338,6 +339,7 @@ pub fn export_booklet(doc: &Document, cache: &Cache, opts: &BookletOptions) -> R
|
||||
tags: Vec::new(),
|
||||
story_tags: HashMap::new(),
|
||||
tag_story: None,
|
||||
text_area: None,
|
||||
para_tags: HashMap::new(),
|
||||
};
|
||||
let pairs = booklet_pairs(n, opts.kind);
|
||||
@@ -468,6 +470,9 @@ pub(crate) struct Exporter<'a> {
|
||||
/// While drawing a story's frame for the structure tree: the story (text runs are tagged
|
||||
/// per paragraph, everything else in the frame is an artifact).
|
||||
pub(crate) tag_story: Option<designcraft_doc::StoryId>,
|
||||
/// While drawing a frame's text: the frame's box in text space, which a gradient on the text
|
||||
/// spans (as InDesign lays it across the frame).
|
||||
pub(crate) text_area: Option<Rect>,
|
||||
/// Marked content of each (story, paragraph index).
|
||||
pub(crate) para_tags: HashMap<(designcraft_doc::StoryId, usize), Vec<Identifier>>,
|
||||
}
|
||||
@@ -925,8 +930,10 @@ impl Exporter<'_> {
|
||||
// Type on a path: the glyphs as outlines along the path.
|
||||
for (si, gp) in designcraft_compose::path_glyphs(&cs, ft, &bp, pt) {
|
||||
let st = &cs.styles[si as usize];
|
||||
if let (Some(c), Some(p)) = (self.swatch_color(&st.fill, st.fill_tint), to_path(&gp)) {
|
||||
s.set_fill(Some(Fill { paint: c.into(), opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
// A gradient spans the path's box.
|
||||
let fill = designcraft_doc::Fill { swatch: st.fill.clone(), tint: st.fill_tint, ..designcraft_doc::Fill::none() };
|
||||
if let (Some(paint), Some(p)) = (self.fill_paint(&fill, bp.bounding_box()), to_path(&gp)) {
|
||||
s.set_fill(Some(Fill { paint, opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
s.draw_path(&p);
|
||||
s.set_fill(None);
|
||||
}
|
||||
@@ -937,7 +944,9 @@ impl Exporter<'_> {
|
||||
if local != Affine::IDENTITY {
|
||||
s.push_transform(&tf(local));
|
||||
}
|
||||
self.text_area = (local.determinant().abs() > 1e-12).then(|| local.inverse().transform_rect_bbox(it.inner_bounds()));
|
||||
self.frame_text(s, &cs, ft, text);
|
||||
self.text_area = None;
|
||||
if local != Affine::IDENTITY {
|
||||
s.pop();
|
||||
}
|
||||
@@ -977,7 +986,13 @@ impl Exporter<'_> {
|
||||
}
|
||||
|
||||
/// A swatch fill (solid or gradient) in the item's inner space. `None` for [None]/unknown.
|
||||
fn fill_paint(&mut self, fill: &designcraft_doc::Fill, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
pub(crate) fn fill_paint(&mut self, fill: &designcraft_doc::Fill, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
self.fill_paint_in(fill, bounds, Affine::IDENTITY)
|
||||
}
|
||||
|
||||
/// [`Self::fill_paint`] for a gradient laid out in a space that `xf` maps to the current one
|
||||
/// (text runs: the frame's text space under the run's origin).
|
||||
pub(crate) fn fill_paint_in(&mut self, fill: &designcraft_doc::Fill, bounds: Rect, xf: Affine) -> Option<krilla::paint::Paint> {
|
||||
let (swatch, tint, angle) = (fill.swatch.as_str(), fill.tint, fill.gradient_angle);
|
||||
if let Some(g) = designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch) {
|
||||
let kind = g.kind;
|
||||
@@ -1028,7 +1043,7 @@ impl Exporter<'_> {
|
||||
cx,
|
||||
cy,
|
||||
cr: r,
|
||||
transform: Transform::identity(),
|
||||
transform: tf(xf),
|
||||
spread_method: SpreadMethod::Pad,
|
||||
stops,
|
||||
anti_alias: false,
|
||||
@@ -1048,7 +1063,7 @@ impl Exporter<'_> {
|
||||
y1: p0.y as f32,
|
||||
x2: p1.x as f32,
|
||||
y2: p1.y as f32,
|
||||
transform: Transform::identity(),
|
||||
transform: tf(xf),
|
||||
spread_method: SpreadMethod::Pad,
|
||||
stops,
|
||||
anti_alias: false,
|
||||
@@ -1577,6 +1592,7 @@ mod image_content_cache_tests {
|
||||
tags: Vec::new(),
|
||||
story_tags: HashMap::new(),
|
||||
tag_story: None,
|
||||
text_area: None,
|
||||
para_tags: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+37
-13
@@ -15,13 +15,31 @@ use designcraft_fonts::{FontDb, FontFace};
|
||||
use designcraft_geom::kurbo::Shape as _;
|
||||
use designcraft_geom::{Affine, BezPath, Rect};
|
||||
use krilla::geom::Point;
|
||||
use krilla::paint::Stroke;
|
||||
use krilla::num::NormalizedF32;
|
||||
use krilla::paint::{Fill, FillRule, Paint, Stroke};
|
||||
use krilla::surface::Surface;
|
||||
use krilla::tagging::{Artifact, ArtifactType, ContentTag, SpanTag};
|
||||
use krilla::text::{Font, GlyphId, KrillaGlyph};
|
||||
|
||||
use crate::export::{Exporter, solid_fill, tf, to_path};
|
||||
|
||||
impl Exporter<'_> {
|
||||
/// A text run's paint: solid, or a gradient spanning the frame's box (`area`, text space),
|
||||
/// for glyphs drawn under `origin` (text space ← run space).
|
||||
fn text_paint(&mut self, swatch: &str, tint: f32, origin: Affine, area: Rect) -> Option<Paint> {
|
||||
if designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch).is_some() && origin.determinant().abs() > 1e-12 {
|
||||
let fill = designcraft_doc::Fill { swatch: swatch.into(), tint, ..designcraft_doc::Fill::none() };
|
||||
return self.fill_paint_in(&fill, self.text_area.unwrap_or(area), origin.inverse());
|
||||
}
|
||||
self.swatch_color(swatch, tint).map(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// An opaque nonzero fill with `paint`.
|
||||
fn paint_fill(paint: Paint) -> Fill {
|
||||
Fill { paint, opacity: NormalizedF32::ONE, rule: FillRule::NonZero }
|
||||
}
|
||||
|
||||
/// Private-use characters DesignCraft stores for markers (page numbers, section markers, breaks).
|
||||
fn is_marker(c: char) -> bool {
|
||||
('\u{E000}'..='\u{E1FF}').contains(&c)
|
||||
@@ -289,17 +307,23 @@ impl Exporter<'_> {
|
||||
fn run(&mut self, s: &mut Surface, cs: &ComposedStory, glyphs: &[PlacedGlyph], baseline: f64, story: &str) {
|
||||
let g0 = &glyphs[0];
|
||||
let st = &cs.styles[g0.style as usize];
|
||||
let fill = self.swatch_color(&st.fill, st.fill_tint);
|
||||
let stroke =
|
||||
if st.stroke != designcraft_color::swatch::NONE && st.stroke_weight > 0.0 { self.swatch_color(&st.stroke, st.stroke_tint) } else { None };
|
||||
if fill.is_none() && stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
let face = g0.face;
|
||||
let size = g0.sy * face.upem;
|
||||
let hs = g0.sx / g0.sy;
|
||||
let skew = if st.skew != 0.0 { Affine::new([1.0, 0.0, -st.skew.to_radians().tan(), 1.0, 0.0, 0.0]) } else { Affine::IDENTITY };
|
||||
let origin = Affine::translate((g0.x, baseline + g0.y)) * skew * Affine::scale_non_uniform(hs, 1.0);
|
||||
// Outside a frame (no text area), a gradient spans the run.
|
||||
let end = glyphs.last().map_or(g0.x, |g| g.x + g.adv);
|
||||
let run_area = Rect::new(g0.x, baseline - size, end.max(g0.x + 1.0), baseline + size * 0.25);
|
||||
let fill = self.text_paint(&st.fill, st.fill_tint, origin, run_area);
|
||||
let stroke = if st.stroke != designcraft_color::swatch::NONE && st.stroke_weight > 0.0 {
|
||||
self.text_paint(&st.stroke, st.stroke_tint, origin, run_area)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if fill.is_none() && stroke.is_none() {
|
||||
return;
|
||||
}
|
||||
// A missing glyph is drawn as its box: PDF/A and PDF/UA forbid showing .notdef as text.
|
||||
if g0.gid == 0 {
|
||||
self.outline_run(s, glyphs, origin, size, fill, stroke, st.stroke_weight);
|
||||
@@ -322,8 +346,8 @@ impl Exporter<'_> {
|
||||
})
|
||||
.collect();
|
||||
s.push_transform(&tf(origin));
|
||||
s.set_fill(fill.map(|c| solid_fill(c, 1.0)));
|
||||
s.set_stroke(stroke.map(|c| Stroke { paint: c.into(), width: st.stroke_weight as f32, ..Default::default() }));
|
||||
s.set_fill(fill.map(paint_fill));
|
||||
s.set_stroke(stroke.map(|paint| Stroke { paint, width: st.stroke_weight as f32, ..Default::default() }));
|
||||
s.draw_glyphs(Point::from_xy(0.0, 0.0), &kg, font, &text, size as f32, false);
|
||||
s.set_fill(None);
|
||||
s.set_stroke(None);
|
||||
@@ -338,8 +362,8 @@ impl Exporter<'_> {
|
||||
glyphs: &[PlacedGlyph],
|
||||
origin: Affine,
|
||||
size: f64,
|
||||
fill: Option<krilla::color::Color>,
|
||||
stroke: Option<krilla::color::Color>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Option<Paint>,
|
||||
stroke_weight: f64,
|
||||
) {
|
||||
let db = FontDb::global();
|
||||
@@ -354,8 +378,8 @@ impl Exporter<'_> {
|
||||
}
|
||||
let Some(p) = to_path(&bp) else { return };
|
||||
s.push_transform(&tf(origin));
|
||||
s.set_fill(fill.map(|c| solid_fill(c, 1.0)));
|
||||
s.set_stroke(stroke.map(|c| Stroke { paint: c.into(), width: stroke_weight as f32, ..Default::default() }));
|
||||
s.set_fill(fill.map(paint_fill));
|
||||
s.set_stroke(stroke.map(|paint| Stroke { paint, width: stroke_weight as f32, ..Default::default() }));
|
||||
s.draw_path(&p);
|
||||
s.set_fill(None);
|
||||
s.set_stroke(None);
|
||||
|
||||
+62
-14
@@ -19,6 +19,45 @@ use vello_cpu::peniko;
|
||||
|
||||
use crate::{Frame, Renderer, color_of, rect_overlaps};
|
||||
|
||||
/// A text colour: solid, or a gradient swatch laid across the frame (InDesign spans a gradient on
|
||||
/// text across its frame's box).
|
||||
#[derive(Clone)]
|
||||
enum TextPaint {
|
||||
Solid(peniko::Color),
|
||||
Gradient(designcraft_doc::Fill),
|
||||
}
|
||||
|
||||
impl TextPaint {
|
||||
fn of(doc: &designcraft_doc::Document, swatch: &str, tint: f32) -> Option<Self> {
|
||||
if designcraft_color::swatch::resolve_gradient(&doc.swatches, swatch).is_some() {
|
||||
return Some(TextPaint::Gradient(designcraft_doc::Fill { swatch: swatch.into(), tint, ..designcraft_doc::Fill::none() }));
|
||||
}
|
||||
doc.resolve_color(swatch, tint).map(|c| TextPaint::Solid(color_of(&c, 1.0)))
|
||||
}
|
||||
|
||||
/// Make this the context's paint; a gradient runs across `area` (text space).
|
||||
fn set(&self, ctx: &mut RenderContext, doc: &designcraft_doc::Document, area: Rect) {
|
||||
match self {
|
||||
TextPaint::Solid(c) => ctx.set_paint(*c),
|
||||
TextPaint::Gradient(fill) => {
|
||||
crate::set_fill_paint(ctx, doc, fill, area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The box a gradient on a frame's text spans: the frame's, in text space (the union of its lines
|
||||
/// when the frame is gone).
|
||||
fn text_area(doc: &designcraft_doc::Document, ft: &FrameText) -> Rect {
|
||||
if let Some(it) = doc.item(ft.frame) {
|
||||
let local = doc.text_local(it);
|
||||
if local.determinant().abs() > 1e-12 {
|
||||
return local.inverse().transform_rect_bbox(it.inner_bounds());
|
||||
}
|
||||
}
|
||||
ft.lines.iter().filter_map(line_box).reduce(|a, b| a.union(b)).unwrap_or(Rect::new(0.0, 0.0, 1.0, 1.0))
|
||||
}
|
||||
|
||||
/// Cached outlines of one composed line.
|
||||
pub(crate) struct LineGlyphs {
|
||||
/// Frame-space bounds of everything drawn for the line (empty lines: `None`).
|
||||
@@ -349,13 +388,16 @@ impl Renderer {
|
||||
ctx.set_transform(m);
|
||||
if !shown.is_empty() {
|
||||
let fg = self.glyphs.get(cs, ft);
|
||||
// Resolve each style's paints once per frame.
|
||||
let mut fills: Vec<Option<Option<peniko::Color>>> = vec![None; cs.styles.len()];
|
||||
let mut fill_of = |si: u32| -> Option<peniko::Color> {
|
||||
*fills[si as usize].get_or_insert_with(|| {
|
||||
let st = &cs.styles[si as usize];
|
||||
doc.resolve_color(&st.fill, st.fill_tint).map(|c| color_of(&c, 1.0))
|
||||
})
|
||||
// Resolve each style's paints once per frame; gradients span the frame.
|
||||
let area = text_area(doc, ft);
|
||||
let mut fills: Vec<Option<Option<TextPaint>>> = vec![None; cs.styles.len()];
|
||||
let mut fill_of = |si: u32| -> Option<TextPaint> {
|
||||
fills[si as usize]
|
||||
.get_or_insert_with(|| {
|
||||
let st = &cs.styles[si as usize];
|
||||
TextPaint::of(doc, &st.fill, st.fill_tint)
|
||||
})
|
||||
.clone()
|
||||
};
|
||||
shown.retain(|&i| fg.lines[i].bounds.is_some_and(|b| rect_overlaps(b, vis)));
|
||||
// Paint all underlines before any glyphs: skewed runs and tightly spaced lines
|
||||
@@ -384,19 +426,21 @@ impl Renderer {
|
||||
None,
|
||||
);
|
||||
}
|
||||
ctx.set_paint(c);
|
||||
c.set(ctx, doc, area);
|
||||
ctx.fill_path(bp);
|
||||
ctx.reset_paint_transform();
|
||||
if op {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
let st = &cs.styles[*si as usize];
|
||||
if st.stroke != designcraft_color::swatch::NONE
|
||||
&& let Some(c) = doc.resolve_color(&st.stroke, st.stroke_tint)
|
||||
&& let Some(c) = TextPaint::of(doc, &st.stroke, st.stroke_tint)
|
||||
{
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
c.set(ctx, doc, area);
|
||||
ctx.set_stroke(kurbo::Stroke::new(st.stroke_weight));
|
||||
ctx.stroke_path(bp);
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,18 +540,22 @@ impl Renderer {
|
||||
) {
|
||||
let doc = f.doc;
|
||||
ctx.set_transform(f.view * xf);
|
||||
// A gradient on type on a path spans the path's box.
|
||||
let area = path.bounding_box();
|
||||
for (si, bp) in designcraft_compose::path_glyphs(cs, ft, path, pt) {
|
||||
let st = &cs.styles[si as usize];
|
||||
if let Some(c) = doc.resolve_color(&st.fill, st.fill_tint) {
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
if let Some(c) = TextPaint::of(doc, &st.fill, st.fill_tint) {
|
||||
c.set(ctx, doc, area);
|
||||
ctx.fill_path(&bp);
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
if st.stroke != designcraft_color::swatch::NONE
|
||||
&& let Some(c) = doc.resolve_color(&st.stroke, st.stroke_tint)
|
||||
&& let Some(c) = TextPaint::of(doc, &st.stroke, st.stroke_tint)
|
||||
{
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
c.set(ctx, doc, area);
|
||||
ctx.set_stroke(kurbo::Stroke::new(st.stroke_weight));
|
||||
ctx.stroke_path(&bp);
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
self.stats.glyphs += 1;
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough).
|
||||
- OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes.
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`.
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML.
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML. Text takes a gradient swatch as its fill or stroke, laid across its frame's box as in InDesign (screen and PDF).
|
||||
- Document Setup (⌥⌘P): intent, number of pages, start page # (an even start begins with a left page), facing pages, size presets and orientation, bleed and slug per edge; `layout.documentSetup {}` reports the setup.
|
||||
- Stroke panel: cap, join, miter limit, alignment, type, Start/End arrowheads (12 kinds, drawn on screen and in PDF; the path is shortened under pointed heads) and gap colour under dashes and dots.
|
||||
- Preferences (⌘K: General, Type, Units & Increments, Grids, Guides & Pasteboard, Display Performance) backed by `prefs.set` (application) and `document.preferences` (units, increments, grids — undoable, saved with the document; right-click a ruler to pick its units, the horizontal and vertical rulers each have their own); scaling applies to content and scales stroke weights (Include Stroke Weight), and X/Y/W/H measure the stroke's outer edge (Dimensions Include Stroke Weight). Quick Apply (⌘Return) finds paragraph, character and object styles as well as commands.
|
||||
|
||||
Reference in New Issue
Block a user