Adobe

decor

Web Platform Team Blog

Making the web awesome

Revised canvas paths

Introduction

Canvas 2D contains a specification for adding a path object. The idea behind a path object is that you can use it as a ‘cache’ of drawing commands that can be sent to the canvas context in a single command. In addition, the path object contains methods for aggregating paths, stroking and converting text to a path.

Since a path conceptually represents an area, the Canvas 2D spec also uses it to specify ‘hit regions‘ that help you build interactivity and support accessibility.

Issues with the current approach

To determine the region of a control, you want to know the area of several ‘fill‘, ‘stroke‘ and text regions.

However, the main problem with today’s specification is that a ‘Path’ is simply an aggregator of path segments. For instance, if you need to know the region of a stroked donut, it seems that you could write:

 
  var f = new Path();
  f.beginPath();
  f.arc(100,100,100,0,Math.PI*2, false);
  f.arc(100,100,55,Math.PI,Math.PI*2, true);
  var s = new Path();
  s.addPathByStrokingPath(f,...);
  var r = new Path();
  r.addPath(f);
  r.addPath(s);

To demonstrate why this doesn’t work as intended, I will show the contents of each path with its winding and region. (Please see my blog post on winding rules for more info on winding rules).

Path f

 
  var f = new Path();
  f.beginPath();
  f.arc(100,100,100,0,Math.PI, false);
  f.arc(100,100,55,Math.PI,Math.PI*2, true);

‘f’ contains 2 circles that have different winding:

path_logic_1

Path s

 
  var s = new Path();
  s.addPathByStrokingPath(f,...);

‘s’ contains the stroke of path ‘f’, which is 4 circles with the following winding:

path_logic_2

so far, so good!

path r

 
  var r = new Path();
  r.addPath(f);
  r.addPath(s);

‘r’ contains all the path outlines of ‘f’ and ‘s':

path_logic_3

Oops! As you can see, there is a part of the region that is considered unpainted…

Why did the problem happen?

If we retrace our steps, path ‘f’ and ‘s’ contained the correct areas. What we really wanted is that the area of path ‘r’ is the combination of areas ‘f’ and ‘s’.

The reason for the discrepancy is because we aggregated the path segments and the segments from the first path started to interact with the segments from the second path.

How can we solve this?

It is clear that the problem lies in the aggregation step (the ‘addPath‘ API). The user never wants the winding of the first path to interact with winding of the second path.

So, this step should not add the paths; instead, it should do a union of the two regions. This ‘union’ operation is familiar for Illustrator or InkScape people since they have this in the ‘pathfinder‘ palette.

This operation happens on the geometry of the 2 regions. How this is done exactly is outside the scope of the blog post. The path of the end result might look completely different from the original paths!

Other issues

Canvas recently gained support for different winding rules. With the existing path proposal, there really is no intuitive way to implement this. Therefore, the path syntax should have support for winding.

A new path proposal

My proposal is to split up the exisiting functionality in 2 parts:

  1. An object that contains the path segments
  2. An object that contains the region

The reason for 2 object is shown above: after aggregating a path, stroking or filling with text you can’t really add path segments to it any more because the winding is already resolved and path segments might have been removed or replaced.

The path object

This object  looks mostly like the existing Canvas path object, except that it doesn’t contain the methods for stroking, text and path addition.

The IDL looks as follows:

 
  [Constructor,
  Constructor(Path), // makes a copy of the path
  Constructor(DOMString) // takes SVG path syntax
class Path implements CanvasPathMethods {
}

02-01-2013: The constructor of Path was updated so it follows the correct WebIDL style since.

The Shape object

This object will style Path and aggregate other Shapes. The name of this object is still undecided so I will go with ‘Shape’ for now.

The IDL looks as follows:

 
  [Constructor,
  Constructor(Path, CanvasWindingRule = "nonzero"),
  Constructor(Path, CanvasDrawingStyles, SVGMatrix?), // strokes a path
  Constructor(DomString text, CanvasDrawingSTyles, SVGMatrix?,
              unrestricted double, unrestricted double,
              optional unrestricted double)]
interface Shape {
    Shape transform(matrix); // returns a transformed path
    Shape stroke(CanvasDrawingStyles); // returns a stroked path
    Shape add(Shape); // returns a path that is the union of the 2 paths

    boolean isPointInPath(unrestricted double x, unrestricted double y);
};

The constructor of Shape behaves like the ‘fill’, ‘stroke’ or ‘fillText’ operator.
‘transform’ will apply the matrix to the region.
‘stroke’ will stroke the region. It’s important to note that this will happen on the geometry of the region. For instance, an open path will be closed and certain path segments might be optimized away.
‘add’ will combine a region with another and return the result.
‘isPointInPath’ will tell you if a certain point falls in the region.

Example 1: region of a stroked circle

 
  var f = new Path();
  f.arc(100,100,100,0,Math.PI*2, false);
  var s = new Shape(f);
  s.add(s.stroke());

Example 2: get outline of text

 
var s = new Shape("some text").stroke(...);

Example 3: get stroked circle of the region of two overlapping circles

 
  var f = new Path();
  f.arc(100,100,100,0,Math.PI*2, false);
  var s = new Shape(f);
  s = s.add(s.Transform([1, 0, 0, 1, 75, 0]));
  s = s.stroke(...);

The region of shape ‘s’ will look like:

path_logic_4

Conclusion

I hope I was able to explain the issues with the current path proposal in Canvas. The current definition is really close and with a couple small changes, this will be a very useful feature.

As always, I  would love to get feedback. People on the mozilla and webKit team are gearing up to implement Paths so if you have any ideas for improvement, this would be the time!

4 Comments

  1. March 08, 2013 at 12:32 am, Abhijeet Kandalkar said:

    The idea you have suggested will really improve interactivity of HTML5 Canvas.When are you planning to implement this changes in webkit main trunk?

    • March 08, 2013 at 9:08 am, Rik Cabanier said:

      Things are moving (slowly as always) on that front.
      WebKit and Mozilla are in the process of adding paths to the canvas API. Once those land and are stable, we can start implementing shapes and regions.

  2. March 20, 2013 at 10:33 pm, Abhijeet Kandalkar said:

    Hi RIK,
    All APIS mentioned in Path.h(http://www.w3.org/html/wg/drafts/2dcontext/html5_canvas/#path-objects) like
    addPath()
    addPathByStrokingPath()
    addText(text,style,transformation,x,y,width)
    addPathByStrokingText(text,style,transformation,x,y,width)
    addText(text,style,transformation,path,width)
    addPathByStrokingText(text,style,transformation,path,width)

    are not implemented in latest webkit code base.Does Adobe has any plan to implement this features in webkit instead of depending on webkit community to do that.

    • March 21, 2013 at 8:34 am, Rik Cabanier said:

      Unclear at this point.
      I’d love for us to implement it but we’re committed to other features. (We are doing a LOT of stuff!)

      Once we have some time and this is not implemented yet, we will work on it.