SVG paths for canvas
Written by Richard Heyes, RGraph author, on 3rd September 2015- Introduction to the path function
- Path commands
- String-based paths
- Get the code
- Example usage
- Changelog
- Changes available from RGraph 5.2
Introduction to the path function
canvas paths are verbose. Unnecessarily so in my opinion. The addition of svg paths to the forthcoming Path2D object goes a long way to resolving this and providing a better solution for creating and working with canvas paths.
But I'm rather impatient, and support for the object is still not widespread enough for it to be in common use. So there must be a better way to creating and using canvas paths...
Enter the RGraph.path function. This is a function that takes the context and the desired path as arguments and calls the relevant functions for you. The path is an array of letters (and numbers) - each representing an action that should be taken. So it's similar to svg paths, but there are a few more options that make it more versatile.
So what does it look like to use? Like the example below:
context.path(['b', 'm', 5, 5, 'l', 45, 45, 'l', 5, 45, 'c', 'f', 'red']);
That would translate to the following sequence of function calls:
context.beginPath(); context.moveTo(5, 5); context.lineTo(45, 45); context.lineTo(5, 45); context.closePath()' context.fillStyle = 'red'; context.fill();
In terms of character size, it's approximately 55% smaller. If you don't compress your code (you really should) that's very significant and larger paths will potentially give you bigger savings. Even when you take compression and minification into account the savings may well be worth it.
Note:
You can further reduce the size of your paths by using
string-based paths.
Path commands
So here's a list of the available path commands and what they represent. The RGraph path function is constantly evolving so if you want to update in the future it shouldn't be too difficult to extract it. The function in the RGraph core common library is a function expression (NOT a statement), so the declaration may at first glance be unfamiliar, but I've tried to make the code that you can download here similar so taking it out of an updated RGraph library and replacing your code should be quite straightforward.
- b context.beginPath() No extra arguments
- c context.closePath() No extra arguments
- m context.moveTo() The next two numbers are used as the arguments
- l context.lineTo() The next two numbers are used as the arguments
- s context.stroke() The next string (which can also be null) is used to first set the strokeStyle color
- f context.fill() The next string (which can also be null) is used to first set the fillStyle color
- qc context.quadraticCurveTo() The next four numbers are used as the arguments
- bc context.bezierCurveTo() The next six numbers are used as the arguments
- r context.rect() The next four numbers are used as the arguments
- e context.ellipse() The next eight numbers are used as the arguments
- a context.arc() The next six numbers are used as the arguments
- at context.arcTo() The next five numbers are used as the arguments
- lw context.lineWidth The next number is used as the lineWidth to set
- lj context.lineJoin The next string is used as the lineJoin to set
- lc context.lineCap The next string is used as the lineCap to set
- sc context.shadowColor The next string is used as the shadowColor to set
- sb context.shadowBlur The next number is used as the shadowBlur to set
- sx context.shadowOffsetX The next number is used as the shadowOffsetX to set
- sy context.shadowOffsetY The next number is used as the shadowOffsetY to set
- fs context.fillStyle The next string is set as the fillStyle (the fill function is NOT called)
- ss context.strokeStyle The next string is set as the strokeStyle (the stroke function is NOT called)
- fr context.fillRect() Technically fillRect is not a path function, but it's here regardless. The next four numbers are used as the arguments
- sr context.strokeRect() Technically strokeRect is not a path function, but it's here regardless. The next four numbers are used as the arguments
- cl context.clip() No extra arguments
- sa context.save() No extra arguments
- rs context.restore() No extra arguments
- tr context.translate() The next two numbers are used as the arguments
- sc context.scale() The next two numbers are used as the arguments
- ro context.rotate() The next number is used as the argument
- tf context.transform() The next six numbers are used as the arguments
- stf context.settransform() The next six numbers are used as the arguments
- cr context.clearRect() The next four numbers are used as the arguments
- ld context.setLineDash() The next array literal is used as the dash settings (example below)
- ldo context.lineDashOffset The next number is used as the argument
- fo context.font The next string is used as the argument
- ft context.fillText() The next string and two numbers are used as the arguments
- st context.strokeText() The next string and two numbers are used as the arguments
- ta context.textAlign The next string is used as the argument
- tbl context.textBaseline The next string is used as the argument
- ga context.globalAlpha The next number is used as the argument
- gco context.globalCompositeOperation The next string is used as the argument
String based paths
If you're not using animation with the path function you can use string-based paths instead of array based paths. These are far easier to write and are significantly less verbose too. Obviously, the strings need to be parsed so there's a tiny performance hit with them. An example of a string-based path is:
context.path('b r 5 5 50 50 s black f red');
Note:
If you want to use the lineDashOffset option - ldo - you need to use
an array-based path. But you could do this to set the lineDashOffset before
you specify your entire path:
context.path(['ldo', [5,5]]); context.path('b r 5 5 56 56 s red');
Get the code
The code for the path function is here. You can include it in your page with a script tag:
<script src="rgraph.path.js"></script>
Example usage
A rectangle example
// Using the path function context.path(['b', 'r', 5, 5, 90, 90, 'f', 'red']); // Using the canvas API context.beginPath(); context.rect(5,5,90,90); context.fillStyle = 'red'; context.fill();
An arc example
// Using the path function context.path(['b', 'a', 50, 50, 50, 0, 3.14, false, 'f', 'red']); // Using the canvas API context.beginPath(); context.arc(50,50,50,0, Math.PI false); context.fillStyle = 'red'; context.fill();
A bezierCurveTo example
// Using the path function context.path(['b', 'm', 5, 100, 'bc', 5, 0, 100, 0, 100, 100, 's', 'red']); // Using the canvas API context.beginPath(); context.moveTo(5, 100); context.bezierCurveTo(5, 0, 100, 0, 100, 100); context.strokeStyle = 'red'; context.stroke();
An arcTo example
// Using the path function context.path(['b', 'm', 5, 100, 'at', 50, 0, 95, 100, 50, 's', 'red']); // Using the canvas API context.beginPath(); context.moveTo(5, 100); context.arcTo(50, 0, 95, 100, 50); context.strokeStyle = 'red'; context.stroke();
A clip example
// Using the path function context.path(['sa', 'b', 'r', 0, 0, 50, 50, 'cl', 'b', 'r', 5, 5, 590, 240, 'f', 'red', 'rs']); // Using the canvas API context.save(); context.beginPath(); context.rect(0, 0, 50, 50); context.clip(); context.beginPath(); context.rect(5, 5, 590, 240); context.fillStyle = 'red'; context.fill(); context.restore();
A setLineDash example
// Using the path function context.path(['ld', [2,6], 'ldo', 4, 'b', 'r', 5, 5, 590, 240, 's', 'red']); // Using the canvas API context.setLineDash([2,6]); context.lineDashOffset = 4; context.beginPath(); context.rect(5, 5, 590, 240); context.strokeStyle = 'red'; context.stroke();
A globalAlpha example
// Using the path function context.path(['ga', 0.25, 'b', 'r', 5, 5, 590, 240, 'f', 'red']); // Using the canvas API context.globalAlpha = 0.25; context.beginPath(); context.rect(5, 5, 590, 240); context.fillStyle = 'red'; context.fill();
Changelog
10th November 2015
- Added the ability to use string-based paths back in albeit with a faster parser. Because they're just so convenient. If you're using the path function in an animation though I would recommend you use array-based paths as there's then no need to parse the string. Parsing the string will be fine if you're not using animation though.
- Removed the ability to use string based paths. Now your paths must be arrays - like the examples above.
15th September 2015
-
Added the ability to use % style substitution like this:
context.path( 'b r 0 0 % % f red', 50, 25 + 25 );
Similar to prepared SQL queries - but a basic substitution. The values are converted to strings - whatever they might end up as.
7th September 2015
- Removed the fu option
- Removed the ct option
-
Added these options:
- e context.ellipse()
- tr context.transform()
- sc context.scale()
- ro context.rotate()
- tf context.transform()
- stf context.setTransform()
- cr context.clearRect()
- ld context.setLineDash()
- ldo context.lineDashOffset
- fo context.font
- ft context.fillText()
- st context.strokeText()
- ta context.textAlign
- tbl context.textBaseline
- ga context.globalAlpha
- gco context.globalCompositeOperation
Changes available from RGraph 5.2
obj.path({ path: 'b r % % % % f %', args: [10,10,100,100,'red'] });
And also this - an even shorter syntax:
obj.path('b r % % % % f %', 10, 10, 100, 100, 'red');