[whatwg] Canvas and Paths

Rik Cabanier cabanier at gmail.com
Wed Mar 12 20:15:14 PDT 2014


>
> > > You can do unions and so forth with just paths, no need for regions.
> >
> > How would you do a union with paths?
> > If you mean that you can just aggregate the segments, sure but that
> doesn't
> > seem very useful.
>
> You say, here are some paths, here are some fill rules, here are some
> operations you should perform, now give me back a path that describes the
> result given a particular fill rule.
>

I think your collapsing a couple of different concepts here:

path + fillrule -> shape
union of shapes -> shape
shape can be converted to a path



> A shape is just a path with a fill rule, essentially.


So, a path can now have a fillrule? Sorry, that makes no sense.
A path is just a set of segments; there should be no magic


> Anything you can do
> with one you can do with the other.
>

No.
You can't add segments from one shape to another as shapes represent
regions.
Likewise, you can't union, intersect or xor path segments.


> > > > The path object should represent the path in the graphics state. You
> > > > can't add a stroked path or text outline to the graphics state and
> > > > then fill/stroke it.
> > >
> > > Why not?
> >
> > As designed today, you could fill it, as long as you use non-zero
> > winding. If you use even-odd, the results will be very wrong. (ie where
> > joins and line segments meet, there will be white regions)
>
> I think "wrong" here implies a value judgement that's unwarranted.
>

"Wrong" meaning:
 if the author has a bunch of geometry and wants to put it in 1 path object
so he can just execute 1 fill operation, he might be under the impression
that "adding" the geometry will just work.
There are very few use cases where you want to add partial path segments
together but I agree that there are some cases that it's useful to have.


> > Stroking will be completely wrong too, because joins and end caps are
> > drawn separately, so they would be stroked as separate paths. This will
> > not give you the effect of a double-stroked path.
>
> I don't understand why you think joins and end caps are drawn separately.
> That is not what the spec requires.
>

Sure it does, for instance from
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#trace-a-path
:

The round value means that a filled arc connecting the two aforementioned
corners of the join, abutting (and not overlapping) the aforementioned
triangle, with the diameter equal to the line width and the origin at the
point of the join, must be added at joins.


If you mean, "drawn with a separate fill call", yes that is true.
What I meant was that they are drawn as a separate closed path that will
interact with other paths as soon as there are different winding rules or
"holes".


> > > We seem to be going around in circles. We're in agreement that
> > > eventually we should add APIs for combining paths such that we get the
> > > equivalent of the union of their fill regions. I agree that converting
> > > text into paths is non-trivial (lots of stuff browsers do is
> > > non-trivial, that's kind of the point -- if it was trivial, we could
> > > leave it for authors). But I don't see how we get from there to you
> > > wanting the existing APIs removed.
> >
> > I want them removed because they will most likely not behave in the way
> > that an author expects. When he "adds" 2 paths, he wouldn't expect that
> > there is 'interference' between them.
>
> I don't see why not. It's exactly what happens today if you were to just
> add the same path creation statements together into the current default
> path and fill or stroke that.
>

Sure but who does that?
These two operations don't look the same:

 ...// draw geometry A

ctx.fill();

...// draw geometry B

ctx.fill();

and

...// draw geometry A
...// draw geometry B
ctx.fill();



> > > The only methods for which the spec currently requires user agents to
> > > ensure that they create only paths that wind clockwise, and for which
> > > this has any practical impact as far as I can tell, are the Path
> > > methods addText(), addPathByStrokingText(), and
> > > addPathByStrokingPath().
> >
> > don't forget tracing a path.
>
> That's addPathByStrokingPath(). (stroke() is essentially just
> addPathByStrokingText() followed by a fill().)
>
>
> > > On Mon, 4 Nov 2013, Rik Cabanier wrote:
> > > >
> > > > In light of this, does anyone have objections to these 2 new
> > > > methods:
> > > >
> > > > Path getCurrentPath();
> > > > void setCurrentPath(Path);
> > >
> > > (The right question is "does anyone want these methods". The bar is
> > > higher than just no objections.)
> > >
> > > What's the use case for setCurrentPath()?
> >
> > It would negate the need for a 'fill(path)', 'stroke(path)' and
> > 'clip(path)'.
>
> That's not a use case, it's just a different way of implementing the same
> use case. Why is it better? It seems distinctly more awkward and less
> idiomatic to have to set an implicit property and then call a method, than
> just call the method with the path.
>
> Analogously, we don't say:
>
>    Math.angle = Math.PI * 3;
>    var x = Math.sin();
>
> ...we say:
>
>    var x = Math.sin(Math.PI * 3);
>
>
> > It also make it more intuitive since you can call:
> > ctx.setCurrentPath(ctx2.getCurrentPath());
>
> I don't understand how that is intuitive. Why would you do this?
>
> Without compelling use cases, I don't think we should add these features.
>

