Skip to content

Commit 7b94bf8

Browse files
matmenclaude
andcommitted
renderText: the outline doesn't show through translucent text
The stroke covers the glyphs as well (it's a distance field from them), and was drawn under them with one minus the fill's alpha: with a translucent text color it showed through. It's now masked by the glyphs' coverage (text and color glyphs), which is the same for opaque text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent 2aafe8e commit 7b94bf8

3 files changed

Lines changed: 22 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
82 instead of 95 ms), with identical output: the predictors run as one loop per mode instead of a switch per pixel,
3636
residual costs come from a table, and the LZ77 search hashes every pixel pair once.
3737

38+
### Fixes
39+
40+
- The outline of `Image.renderText` (`strokeWidth`) showed through translucent text, it now only shows around the
41+
glyphs (opaque text renders as before).
42+
3843
## 3.2.1
3944

4045
### Performance

‎src/image.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,14 +2095,14 @@ export class Image {
20952095

20962096
// the outline goes around the color glyphs as well
20972097
let shape = coverage;
2098-
if (colored && strokeWidth) {
2098+
if (colored) {
20992099
shape = coverage.slice();
21002100
for (let i = 0; i < shape.length; i++) shape[i] = Math.max(shape[i], colored[i * 4 + 3]);
21012101
}
21022102
const stroke = strokeWidth ? outline(shape, width, height, strokeWidth) : null;
21032103
const [sr, sg, sb, sa] = Image.colorToRGBA(strokeColor);
21042104

2105-
// color glyphs over the text (premultiplied, with the text's alpha), both over their outline
2105+
// color glyphs over the text (premultiplied, with the text's alpha), both within their outline
21062106
for (let i = 0; i < coverage.length; i++) {
21072107
let af = coverage[i] / 255 * a / 255, pr = r * af, pg = g * af, pb = b * af;
21082108
if (colored && colored[i * 4 + 3]) {
@@ -2113,7 +2113,9 @@ export class Image {
21132113
af = ca + af * keep;
21142114
}
21152115
if (stroke) {
2116-
const as = stroke[i] * sa / 255 * (1 - af);
2116+
// the outline shows around the glyphs only, not through translucent ones (for opaque text it's the
2117+
// same as drawing the glyphs over it)
2118+
const as = stroke[i] * sa / 255 * (1 - shape[i] / 255);
21172119
pr += sr * as;
21182120
pg += sg * as;
21192121
pb += sb * as;

‎tests/regressions.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,18 @@ const rgba = (image, x, y) => Image.colorToRGBA(image.getPixelAt(x, y));
457457
if (color === 0xffffffff) white++;
458458
}
459459
assert(red > 100 && white > 100, `outlined text should contain both colors (${red} red, ${white} white)`);
460+
461+
// the stroke goes around translucent text, it doesn't show through it
462+
const translucent = await Image.renderText(font, 40, 'Hi', 0x0000ff80, undefined, {strokeWidth: 3, strokeColor: 0xff0000ff});
463+
const fill = await Image.renderText(font, 40, 'Hi', 0x0000ff80);
464+
let inside = 0, tinted = 0;
465+
// pixels the glyphs cover fully (the stroked image is 3 pixels larger on every side)
466+
for (const [x, y, color] of fill.iterateWithColors()) {
467+
if (color !== 0x0000ff80) continue;
468+
inside++;
469+
if (translucent.getPixelAt(x + 3, y + 3) !== 0x0000ff80) tinted++;
470+
}
471+
assert(inside > 100 && !tinted, `translucent text should show no stroke inside (${tinted} of ${inside} pixels tinted)`);
460472
}
461473

462474
// PNGs are oriented by their eXIf chunk, ImageMagick's Exif text or XMP, in that order

0 commit comments

Comments
 (0)