M9.2: replace Find Edges with Glowing Edges

This commit is contained in:
Vikram Bhaskaran authored and bflatastic committed 2026-10-10 09:50:05 +07:00
1 parent 2d371849df
commit 11aaf816d0
15 files changed
+99 -38

No files matched your search

+2 -2
View File
@@ -57,7 +57,7 @@ Ordered by how much each gap blocks someone from switching. Sizes are one-agent
Pixelate, Sketch, Stylize, Texture, Video) and the Effect Gallery. The pixel-filter pipeline is in place
(`vectorcraft_effects::pixel`: the object rendered offscreen, filtered in stack order with Gaussian Blur and
Feather, cached while panning, images in PDF/SVG/Expand Appearance) with Blur › Radial Blur and Smart Blur
(Normal mode only), Sharpen › Unsharp Mask and Stylize › Find Edges; each new filter is now one function, a catalogue row, menu and
(Normal mode only), Sharpen › Unsharp Mask and Stylize › Glowing Edges; each new filter is now one function, a catalogue row, menu and
translations. Parallelizes well across agents. 26–40 h.
3. **3D and Materials:** Extrude & Bevel, Revolve, Inflate and Rotate with lighting and materials, using a software
renderer in its own crate (layering allows it below L6), with output in SVG/PDF as rasters or projected vectors.
@@ -219,7 +219,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
| Strokes, brushes, width profiles | 5 | 88% | brush options depth, brush libraries (generated in code), brushes driven by pen tilt, bearing and barrel rotation (6D art pens) | 12–20 |
| Appearance, transparency, graphic styles, masks | 5 | 98% | raster effect reach in preview bounds (flattener presets apply in PDF, EPS and print) | 1–2 |
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters | 6–10 |
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 25% | ~51 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video), Smart Blur's Edge Only / Overlay Edge modes and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur, Unsharp Mask and Find Edges are done | 26–40 |
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 25% | ~51 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video), Smart Blur's Edge Only / Overlay Edge modes and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur, Unsharp Mask and Glowing Edges are done | 26–40 |
| 3D and Materials | 4 | 0% | Extrude & Bevel, Revolve, Inflate, Rotate, lighting, materials (software renderer) | 50–80 |
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, digit types, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); Hyphenation… and Justification… options (Every-line Composer for justified and ragged text, per object, is done; it is tuned by eye, not yet compared side by side with Illustrator; mixed type sizes in a paragraph still break single-line), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, Transform Again after a type-area resize (it scales the type), partial selection of type-area anchors by marquee | 14–19 |
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 |
+9 -2
View File
@@ -6,7 +6,7 @@
//! - **Geometry effects** (Distort & Transform, Path, Convert to Shape, Round Corners, Scribble,
//! Warp) rewrite a path: [`apply_geometry`] evaluates them in stack order.
//! - **Raster effects** (Drop Shadow, Inner/Outer Glow, Feather, Gaussian Blur, and the
//! Photoshop-style filters of [`pixel`]: Radial Blur, Smart Blur, Unsharp Mask) are described by
//! Photoshop-style filters of [`pixel`]: Radial Blur, Smart Blur, Unsharp Mask and Glowing Edges) are described by
//! [`raster_effects`] and painted by the renderer; [`outset`] says how far they reach beyond
//! the geometry.
//! - **Stroke geometry** ([`stroke`]): arrowheads, dash patterns and width profiles, shared by the
@@ -125,6 +125,7 @@ fn lengths_of(id: &str) -> Lengths {
"stylize.scribble" => always(&["overlap", "strokeWidth", "spacing", "variation"]),
"stylize.dropShadow" => always(&["x", "y", "blur"]),
"stylize.innerGlow" | "stylize.outerGlow" => always(&["blur"]),
"stylize.glowingEdges" => always(&["edgeWidth", "smoothness"]),
_ => Lengths::default(),
}
}
@@ -290,7 +291,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
"{amount: % 1..500 (50), radius: pt 0.1..250 (1; σ of the blur edges are found against), threshold: levels 0..255 (0; smaller differences are left alone)}",
json!({"amount": 50.0, "radius": 1.0, "threshold": 0.0}),
),
r("stylize.findEdges", "Find Edges", STYLIZE, "{} replaces colour with a Sobel edge map while preserving transparency", json!({})),
r(
"stylize.glowingEdges",
"Glowing Edges…",
STYLIZE,
"{edgeWidth: pt 1..14 (2), edgeBrightness: 0..20 (6), smoothness: pt 1..15 (5)} finds alpha-weighted Sobel edges and draws bright coloured outlines on black",
json!({"edgeWidth": 2.0, "edgeBrightness": 6.0, "smoothness": 5.0}),
),
];
v.extend([
g(
+30 -22
View File
@@ -1,30 +1,38 @@
//! Stylize › Find Edges.
//! Stylize › Glowing Edges.
use super::Px16;
use super::blur_plane;
/// Convert premultiplied RGBA pixels to a Sobel edge map in place.
pub(super) fn find(px: &mut [[u8; 4]], w: usize, h: usize) {
if w < 3 || h < 3 || w.checked_mul(h) != Some(px.len()) {
/// Find Sobel edges using alpha-weighted straight colour, smooth and expand them, then draw
/// coloured outlines on black. Samples outside the raster are transparent.
pub(super) fn glow(px: &mut [[u8; 4]], w: usize, h: usize, width: f64, brightness: f64, smoothness: f64) {
if w == 0 || h == 0 || w.checked_mul(h) != Some(px.len()) {
return;
}
let src: Vec<Px16> = px.iter().map(|p| p.map(u16::from)).collect();
let luminance = |x: usize, y: usize| -> f32 {
src.get(y * w + x).map_or(0.0, |p| {
(0.2126 * f32::from(p[0]) + 0.7152 * f32::from(p[1]) + 0.0722 * f32::from(p[2])) / 257.0
})
let src = px.to_vec();
let signal = |x: isize, y: isize| -> f32 {
if x < 0 || y < 0 { return 0.0; }
let Some(p) = src.get(y as usize * w + x as usize) else { return 0.0 };
let alpha = f32::from(p[3]) / 255.0;
let straight = if alpha > 0.0 { [f32::from(p[0]) / 255.0 / alpha, f32::from(p[1]) / 255.0 / alpha, f32::from(p[2]) / 255.0 / alpha] } else { [0.0; 3] };
(0.2126 * straight[0] + 0.7152 * straight[1] + 0.0722 * straight[2]) * alpha
};
for y in 1..h - 1 {
for x in 1..w - 1 {
let gx = -luminance(x - 1, y - 1) + luminance(x + 1, y - 1)
- 2.0 * luminance(x - 1, y) + 2.0 * luminance(x + 1, y)
- luminance(x - 1, y + 1) + luminance(x + 1, y + 1);
let gy = -luminance(x - 1, y - 1) - 2.0 * luminance(x, y - 1) - luminance(x + 1, y - 1)
+ luminance(x - 1, y + 1) + 2.0 * luminance(x, y + 1) + luminance(x + 1, y + 1);
let edge = gx.hypot(gy).clamp(0.0, 255.0) as u8;
if let Some(out) = px.get_mut(y * w + x) {
let alpha = out[3];
*out = [edge.min(alpha), edge.min(alpha), edge.min(alpha), alpha];
}
let mut edges = vec![0.0f32; px.len()];
for y in 0..h {
for x in 0..w {
let x = x as isize;
let y = y as isize;
let gx = -signal(x - 1, y - 1) + signal(x + 1, y - 1) - 2.0 * signal(x - 1, y) + 2.0 * signal(x + 1, y) - signal(x - 1, y + 1) + signal(x + 1, y + 1);
let gy = -signal(x - 1, y - 1) - 2.0 * signal(x, y - 1) - signal(x + 1, y - 1) + signal(x - 1, y + 1) + 2.0 * signal(x, y + 1) + signal(x + 1, y + 1);
if let Some(edge) = edges.get_mut(y as usize * w + x as usize) { *edge = gx.hypot(gy).clamp(0.0, 1.0); }
}
}
blur_plane(&mut edges, w, h, smoothness.max(0.0));
if width > 0.5 { blur_plane(&mut edges, w, h, width * 0.5); }
for (out, (original, edge)) in px.iter_mut().zip(src.iter().zip(edges)) {
let alpha = f32::from(original[3]) / 255.0;
let glow = (edge * brightness as f32 / 6.0).clamp(0.0, 1.0);
let straight = if original[3] > 0 { [f32::from(original[0]) / 255.0 / alpha.max(1e-6), f32::from(original[1]) / 255.0 / alpha.max(1e-6), f32::from(original[2]) / 255.0 / alpha.max(1e-6)] } else { [1.0, 1.0, 1.0] };
let a = alpha.max(glow);
*out = [(straight[0] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (straight[1] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (straight[2] * glow * a * 255.0).round().clamp(0.0, 255.0) as u8, (a * 255.0).round() as u8];
}
}
+12 -8
View File
@@ -1,5 +1,5 @@
//! Photoshop-style raster effects (Effect › Blur › Radial Blur and Smart Blur, Sharpen › Unsharp
//! Mask, and Stylize › Find Edges): filters over premultiplied RGBA8 pixels.
//! Mask, and Stylize › Glowing Edges): filters over premultiplied RGBA8 pixels.
//!
//! - Every distance is in document units (points) and becomes pixels through the raster's
//! [`PixelSpace::px`], so an effect looks the same at any zoom and at any Document Raster Effects
@@ -18,7 +18,7 @@ use vectorcraft_geom::{Affine, Point, Rect};
use crate::util::{num, text};
/// The Photoshop-style effect ids (all raster effects, see [`crate::is_raster`]).
pub const PIXEL_EFFECTS: [&str; 4] = ["blur.radial", "blur.smart", "sharpen.unsharpMask", "stylize.findEdges"];
pub const PIXEL_EFFECTS: [&str; 4] = ["blur.radial", "blur.smart", "sharpen.unsharpMask", "stylize.glowingEdges"];
/// A Photoshop-style raster effect with its parameters read and clamped to their ranges. Lengths
/// are in document units.
@@ -34,8 +34,8 @@ pub enum PixelFx {
/// Sharpen › Unsharp Mask: colours pushed away from a Gaussian blur of σ = `radius` by `amount`
/// (1 = 100 %), where they differ from it by at least `threshold` levels.
UnsharpMask { amount: f64, radius: f64, threshold: f64 },
/// Stylize › Find Edges: replace colour with a Sobel edge map while preserving opacity.
FindEdges,
/// Stylize › Glowing Edges: bright coloured Sobel outlines on black.
GlowingEdges { width: f64, brightness: f64, smoothness: f64 },
}
/// Where a raster's pixels lie in the document.
@@ -72,7 +72,11 @@ pub(crate) fn parse(id: &str, p: &Value) -> Option<PixelFx> {
radius: num(p, "radius", 1.0).clamp(0.1, 250.0),
threshold: num(p, "threshold", 0.0).clamp(0.0, 255.0),
},
"stylize.findEdges" => PixelFx::FindEdges,
"stylize.glowingEdges" => PixelFx::GlowingEdges {
width: num(p, "edgeWidth", 2.0).clamp(1.0, 14.0),
brightness: num(p, "edgeBrightness", 6.0).clamp(0.0, 20.0),
smoothness: num(p, "smoothness", 5.0).clamp(1.0, 15.0),
},
_ => return None,
})
}
@@ -89,7 +93,7 @@ impl PixelFx {
PixelFx::RadialBlur { amount, zoom: true, .. } => far * (blur::zoom_extent(amount).exp() - 1.0),
PixelFx::SmartBlur { radius, .. } => radius,
PixelFx::UnsharpMask { .. } => 0.0,
PixelFx::FindEdges => 0.0,
PixelFx::GlowingEdges { width, smoothness, .. } => width.max(smoothness * 3.0),
}
}
@@ -100,7 +104,7 @@ impl PixelFx {
PixelFx::RadialBlur { .. } => None,
PixelFx::SmartBlur { radius, .. } => Some(radius),
PixelFx::UnsharpMask { radius, .. } => Some(3.0 * radius),
PixelFx::FindEdges => Some(1.0),
PixelFx::GlowingEdges { width, smoothness, .. } => Some(width.max(smoothness * 3.0)),
}
}
@@ -113,7 +117,7 @@ impl PixelFx {
PixelFx::RadialBlur { amount, zoom, passes } => blur::radial(px, w, h, space, amount, zoom, passes),
PixelFx::SmartBlur { radius, threshold, samples } => blur::smart(px, w, h, to_px(radius), threshold, samples),
PixelFx::UnsharpMask { amount, radius, threshold } => sharpen::unsharp(px, w, h, amount, to_px(radius), threshold),
PixelFx::FindEdges => edges::find(px, w, h),
PixelFx::GlowingEdges { width, brightness, smoothness } => edges::glow(px, w, h, to_px(width), brightness, to_px(smoothness)),
}
}
}
+4 -4
View File
@@ -55,7 +55,7 @@ fn params_take_defaults_and_stay_in_range() {
fx("sharpen.unsharpMask", json!({"amount": 1e9, "radius": 0, "threshold": 1e9})),
PixelFx::UnsharpMask { amount: 5.0, radius: 0.1, threshold: 255.0 }
);
assert_eq!(fx("stylize.findEdges", json!({})), PixelFx::FindEdges);
assert_eq!(fx("stylize.glowingEdges", json!({})), PixelFx::GlowingEdges { width: 2.0, brightness: 6.0, smoothness: 5.0 });
for id in PIXEL_EFFECTS {
assert!(crate::is_raster(id) && crate::effect_info(id).is_some_and(|e| e.raster), "{id}");
}
@@ -122,13 +122,13 @@ fn every_effect_is_deterministic() {
}
#[test]
fn find_edges_detects_a_boundary_and_preserves_alpha() {
fn glowing_edges_draw_a_bright_outline_and_preserve_premultiplication() {
let (w, h) = (9, 9);
let src = image(w, h, |x, _| if x < 4 { [0, 0, 0, 200] } else { [200, 200, 200, 200] });
let mut out = src.clone();
fx("stylize.findEdges", json!({})).apply(&mut out, w, h, &space(w, h));
fx("stylize.glowingEdges", json!({"edgeWidth": 1, "smoothness": 1, "edgeBrightness": 20})).apply(&mut out, w, h, &space(w, h));
assert!(at(&out, w, 3, 4)[0] > 0 || at(&out, w, 4, 4)[0] > 0);
assert!(out.chunks_exact(4).all(|p| p[3] == 200 && p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3]));
assert!(out.chunks_exact(4).all(|p| p[0] <= p[3] && p[1] <= p[3] && p[2] <= p[3]));
}
#[test]
+1
View File
@@ -9,6 +9,7 @@
# back to English. Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name,
# language names, OpenType, the built-in workspace names and the bracketed perspective grid preset
# names.
Glowing Edges… Zářící okraje…
About VectorCraft O aplikaci VectorCraft
Align to Path Zarovnat k cestě
Cut Path at Selected Anchor Points Rozdělit cestu ve vybraných kotevních bodech
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -12,6 +12,11 @@
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Bordes resplandecientes…
Glowing Edges Bordes resplandecientes
Edge Width Ancho del borde
Edge Brightness Brillo del borde
Smoothness Suavizado
Radial Blur… Desenfoque radial…
Smart Blur… Desenfoque inteligente…
Sharpen Enfocar
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -13,6 +13,11 @@
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Bords lumineux…
Glowing Edges Bords lumineux
Edge Width Largeur du bord
Edge Brightness Luminosité du bord
Smoothness Lissage
Radial Blur… Flou radial…
Smart Blur… Flou intelligent…
Sharpen Netteté
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -12,6 +12,11 @@
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Bordi luminosi…
Glowing Edges Bordi luminosi
Edge Width Larghezza bordo
Edge Brightness Luminosità bordo
Smoothness Uniformità
Radial Blur… Sfocatura radiale…
Smart Blur… Sfocatura intelligente…
Sharpen Nitidezza
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -4,6 +4,11 @@
# Clean-room: menu and feature names follow the names Japanese users of vector illustration
# software know, written from the meaning of VectorCraft's English labels; no other application's
# Japanese localisation was copied. Toggles read as their action (「…を表示」「…を隠す」).
Glowing Edges… エッジの光彩…
Glowing Edges エッジの光彩
Edge Width エッジの幅
Edge Brightness エッジの明るさ
Smoothness 滑らかさ
#
# Coverage: the whole interface — every menu string and every `tl!` literal (`complete_menus`,
# tested) — and the status and error messages (`@msg` rows at the end, `COMPLETE_MESSAGES`, tested).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -14,6 +14,7 @@
# Brazilian Portuguese computing vocabulary (arquivo, camada, caminho, preenchimento, traçado,
# pincel, amostra, mesa de trabalho ...), following common desktop-software conventions (commands
# in the infinitive, menus as nouns); no proprietary localisation resource, string table or product
Glowing Edges… Bordas brilhantes…
# UI was consulted or copied; add entries the same way. Product and technology names (VectorCraft,
# ArtCraft, SVG, OpenType, CMYK, Lab ...) stay in Latin letters; the trailing "…" of dialog
# commands is kept.
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -10,6 +10,11 @@
# tested). Left in English on purpose (`MENU_KEEP_AS_IS` in the tests): the product name, language
# names, OpenType, the built-in workspace names and the bracketed perspective grid preset names.
# Photoshop-style raster effects (Effect menu, their dialogs).
Glowing Edges… Светящиеся края…
Glowing Edges Светящиеся края
Edge Width Ширина края
Edge Brightness Яркость края
Smoothness Сглаживание
Radial Blur… Радиальное размытие…
Smart Blur… Умное размытие…
Sharpen Резкость
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -10,6 +10,11 @@
# Coverage: every menu string, tl! literal, plural, status and error message (enforced in tests).
# Product, language, built-in workspace and perspective preset names follow MENU_KEEP_AS_IS.
# Keep placeholders, ellipses and leading/trailing spaces of fragments when editing.
Glowing Edges… Сяйливі краї…
Glowing Edges Сяйливі краї
Edge Width Ширина краю
Edge Brightness Яскравість краю
Smoothness Згладжування
Radial Blur… Радіальне розмиття…
Smart Blur… Розумне розмиття…
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
10 # Coverage: every menu string, tl! literal, plural, status and error message (enforced in tests).
11 # Product, language, built-in workspace and perspective preset names follow MENU_KEEP_AS_IS.
12 # Keep placeholders, ellipses and leading/trailing spaces of fragments when editing.
13
14
15
16
17
18
19
20
+5
View File
@@ -9,6 +9,11 @@
# proprietary localisation resource, string table or product UI may be copied. Product and
# technology names stay in Latin letters. Full-width punctuation (,。:“”) in sentences, half-width
# parentheses.
Glowing Edges… 发光边缘…
Glowing Edges 发光边缘
Edge Width 边缘宽度
Edge Brightness 边缘亮度
Smoothness 平滑度
— missing — 缺失
to search every command 以搜索所有命令
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -17,6 +17,11 @@
# commands is kept. Full-width punctuation (,。:) inside sentences; half-width parentheses and
# the " · " separators as in the English. Terms shared with PhotoCraft use the same words as its
# zh-hant catalog (圖層, 選取, 偏好設定, 混合模式 ...), so the two apps read alike.
Glowing Edges… 發光邊緣…
Glowing Edges 發光邊緣
Edge Width 邊緣寬度
Edge Brightness 邊緣亮度
Smoothness 平滑度
— missing — 遺失
to search every command 以搜尋所有指令
Can't render this file because it contains an unexpected character in line 6 and column 47.