mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
Merge #825: Gradient swatches on text, laid across the text frame
# Conflicts: # ROADMAP.md # crates/pdf/src/export.rs # crates/pdf/src/text.rs # crates/render/src/text.rs # docs/target-app-parity.md
This commit is contained in:
@@ -128,6 +128,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-13** · Gradient swatches work on strokes: drawn across the path's box on screen and in PDF (they drew nothing), set with `object.gradient {target: stroke}` and in the Gradient panel when the Stroke box is in front; the stroke's gradient vector moves with the shape and round-trips through IDML. No readiness percentage change.
|
||||
- **2026-10-13** · The Gradient panel follows InDesign's layout (thumbnail, Type, Location, Angle, Reverse, ramp with midpoint diamonds) and takes swatches dragged from the Swatches panel: dropped on a stop they colour it, elsewhere they add a stop. Stops keep their CMYK through panel edits; `object.gradient` stops accept a `swatch`. No readiness percentage change.
|
||||
- **2026-10-13** · New Swatch works as InDesign's: a gradient made in the Gradient panel becomes "New Gradient Swatch" (still applied), an unnamed colour a listed swatch, a named swatch a "copy" (`swatch.new`); `swatch.create {gradient}` makes a gradient swatch from colour swatches. No readiness percentage change.
|
||||
|
||||
@@ -2942,3 +2942,58 @@ mod reserve_tests {
|
||||
assert_eq!(s.journal, journal);
|
||||
}
|
||||
}
|
||||
|
||||
#[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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,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 {
|
||||
@@ -367,6 +368,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);
|
||||
@@ -496,6 +498,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>>,
|
||||
}
|
||||
@@ -964,8 +969,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);
|
||||
}
|
||||
@@ -976,7 +983,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();
|
||||
}
|
||||
@@ -1030,7 +1039,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;
|
||||
@@ -1080,7 +1095,7 @@ impl Exporter<'_> {
|
||||
cx,
|
||||
cy,
|
||||
cr: rx as f32,
|
||||
transform: tf(stretch),
|
||||
transform: tf(xf * stretch),
|
||||
spread_method: SpreadMethod::Pad,
|
||||
stops,
|
||||
anti_alias: false,
|
||||
@@ -1100,7 +1115,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,
|
||||
@@ -1675,6 +1690,7 @@ mod image_content_cache_tests {
|
||||
tags: Vec::new(),
|
||||
story_tags: HashMap::new(),
|
||||
tag_story: None,
|
||||
text_area: None,
|
||||
para_tags: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
+42
-18
@@ -21,13 +21,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)
|
||||
@@ -428,21 +446,27 @@ 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;
|
||||
}
|
||||
if st.overprint_fill || st.overprint_stroke {
|
||||
self.warn("overprint is not exported yet");
|
||||
}
|
||||
let face = g0.face;
|
||||
let size = g0.sy * face.upem;
|
||||
let hs = g0.sx / g0.sy;
|
||||
let tan = if st.skew != 0.0 { st.skew.to_radians().tan() } else { 0.0 };
|
||||
let skew = if st.skew != 0.0 { Affine::new([1.0, 0.0, -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;
|
||||
}
|
||||
if st.overprint_fill || st.overprint_stroke {
|
||||
self.warn("overprint is not exported yet");
|
||||
}
|
||||
// Stroke behind fill: a double-width stroke first, as outlines so the text is not
|
||||
// duplicated, then the fill over its inner half. With no fill, the stroke is clipped
|
||||
// to the outside of the glyphs instead.
|
||||
@@ -487,8 +511,8 @@ impl Exporter<'_> {
|
||||
size: f64,
|
||||
hs: f64,
|
||||
tan: f64,
|
||||
fill: Option<krilla::color::Color>,
|
||||
stroke: Option<krilla::color::Color>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Option<Paint>,
|
||||
stroke_weight: f64,
|
||||
) {
|
||||
let Some(g0) = glyphs.first() else { return };
|
||||
@@ -510,8 +534,8 @@ impl Exporter<'_> {
|
||||
.collect();
|
||||
let start = order.first().and_then(|&i| glyphs.get(i)).map_or(0.0, |g| (g.x - g0.x) / hs);
|
||||
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_glyphs(Point::from_xy(start as f32, 0.0), &kg, font, &text, size as f32, false);
|
||||
s.set_fill(None);
|
||||
s.set_stroke(None);
|
||||
@@ -524,14 +548,14 @@ impl Exporter<'_> {
|
||||
s: &mut Surface,
|
||||
outlines: &BezPath,
|
||||
origin: Affine,
|
||||
fill: Option<krilla::color::Color>,
|
||||
stroke: Option<krilla::color::Color>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Option<Paint>,
|
||||
stroke_weight: f64,
|
||||
) {
|
||||
let Some(p) = to_path(outlines) 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);
|
||||
|
||||
+73
-21
@@ -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`).
|
||||
@@ -359,13 +398,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
|
||||
@@ -385,7 +427,7 @@ impl Renderer {
|
||||
let st = &cs.styles[*si as usize];
|
||||
let fill = fill_of(*si).map(|c| (c, crate::overprints(f, &st.fill, st.fill_tint, st.overprint_fill)));
|
||||
let stroke = stroke_of(doc, st).map(|c| (c, crate::overprints(f, &st.stroke, st.stroke_tint, st.overprint_stroke)));
|
||||
paint_glyphs(ctx, bp, fill, stroke, st.stroke_weight, st.stroke_behind_fill);
|
||||
paint_glyphs(ctx, doc, area, bp, fill, stroke, st.stroke_weight, st.stroke_behind_fill);
|
||||
}
|
||||
}
|
||||
for &i in &shown {
|
||||
@@ -470,11 +512,11 @@ impl Renderer {
|
||||
}
|
||||
|
||||
/// A run's stroke colour, if it has one.
|
||||
fn stroke_of(doc: &designcraft_doc::Document, st: &designcraft_compose::RunStyle) -> Option<peniko::Color> {
|
||||
fn stroke_of(doc: &designcraft_doc::Document, st: &designcraft_compose::RunStyle) -> Option<TextPaint> {
|
||||
if st.stroke == designcraft_color::swatch::NONE {
|
||||
return None;
|
||||
}
|
||||
doc.resolve_color(&st.stroke, st.stroke_tint).map(|c| color_of(&c, 1.0))
|
||||
TextPaint::of(doc, &st.stroke, st.stroke_tint)
|
||||
}
|
||||
|
||||
/// Run `paint` in a Multiply layer when `overprint` is set.
|
||||
@@ -492,19 +534,24 @@ fn overprinting(ctx: &mut RenderContext, overprint: bool, paint: impl FnOnce(&mu
|
||||
/// (`(colour, overprint)`). With stroke behind fill a double-width stroke goes first and the fill
|
||||
/// covers its inner half; with no fill to cover it, the glyph shapes are cut out of the stroke
|
||||
/// instead.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn paint_glyphs(
|
||||
ctx: &mut RenderContext,
|
||||
doc: &designcraft_doc::Document,
|
||||
area: Rect,
|
||||
bp: &BezPath,
|
||||
fill: Option<(peniko::Color, bool)>,
|
||||
stroke: Option<(peniko::Color, bool)>,
|
||||
fill: Option<(TextPaint, bool)>,
|
||||
stroke: Option<(TextPaint, bool)>,
|
||||
weight: f64,
|
||||
behind: bool,
|
||||
) {
|
||||
let has_fill = fill.is_some();
|
||||
let paint_fill = |ctx: &mut RenderContext| {
|
||||
let Some((c, overprint)) = fill else { return };
|
||||
overprinting(ctx, overprint, |ctx| {
|
||||
ctx.set_paint(c);
|
||||
let Some((c, overprint)) = &fill else { return };
|
||||
overprinting(ctx, *overprint, |ctx| {
|
||||
c.set(ctx, doc, area);
|
||||
ctx.fill_path(bp);
|
||||
ctx.reset_paint_transform();
|
||||
});
|
||||
};
|
||||
let Some((sc, stroke_overprint)) = stroke else {
|
||||
@@ -514,29 +561,32 @@ fn paint_glyphs(
|
||||
if !behind {
|
||||
paint_fill(ctx);
|
||||
overprinting(ctx, stroke_overprint, |ctx| {
|
||||
ctx.set_paint(sc);
|
||||
sc.set(ctx, doc, area);
|
||||
ctx.set_stroke(kurbo::Stroke::new(weight));
|
||||
ctx.stroke_path(bp);
|
||||
ctx.reset_paint_transform();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if fill.is_some() {
|
||||
if has_fill {
|
||||
overprinting(ctx, stroke_overprint, |ctx| {
|
||||
ctx.set_paint(sc);
|
||||
sc.set(ctx, doc, area);
|
||||
ctx.set_stroke(kurbo::Stroke::new(weight * 2.0));
|
||||
ctx.stroke_path(bp);
|
||||
ctx.reset_paint_transform();
|
||||
});
|
||||
paint_fill(ctx);
|
||||
} else {
|
||||
overprinting(ctx, stroke_overprint, |ctx| {
|
||||
ctx.push_layer(None, None, None, None, None);
|
||||
ctx.set_paint(sc);
|
||||
sc.set(ctx, doc, area);
|
||||
ctx.set_stroke(kurbo::Stroke::new(weight * 2.0));
|
||||
ctx.stroke_path(bp);
|
||||
ctx.push_layer(None, Some(peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut)), None, None, None);
|
||||
ctx.fill_path(bp);
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
ctx.reset_paint_transform();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -571,10 +621,12 @@ 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];
|
||||
let fill = doc.resolve_color(&st.fill, st.fill_tint).map(|c| (color_of(&c, 1.0), false));
|
||||
paint_glyphs(ctx, &bp, fill, stroke_of(doc, st).map(|c| (c, false)), st.stroke_weight, st.stroke_behind_fill);
|
||||
let fill = TextPaint::of(doc, &st.fill, st.fill_tint).map(|c| (c, false));
|
||||
paint_glyphs(ctx, doc, area, &bp, fill, stroke_of(doc, st).map(|c| (c, false)), st.stroke_weight, st.stroke_behind_fill);
|
||||
self.stats.glyphs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +433,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 (InDesign's layout: thumbnail, Type, Location, Angle, Reverse) sets type, angle, reverse and edits stops and midpoints on a ramp (drag, add, drag off to remove, location; drop a swatch from the Swatches panel on a stop or the ramp; stops keep their CMYK), and follows the Fill/Stroke proxy. Edited gradients are unnamed swatches until the Swatches panel's New Swatch names one "New Gradient Swatch" (`swatch.new`, which also duplicates a named swatch); `swatch.create {gradient}` builds one from colour swatches; `object.gradient` (`target: stroke` for a stroke's gradient, drawn across the path's box on screen and in PDF); the vector round-trips through IDML (`GradientFill…`/`GradientStroke…`). A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF).
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel (InDesign's layout: thumbnail, Type, Location, Angle, Reverse) sets type, angle, reverse and edits stops and midpoints on a ramp (drag, add, drag off to remove, location; drop a swatch from the Swatches panel on a stop or the ramp; stops keep their CMYK), and follows the Fill/Stroke proxy. Edited gradients are unnamed swatches until the Swatches panel's New Swatch names one "New Gradient Swatch" (`swatch.new`, which also duplicates a named swatch); `swatch.create {gradient}` builds one from colour swatches; `object.gradient` (`target: stroke` for a stroke's gradient, drawn across the path's box on screen and in PDF); the vector round-trips through IDML (`GradientFill…`/`GradientStroke…`). A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF). Text takes a gradient swatch as its fill or stroke, laid across its frame's box as in InDesign (screen and PDF).
|
||||
- New Document (⌘N) as in InDesign: Recent, Saved, Print, Web and Mobile tabs of page cards (double-click creates) beside the preset's details: document name, size with arrows, units, orientation, pages, start page #, facing pages, primary text frame, columns, margins, bleed and slug per edge (linked or not); Save Document Preset (`file.savePreset`, `file.deletePreset`; saved presets are kept with the preferences, listed by `file.presets` and made by `file.new {preset: name}`); `file.new` takes `intent`, `units`, `startPage` and per-edge `bleed`/`slug`. Preview shows a document with the window's settings in the workspace while it is open; Create keeps it, Close removes it.
|
||||
- Document Setup (⌥⌘P), laid out like InDesign's: intent, number of pages, start page # (an even start begins with a left page), facing pages, binding, Page Size (presets keeping the orientation, width and height with arrows, orientation), margins for every page and parent page, bleed and slug per edge (each group linked or not), and Adjust Layout (objects follow the new page size and margins), all in one undo step; measures are read in the document's units, and a value that can't be read keeps the dialog open; `layout.documentSetup {}` reports the setup, margins included.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user