diff --git a/docs/drawing_paths.rst b/docs/drawing_paths.rst index 7e6803c..4471a10 100644 --- a/docs/drawing_paths.rst +++ b/docs/drawing_paths.rst @@ -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. @@ -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 diff --git a/ipycanvas/canvas.py b/ipycanvas/canvas.py index 76a0fe9..98919ed 100644 --- a/ipycanvas/canvas.py +++ b/ipycanvas/canvas.py @@ -60,6 +60,7 @@ "beginPath", "closePath", "stroke", + "strokePath", "fillPath", "fill", "moveTo", @@ -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. diff --git a/src/widget.ts b/src/widget.ts index 23083d9..1ef52af 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -85,6 +85,7 @@ const COMMANDS = [ 'beginPath', 'closePath', 'stroke', + 'strokePath', 'fillPath', 'fill', 'moveTo', @@ -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; @@ -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); diff --git a/ui-tests/tests/ipycanvas.test.ts-snapshots/ipycanvas-ipynb-cell-37-linux.png b/ui-tests/tests/ipycanvas.test.ts-snapshots/ipycanvas-ipynb-cell-37-linux.png new file mode 100644 index 0000000..2b2033c Binary files /dev/null and b/ui-tests/tests/ipycanvas.test.ts-snapshots/ipycanvas-ipynb-cell-37-linux.png differ diff --git a/ui-tests/tests/notebooks/ipycanvas.ipynb b/ui-tests/tests/notebooks/ipycanvas.ipynb index c4e955e..8751fe5 100644 --- a/ui-tests/tests/notebooks/ipycanvas.ipynb +++ b/ui-tests/tests/notebooks/ipycanvas.ipynb @@ -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": {