From c3ec78261961f6fd5c87efef367ee48a9c385e48 Mon Sep 17 00:00:00 2001 From: Andrey Bakhvalov Date: Sun, 25 Oct 2026 19:47:42 +0100 Subject: [PATCH] fix(chart): show cropped photos on the chart in Firefox Firefox lays a CSS clip-path shape out inside the frame's view box, so a CROP away from the image's top left corner clipped the whole portrait away and only the initials showed. The circle is now an SVG clipPath on a group around the frame. Co-Authored-By: Claude Opus 5.5 --- src/chart/chart.css | 4 ---- src/chart/renderer.ts | 18 ++++++++++++++++-- tests/e2e/photos.spec.ts | 9 +++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/chart/chart.css b/src/chart/chart.css index 4883d33..0391d28 100644 --- a/src/chart/chart.css +++ b/src/chart/chart.css @@ -140,10 +140,6 @@ fill: var(--text-2); } -.gv-photo-frame { - clip-path: circle(50%); -} - .gv-photo-ring { fill: none; stroke: var(--border); diff --git a/src/chart/renderer.ts b/src/chart/renderer.ts index 5e51d29..e3d17a0 100644 --- a/src/chart/renderer.ts +++ b/src/chart/renderer.ts @@ -44,6 +44,8 @@ const NAME_TOP = PAD + PHOTO + 10 + 14; // baseline of the first name line const NAME_LINE = 18.5; /** Life dates are set at 13px regular; `measure` is the 15px medium name font. */ const LIFE_SCALE = 0.85; +/** Numbers the portraits' clip paths: each needs an id of its own. */ +let clipCount = 0; export interface RendererContext { tree: LoadedTree; @@ -404,7 +406,19 @@ export class GedviewRenderer extends CompositeRenderer implements Renderer { // A shown photo opens the media viewer (pointer only: on the keyboard // the card's portrait does it). p.attr('data-photo', ''); - const frame = p + // The circle is cut by a clip path on a group, not by CSS on the frame: + // Firefox lays CSS clip-path shapes out inside the frame's view box, so + // a crop away from the image's corner cut the whole photo away. + const r = PHOTO / 2; + const id = `gv-photo-clip-${++clipCount}`; + p.append('clipPath') + .attr('id', id) + .append('circle') + .attr('cx', r) + .attr('cy', r) + .attr('r', r); + const clipped = p.append('g').attr('clip-path', `url(#${id})`); + const frame = clipped .append('svg') .attr('class', 'gv-photo-frame') .attr('width', PHOTO) @@ -432,7 +446,7 @@ export class GedviewRenderer extends CompositeRenderer implements Renderer { image.attr('width', PHOTO).attr('height', PHOTO); } image.on('error', () => { - frame.remove(); + clipped.remove(); p.attr('data-photo', null); }); } diff --git a/tests/e2e/photos.spec.ts b/tests/e2e/photos.spec.ts index 77587ac..d004b74 100644 --- a/tests/e2e/photos.spec.ts +++ b/tests/e2e/photos.spec.ts @@ -76,6 +76,15 @@ test('people with a photo show it on the chart, cropped, or their initials', asy 'viewBox', '1 1 2 2', ); + // The circle is a clip path on a group around the frame: a CSS clip-path + // on the frame itself hides a cropped photo in Firefox. + await expect(annaPhoto.locator('g[clip-path] > .gv-photo-frame')).toHaveCount( + 1, + ); + await expect(annaPhoto.locator('.gv-photo-frame')).toHaveCSS( + 'clip-path', + 'none', + ); await expect(annaPhoto.locator('.gv-photo-ring')).toHaveClass(/gv-F/); // The image failed: the initials stay. await expect(petrPhoto.locator('.gv-photo-frame')).toHaveCount(0);