Skip to content

Commit

Permalink
Merge pull request #383 from danyeaw/canvas-stack
Browse files Browse the repository at this point in the history
Convert Canvas Widget to Use a Drawing Stack
  • Loading branch information
danyeaw committed Jun 12, 2018
2 parents 3358a82 + 368921e commit cf9cc63
Show file tree
Hide file tree
Showing 8 changed files with 1,695 additions and 624 deletions.
55 changes: 54 additions & 1 deletion docs/reference/api/widgets/canvas.rst
Expand Up @@ -15,16 +15,69 @@ The canvas is used for creating a blank widget that you can draw on.
Usage
-----

Simple usage to draw a black circle on the screen using the arc drawing object:

.. code-block:: Python
import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.fill() as fill:
fill.arc(50, 50, 15)
More advanced usage for something like a vector drawing app where you would
want to modify the parameters of the drawing objects. Here we draw a black
circle and black rectangle. We then change the size of the circle, move the
rectangle, and finally delete the rectangle.

.. code-block:: Python
import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.fill() as fill:
arc1 = fill.arc(x=50, y=50, radius=15)
rect1 = fill.rect(x=50, y=50, width=15, height=15)
arc1.x, arc1.y, arc1.radius = (25, 25, 5)
rect1.x = 75
fill.remove(rect1)
Use of drawing contexts, for example with a platformer game. Here you would
want to modify the x/y coordinate of a drawing context that draws each
character on the canvas. First, we create a hero context. Next, we create a
black circle and a black outlined rectangle for the hero's body. Finally, we
move the hero by 10 on the x-axis.

.. code-block:: Python
import toga
canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[canvas])
with canvas.context() as hero:
with hero.fill() as body:
body.arc(50, 50, 15)
with hero.stroke() as outline:
outline.rect(50, 50, 15, 15)
hero.translate(10, 0)
canvas = toga.Canvas()
Reference
---------

Main Interface
^^^^^^^^^^^^^^

.. autoclass:: toga.widgets.canvas.Canvas
:members:
:undoc-members:
:inherited-members:
:exclude-members: canvas, add_draw_obj

Lower-Level Classes
^^^^^^^^^^^^^^^^^^^

.. automodule:: toga.widgets.canvas
:members:
:exclude-members: Canvas, add_draw_obj
35 changes: 18 additions & 17 deletions docs/tutorial/tutorial-4.rst
Expand Up @@ -12,33 +12,37 @@ One of the main capabilities needed to create many types of GUI applications is
the ability to draw and manipulate lines, shapes, text, and other graphics. To
do this in Toga, we use the Canvas Widget.

Utilizing the Canvas is easy as determining the drawing operations you want to
perform, placing them in a function, and then creating a new Canvas while
passing the function to the on_draw parameter.
Utilizing the Canvas is as easy as determining the drawing operations you want to
perform and then creating a new Canvas. All drawing objects that are created
with one of the drawing operations are returned so that they can be modified or
removed.

1. We first define the drawing operations we want to perform in a new function::

def draw_tiberius(self, canvas, context):
self.canvas.set_context(context)
self.fill_head()
def draw_eyes(self):
with self.canvas.fill(color=WHITE) as eye_whites:
eye_whites.arc(58, 92, 15)
eye_whites.arc(88, 92, 15, math.pi, 3 * math.pi)

The function you want to draw with should also be defined to include canvas and
context arguments, and make use of the set_context method.
Notice that we also created and used a new fill context called eye_whites. The
"with" keyword that is used for the fill operation causes everything draw using
the context to be filled with a color. In this example we filled two circular
eyes with the color white.

2. Next we create a new Canvas, and pass in the draw_tiberius method::
2. Next we create a new Canvas::

self.canvas = toga.Canvas(on_draw=self.draw_tiberius)
self.canvas = toga.Canvas(style=Pack(flex=1))

That's all there is to! In this example we also add our canvas to the MainWindow
through use of the Box Widget::

box = toga.Box(children=[self.canvas])
self.main_window.content = box

You'll also notice in the full example below that some of the drawing operations
use the "with" keyword to utilize context managers including closed_path, fill,
and stroke. This reduces the repetition of commands while utilizing these basic
drawing capabilities.
You'll also notice in the full example below that the drawing operations utilize
contexts in addition to fill including context, closed_path, and stroke. This
reduces the repetition of commands as well as groups drawing operations so that
they can be modified together.

