Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(shadow): specify the shadow filter units as userSpaceOnUse. #792

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

plainheart
Copy link
Collaborator

@plainheart plainheart commented Aug 4, 2021

Before

Before

After

After

A simple demo

// use SVG renderer
option = {
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: [0, 0, 0, 0, 0, 0, 0],
        type: 'line',
        stack: true,
        color: '#7f64ff',
        symbol: 'none',
        smooth: true,
        lineStyle: {
            shadowColor: 'red',
            shadowBlur: 5,
            shadowOffsetX: 0,
            shadowOffsetY: 0
        }
    }]
};

@pissang
Copy link
Contributor

pissang commented Aug 4, 2021

Do we need to change x, y, width, and height after using userSpaceOnUse filterUnits?

Also, this seems to be a bug in chrome. The shadow filter should have no reason to affect the display of the original graphic. I tested it on safari and the line won't disappear.

@plainheart
Copy link
Collaborator Author

Also, this seems to be a bug in chrome. The shadow filter should have no reason to affect the display of the original graphic. I tested it on safari and the line won't disappear.

Seems so. It also works in Firefox.

@plainheart
Copy link
Collaborator Author

plainheart commented Aug 5, 2021

Just one more thing, the shadow seems buggy when hovering.

shadow

Two series will use the same shadow filter id when hovering. That is not expected.

shadow

option = {
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: [0, 0, 10, 0, 0, 0, 0],
        type: 'line',
        stack: true,
        color: '#7f64ff',
        symbol: 'none',
        smooth: true,
        lineStyle: {
            shadowColor: 'red',
            shadowBlur: 5,
            shadowOffsetX: 0,
            shadowOffsetY: 0
        }
    }, {
        data: [0, 80, 0, 50, 0, 0, 0],
        type: 'line',
        stack: true,
        color: '#7f64ff',
        symbol: 'none',
        smooth: true,
        lineStyle: {
            shadowColor: 'blue',
            shadowBlur: 5,
            shadowOffsetX: 0,
            shadowOffsetY: 0
        }
    }]
};

@Ovilia
Copy link
Member

Ovilia commented Sep 10, 2021

I opened #822 before seeing this PR...

Perhaps it's better to add this attribute in _getFromPool because this does not need to be updated. Anyway, welcome to close either one...

Also, this seems to be a bug in chrome.

https://bugs.chromium.org/p/chromium/issues/detail?id=1247310#c3

@pissang
Copy link
Contributor

pissang commented Sep 11, 2021

Let's keep the discussion in this PR. The major concern from me is calculating x, y, width, and height after changing to userSpaceOnUse. The unit has been changed, and we need to consider scale to avoid creating a too large clipping region which may lead to performance issues.

@pissang
Copy link
Contributor

pissang commented Sep 16, 2021

Merged the test case from #822

@plainheart
Copy link
Collaborator Author

plainheart commented Jan 9, 2022

Get this updated slightly. Here are two strategies I can think out.

1. Add a tiny offset to make it not entirely straight. (Ref)

It can make the line displayed but the result is strange and unexpected.

2. Specify especially filterUnits as userSpaceOnUse when the element is Line and is a horizontal or vertical straight line.

It seems to be working for me. But it needs some extra logic to check.

export function getShadowKey(displayable: Displayable) {
    const style = displayable.style;
    const globalScale = displayable.getGlobalScale();
    let isHorizontalVerticalStraightLine = 0;
    // FIXME Use `userSpaceOnUse` as the unit when applying filters on a horizontal or vertical straight line
    if (displayable.type === 'line') {
        const shape = (displayable as Line).shape;
        const rad = Math.atan2(shape.y2 - shape.y1, shape.x2 - shape.x1);
        isHorizontalVerticalStraightLine = +!(rad % (Math.PI / 2));
    }
    return [
        [
            style.shadowColor,
            (style.shadowBlur || 0).toFixed(2), // Reduce the precision
            (style.shadowOffsetX || 0).toFixed(2),
            (style.shadowOffsetY || 0).toFixed(2),
            globalScale[0],
            globalScale[1],
            isHorizontalVerticalStraightLine
        ].join(','),
        isHorizontalVerticalStraightLine
    ];
}

