Skip to content

Commit

Permalink
Add stroke path to Path2D interface per html canvas spec (#350)
Browse files Browse the repository at this point in the history
* Add stroke path to Path2D interface per html canvas spec (#1)

* add stroke(path: Path2D) form to canvas

---------

Co-authored-by: Chris Lee-Messer <chris@lee-messer.net>

* add visual test of canvas.stroke(path)

* Update Playwright Snapshots

* black ipycanvas/canvas.py

* fix bug falling through to fillPath-add missing break

* initial documentation stroke(Path2D)

* update initial stroke(path) uitest to replicate SVG fig

* black format code block in drawing_paths.rst

* Update Playwright Snapshots

---------

Co-authored-by: Chris Lee-Messer <chris@lee-messer.net>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 6, 2024
1 parent 5ade708 commit 37cf1ea
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 6 deletions.
9 changes: 7 additions & 2 deletions docs/drawing_paths.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ There are two ways for creating and drawing a path in ipycanvas.
Using Path2D
------------

You can define a Path2D given an SVG path. Note that once the path is created, it is read only, you cannot dynamically change the path value.
Using the Path2D class is very useful and efficient when you want to reuse the same path multiple times.
You can define a Path2D given an SVG path. Note that once the path is created, it is read only, you cannot dynamically change the path value. This means they do not (yet) support Path2D methods like Path2D.move_to, draw_line, etc.
Using the Path2D class is very useful and efficient when you want to reuse the same path multiple times. Path2D objects may be stroked or filled.

See https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths for documentation about SVG paths.

Expand Down Expand Up @@ -37,6 +37,11 @@ See https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths for document
canvas.fill_style = "blue"
canvas.fill(path4)
# draw a sinusoidal curve using quadratic bezier curves
path5 = Path2D("M 10 150 Q 52.5 10 95 150 T 180 150")
canvas.line_width = 2.5
canvas.stroke_style = "black"
canvas.stroke(path5)
canvas
.. image:: images/path2d.png
Expand Down
19 changes: 16 additions & 3 deletions ipycanvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"beginPath",
"closePath",
"stroke",
"strokePath",
"fillPath",
"fill",
"moveTo",
Expand Down Expand Up @@ -1247,9 +1248,21 @@ def close_path(self):
"""
self._canvas_manager.send_draw_command(self, COMMANDS["closePath"])

def stroke(self):
"""Stroke (outlines) the current path with the current ``stroke_style``."""
self._canvas_manager.send_draw_command(self, COMMANDS["stroke"])
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/stroke
# stroke(), stroke(path)
def stroke(self, path2d: Path2D = None):
"""Stroke (outlines) the current path with the current ``stroke_style``.
If @path2d is passed, that Path2D object will be rendered with the
current ``stroke_style``"""

if isinstance(path2d, Path2D):
self._canvas_manager.send_draw_command(
self,
COMMANDS["strokePath"],
[widget_serialization["to_json"](path2d, None)],
)
else:
self._canvas_manager.send_draw_command(self, COMMANDS["stroke"])

def fill(self, rule_or_path="nonzero"):
"""Fill the current path with the current ``fill_style`` and given the rule, or fill the given Path2D.
Expand Down
12 changes: 11 additions & 1 deletion src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const COMMANDS = [
'beginPath',
'closePath',
'stroke',
'strokePath',
'fillPath',
'fill',
'moveTo',
Expand Down Expand Up @@ -279,6 +280,9 @@ export class CanvasManagerModel extends WidgetModel {
case 'strokePolygon':
this.currentCanvas.strokePolygon(args, buffers);
break;
case 'strokePath':
await this.currentCanvas.strokePath(args, buffers);
break;
case 'fillPath':
await this.currentCanvas.fillPath(args, buffers);
break;
Expand Down Expand Up @@ -1056,9 +1060,15 @@ export class CanvasModel extends DOMWidgetModel {
this.ctx.stroke();
}

async fillPath(args: any[], buffers: any) {
async strokePath(args: any[], buffers: any) {
const [serializedPath] = args;
const path = await unpack_models(serializedPath, this.widget_manager);

this.ctx.stroke(path.value);
}

async fillPath(args: any[], buffers: any) {
const [serializedPath] = args;
const path = await unpack_models(serializedPath, this.widget_manager);

this.ctx.fill(path.value);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions ui-tests/tests/notebooks/ipycanvas.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,41 @@
"canvas.fill_rect(0, 0, canvas.width, canvas.height)\n",
"canvas"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7be13b55",
"metadata": {},
"outputs": [],
"source": [
"# test canvas.stroke(path: Path2D)\n",
"from ipycanvas import Path2D, Canvas\n",
"\n",
"canvas = Canvas(width=320, height=320)\n",
"\n",
"canvas.fill_style = \"green\"\n",
"canvas.stroke_style = \"black\"\n",
"canvas.line_width = 2\n",
"\n",
"# This more complicated path is from\n",
"# https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths\n",
"# under the Arcs section. There is no equivalent to fill-opacity:\"0.5\"\n",
"# instead use global_alpha for the fill.\n",
"p = Path2D(\"\"\"\n",
" M 10 315\n",
" L 110 215\n",
" A 30 50 0 0 1 162.55 162.45\n",
" L 172.55 152.45\n",
" A 30 50 -45 0 1 215.1 109.9\n",
" L 315 10\"\"\"\n",
")\n",
"canvas.global_alpha = 0.5\n",
"canvas.fill(p)\n",
"canvas.global_alpha = 1.0\n",
"canvas.stroke(p)\n",
"canvas"
]
}
],
"metadata": {
Expand Down

0 comments on commit 37cf1ea

Please sign in to comment.