.. image:: screenshots/tutorial-4.png

Expand All @@ -47,7 +51,4 @@ Here's the source code
.. literalinclude:: /../examples/tutorial4/tutorial/app.py
:language: python

Although not shown in this tutorial, it is also possible to directly do simple
draw operations without passing in the on_draw callable function.

In this example, we see a new Toga widget - :class:`.Canvas`.
7 changes: 4 additions & 3 deletions examples/canvas/canvas/app.py
Expand Up @@ -15,9 +15,10 @@ def startup(self):
# Show the main window
self.main_window.show()

with canvas.stroke():
with canvas.closed_path(50, 50):
canvas.line_to(100, 100)
with canvas.stroke() as stroker:
with stroker.closed_path(50, 50) as closer:
closer.line_to(100, 100)
closer.line_to(100, 50)


def main():
Expand Down
139 changes: 64 additions & 75 deletions examples/tutorial4/tutorial/app.py
@@ -1,17 +1,20 @@
import math

import toga
from toga.color import WHITE, rgb
from toga.font import SANS_SERIF
from toga.style import Pack


class StartApp(toga.App):
def startup(self):
# Window class
# Main window of the application with title and size
# Main window of the application with title and size
self.main_window = toga.MainWindow(title=self.name, size=(148, 200))

self.canvas = toga.Canvas(on_draw=self.draw_tiberius, style=Pack(flex=1))
# Create canvas and draw tiberius on it
self.canvas = toga.Canvas(style=Pack(flex=1))
box = toga.Box(children=[self.canvas])
self.draw_tiberius()

# Add the content on the main window
self.main_window.content = box
Expand All @@ -20,91 +23,77 @@ def startup(self):
self.main_window.show()

def fill_head(self):
with self.canvas.fill():
self.canvas.fill_style('rgba(149.0, 119, 73, 1)')
self.canvas.move_to(112, 103)
self.canvas.line_to(112, 113)
self.canvas.ellipse(73, 114, 39, 47, 0, 0, math.pi)
self.canvas.line_to(35, 84)
self.canvas.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
self.canvas.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)
with self.canvas.fill(color=rgb(149, 119, 73)) as head_filler:
head_filler.move_to(112, 103)
head_filler.line_to(112, 113)
head_filler.ellipse(73, 114, 39, 47, 0, 0, math.pi)
head_filler.line_to(35, 84)
head_filler.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
head_filler.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)

def stroke_head(self):
with self.canvas.stroke():
with self.canvas.closed_path(112, 103):
self.canvas.line_width(4.0)
self.canvas.stroke_style()
self.canvas.line_to(112, 113)
self.canvas.ellipse(73, 114, 39, 47, 0, 0, math.pi)
self.canvas.line_to(35, 84)
self.canvas.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
self.canvas.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)
with self.canvas.stroke(line_width=4.0) as head_stroker:
with head_stroker.closed_path(112, 103) as closed_head:
closed_head.line_to(112, 113)
closed_head.ellipse(73, 114, 39, 47, 0, 0, math.pi)
closed_head.line_to(35, 84)
closed_head.arc(65, 84, 30, math.pi, 3 * math.pi / 2)
closed_head.arc(82, 84, 30, 3 * math.pi / 2, 2 * math.pi)

def draw_eyes(self):
self.canvas.line_width(4.0)
with self.canvas.fill():
self.canvas.fill_style('rgba(255, 255, 255, 1)')
self.canvas.arc(58, 92, 15)
self.canvas.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.stroke():
self.canvas.stroke_style('rgba(0, 0, 0, 1)')
self.canvas.arc(58, 92, 15)
self.canvas.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.fill():
self.canvas.arc(58, 97, 3)
self.canvas.arc(88, 97, 3)
with self.canvas.fill(color=WHITE) as eye_whites:
eye_whites.arc(58, 92, 15)
eye_whites.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.stroke(line_width=4.0) as eye_outline:
eye_outline.arc(58, 92, 15)
eye_outline.arc(88, 92, 15, math.pi, 3 * math.pi)
with self.canvas.fill() as eye_pupils:
eye_pupils.arc(58, 97, 3)
eye_pupils.arc(88, 97, 3)

