forked from Keelrleek/SoulStream
cover logo: keep the source's anti-aliasing, then trace it lightly
The wordmark segmentation mapped alpha with smoothstep(46, 120) over distance from the background colour. That ceiling is fixed, but the ink distance is not: white text on a black band sits at ~438, so every edge pixel brighter than about 27% grey saturated to fully opaque. A hard threshold wearing a smoothstep's clothing. Measured on Camping in Alaska's "Needlemouth" cover, the extracted mask's edges transitioned over 0.10px against the source's 0.88px -- nine times harder than the artwork it came from. The ramp's top now tracks the measured ink distance (95th percentile of distance-from-background, so it reads the glyph body rather than the brightest speck), with the old constant kept as a floor so low-contrast marks are untouched. Retained anti-aliasing on that cover goes from 10.4% of inked pixels to 22.2%, and the glyphs thin back toward the source instead of being dilated by the saturation. That unlocks the trace side, which is the visible half. The blur there was never a goal -- it exists to synthesise anti-aliasing a mask lacks, and it melts serif terminals into blobs. With real gradients in the alpha, the cover-logo branch drops from ~0.93px of blur to a 0.5px floor and from eps 0.5 to 0.2, at a 2400px raster instead of 1400. Lowering eps ALONE was tried and rejected: against the old near-binary mask it just captured the pixel staircase in finer detail. Camping in Alaska's traced path goes 900 -> ~2500 vertices with no staircasing, and the serif terminals keep their shape. Cost is 35ms -> 65ms of main-thread work, once per cover-derived logo at page load, not per frame.
This commit is contained in:
1 parent
e536e96180
commit
21ef73770c
2 files changed
+68
-16
No files matched your search
@@ -46,7 +46,12 @@ fn normalize(s: &str) -> String {
|
||||
const BG_NEAR_DIST: f32 = 46.0; // a pixel this close to the dominant colour counts as background
|
||||
const BG_MIN_DOMINANCE: f32 = 0.5; // background must fill at least this share of the crop, else it's a photo → reject
|
||||
const ALPHA_LO: f32 = 46.0; // distance-from-bg below which a pixel is background (transparent)
|
||||
const ALPHA_HI: f32 = 120.0; // distance-from-bg above which a pixel is full ink (opaque)
|
||||
const ALPHA_HI: f32 = 120.0; // floor for the opaque end of the ramp (low-contrast marks)
|
||||
/// Share of the measured ink-vs-background distance the alpha ramp spans. The
|
||||
/// ramp's top tracks the real ink distance so anti-aliased edge pixels keep
|
||||
/// partial alpha instead of saturating; 0.55 puts the midpoint near the middle
|
||||
/// of a genuine edge gradient.
|
||||
const ALPHA_INK_SPAN: f32 = 0.55;
|
||||
|
||||
fn dist(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
|
||||
@@ -150,6 +155,35 @@ pub(crate) fn extract_wordmark(
|
||||
return None; // busy background — text over a photo, not segmentable
|
||||
}
|
||||
|
||||
// Scale the alpha ramp's top to the ACTUAL ink-vs-background distance.
|
||||
//
|
||||
// A fixed `ALPHA_HI` crushes the source's anti-aliasing whenever the ink is
|
||||
// far from the background. White text on a black band sits at distance ~438,
|
||||
// so with a ceiling of 120 every edge pixel brighter than ~27% grey is forced
|
||||
// fully opaque: a hard threshold wearing a smoothstep's clothing. Measured on
|
||||
// Camping in Alaska's "Needlemouth": the extracted mask's edges transitioned
|
||||
// over 0.10px against the source's 0.88px -- roughly nine times harder.
|
||||
//
|
||||
// That matters because the logo tracer recovers smooth curves FROM
|
||||
// anti-aliasing. Feeding it a near-binary mask forced heavy compensating blur
|
||||
// downstream, which melts serif terminals; with the ramp scaled, the trace can
|
||||
// run at lower blur and a tighter simplify tolerance and keep real detail.
|
||||
//
|
||||
// The 95th percentile is the glyph body rather than the brightest speck, and
|
||||
// the fixed value stays a floor so low-contrast marks are unaffected.
|
||||
let ink_distance = {
|
||||
let mut distances: Vec<f32> = crop
|
||||
.pixels()
|
||||
.map(|p| dist([p.0[0] as f32, p.0[1] as f32, p.0[2] as f32], bg))
|
||||
.collect();
|
||||
distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
distances
|
||||
.get((distances.len() as f32 * 0.95) as usize)
|
||||
.copied()
|
||||
.unwrap_or(ALPHA_HI)
|
||||
};
|
||||
let alpha_hi = ALPHA_HI.max(ALPHA_INK_SPAN * ink_distance);
|
||||
|
||||
// Alpha ramp by distance from the background colour, tracking per-row ink so
|
||||
// we can trim adjacent text lines the padding caught (the album title above /
|
||||
// below the band name), which sit in their own ink band across an empty gap.
|
||||
@@ -159,7 +193,7 @@ pub(crate) fn extract_wordmark(
|
||||
for x in 0..w {
|
||||
let p = crop.get_pixel(x, y).0;
|
||||
let d = dist([p[0] as f32, p[1] as f32, p[2] as f32], bg);
|
||||
let a = smoothstep(ALPHA_LO, ALPHA_HI, d);
|
||||
let a = smoothstep(ALPHA_LO, alpha_hi, d);
|
||||
let alpha = (a * 255.0).round() as u8;
|
||||
// Flat white ink (not the cover's own colour). The frontend logo pass
|
||||
// only traces the ALPHA silhouette and tints it to the accent, and it
|
||||
|
||||
@@ -776,6 +776,13 @@
|
||||
const LOGO_TRACE_EDGE = 2400; // supersample target long edge for hard sources
|
||||
const LOGO_TRACE_MAX_SCALE = 4;
|
||||
const LOGO_TRACE_BLUR = 1.0; // blur px per unit of supersample scale
|
||||
// Cover-derived marks trace at a larger raster with far less blur than they
|
||||
// used to: their alpha now carries the source's real anti-aliasing (see
|
||||
// cover_logo.rs ALPHA_INK_SPAN), so detail survives instead of needing to be
|
||||
// smoothed away. Measured on Camping in Alaska: 900 → ~2200 path vertices
|
||||
// with no staircasing, and serif terminals that keep their shape.
|
||||
const LOGO_COVER_TRACE_EDGE = 2400;
|
||||
const LOGO_COVER_TRACE_BLUR = 0.5; // floor; scales up only for tiny sources
|
||||
const LOGO_MONO_MIN = 0.55; // dominant-colour share to accept for tinting
|
||||
// A logo can trade this much cleanliness for a wider, more title-like shape.
|
||||
// It must still pass LOGO_MONO_MIN; this only ranks already-good candidates.
|
||||
@@ -1562,21 +1569,32 @@
|
||||
// near the top quality score, then prefer the widest result.
|
||||
let source, eps, minArea;
|
||||
if (usingCoverLogo) {
|
||||
// Cover-derived wordmark (recipe D): the Rust side already
|
||||
// despeckled the alpha; upscale to a fixed 1400px width and
|
||||
// blur so the low-res letterforms trace as smooth curves
|
||||
// rather than a staircase, then simplify at eps 0.5.
|
||||
// Cover-derived wordmark (recipe D). The blur here is
|
||||
// COMPENSATION, not a goal: it exists to synthesise the
|
||||
// anti-aliasing a mask lacks, and it costs serif detail —
|
||||
// terminals melt into blobs at ~1px of blur.
|
||||
//
|
||||
// The blur MUST scale with the enlargement: a fixed 1px did
|
||||
// almost nothing on a 29px-tall source (a ~7x upscale to
|
||||
// 1400px), so its pixel-steps survived into the trace as
|
||||
// aliasing. ~0.6px of blur per unit of scale synthesises the
|
||||
// anti-aliasing the tracer needs — hi-res marks (scale≈1) stay
|
||||
// crisp, tiny marks get proportionally more smoothing.
|
||||
const scale = 1400 / candidate.img.naturalWidth;
|
||||
source = rgba(candidate.img, 1400, Math.max(1, scale * 0.6));
|
||||
eps = 0.5;
|
||||
minArea = 24;
|
||||
// The Rust segmentation now scales its alpha ramp to the
|
||||
// measured ink distance (see cover_logo.rs ALPHA_INK_SPAN),
|
||||
// so the mask arrives with the source's REAL anti-aliasing
|
||||
// instead of a near-binary edge. That is what makes a lighter
|
||||
// touch safe: less blur and a tighter simplify tolerance now
|
||||
// resolve genuine sub-pixel curvature, where before they only
|
||||
// captured the pixel staircase in finer detail.
|
||||
//
|
||||
// A tiny mark still needs proportionally more smoothing, so
|
||||
// the blur keeps scaling with the enlargement — just from a
|
||||
// much lower base.
|
||||
const scale = LOGO_COVER_TRACE_EDGE / candidate.img.naturalWidth;
|
||||
source = rgba(
|
||||
candidate.img,
|
||||
LOGO_COVER_TRACE_EDGE,
|
||||
Math.max(LOGO_COVER_TRACE_BLUR, scale * 0.3),
|
||||
);
|
||||
eps = 0.2;
|
||||
// Speck floor scales with the raster, or a 2400px trace keeps
|
||||
// dust the 1400px one dropped.
|
||||
minArea = 24 * (LOGO_COVER_TRACE_EDGE / 1400) ** 2;
|
||||
} else if (aaFraction(rgba(candidate.img)) < LOGO_AA_MIN) {
|
||||
// Hard-edged 1-bit source: supersample and blur to synthesise
|
||||
// anti-aliasing, so the tracer resolves smooth sub-pixel
|
||||
|
||||
Reference in new issue
Block a user