Closed
Bug 1474284
Opened 6 years ago
Closed 6 years ago
SVG path interpolation broken for certain bezier curves
Categories
(Core :: Graphics, defect, P3)
Tracking
()
RESOLVED
FIXED
mozilla63
People
(Reporter: paul.lebeau, Assigned: longsonr)
Details
(Keywords: correctness, regression, Whiteboard: [gfx-noted])
Attachments
(3 files, 2 obsolete files)
471 bytes,
image/svg+xml
|
Details | |
440 bytes,
image/svg+xml
|
Details | |
2.20 KB,
patch
|
bas.schouten
:
review+
ritu
:
approval-mozilla-beta+
|
Details | Diff | Splinter Review |
User Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0
Build ID: 20180621125625
Steps to reproduce:
Try the following animateMotion test:
<svg width="500" height="500" viewBox="0 0 112 112">
<path id="path1" fill="none" stroke="#000"
d="M 40.500,32.500
C 40.500,32.500 24.500,32.500 24.500,16.500"/>
<circle cx="0" cy="0" r="2" fill="rgba(224,0,0,0.5)">
<animateMotion dur="2s" repeatCount="indefinite" rotate="auto" >
<mpath xlink:href="#path1"/>
</animateMotion>
</circle>
</svg>
See fiddle: https://jsfiddle.net/29avn0t7/9/
The animated circle is not following the curve as it is supposed to.
It appears that path interpolation has a bug for this particular bezier (which was extracted from a longer original path). This can also be seen in the following test of getPointAtLength():
<svg width="500" height="500" viewBox="0 0 112 112">
<path id="path1" fill="none" stroke="#000"
d="M 40.500,32.500
C 40.500,32.500 24.500,32.500 24.500,16.500"/>
<circle cx="0" cy="0" r="2" fill="rgba(224,0,0,0.5)">
<animateMotion dur="2s" repeatCount="indefinite" rotate="auto" >
<mpath xlink:href="#path1"/>
</animateMotion>
</circle>
</svg>
var path = document.getElementById("path1");
var len = path.getTotalLength();
var halfway = path.getPointAtLength(len/2);
var circle = document.createElementNS(path.namespaceURI, "circle");
circle.cx.baseVal.value = halfway.x;
circle.cy.baseVal.value = halfway.y;
circle.r.baseVal.value = 1;
path.parentElement.appendChild(circle);
See fiddle: https://jsfiddle.net/29avn0t7/14/
Actual results:
Small circle leaves the curve and instead moves linearly from the start to the end.
Expected results:
Circle should follow path as rendered.
Assignee | ||
Comment 1•6 years ago
|
||
a build from 2011-01-01 works. Having trouble finding a regression window though.
Keywords: regressionwindow-wanted
Comment 2•6 years ago
|
||
2011-era Firefox builds don't seem to like jsfiddle, so here's a standalone testcase that works even in old builds.
Comment 3•6 years ago
|
||
Tracking down a regression range.
The testcase goes from animating to not-animating-at-all with this regression range:
https://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=a475f94bb1b1&tochange=f2adb62d07eb
(likely from "Bug 937994 - Make the SMIL animateMotion code use a Moz2D PathBuilder instead of gfxContext.")
Now tracking down when it started animating (incorrectly) after that...
Keywords: regressionwindow-wanted
Comment 4•6 years ago
|
||
The second behavior-change (from not-animating-at-all to animating-incorrectly-straight-across) happened in this range:
https://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=57e4e9c33bef&tochange=55f3224d7513
My guess is that that's from:
https://hg.mozilla.org/mozilla-central/rev/b9458d466161
Bug 1068603: Extend FlattenBezier to handle case where full path can be approximated by a linear segment.
Comment 5•6 years ago
|
||
So, bottom line, it seems Moz2D's "FlattenBezier" function is having trouble flattening this Bezier curve accurately.
We're hitting a case that was initially unhandled in Moz2D (hence the first breakage, comment 3) and now is handled using a straight line (hence the only partial restoration of functionality, comment 4).
I took a quick look at the "one line segment" clause in gdb:
> // Process ranges. [t1min, t1max] and [t2min, t2max] are approximated by line
> // segments.
> if (count == 1 && t1min <= 0 && t1max >= 1.0) {
> // The whole range can be approximated by a line segment.
> aSink->LineTo(aControlPoints.mCP4.ToPoint());
> return;
> }
When we get here, we have count = 1, t1 = 0, t1min = -inf and t1max = +inf.
I'm not sure what that means, but it seems maybe pathological/unexpected.
I think the infinity values come from this expression in FindInflectionApproximationRange:
> // Use the absolute value so that Min and Max will correspond with the
> // minimum and maximum of the range.
> *aMin = aT - CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
> *aMax = aT + CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
Here, we're dividing by 0., because cp41.x and .y are the same value (they happen to both be -16), which means (cp41.x - cp41.y) is 0. So we're doing aT - CubicRoot(inf) and aT + CubicRoot(inf), basically, which is -inf and +inf.
Bas, it looks like you added this subtraction in bug 941585 ( https://hg.mozilla.org/mozilla-central/rev/22c11c35d1a3#l1.43 ) -- do you know what it means when .x and .y are the same here, and is there a way we can handle it better? (Divide-by-0 seems clearly undesirable, particularly given that it produces incorrect results.)
Status: UNCONFIRMED → NEW
Component: SVG → Graphics
Ever confirmed: true
Flags: needinfo?(bas)
Comment 6•6 years ago
|
||
Updated•6 years ago
|
Updated•6 years ago
|
Keywords: regression
Comment 7•6 years ago
|
||
(In reply to Daniel Holbert [:dholbert] from comment #5)
> So, bottom line, it seems Moz2D's "FlattenBezier" function is having trouble
> flattening this Bezier curve accurately.
>
> We're hitting a case that was initially unhandled in Moz2D (hence the first
> breakage, comment 3) and now is handled using a straight line (hence the
> only partial restoration of functionality, comment 4).
>
> I took a quick look at the "one line segment" clause in gdb:
> > // Process ranges. [t1min, t1max] and [t2min, t2max] are approximated by line
> > // segments.
> > if (count == 1 && t1min <= 0 && t1max >= 1.0) {
> > // The whole range can be approximated by a line segment.
> > aSink->LineTo(aControlPoints.mCP4.ToPoint());
> > return;
> > }
>
> When we get here, we have count = 1, t1 = 0, t1min = -inf and t1max = +inf.
>
> I'm not sure what that means, but it seems maybe pathological/unexpected.
>
> I think the infinity values come from this expression in
> FindInflectionApproximationRange:
> > // Use the absolute value so that Min and Max will correspond with the
> > // minimum and maximum of the range.
> > *aMin = aT - CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
> > *aMax = aT + CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
>
> Here, we're dividing by 0., because cp41.x and .y are the same value (they
> happen to both be -16), which means (cp41.x - cp41.y) is 0. So we're doing
> aT - CubicRoot(inf) and aT + CubicRoot(inf), basically, which is -inf and
> +inf.
>
>
> Bas, it looks like you added this subtraction in bug 941585 (
> https://hg.mozilla.org/mozilla-central/rev/22c11c35d1a3#l1.43 ) -- do you
> know what it means when .x and .y are the same here, and is there a way we
> can handle it better? (Divide-by-0 seems clearly undesirable, particularly
> given that it produces incorrect results.)
Yes, this means that s3 == 0. Note the limit being solved is lim[n->0] (cp41.x * n) / n - (cp41.y * n) / n = cp41.x - cp41.y in order to find s3. Essentially '-inf' and '+inf' are correct here, we just need to make them into something sensible code-wise. Further down in the function is a codepath to deal with the s3 == 0 situation, we're simply missing this codepath when we're in this branch. Adding it there should be trivial, let me know if you can do it or whether you want me to do it.
Flags: needinfo?(bas)
Updated•6 years ago
|
Flags: needinfo?(dholbert)
Comment 8•6 years ago
|
||
(In reply to Bas Schouten (:bas.schouten) from comment #7)
> Yes, this means that s3 == 0. [...] Further down in the function is a
> codepath to deal with the s3 == 0 situation, we're simply missing this
> codepath when we're in this branch. Adding it there should be trivial, let
> me know if you can do it or whether you want me to do it.
I took a crack at it, but wasn't successful. I spun up a trivial patch to do what I think you suggested (checking for cp41.x == cp41.y and duplicating our "s3==0" return logic stuff if so), but it doesn't change our behavior here. See attached (non-helping) patch.
(I verified in a debugger that we *are* hitting this patch's newly-added return statement, too.)
Perhaps it makes sense that it doesn't change our behavior, because the s3==0 scenario is trying to use a straight line approximation, and this bug happens to be a case where we're incorrectly animating in a straight line.
Bas, would you mind taking a crack at this?
Flags: needinfo?(dholbert) → needinfo?(bas)
Comment 9•6 years ago
|
||
I simplified the numbers a bit to be rounder & to make the curve more extreme.
This testcase has this path "d" command:
M 50,40
C 50,40 0,60 30,20
...which we are apparently unable to flatten.
Comment 10•6 years ago
|
||
BTW if I call the JS getTotalLength() function (which uses the same flattening code) on the path in testcase 2...
- Firefox gives me ~28px (following the same shorter/straight path)
- Chrome gives me ~55px
Reporter | ||
Comment 11•6 years ago
|
||
Sorry for sticking my nose into code I probably don't understand well :)
but...
The root problem with these two beziers is that CP1 and CP2 are co-incident. If you check for this and
instead use CP3 in that test, does it result in better behaviour?
I.e.:
Point cp21 = aControlPoints.mCP2 - aControlPoints.mCP1;
Point cp41 = aControlPoints.mCP4 - aControlPoints.mCP1;
+ if (cp21.x == 0.f && cp21.y == 0.f) {
+ Point cp21 = aControlPoints.mCP3 - aControlPoints.mCP1;
+ }
if (cp21.x == 0.f && cp21.y == 0.f) {
// In this case s3 becomes lim[n->0] (cp41.x * n) / n - (cp41.y * n) / n = cp41.x - cp41.y.
// Use the absolute value so that Min and Max will correspond with the
// minimum and maximum of the range.
*aMin = aT - CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
*aMax = aT + CubicRoot(std::abs(aTolerance / (cp41.x - cp41.y)));
return;
}
This returns an inflection point approximation of:
t1min=-0.33756467101443444 t1max=0.33756467101443444
that looks like it will result in the correct behaviour in FlattenBezier().
And if CP1 & CP3 are also coincident, then the linear approximation is justified then.
Unfortunately, I don't have the ability to check out the code, and test this suggestion, right now.
So there is a good chance I am wrong. :)
Assignee | ||
Comment 12•6 years ago
|
||
Assignee: nobody → longsonr
Attachment #8993531 -
Attachment is obsolete: true
Assignee | ||
Comment 13•6 years ago
|
||
Attachment #8993904 -
Attachment is obsolete: true
Attachment #8993950 -
Flags: review?(bas)
Updated•6 years ago
|
Attachment #8993950 -
Flags: review?(bas) → review+
Updated•6 years ago
|
Flags: needinfo?(bas)
Comment 14•6 years ago
|
||
Pushed by longsonr@gmail.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/44e34213f29d
cope with degenerate bezier curves r=baz
Assignee | ||
Updated•6 years ago
|
Flags: in-testsuite+
Comment 15•6 years ago
|
||
Thanks, Paul & Robert!
Comment 16•6 years ago
|
||
bugherder |
Status: NEW → RESOLVED
Closed: 6 years ago
status-firefox63:
--- → fixed
Resolution: --- → FIXED
Target Milestone: --- → mozilla63
Comment 17•6 years ago
|
||
Is this worth a Beta backport nomination?
status-firefox61:
--- → wontfix
status-firefox62:
--- → affected
status-firefox-esr52:
--- → wontfix
status-firefox-esr60:
--- → wontfix
Flags: needinfo?(longsonr)
Assignee | ||
Comment 18•6 years ago
|
||
Comment on attachment 8993950 [details] [diff] [review]
crowdsourced patch with mochitest
Approval Request Comment
[Feature/Bug causing the regression]: bug 937994 and bug 1068603
[User impact if declined]: wrong path length/position calculations
[Is this code covered by automated tests?]: yes
[Has the fix been verified in Nightly?]: yes
[Needs manual test from QE? If yes, steps to reproduce]: no
[List of other uplifts needed for the feature/fix]: none
[Is the change risky?]: no
[Why is the change risky/not risky?]: code is unlikely to be hit and change is very small
[String changes made/needed]: none
Flags: needinfo?(longsonr)
Attachment #8993950 -
Flags: approval-mozilla-beta?
Reporter | ||
Comment 19•6 years ago
|
||
Thanks for applying and testing the fix Robert.
Comment on attachment 8993950 [details] [diff] [review]
crowdsourced patch with mochitest
Low risk fix, verified on Nightly, beta62+
Attachment #8993950 -
Flags: approval-mozilla-beta? → approval-mozilla-beta+
Comment 21•6 years ago
|
||
bugherder uplift |
You need to log in
before you can comment on or make changes to this bug.
Description
•