Sure. We can punt on them.


> > > On Mon, 4 Nov 2013, Robert O'Callahan wrote:
> > > >
> > > > If you return a path in user-space, what do you get if you call
> > > > getCurrentPath with a singular transform?
> > > >   ctx.moveTo(0,0);
> > > >   ctx.lineTo(1,1);
> > > >   ctx.scale(0,0);
> > > >   var p = ctx.getCurrentPath();
> > > > ?
> > >
> > > The scale() call here has no effect. The default path is affected by
> > > transforms when you add the path segments, not when you paint. It
> would be
> > > consistent to do the same when converting it to a Path object.
> >
> > No, the CTM definitely affects what you paint. Stroke width and
> > gradients for instance.
>
> That's not the path, but sure.
>
>
> > On Sat, 2 Nov 2013, Rik Cabanier wrote:
> > > On Mon, 4 Nov 2013, Jürg Lehni wrote:
> > > >
> > > > I like this feature a lot. One advantage to not underestimate is the
> > > > amount of effort it takes to change existing code to make use of the
> > > > new Path feature, while staying backward compatible with older
> > > > browsers that don't implement this spec. For example, in Paper.js it
> > > > took only three added lines of code to use cached paths if they
> > > > exist rather than redrawing them each time.
> > >
> > > Being able to get a Path from the context if it exists seems
> > > reasonable, though it's non-trivial to define what that is given the
> > > way that transforms affect the context implied path.
> >
> > Why is it non-trivial? UA have to calculate this path (wrt the current
> > CTM anyway).
> >
> > > This would, of course, mean that this wouldn't have the expected
> > > effect with transforms, since they'd be amplified:
> > >
> > >    c.beginPath();
> > >    c.scale(2,2);
> > >    c.moveTo(0,0);
> > >    c.lineTo(1,1);
> > >    var p = new Path(c);
> > >    c.stroke(); // strokes the current default path from 0,0 to 2,2
> > >    c.stroke(p); // strokes p from 0,0 to 4,4 !
> > >
> > >    c.resetTransform();
> > >    c.stroke(p); // strokes the current default path from 0,0 to 2,2
> >
> > No, these will look the same. 'new Path(c)' establishes the same path in
> > the same user coordinate space.
>
> That's... highly unintuitive and very confusing.
>
> Consider these. Which is wrong?
>
>     c.beginPath();
>     c.scale(2,2);
>     c.moveTo(0,0);
>     c.lineTo(1,0);
>     c.resetTransform();
>     c.lineTo(0,1);
>     var p = new Path(c);
>     c.stroke(); // strokes the current default path from 0,0 to 2,0 to 0,1
>     c.stroke(p); // strokes p from 0,0 to 2,0 to 0,1
>

correct


>     c.beginPath();
>     c.scale(2,2);
>     c.moveTo(0,0);
>     c.lineTo(1,0);
>     c.lineTo(0,0.5);
>     c.resetTransform();
>     var p = new Path(c);
>     c.stroke(); // strokes the current default path from 0,0 to 2,0 to 0,1
>     c.stroke(p); // strokes p from 0,0 to 2,0 to 0,1?
>

correct


>     c.beginPath();
>     c.scale(2,2);
>     c.moveTo(0,0);
>     c.lineTo(1,0);
>     c.lineTo(0,0.5);
>     var p = new Path(c);
>     c.resetTransform();
>     c.stroke(); // strokes the current default path from 0,0 to 2,0 to 0,1
>     c.stroke(p); // strokes p from 0,0 to 2,0 to 0,1?
>

wrong


>     c.beginPath();
>     c.scale(2,2);
>     c.moveTo(0,0);
>     c.lineTo(1,0);
>     c.lineTo(0,0.5);
>     var p = new Path(c);
>     c.stroke(); // strokes the current default path from 0,0 to 2,0 to 0,1
>     c.resetTransform();
>     c.stroke(p); // strokes p from 0,0 to 2,0 to 0,1?
>

wrong

    c.beginPath();
>     c.scale(2,2);
>     c.moveTo(0,0);
>     c.lineTo(1,0);
>     c.lineTo(0,0.5);
>     var p = new Path(c);
>     c.stroke(); // strokes the current default path from 0,0 to 2,0 to 0,1
>     c.stroke(p); // strokes p from 0,0 to 2,0 to 0,1?
>     c.resetTransform();
>

correct


> This is why it's non-trivial.
>

Not sure what's non-trivial about this.
Any transform will be applied in reverse to the existing path.

Just to be sure, these 2 fill calls should look different, right?

var p = new Path2D();
p.rect(0,0,10,10);

ctx.fill(p);

ctx.scale(2,2);

ctx.fill(p);