function setShadow(
    el: Displayable,
    attrs: SVGVNodeAttrs,
    scope: BrushScope
) {
    const style = el.style;
    if (hasShadow(style)) {
        const [shadowKey, isHorizontalVerticalStraightLine] = getShadowKey(el);
        const shadowCache = scope.shadowCache;
        let shadowId = shadowCache[shadowKey];
        if (!shadowId) {
            const globalScale = el.getGlobalScale();
            const scaleX = globalScale[0];
            const scaleY = globalScale[1];
            if (!scaleX || !scaleY) {
                return;
            }

            const offsetX = style.shadowOffsetX || 0;
            const offsetY = style.shadowOffsetY || 0;
            const blur = style.shadowBlur;
            const {opacity, color} = normalizeColor(style.shadowColor);
            const stdDx = blur / 2 / scaleX;
            const stdDy = blur / 2 / scaleY;
            const stdDeviation = stdDx + ' ' + stdDy;
            // Use a simple prefix to reduce the size
            shadowId = scope.zrId + '-s' + scope.shadowIdx++;
            const filterAttrs: SVGVNodeAttrs = {
                'id': shadowId,
                'x': '-100%',
                'y': '-100%',
                'width': '300%',
                'height': '300%'
            };
            // 
            isHorizontalVerticalStraightLine && (filterAttrs.filterUnits = 'userSpaceOnUse');
            scope.defs[shadowId] = createVNode(
                'filter', shadowId, filterAttrs,
                [
                    createVNode('feDropShadow', '', {
                        'dx': offsetX / scaleX,
                        'dy': offsetY / scaleY,
                        'stdDeviation': stdDeviation,
                        'flood-color': color,
                        'flood-opacity': opacity
                    })
                ]
            );
            shadowCache[shadowKey] = shadowId;
        }
        attrs.filter = getIdURL(shadowId);
    }
}
Before After
Before After

Here is another article that states the same issue:
https://www.amcharts.com/docs/v4/tutorials/fixing-gradients-and-filters-on-straight-lines/

@pissang
Copy link
Contributor

pissang commented Jan 12, 2022

In your second solution. Checking zero area bounding rect without lineWidth will be more accurate than checking type because line chart use an extended path. If we use userSpaceOnUse only on the zero area bounding rect.

I'm not sure but is it possible to use userSpaceOnUse on all cases and consider the scale of element?

@freshollie
Copy link

I think the fix also needs to apply to the svg/graphic.ts

diff --git a/src/svg/graphic.ts b/src/svg/graphic.ts
index 096014967613dcc0bfecd8e9215c5ee900abf79a..f4b41016e301240dd060aae8368172ca01a29d84 100644
--- a/src/svg/graphic.ts
+++ b/src/svg/graphic.ts
@@ -393,7 +393,8 @@ function setShadow(
                     'x': '-100%',
                     'y': '-100%',
                     'width': '300%',
-                    'height': '300%'
+                    'height': '300%',
+                    'filterUnits': 'userSpaceOnUse'
                 },
                 [
                     createVNode('feDropShadow', '', {
diff --git a/src/svg-legacy/helper/ShadowManager.ts b/src/svg-legacy/helper/ShadowManager.ts
index cc2c76e891e821becfb40d3efebada921efe3102..c27219fe8a59a907266abb67ef63918b10668d9a 100644
--- a/src/svg-legacy/helper/ShadowManager.ts
+++ b/src/svg-legacy/helper/ShadowManager.ts
@@ -36,6 +36,7 @@ export default class ShadowManager extends Definable {
         if (!shadowDom) {
             shadowDom = createElement('filter') as SVGFilterElement;
             shadowDom.setAttribute('id', 'zr' + this._zrId + '-shadow-' + this.nextId++);
+            shadowDom.setAttribute('filterUnits', 'userSpaceOnUse');
             const domChild = createElement('feDropShadow');
             shadowDom.appendChild(domChild);
             this.addDom(shadowDom);
@@ -118,6 +119,7 @@ export default class ShadowManager extends Definable {
         shadowDom.setAttribute('y', '-100%');
         shadowDom.setAttribute('width', '300%');
         shadowDom.setAttribute('height', '300%');
+        shadowDom.setAttribute('filterUnits', 'userSpaceOnUse');
 
         // Store dom element in shadow, to avoid creating multiple
         // dom instances for the same shadow element

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants