zrender icon indicating copy to clipboard operation
zrender copied to clipboard

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

Open plainheart opened this issue 4 years ago • 9 comments

  • Resolves apache/echarts#15467

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
        }
    }]
};

plainheart avatar Aug 04 '21 06:08 plainheart

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.

pissang avatar Aug 04 '21 06:08 pissang

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 avatar Aug 04 '21 07:08 plainheart

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
        }
    }]
};

plainheart avatar Aug 05 '21 05:08 plainheart

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

Ovilia avatar Sep 10 '21 09:09 Ovilia

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 avatar Sep 11 '21 08:09 pissang

Merged the test case from https://github.com/ecomfe/zrender/pull/822

pissang avatar Sep 16 '21 01:09 pissang

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/

plainheart avatar Jan 09 '22 10:01 plainheart

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?

pissang avatar Jan 12 '22 02:01 pissang