mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
E: Directional Feather
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9ecd3fb9fd
commit
84d8f498eb
7 files changed
+95
-5
No files matched your search
@@ -107,6 +107,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Math expressions (Type ▸ Insert Math Expression…, `math.insert`, `math.svg`): a LaTeX subset (fractions, scripts, roots, big operators with limits, Greek, relations and operators with math spacing, `\left…\right`, `\text`) typeset by our own engine into SVG, placed inline at the cursor or as a frame; the LaTeX is kept as alt text and can be edited.
|
||||
- Object Layer Options for placed PDFs (`object.pdfLayers`, `object.layerOptions`, Object ▸ Object Layer Options…): the file's optional-content layers listed and shown or hidden per frame on screen; in exported PDFs such graphics go out as 300 ppi images of their visible layers.
|
||||
- Effects: Inner Glow (edge or centre), Bevel and Emboss (inner bevel lit from the light angle), Satin (`object.innerGlow`, `object.bevel`, `object.satin`); PDF export now keeps soft effects: objects with shadows, glows, feathers, bevels or satin go out as 300 ppi transparent images of their appearance (threaded text frames excepted).
|
||||
- Directional Feather (`object.directionalFeather {widths: [top, left, bottom, right]}`): each side fades over its own width.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
||||
@@ -718,6 +718,17 @@ pub struct Effects {
|
||||
pub bevel: Bevel,
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub satin: Satin,
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub directional_feather: DirectionalFeather,
|
||||
}
|
||||
|
||||
/// Directional Feather: each side fades over its own width (item space, before rotation).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct DirectionalFeather {
|
||||
pub on: bool,
|
||||
/// Top, left, bottom, right widths in points.
|
||||
pub widths: [f64; 4],
|
||||
}
|
||||
|
||||
/// Inner Glow: a glow inside the edge (or from the centre).
|
||||
@@ -843,6 +854,7 @@ impl Effects {
|
||||
|| self.inner_glow.on
|
||||
|| self.bevel.on
|
||||
|| self.satin.on
|
||||
|| self.directional_feather.on
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,14 @@ fn threaded(d: &designcraft_doc::Document, it: &designcraft_doc::Item) -> bool {
|
||||
/// Soft effects other than the gradient feather (which PDF export draws itself).
|
||||
fn raster_effects(it: &designcraft_doc::Item) -> bool {
|
||||
let e = &it.effects;
|
||||
e.drop_shadow.on || e.feather > 0.0 || e.inner_shadow.on || e.outer_glow.on || e.inner_glow.on || e.bevel.on || e.satin.on
|
||||
e.drop_shadow.on
|
||||
|| e.feather > 0.0
|
||||
|| e.inner_shadow.on
|
||||
|| e.outer_glow.on
|
||||
|| e.inner_glow.on
|
||||
|| e.bevel.on
|
||||
|| e.satin.on
|
||||
|| e.directional_feather.on
|
||||
}
|
||||
|
||||
/// A copy of `d` where spread-level objects with soft effects are 300 ppi images of their whole
|
||||
|
||||
@@ -403,6 +403,38 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
)
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"object.directionalFeather",
|
||||
"Directional Feather",
|
||||
["Object", "Effects"],
|
||||
None,
|
||||
"{on?: bool, widths?: [top, left, bottom, right] | number, ids?}",
|
||||
has_selection,
|
||||
|s, p| {
|
||||
let p2 = p.clone();
|
||||
set_flag(
|
||||
s,
|
||||
p,
|
||||
move |i| {
|
||||
let e = &mut i.effects.directional_feather;
|
||||
e.on = p2.get("on").and_then(Value::as_bool).unwrap_or(!e.on);
|
||||
match p2.get("widths") {
|
||||
Some(Value::Number(n)) => e.widths = [n.as_f64().unwrap_or(0.0).max(0.0); 4],
|
||||
Some(v @ Value::Array(_)) => {
|
||||
if let Ok(w) = serde_json::from_value::<[f64; 4]>(v.clone()) {
|
||||
e.widths = w.map(|x| x.max(0.0));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if e.on && e.widths.iter().all(|w| *w <= 0.0) {
|
||||
e.widths = [9.0; 4];
|
||||
}
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
),
|
||||
cmd!("object.feather", "Basic Feather", ["Object", "Effects"], None, "{width (0 = off), ids?}", has_selection, |s, p| {
|
||||
let w = f64_or(p, "width", 9.0).max(0.0);
|
||||
set_flag(s, p, move |i| i.effects.feather = w, false)
|
||||
@@ -2150,6 +2182,12 @@ mod fitting_tests {
|
||||
.flat_map(|x| (110..290).step_by(10).map(move |y| (x, y)))
|
||||
.any(|(x, y)| (st.pixel(x, y)[1] as i32) < plain.pixel(x, y)[1] as i32 - 30);
|
||||
assert!(darker);
|
||||
s.execute("object.satin", &json!({"ids": [id], "on": false})).unwrap();
|
||||
// Directional feather: only the left edge fades.
|
||||
s.execute("object.directionalFeather", &json!({"ids": [id], "on": true, "widths": [0, 40, 0, 0]})).unwrap();
|
||||
let df = shot(&s);
|
||||
assert!(df.pixel(102, 200)[0] > 200, "left edge faded to paper: {:?}", df.pixel(102, 200));
|
||||
assert_eq!(df.pixel(298, 200), plain.pixel(298, 200), "right edge untouched");
|
||||
// PDF export carries effects (as an image of the object).
|
||||
let r = s.execute("file.exportPdf", &json!({})).unwrap();
|
||||
assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().unwrap().contains("soft effects")), "{r}");
|
||||
|
||||
+32
-1
@@ -108,7 +108,8 @@ impl Renderer {
|
||||
// Gradient feather: the object (feathered or not) drawn in its own layer, then kept as
|
||||
// much as the opacity gradient says.
|
||||
let gf = &e.gradient_feather;
|
||||
if gf.on {
|
||||
let directional = e.directional_feather.on && e.directional_feather.widths.iter().any(|w| *w > 0.0);
|
||||
if gf.on || directional {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, None, None, None, None);
|
||||
}
|
||||
@@ -140,6 +141,34 @@ impl Renderer {
|
||||
} else {
|
||||
self.draw_body(ctx, f, it, bp, xf, page_name);
|
||||
}
|
||||
// Directional feather: one fading ramp per side, multiplied in.
|
||||
let df = &e.directional_feather;
|
||||
if df.on && df.widths.iter().any(|w| *w > 0.0) {
|
||||
let b = it.inner_bounds();
|
||||
let area = if xf.determinant().abs() > 1e-12 { xf.inverse().transform_rect_bbox(reach) } else { b };
|
||||
let ramps = [
|
||||
(df.widths[0], kurbo::Point::new(b.x0, b.y0), kurbo::Point::new(b.x0, b.y0 + df.widths[0])),
|
||||
(df.widths[1], kurbo::Point::new(b.x0, b.y0), kurbo::Point::new(b.x0 + df.widths[1], b.y0)),
|
||||
(df.widths[2], kurbo::Point::new(b.x0, b.y1), kurbo::Point::new(b.x0, b.y1 - df.widths[2])),
|
||||
(df.widths[3], kurbo::Point::new(b.x1, b.y0), kurbo::Point::new(b.x1 - df.widths[3], b.y0)),
|
||||
];
|
||||
for (w, p0, p1) in ramps {
|
||||
if w <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let stops = [
|
||||
peniko::ColorStop::from((0.0, peniko::Color::from_rgba8(0, 0, 0, 0))),
|
||||
peniko::ColorStop::from((1.0, peniko::Color::from_rgba8(0, 0, 0, 255))),
|
||||
];
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
|
||||
ctx.set_transform(f.view * xf);
|
||||
ctx.set_paint(peniko::Gradient::new_linear(p0, p1).with_stops(stops.as_slice()));
|
||||
ctx.fill_rect(&area.inflate(4.0, 4.0));
|
||||
ctx.reset_paint_transform();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
if gf.on {
|
||||
let (p0, p1) = gf.points(it.inner_bounds());
|
||||
let stop = |o: f32, a: f32| peniko::ColorStop::from((o, peniko::Color::from_rgba8(0, 0, 0, (a.clamp(0.0, 1.0) * 255.0).round() as u8)));
|
||||
@@ -157,6 +186,8 @@ impl Renderer {
|
||||
ctx.fill_rect(&area.inflate(4.0, 4.0));
|
||||
ctx.reset_paint_transform();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
if gf.on || directional {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
if !it.path.is_closed() {
|
||||
|
||||
@@ -446,6 +446,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"cmd:object.bevel",
|
||||
"cmd:object.satin",
|
||||
"cmd:object.feather",
|
||||
"cmd:object.directionalFeather",
|
||||
"<",
|
||||
">Select",
|
||||
"cmd:select.firstAbove",
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 94 | 3 | 0 | 98% |
|
||||
| P2 | 76 | 46 | 30 | 0 | 80% |
|
||||
| P2 | 76 | 47 | 29 | 0 | 81% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -21,7 +21,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Frames, shapes & paths | 100% |
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 99% |
|
||||
| Effects & transparency | 79% |
|
||||
| Effects & transparency | 83% |
|
||||
| Placing & links | 91% |
|
||||
| Type & text frames | 99% |
|
||||
| Typography | 98% |
|
||||
@@ -162,7 +162,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| Effects & transparency | Isolate Blending / Knockout Group | P2 | P |
|
||||
| Effects & transparency | Drop Shadow | P1 | D |
|
||||
| Effects & transparency | Inner Shadow, Outer Glow, Inner Glow, Bevel, Satin | P2 | D |
|
||||
| Effects & transparency | Basic/Directional/Gradient Feather | P2 | P |
|
||||
| Effects & transparency | Basic/Directional/Gradient Feather | P2 | D |
|
||||
| Effects & transparency | Global Light | P2 | D |
|
||||
| Effects & transparency | Transparency blend space | P1 | P |
|
||||
| Effects & transparency | Transparency flattener | P2 | P |
|
||||
|
||||
Reference in new issue
Block a user