def draw_horns(self):
# Right horn
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(112, 99)
self.canvas.quadratic_curve_to(145, 65, 139, 36)
self.canvas.quadratic_curve_to(130, 60, 109, 75)
with self.canvas.stroke():
self.canvas.stroke_style()
self.canvas.move_to(112, 99)
self.canvas.quadratic_curve_to(145, 65, 139, 36)
self.canvas.quadratic_curve_to(130, 60, 109, 75)
# Left horn
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(35, 99)
self.canvas.quadratic_curve_to(2, 65, 6, 36)
self.canvas.quadratic_curve_to(17, 60, 37, 75)
with self.canvas.stroke():
self.canvas.stroke_style()
self.canvas.move_to(35, 99)
self.canvas.quadratic_curve_to(2, 65, 6, 36)
self.canvas.quadratic_curve_to(17, 60, 37, 75)
with self.canvas.context() as r_horn:
with r_horn.fill(color=rgb(212, 212, 212)) as r_horn_filler:
r_horn_filler.move_to(112, 99)
r_horn_filler.quadratic_curve_to(145, 65, 139, 36)
r_horn_filler.quadratic_curve_to(130, 60, 109, 75)
with r_horn.stroke(line_width=4.0) as r_horn_stroker:
r_horn_stroker.move_to(112, 99)
r_horn_stroker.quadratic_curve_to(145, 65, 139, 36)
r_horn_stroker.quadratic_curve_to(130, 60, 109, 75)
with self.canvas.context() as l_horn:
with l_horn.fill(color=rgb(212, 212, 212)) as l_horn_filler:
l_horn_filler.move_to(35, 99)
l_horn_filler.quadratic_curve_to(2, 65, 6, 36)
l_horn_filler.quadratic_curve_to(17, 60, 37, 75)
with l_horn.stroke(line_width=4.0) as l_horn_stroker:
l_horn_stroker.move_to(35, 99)
l_horn_stroker.quadratic_curve_to(2, 65, 6, 36)
l_horn_stroker.quadratic_curve_to(17, 60, 37, 75)

def draw_nostrils(self):
with self.canvas.fill():
self.canvas.fill_style('rgba(212, 212, 212, 1)')
self.canvas.move_to(45, 145)
self.canvas.bezier_curve_to(51, 123, 96, 123, 102, 145)
self.canvas.ellipse(73, 114, 39, 47, 0, math.pi / 4, 3 * math.pi / 4)
with self.canvas.fill():
self.canvas.fill_style()
self.canvas.arc(63, 140, 3)
self.canvas.arc(83, 140, 3)
with self.canvas.stroke():
self.canvas.move_to(45, 145)
self.canvas.bezier_curve_to(51, 123, 96, 123, 102, 145)
with self.canvas.fill(color=rgb(212, 212, 212)) as nose_filler:
nose_filler.move_to(45, 145)
nose_filler.bezier_curve_to(51, 123, 96, 123, 102, 145)
nose_filler.ellipse(73, 114, 39, 47, 0, math.pi / 4, 3 * math.pi / 4)
with self.canvas.fill() as nostril_filler:
nostril_filler.arc(63, 140, 3)
nostril_filler.arc(83, 140, 3)
with self.canvas.stroke(line_width=4.0) as nose_stroker:
nose_stroker.move_to(45, 145)
nose_stroker.bezier_curve_to(51, 123, 96, 123, 102, 145)

def draw_text(self):
x = 32
y = 185
font = toga.Font(family='sans-serif', size=20)
font = toga.Font(family=SANS_SERIF, size=20)
width, height = font.measure('Tiberius', tight=True)
with self.canvas.stroke():
self.canvas.rect(x - 10, y - height + 2, width, height + 2)
with self.canvas.fill():
self.canvas.fill_style('rgba(149.0, 119, 73, 1)')
self.canvas.write_text('Tiberius', x, y, font)

def draw_tiberius(self, canvas, context):
self.canvas.set_context(context)
with self.canvas.stroke(line_width=4.0) as rect_stroker:
rect_stroker.rect(x - 10, y - height + 2, width, height + 2)
with self.canvas.fill(color=rgb(149, 119, 73)) as text_filler:
text_filler.write_text('Tiberius', x, y, font)

def draw_tiberius(self):
self.fill_head()
self.draw_eyes()
self.draw_horns()
Expand Down

0 comments on commit cf9cc63

Please sign in to comment.