Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stroke path to Path2D interface per html canvas spec #350

Merged
merged 10 commits into from
Sep 6, 2024
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
Loading