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:
moppeeer
2026-10-13 15:09:03 +02:00
committed by paaspaas00
co-authored by Claude Opus 5.5
parent 48f7ebfd50
commit a3b971654f
12 changed files with 237 additions and 19 deletions
+1
View File
@@ -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
View File
@@ -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,
}
}
}
+26 -1
View File
@@ -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
View File
@@ -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
+41
View File
@@ -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));
}
+50 -1
View File
@@ -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" => {
+7
View File
@@ -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…", "効果…", "效果…"]),
+7
View File
@@ -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…", "التأثيرات…"),
+7
View File
@@ -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…"),
+7
View File
@@ -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…", "効果…"),
+7
View File
@@ -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…"),
+7
View File
@@ -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…", "Ефекти…"),