> > > On Mon, 4 Nov 2013, Rik Cabanier wrote:
> > > >
> > > > However, for your example, I'm unsure what the right solution is.
> > > > The canvas specification is silent on what the behavior is for
> > > > non-invertible matrices.
> > >
> > > What question do you think the spec doesn't answer?
> > >
> > > > I think setting scale(0,0) or another matrix operation that is not
> > > > reversible, should remove drawing operations from the state because:
> > > > - how would you stroke with such a matrix?
> > >
> > > You'd get a point.
> >
> > How would you get a point? the width is scaled to 0.
>
> That's how you get a point -- scale(0,0) essentially reverts everything to
> a zero dimensional point.
>

OK, but the width of the point is also transformed to 0 so you get nothing.


> > > > - how do patterns operate? the same for gradient fills.
> > >
> > > This is defined:
> > >
> > > "If a radial gradient or repeated pattern is used when the
> > > transformation matrix is singular, the resulting style must be
> > > transparent black (otherwise the gradient or pattern would be
> > > collapsed to a point or line, leaving the other pixels undefined).
> > > Linear gradients and solid colors always define all points even with
> > > singular tranformation matrices."
> >
> > How is this better than not drawing?
>
> I'm not arguing that it's better, just that's it's defined. We went
> through a lot of effort to make sure all these things were defined, taking
> into account author feedback, implementor feedback, etc.
>
>
> On Wed, 8 Jan 2014, Rik Cabanier wrote:
> >
> > I'll reiterate my proposal:
> > 1. remove all the addxxx methods from the Path2D object [3]
> > Path object are just containers for segments. Aggregating segments will
> > generally not give the desired results since the segments will interact
> > (see [1]).
> > AddPath *could* be kept if people see a strong use case.
> >
> > 2. introduce a new class 'Shape2D'
>
> When making proposals, it's better to start from use cases and then
> explore those, rather than starting from APIs and then defending them.
> Without knowing what the use cases are, it's hard to evaluate the
> proposals. It's especially hard to evaluate proposals against each other.
>

We've gone over this several times now.
The APIs that you define, have use cases and I agree with them.
However the way you defined those APIs does not make sense and will not
give the result that authors want.

<snip>
>
> On Mon, 27 Jan 2014, Rik Cabanier wrote:
> >
> > For instance, let's say you request the path for the letter 'o'. It's
> > represented by 2 closed paths.
> > What winding rule should you use when you fill that path? Should the
> paths
> > be oriented so you have to use a NZO fill or so you can use any winding
> > rule?
> >
> > What happens if the path for a letter intersect with another path (or
> > another letter)?
>
> Good point. I've changed the algorithm a bit so that the resulting path
> that is added has no ambiguous overlap like this.
>

Great! (see
http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-path-addtextfor
the changed algorithm)
What you specified there is called "planarization". This is when you
calculate the intersections within and between closed shapes and remove the
line segments that are filled on both sides.
By specifying this:

The subpaths in merged path must be oriented such that for any point, the
number of times a half-infinite straight line drawn from that point crosses
a subpath is even if and only if the number of times a half-infinite
straight line drawn from that same point crosses a subpath going in one
direction is equal to the number of times it crosses a subpath going in the
other direction.

and relying on segment removal, you also get the same fill behavior for
even-odd. (Meaning that the end result can be used with either winding rule)
This is not something that is needed for just text but also when you do a
union of shapes.

The bad news is that this algorithm is very expensive and there are few
libraries that do a decent job (I only know of 1).
So, it's not realistic to add this to the Path2D object.
The reason for that is that even though a UA could emulate the union by
doing multiple fill operations, Path2D allows you to stroke another path
object. At that point, you really have to do planarization. By defining a
Shape2D object and not allowing it to be stroked, we can work around this.



> On Tue, 18 Feb 2014, Rik Cabanier wrote:
> >
> > Some people were unhappy with the SVGMatrix in the argument list but
> > unless we have something better soon, it's fine to implement. (We can
> > always use a union later)
> >
> > Ian, could you remove the other APIs from the spec:
>
> I don't think the arguments for removing these are compelling. The
> problems with the APIs have been addressed (e.g. there's no ambiguity in
> the case of overlapping text), the use cases are clear (e.g. drawing text
> around an arc or drawing a label along a line graph's line), and the API
> now supports the constructs to do unions of fill regions.
>

Where is the union of fill regions specified? All I see is segments
aggregation.


> > No one has implemented them and they are confusing the browser vendors.
> I don't think they're confusing anyone.
>

The blink people were looking at adding this until they thought it through
and realized that it wouldn't work.


> > Until we have support for shapes, the output of these methods won't be
> > stable.
>
> These methods have been very stable. They have barely changed since they
> were added, except for some minor tweaks to fix bugs.
>

How can you make that statement? No one has implemented them yet.



More information about the whatwg mailing list