mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 02:37:00 -06:00
feat(effects): Inner Glow mode and technique; Basic Feather choke and corners
InDesign's Effects dialog sets an Inner Glow's blend Mode (Screen by default) and Technique (Softer or Precise), and a Basic Feather's Choke and Corners (Sharp, Rounded, Diffused). LYH Studio episode 14 shows them at 12:05–12:20 (Screen 75 %, Softer, 0.25 in; 0.0625 in, Diffused). - doc: InnerGlow.mode and .technique (GlowTechnique), Effects. feather_options (FeatherOptions: choke %, FeatherCorners), with InDesign's defaults; old documents read as before. - render: the glow uses its mode; Precise keeps it nearer the edge; the feather's choke shortens the fade, Sharp and Rounded corners follow the shape more tightly or softly than Diffused (unchanged, so the goldens stay). - render fix: an inner glow's (and a bevel's) blend mode was applied inside the clip layer, against its empty backdrop, so Screen and Multiply acted as Normal; the blend layer now wraps the finished edge and mixes with the object below. - engine: choke clamped to 0–100; the params documented. - Effects dialog: Mode and Technique on Inner Glow; Choke and Corners on Basic Feather; strings translated. Builds on #643 (the Effects dialog). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
paaspaas00
co-authored by
Claude Opus 5.5
parent
48f7ebfd50
commit
a3b971654f
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-13** · Inner Glow takes a blend mode (Screen by default) and a Technique (Softer or Precise); Basic Feather takes Choke and Corners (Sharp, Rounded, Diffused), in the Effects dialog and `object.effects`. Inner glow and bevel blend modes now mix with the object below them instead of an empty layer. Noise isn't drawn. 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.
|
||||
|
||||
+49
-1
@@ -777,6 +777,9 @@ pub struct Effects {
|
||||
pub drop_shadow: DropShadow,
|
||||
/// Basic feather width (0 = off).
|
||||
pub feather: f64,
|
||||
/// Basic feather's choke and corners.
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub feather_options: FeatherOptions,
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
pub inner_shadow: InnerShadow,
|
||||
#[serde(skip_serializing_if = "is_default")]
|
||||
@@ -793,6 +796,39 @@ pub struct Effects {
|
||||
pub directional_feather: DirectionalFeather,
|
||||
}
|
||||
|
||||
/// Basic Feather › Corners: how the fade follows the shape's corners.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FeatherCorners {
|
||||
/// Mitred: the fade keeps the corners sharp.
|
||||
Sharp,
|
||||
/// The fade rounds the corners by its width.
|
||||
Rounded,
|
||||
/// From opaque to transparent, softly (InDesign's default).
|
||||
#[default]
|
||||
Diffused,
|
||||
}
|
||||
|
||||
/// Basic Feather options besides its width.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct FeatherOptions {
|
||||
/// Percentage (0–100) of the width that stays opaque: the fade gets shorter.
|
||||
pub choke: f64,
|
||||
pub corners: FeatherCorners,
|
||||
}
|
||||
|
||||
/// Inner/Outer Glow › Technique.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum GlowTechnique {
|
||||
/// A soft, blurred glow.
|
||||
#[default]
|
||||
Softer,
|
||||
/// A glow that keeps closer to the shape's edges.
|
||||
Precise,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
@@ -814,11 +850,23 @@ pub struct InnerGlow {
|
||||
pub choke: f64,
|
||||
/// Glow from the centre instead of the edges.
|
||||
pub center: bool,
|
||||
/// How the glow blends with the object (InDesign: Screen).
|
||||
pub mode: BlendMode,
|
||||
pub technique: GlowTechnique,
|
||||
}
|
||||
|
||||
impl Default for InnerGlow {
|
||||
fn default() -> Self {
|
||||
InnerGlow { on: false, color: designcraft_color::swatch::PAPER.into(), opacity: 0.75, size: 7.0, choke: 0.0, center: false }
|
||||
InnerGlow {
|
||||
on: false,
|
||||
color: designcraft_color::swatch::PAPER.into(),
|
||||
opacity: 0.75,
|
||||
size: 7.0,
|
||||
choke: 0.0,
|
||||
center: false,
|
||||
mode: BlendMode::Screen,
|
||||
technique: GlowTechnique::Softer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Effects",
|
||||
[],
|
||||
None,
|
||||
"{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, feather? (width), directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step",
|
||||
"{effects?: {dropShadow?: {on?, color?, opacity?, angle?, globalLight?, distance?, size?, spread?}, innerShadow?, outerGlow?, innerGlow? {on?, color?, opacity?, size?, choke?, center?, mode? (blend mode: Screen, Multiply, …; Screen by default), technique?: softer|precise}, bevel?, satin?, feather? (width), featherOptions? {choke? (%), corners?: sharp|rounded|diffused}, directionalFeather?, gradientFeather?} (the fields given replace the object's, as the Effects dialog sets them), opacity?: 0..1, blend?, ids?} — every effect at once, one undo step",
|
||||
has_selection,
|
||||
set_effects
|
||||
),
|
||||
@@ -1553,6 +1553,7 @@ fn sanitize_effects(e: &mut designcraft_doc::Effects) -> Result<()> {
|
||||
len(&mut sa.distance)?;
|
||||
len(&mut sa.size)?;
|
||||
len(&mut e.feather)?;
|
||||
pct(&mut e.feather_options.choke)?;
|
||||
for w in &mut e.directional_feather.widths {
|
||||
len(w)?;
|
||||
}
|
||||
@@ -3508,3 +3509,27 @@ mod effects_command_tests {
|
||||
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod glow_feather_options_tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn glow_mode_technique_and_feather_options_are_kept() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = s.execute("frame.create", &json!({"rect": [0, 0, 50, 50]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute(
|
||||
"object.effects",
|
||||
&json!({"ids": [id], "effects": {"innerGlow": {"on": true, "mode": "Multiply", "technique": "precise"}, "feather": 4.5, "featherOptions": {"choke": 140, "corners": "sharp"}}}),
|
||||
)
|
||||
.unwrap();
|
||||
let e = s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().effects.clone();
|
||||
assert_eq!((e.inner_glow.mode, e.inner_glow.technique), (designcraft_color::BlendMode::Multiply, designcraft_doc::GlowTechnique::Precise));
|
||||
assert_eq!((e.feather_options.choke, e.feather_options.corners), (100.0, designcraft_doc::FeatherCorners::Sharp), "choke is a percentage");
|
||||
// InDesign's defaults: Screen and Softer; Diffused corners, no choke.
|
||||
let d = designcraft_doc::Effects::default();
|
||||
assert_eq!((d.inner_glow.mode, d.inner_glow.technique), (designcraft_color::BlendMode::Screen, designcraft_doc::GlowTechnique::Softer));
|
||||
assert_eq!((d.feather_options.choke, d.feather_options.corners), (0.0, designcraft_doc::FeatherCorners::Diffused));
|
||||
}
|
||||
}
|
||||
|
||||
+28
-16
@@ -113,10 +113,17 @@ impl Renderer {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, None, None, None, None);
|
||||
}
|
||||
// The object, feathered or not.
|
||||
// The object, feathered or not. Choke keeps that much of the width opaque (a shorter
|
||||
// fade); Sharp and Rounded corners follow the inset shape more tightly than Diffused.
|
||||
if e.feather > 0.0 && it.path.is_closed() {
|
||||
let w = e.feather;
|
||||
self.with_filters(ctx, f, reach, w / 4.0, |me, c, fr| {
|
||||
let fo = &e.feather_options;
|
||||
let w = e.feather * (1.0 - fo.choke.clamp(0.0, 100.0) / 100.0).max(0.01);
|
||||
let (sigma, join) = match fo.corners {
|
||||
designcraft_doc::FeatherCorners::Diffused => (w / 4.0, kurbo::Join::Round),
|
||||
designcraft_doc::FeatherCorners::Rounded => (w / 3.0, kurbo::Join::Round),
|
||||
designcraft_doc::FeatherCorners::Sharp => (w / 6.0, kurbo::Join::Miter),
|
||||
};
|
||||
self.with_filters(ctx, f, reach, sigma, |me, c, fr| {
|
||||
c.set_transform(fr.view * xf);
|
||||
c.push_clip_layer(bp);
|
||||
me.draw_body(c, fr, it, bp, xf, page);
|
||||
@@ -124,14 +131,14 @@ impl Renderer {
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestIn)), None, None, None);
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, None, None, None, Some(blur(w / 4.0)));
|
||||
c.push_layer(None, None, None, None, Some(blur(sigma)));
|
||||
c.set_transform(fr.view * xf);
|
||||
c.set_paint(peniko::Color::BLACK);
|
||||
c.fill_path(bp);
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None);
|
||||
c.set_transform(fr.view * xf);
|
||||
c.set_stroke(kurbo::Stroke::new(w).with_join(kurbo::Join::Round));
|
||||
c.set_stroke(kurbo::Stroke::new(w).with_join(join));
|
||||
c.stroke_path(bp);
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
@@ -208,13 +215,16 @@ impl Renderer {
|
||||
{
|
||||
let g = &e.inner_glow;
|
||||
let (sigma, choke) = split(g.size, g.choke);
|
||||
// Precise keeps the glow close to the edge: a tighter blur.
|
||||
let sigma = if g.technique == designcraft_doc::GlowTechnique::Precise { sigma * 0.4 } else { sigma };
|
||||
let mix = crate::blend_mode(g.mode).mix;
|
||||
let paint = color_of(&c, g.opacity.clamp(0.0, 1.0));
|
||||
if g.center {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None);
|
||||
self.with_filters(ctx, f, reach, sigma, |_, c, fr| {
|
||||
c.set_transform(fr.view * xf);
|
||||
c.push_clip_layer(bp);
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(BlendMode::new(Mix::Screen, Compose::SrcOver)), None, None, None);
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, None, None, None, Some(blur(sigma)));
|
||||
c.set_transform(fr.view * xf);
|
||||
@@ -230,10 +240,10 @@ impl Renderer {
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
c.pop_layer();
|
||||
});
|
||||
ctx.pop_layer();
|
||||
} else {
|
||||
self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, Mix::Screen);
|
||||
self.inner_edge(ctx, f, reach, bp, xf, Vec2::ZERO, sigma, choke, paint, mix);
|
||||
}
|
||||
}
|
||||
// Satin: the shape shifted both ways along the angle, blurred, where exactly one covers.
|
||||
@@ -304,13 +314,15 @@ impl Renderer {
|
||||
paint: peniko::Color,
|
||||
mix: Mix,
|
||||
) {
|
||||
// The blend mode applies to the finished (clipped, blurred) edge against what's below it:
|
||||
// inside the clip layer it would only meet that layer's transparent backdrop.
|
||||
if mix != Mix::Normal {
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None);
|
||||
}
|
||||
self.with_filters(ctx, f, reach, sigma, |_, c, fr| {
|
||||
c.set_transform(fr.view * xf);
|
||||
c.push_clip_layer(bp);
|
||||
if mix != Mix::Normal {
|
||||
c.set_transform(Affine::IDENTITY);
|
||||
c.push_layer(None, Some(BlendMode::new(mix, Compose::SrcOver)), None, None, None);
|
||||
}
|
||||
c.set_transform(fr.view);
|
||||
c.push_layer(None, None, None, None, Some(blur(sigma)));
|
||||
// Everything outside the offset shape, so the colour bleeds in from the edges.
|
||||
@@ -329,11 +341,11 @@ impl Renderer {
|
||||
c.stroke_path(&shape);
|
||||
}
|
||||
c.pop_layer();
|
||||
if mix != Mix::Normal {
|
||||
c.pop_layer();
|
||||
}
|
||||
c.pop_layer();
|
||||
});
|
||||
if mix != Mix::Normal {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `draw`, which pushes filter layers. Single-threaded contexts draw directly. On a
|
||||
|
||||
@@ -224,3 +224,44 @@ fn multithreaded_effects_match_single_threaded() {
|
||||
assert!(worst <= 4, "case {i}: max channel difference {worst}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inner_glow_mode_and_technique() {
|
||||
let mut d = doc();
|
||||
let id = rect_item(&mut d, Rect::new(30.0, 30.0, 130.0, 130.0), "[Paper]");
|
||||
let g = &mut d.item_mut(id).unwrap().effects.inner_glow;
|
||||
g.on = true;
|
||||
g.color = "[Black]".into();
|
||||
g.opacity = 1.0;
|
||||
g.size = 20.0;
|
||||
// Screen (the default) can't darken white paper; Multiply can.
|
||||
let screen = render(&d);
|
||||
assert!(lum(screen.pixel(32, 80)) > 740, "{:?}", screen.pixel(32, 80));
|
||||
d.item_mut(id).unwrap().effects.inner_glow.mode = designcraft_color::BlendMode::Multiply;
|
||||
let soft = render(&d);
|
||||
assert!(lum(soft.pixel(32, 80)) + 150 < lum(screen.pixel(32, 80)), "a dark edge: {:?}", soft.pixel(32, 80));
|
||||
// Precise keeps the glow nearer the edge than Softer.
|
||||
d.item_mut(id).unwrap().effects.inner_glow.technique = designcraft_doc::GlowTechnique::Precise;
|
||||
let precise = render(&d);
|
||||
assert!(lum(precise.pixel(42, 80)) > lum(soft.pixel(42, 80)) + 30, "{:?} vs {:?}", precise.pixel(42, 80), soft.pixel(42, 80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feather_choke_and_corners() {
|
||||
let mut d = doc();
|
||||
let id = rect_item(&mut d, Rect::new(30.0, 30.0, 130.0, 130.0), "[Black]");
|
||||
d.item_mut(id).unwrap().effects.feather = 16.0;
|
||||
let diffused = render(&d);
|
||||
golden("feather", &diffused);
|
||||
// Choke: half the width stays opaque, so the fade is shorter.
|
||||
d.item_mut(id).unwrap().effects.feather_options.choke = 50.0;
|
||||
let choked = render(&d);
|
||||
assert!(lum(choked.pixel(38, 80)) + 60 < lum(diffused.pixel(38, 80)), "{:?} vs {:?}", choked.pixel(38, 80), diffused.pixel(38, 80));
|
||||
assert!(lum(choked.pixel(30, 80)) > 500, "still transparent at the path");
|
||||
// Sharp corners keep more of the corner than Rounded ones (past the fade, near the corner).
|
||||
d.item_mut(id).unwrap().effects.feather_options = designcraft_doc::FeatherOptions { choke: 0.0, corners: designcraft_doc::FeatherCorners::Sharp };
|
||||
let sharp = render(&d);
|
||||
d.item_mut(id).unwrap().effects.feather_options.corners = designcraft_doc::FeatherCorners::Rounded;
|
||||
let rounded = render(&d);
|
||||
assert!(lum(sharp.pixel(41, 41)) + 20 < lum(rounded.pixel(41, 41)), "{:?} vs {:?}", sharp.pixel(41, 41), rounded.pixel(41, 41));
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub fn open(app: &mut DesignApp, page: &str) {
|
||||
|
||||
/// Every effect's settings, those still at their defaults included (the document leaves them out).
|
||||
fn effects_json(e: &designcraft_doc::Effects) -> Value {
|
||||
use designcraft_doc::{Bevel, DirectionalFeather, DropShadow, GradientFeather, InnerGlow, InnerShadow, OuterGlow, Satin};
|
||||
use designcraft_doc::{Bevel, DirectionalFeather, DropShadow, FeatherOptions, GradientFeather, InnerGlow, InnerShadow, OuterGlow, Satin};
|
||||
let mut fx = serde_json::to_value(e).unwrap_or_else(|_| json!({}));
|
||||
let defaults = [
|
||||
("dropShadow", serde_json::to_value(DropShadow::default())),
|
||||
@@ -77,6 +77,7 @@ fn effects_json(e: &designcraft_doc::Effects) -> Value {
|
||||
("satin", serde_json::to_value(Satin::default())),
|
||||
("directionalFeather", serde_json::to_value(DirectionalFeather::default())),
|
||||
("gradientFeather", serde_json::to_value(GradientFeather::default())),
|
||||
("featherOptions", serde_json::to_value(FeatherOptions::default())),
|
||||
];
|
||||
for (k, v) in defaults {
|
||||
if !fx[k].is_object()
|
||||
@@ -351,6 +352,23 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
|
||||
let key = page;
|
||||
heading(ui, &tr("Blending"));
|
||||
grid("b").show(ui, |ui| {
|
||||
// Inner Glow: its blend mode (Screen, as InDesign starts it).
|
||||
if key == "innerGlow" {
|
||||
row(ui, "Mode:");
|
||||
let cur = d.fields.get("fx").and_then(|f| f["innerGlow"]["mode"].as_str()).unwrap_or("Screen").to_string();
|
||||
let label = |m: &designcraft_color::BlendMode| {
|
||||
serde_json::to_value(m).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default()
|
||||
};
|
||||
let shown = designcraft_color::BlendMode::ALL.iter().find(|m| label(m) == cur).map_or("Screen", |m| m.label());
|
||||
egui::ComboBox::from_id_salt("fx_glow_mode").selected_text(tr(shown)).width(150.0).show_ui(ui, |ui| {
|
||||
for m in designcraft_color::BlendMode::ALL {
|
||||
if ui.selectable_label(label(&m) == cur, tr(m.label())).clicked() {
|
||||
set(d, &["innerGlow", "mode"], json!(label(&m)));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
row(ui, "Color:");
|
||||
swatch(app, ui, d, &[key, "color"]);
|
||||
row(ui, "Opacity:");
|
||||
@@ -368,6 +386,19 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
|
||||
row(ui, "Choke:");
|
||||
percent(ui, d, &[key, "choke"]);
|
||||
ui.end_row();
|
||||
row(ui, "Technique:");
|
||||
let cur = d.fields.get("fx").and_then(|f| f["innerGlow"]["technique"].as_str()).unwrap_or("softer").to_string();
|
||||
egui::ComboBox::from_id_salt("fx_technique").selected_text(tr(if cur == "precise" { "Precise" } else { "Softer" })).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (v, l) in [("softer", "Softer"), ("precise", "Precise")] {
|
||||
if ui.selectable_label(cur == v, tr(l)).clicked() {
|
||||
set(d, &["innerGlow", "technique"], json!(v));
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.end_row();
|
||||
row(ui, "Source:");
|
||||
let mut center = d.fields.get("fx").is_some_and(|f| f["innerGlow"]["center"].as_bool().unwrap_or(false));
|
||||
let label = |c: bool| tr(if c { "Center" } else { "Edge" });
|
||||
@@ -442,6 +473,24 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
|
||||
row(ui, "Feather Width:");
|
||||
measure(ui, d, &["feather"], unit);
|
||||
ui.end_row();
|
||||
row(ui, "Choke:");
|
||||
percent(ui, d, &["featherOptions", "choke"]);
|
||||
ui.end_row();
|
||||
row(ui, "Corners:");
|
||||
let cur = d.fields.get("fx").and_then(|f| f["featherOptions"]["corners"].as_str()).unwrap_or("diffused").to_string();
|
||||
let shown = match cur.as_str() {
|
||||
"sharp" => "Sharp",
|
||||
"rounded" => "Rounded",
|
||||
_ => "Diffused",
|
||||
};
|
||||
egui::ComboBox::from_id_salt("fx_corners").selected_text(tr(shown)).show_ui(ui, |ui| {
|
||||
for (v, l) in [("sharp", "Sharp"), ("rounded", "Rounded"), ("diffused", "Diffused")] {
|
||||
if ui.selectable_label(cur == v, tr(l)).clicked() {
|
||||
set(d, &["featherOptions", "corners"], json!(v));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
"directionalFeather" => {
|
||||
|
||||
@@ -55,6 +55,13 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]),
|
||||
("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]),
|
||||
("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]),
|
||||
("Technique:", ["Technik:", "Technique :", "Técnica:", "テクニック:", "方法:"]),
|
||||
("Softer", ["Weicher", "Plus flou", "Más suave", "さらにソフトに", "柔和"]),
|
||||
("Precise", ["Präzise", "Précis", "Precisa", "精細", "精确"]),
|
||||
("Corners:", ["Ecken:", "Angles :", "Esquinas:", "角:", "角点:"]),
|
||||
("Sharp", ["Spitz", "Vifs", "Nítidas", "シャープ", "锐化"]),
|
||||
("Rounded", ["Abgerundet", "Arrondis", "Redondeadas", "角丸", "圆角"]),
|
||||
("Diffused", ["Weich", "Diffus", "Difusas", "拡散", "扩散"]),
|
||||
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
|
||||
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
|
||||
("Effects…", ["Effekte…", "Effets…", "Efectos…", "効果…", "效果…"]),
|
||||
|
||||
@@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "قص"),
|
||||
("Copy", "نسخ"),
|
||||
("Paste", "لصق"),
|
||||
("Technique:", "التقنية:"),
|
||||
("Softer", "أنعم"),
|
||||
("Precise", "دقيق"),
|
||||
("Corners:", "الزوايا:"),
|
||||
("Sharp", "حادة"),
|
||||
("Rounded", "مستديرة"),
|
||||
("Diffused", "منتشرة"),
|
||||
("Reverse Path", "عكس المسار"),
|
||||
("Set Anchor", "تعيين نقطة الارتساء"),
|
||||
("Effects…", "التأثيرات…"),
|
||||
|
||||
@@ -28,6 +28,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Taglia"),
|
||||
("Copy", "Copia"),
|
||||
("Paste", "Incolla"),
|
||||
("Technique:", "Tecnica:"),
|
||||
("Softer", "Più morbido"),
|
||||
("Precise", "Preciso"),
|
||||
("Corners:", "Angoli:"),
|
||||
("Sharp", "Netti"),
|
||||
("Rounded", "Arrotondati"),
|
||||
("Diffused", "Diffusi"),
|
||||
("Reverse Path", "Inverti tracciato"),
|
||||
("Set Anchor", "Imposta ancoraggio"),
|
||||
("Effects…", "Effetti…"),
|
||||
|
||||
@@ -30,6 +30,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "カット"),
|
||||
("Copy", "コピー"),
|
||||
("Paste", "ペースト"),
|
||||
("Technique:", "テクニック:"),
|
||||
("Softer", "さらにソフトに"),
|
||||
("Precise", "精細"),
|
||||
("Corners:", "角:"),
|
||||
("Sharp", "シャープ"),
|
||||
("Rounded", "角丸"),
|
||||
("Diffused", "拡散"),
|
||||
("Reverse Path", "パスの方向反転"),
|
||||
("Set Anchor", "アンカーポイントを設定"),
|
||||
("Effects…", "効果…"),
|
||||
|
||||
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Recortar"),
|
||||
("Copy", "Copiar"),
|
||||
("Paste", "Colar"),
|
||||
("Technique:", "Técnica:"),
|
||||
("Softer", "Mais suave"),
|
||||
("Precise", "Preciso"),
|
||||
("Corners:", "Cantos:"),
|
||||
("Sharp", "Nítidos"),
|
||||
("Rounded", "Arredondados"),
|
||||
("Diffused", "Difusos"),
|
||||
("Reverse Path", "Inverter demarcador"),
|
||||
("Set Anchor", "Definir ponto de ancoragem"),
|
||||
("Effects…", "Efeitos…"),
|
||||
|
||||
@@ -31,6 +31,13 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Вирізати"),
|
||||
("Copy", "Копіювати"),
|
||||
("Paste", "Вставити"),
|
||||
("Technique:", "Техніка:"),
|
||||
("Softer", "М'якше"),
|
||||
("Precise", "Точно"),
|
||||
("Corners:", "Кути:"),
|
||||
("Sharp", "Гострі"),
|
||||
("Rounded", "Заокруглені"),
|
||||
("Diffused", "Розсіяні"),
|
||||
("Reverse Path", "Розвернути контур"),
|
||||
("Set Anchor", "Задати прив’язку"),
|
||||
("Effects…", "Ефекти…"),
|
||||
|
||||
Reference in New Issue
Block a user