diff --git a/docs/source/index.rst b/docs/source/index.rst index 9a3df419c..d2fca2992 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,6 +10,7 @@ Enable Documentation kiva/compiled_path.rst kiva/state.rst kiva/quickref.rst + kiva_tutorial/index.rst enable/overview.rst enable/constraints_layout.rst diff --git a/docs/source/kiva_tutorial/images/step_1.png b/docs/source/kiva_tutorial/images/step_1.png new file mode 100644 index 000000000..ad4c909b0 Binary files /dev/null and b/docs/source/kiva_tutorial/images/step_1.png differ diff --git a/docs/source/kiva_tutorial/images/step_2.png b/docs/source/kiva_tutorial/images/step_2.png new file mode 100644 index 000000000..832e6c26a Binary files /dev/null and b/docs/source/kiva_tutorial/images/step_2.png differ diff --git a/docs/source/kiva_tutorial/images/step_3.png b/docs/source/kiva_tutorial/images/step_3.png new file mode 100644 index 000000000..7897d1cee Binary files /dev/null and b/docs/source/kiva_tutorial/images/step_3.png differ diff --git a/docs/source/kiva_tutorial/images/step_45.png b/docs/source/kiva_tutorial/images/step_45.png new file mode 100644 index 000000000..ae147dddc Binary files /dev/null and b/docs/source/kiva_tutorial/images/step_45.png differ diff --git a/docs/source/kiva_tutorial/images/step_6.png b/docs/source/kiva_tutorial/images/step_6.png new file mode 100644 index 000000000..db2d2bd70 Binary files /dev/null and b/docs/source/kiva_tutorial/images/step_6.png differ diff --git a/docs/source/kiva_tutorial/images/tutorial.png b/docs/source/kiva_tutorial/images/tutorial.png new file mode 100644 index 000000000..e5e5b4142 Binary files /dev/null and b/docs/source/kiva_tutorial/images/tutorial.png differ diff --git a/docs/source/kiva_tutorial/images/tutorial_advanced.png b/docs/source/kiva_tutorial/images/tutorial_advanced.png new file mode 100644 index 000000000..bed04d8dd Binary files /dev/null and b/docs/source/kiva_tutorial/images/tutorial_advanced.png differ diff --git a/docs/source/kiva_tutorial/index.rst b/docs/source/kiva_tutorial/index.rst new file mode 100644 index 000000000..c3bf093c4 --- /dev/null +++ b/docs/source/kiva_tutorial/index.rst @@ -0,0 +1,9 @@ +Kiva Tutorial +============= + +.. toctree:: + :maxdepth: 3 + + tutorial + tutorial_code + tutorial_advanced_code diff --git a/docs/source/kiva_tutorial/tutorial.py b/docs/source/kiva_tutorial/tutorial.py new file mode 100644 index 000000000..dfeb3d195 --- /dev/null +++ b/docs/source/kiva_tutorial/tutorial.py @@ -0,0 +1,137 @@ +from math import tau + +import numpy as np + +from kiva.api import CAP_ROUND, CIRCLE_MARKER, Font, STROKE +from kiva.image import GraphicsContext, CompiledPath + +gc = GraphicsContext((600, 300)) + +# step 1) draw wires +gc.rect(50, 50, 500, 100) +gc.rect(200, 150, 200, 50) +gc.rect(200, 200, 200, 50) +gc.stroke_path() +gc.save("images/step_1.png") + +# step 2) draw dots for wire connections +points = np.array([ + [200., 150.], + [200., 200.], + [400., 150.], + [400., 200.], + [550., 130.] +]) +gc.draw_marker_at_points(points, 4.0, CIRCLE_MARKER) +gc.save("images/step_2.png") + +# step 3) Ammeter and Voltmeter +font = Font('Times New Roman', size=20) +gc.set_font(font) +with gc: # Voltmeter + gc.translate_ctm(50, 100) + gc.set_fill_color((.9, .9, 0.5, 1.0)) + gc.set_line_width(3) + gc.arc(0, 0, 20, 0.0, tau) + gc.draw_path() + + gc.set_fill_color((0., 0., 0., 1.0)) + x, y, w, h = gc.get_text_extent('A') + gc.show_text_at_point('A', -w/2, -h/2) + +with gc: # Ammeter + gc.translate_ctm(300, 250) + gc.set_fill_color((0.5, .9, 0.5, 1.0)) + gc.set_line_width(3) + gc.arc(0, 0, 20, 0.0, tau) + gc.draw_path() + + gc.set_fill_color((0., 0., 0., 1.0)) + x, y, w, h = gc.get_text_extent('V') + gc.show_text_at_point('V', -w/2, -h/2) +gc.save("images/step_3.png") + +# step 5) clear some space for the resistors +clear_resistor_path = CompiledPath() +clear_resistor_path.move_to(0,0) +clear_resistor_path.line_to(80, 0) + +resistor_locations = [ + (150, 50), + (350, 50), + (260, 150), + (260, 200) +] +with gc: + gc.set_stroke_color((1., 1., 1., 1.)) + gc.set_line_width(2) + gc.draw_path_at_points(resistor_locations, clear_resistor_path, STROKE) + +#step 4) resistors +resistor_path = CompiledPath() +resistor_path.move_to(0,0) +resistor_path_points = [(i*10+5, 10*(-1)**i) for i in range(8)] +for x, y in resistor_path_points: + resistor_path.line_to(x,y) +resistor_path.line_to(80, 0) +gc.draw_path_at_points(resistor_locations, resistor_path, STROKE) +gc.save("images/step_45.png") + +# step 6) switch +# white out the wire +with gc: + gc.translate_ctm(550, 130) + # wire connection dot markers have size 4 and we don't want to clear that + gc.move_to(0, -4) + gc.set_stroke_color((1., 1., 1., 1.)) + gc.set_line_width(2) + gc.line_to(0, -30) + gc.stroke_path() +# draw the switch +with gc: + # move to the connected side of the switch and rotate coordinates + # to the angle we want to draw the switch + gc.translate_ctm(550, 100) + gc.rotate_ctm(tau/6) + gc.move_to(0, 0) + gc.line_to(30, 0) + gc.stroke_path() +gc.save("images/step_6.png") + +# step 7) battery +with gc: + gc.translate_ctm(550, 90) + # wire connection dot markers have size 4 and we don't want to clear that + gc.move_to(0, 0) + gc.set_stroke_color((1., 1., 1., 1.)) + gc.set_line_width(2) + gc.line_to(0, -30) + gc.stroke_path() + +with gc: + gc.translate_ctm(550, 90) + gc.move_to(0, 0) + thin_starts = [ + (-20, 0), + (-20, -18) + ] + thin_ends = [ + (20,0), + (20, -18), + ] + gc.line_set(thin_starts, thin_ends) + gc.stroke_path() + thick_starts = [ + (-8, -10), + (-8, -28) + ] + thick_ends = [ + (8, -10), + (8, -28), + ] + gc.set_line_width(8) + gc.set_line_cap(CAP_ROUND) + gc.line_set(thick_starts, thick_ends) + gc.stroke_path() + +gc.save("images/tutorial.png") diff --git a/docs/source/kiva_tutorial/tutorial.rst b/docs/source/kiva_tutorial/tutorial.rst new file mode 100644 index 000000000..87c45c2fc --- /dev/null +++ b/docs/source/kiva_tutorial/tutorial.rst @@ -0,0 +1,171 @@ +Introduction +============ + +This tutorial is intended to help you get up and running working with Kiva. +Kiva is a backend agnostic 2D vector drawing interface. In other words, it is +a Python interface layer which sits on top of many different backends that +provide 2D vector drawing functionality such as Quartz, Cairo, etc. Many of +the concepts that will be covered here are generalizations of the ideas that +govern the underlying backends. As such, new Kiva users may find it useful for +their general understanding of what Kiva is all about to go through any of the +numerous other tutorials and documentation out there for specific backends. +Here are some we recommend: + +- `Quartz `_ +- `HTML5 Canvas CanvasRenderingContext2D `_ + + +Before we dive in, we suggest at least skimming the Kiva documentation before +going through the tutorial so you are familiar with the relevant terms and +concepts. + +Circuit Diagram Example +======================= + +In this tutorial, we will go through the process of drawing the basic circuit +diagram shown below step by step with Kiva. As mentioned, Kiva supports a +variety of different backends, but for this tutorial we will work with the +default agg backend. + +.. image:: images/tutorial.png + :width: 600 + :height: 300 + +Starting from the beginning, we will need a GraphicsContext so we import it +from our desired backend and instantiate it. Disregard the other imports for +now, they will come into play later in the tutorial. + +.. literalinclude:: tutorial.py + :lines: 1-8 + + +Now we are ready to use it to start drawing - simple as that. Let's start with +just drawing the wires. Given that they are rectangles, this can be done +quite easily using the graphics context's :py:meth:`rect` method. + +.. literalinclude:: tutorial.py + :lines: 10-14 + +.. image:: images/step_1.png + :width: 600 + :height: 300 + +That was easy. Now, let's draw the dots indicating wire connections. To do +this, we can use the optimized :py:meth:`draw_marker_at_points` method. + +.. note:: + :py:meth:`draw_marker_at_points` is currently only implemented by the + ``kiva.agg`` backend which may soon be deprecated. We can guard against this + to make our code backend agnostic, but for the purposes of keeping the + tutorial simple we do not do that here. See the + :ref:`advanced version of the tutorial ` for + how to do so. + +In this case, with only 5 dots to draw, the speed up is likely negligible. +However, in scenarios where you need to draw many markers this method can +provide a significant boost as opposed to just using a for loop (on the order +of hundreds or more). Here we simply define the points of the markers and call +the method with the points, a marker size, and a marker (the ``CIRCLE_MARKER`` +we imported from ``kiva.api``) passed as arguments. + +.. literalinclude:: tutorial.py + :lines: 17-25 + +.. image:: images/step_2.png + :width: 600 + :height: 300 + +Next, we will draw the Ammeter and Voltmeter symbols. To do this, we are going +to want to modify the graphics context's state to transform our coordinate +system to draw in the correct locations, and also to change things like the +fill color, line width, etc. However, we only want these state modifications +to apply to this specific part of the drawing. Luckily, to manage this, the +graphics context object can actually be used as a python +`context manager `_. +This allows us to meddle with state temporarily, do some drawing, and then have +the state reset to where it was once we are finished. + +In the following code, we instantiate a :class:`Font` object and set it to be +the graphics context's font. We do this first as it is the only font we intend +to use, so it can be a persistent modification to the graphics state. Then, we +use two ``with`` blocks to temporarily modify state. In each, we move to the +desired location to draw the symbol, change fill color, adjust the line width, +define a circular path with the :py:meth:`arc` method, and then finally draw +our path. Once the path is drawn, we draw the text inside. To do this we first +set the fill color back to black. Next, we ensure the font is centered in the +circular path. We do this be calling :py:meth:`get_text_extent` to determine +the width and height that the text takes up. When we call +:py:meth:`show_text_at_point`, the arguments specifying the point to draw at +represent the lower left corner of the resulting text. Thus, since the origin +of our coordinate system is currently at the center of the circle, in order for +the text to be centered we need to draw the text at (-w/2, -h/2). + +.. literalinclude:: tutorial.py + :lines: 28-51 + +.. image:: images/step_3.png + :width: 600 + :height: 300 + +As you may have noticed, most of the code for drawing the Ammeter and the +Voltmeter was effectively the same. Sometimes it is useful to work with an +independent path instance as opposed to specifically messing with the current +path of the graphics context. This brings us to the notion of CompiledPaths, +which we will now use to draw the resistors. As you can see the path for each +resistor will be exactly the same. Rather than moving our coordinate system +around to each location and redrawing the same path at each location, we will +instead define the path using a :class:`CompiledPath` and draw it at each of +the various desired locations using the :py:meth:`draw_path_at_points` method. +To do this we instantiate a :class:`CompiledPath`, and then define our path +just as we would with the graphics context's current path. The interface uses +the same :ref:`kiva_path_functions`. Finally, we can simply call +:py:meth:`draw_path_at_points` passing in the locations where we want to draw +the path, our compiled path, and the drawing mode. + +.. literalinclude:: tutorial.py + :lines: 70-77 + +If you run just this code, you will notice that things don't look quite right, +as the original lines for the wires still show underneath our resistors. We can +get rid of these by drawing white lines over the relevant portions of the wire +before we call the above code. Note that this is not the most performant +approach we could take especially in the context of an interactive application. +In general we want to strive to touch each pixel as few times as necessary. +However, in this case to keep the tutorial simple, we can run the following +code before drawing the resistors. + +.. literalinclude:: tutorial.py + :lines: 54-68 + +.. image:: images/step_45.png + :width: 600 + :height: 300 + + +Now, for the switch. Just like above, we are going to want to "erase" the +previously drawn wire. Then, for the actual switch, we want to effectively just +rotate that segment of wire outwards. To do this we tranform our coordinates so +that we can simply draw a line along the x axis of the same length as the gap +we just created. Thus, we translate to the lower edge of the switch, and rotate +our axis to the desired angle. Upon exit from the context manager, the +coordinate system is reverted back to as it was before. + +.. literalinclude:: tutorial.py + :lines: 80-98 + +.. image:: images/step_6.png + :width: 600 + :height: 300 + +We will leave the drawing of the battery as an exercise for the reader, but the +full code for the example is available :ref:`here `. + +This tutorial was intended as a ramp up for drawing with Kiva. Many of the +approaches taken were chosen with a focus on teaching, not writing optimal +code. In pactice, you probably would not want to implement drawing in this way +because it is not as performant as it could be. For example, drawings lines and +then "erasing" them by drawing a white line on top. Now that you better +understand the basics of what Kiva drawing is all about, please refer to the +:ref:`advanced version of the tutorial ` to see +what a more "production" level version of code for this drawing might look +like. diff --git a/docs/source/kiva_tutorial/tutorial_advanced.py b/docs/source/kiva_tutorial/tutorial_advanced.py new file mode 100644 index 000000000..c638ef5c2 --- /dev/null +++ b/docs/source/kiva_tutorial/tutorial_advanced.py @@ -0,0 +1,279 @@ +from math import tau + +import numpy as np + +from kiva.api import CAP_ROUND, CIRCLE_MARKER, FILL, Font, STROKE +from kiva.image import GraphicsContext, CompiledPath + + +def draw_wire_with_components(gc, start, end, component_locations): + """ + Draws a straight, axis aligned, wire with gaps in it for components. This + function assumes the component locations are in order they are encountered + when moving from start to end. + + Parameters + ---------- + gc : GraphicsContext + The Graphics context doing the drawing + start : 2-tuple + The start point of the wire + end : 2-tuple + The end point of the wire + component_locations : List of pairs of 2-tuple + The start and end points of the components in order encountered + """ + with gc: + gc.set_stroke_color((0., 0., 0., 1.)) + gc.set_line_width(1.0) + + gc.move_to(*start) + for comp_start, comp_end in component_locations: + gc.line_to(*comp_start) + gc.move_to(*comp_end) + gc.line_to(*end) + gc.stroke_path() + + +def draw_rect_wire_frame_with_components(gc, x, y, w, h, component_locations): + """ + Draws an axis aligned rectangle of wire with gaps in it for components. + component_locations is a list of pairs of points corresponding to the start + and end of a component. The function assumes the component locations are + in order they are encountered when moving clockwise around the rectangle + starting at the lower left corner. + + Parameters + ---------- + gc : GraphicsContext + The Graphics context doing the drawing + x : int + The left X coordinate of the rectangle + y : int + The bottom Y coordinate of the rectangle + w : int + The width of the rectangle + h : int + The height of the rectangle + component_locations : List of pairs of 2-tuple + The start and end points of the components in order encountered + """ + left_comps = [ + comp_loc for comp_loc in component_locations if comp_loc[0][0] == x + ] + top_comps = [ + comp_loc for comp_loc in component_locations if comp_loc[0][1] == y + h + ] + right_comps =[ + comp_loc for comp_loc in component_locations if comp_loc[0][0] == x + w + ] + bottom_comps = [ + comp_loc for comp_loc in component_locations if comp_loc[0][1] == y + ] + + draw_wire_with_components(gc, (x, y), (x, y + h), left_comps) + draw_wire_with_components(gc, (x, y + h), (x + w, y + h), top_comps) + draw_wire_with_components(gc, (x + w, y + h), (x + w, y), right_comps) + draw_wire_with_components(gc, (x + w, y), (x, y), bottom_comps) + + +def draw_wire_connections_at_points(gc, points): + """ + Draw wire connections at each of the given points. This function checks if + the graphics context implements optimized methods for doing so, and draws + using the most optimal approach available. + + Parameters + ---------- + gc : GraphicsContext + The Graphics context doing the drawing + points : List of pairs of 2-tuple + The points where wire connections are to be drawn + """ + + if hasattr(gc, 'draw_marker_at_points'): + gc.draw_marker_at_points(points, 4.0, CIRCLE_MARKER) + + else: + wire_connection_path = CompiledPath() + wire_connection_path.move_to(0,0) + wire_connection_path.arc(0, 0, 4, 0, tau) + + if hasattr(gc, 'draw_path_at_points'): + gc.draw_path_at_points(points, wire_connection_path, FILL) + else: + for point in points: + with gc: + gc.translate_ctm(point[0], point[1]) + gc.add_path(wire_connection_path) + gc.fill_path() + + +def create_resistor_path(): + """ + Creates a CompiledPath for a resistor which can then be re-used as needed. + + Returns + ------- + CompiledPath + The reistor compiled path + """ + resistor_path = CompiledPath() + resistor_path.move_to(0,0) + resistor_path_points = [(i*10+5, 10*(-1)**i) for i in range(8)] + for x, y in resistor_path_points: + resistor_path.line_to(x,y) + resistor_path.line_to(80, 0) + + return resistor_path + + +def draw_resistors_at_points(gc, points, resistor_path): + """ + Draw a resistor at each of the given points. This function checks if + the graphics context implements an optimized method for doing so, and draws + using the most optimal approach available. + + Parameters + ---------- + gc : GraphicsContext + The graphics context doing the drawing. + points : List of pairs of 2-tuple + The points where resistors are to be drawn + resistor_path : CompiledPath + The resistor path we wish to draw + """ + + if hasattr(gc, 'draw_path_at_points'): + gc.draw_path_at_points(points, resistor_path, STROKE) + else: + for point in points: + with gc: + gc.translate_ctm(point[0], point[1]) + gc.add_path(resistor_path) + gc.stroke_path() + + +def draw_meter(gc, location, color, text): + """ + Draws a meter of the given color, with the given text, at the given + location. + + Parameters + ---------- + gc : GraphicsContext + The graphics context doing the drawing. + location : 2-tuple + The point where the meter is to be drawn + color : 3 or 4 component tuple (R, G, B[, A]) + The color of the meter + text : str + The text to be placed in the center of the meter symbol + """ + font = Font('Times New Roman', size=20) + with gc: + gc.set_font(font) + gc.set_fill_color(color) + gc.set_line_width(3) + gc.translate_ctm(*location) + + gc.arc(0, 0, 20, 0.0, tau) + gc.draw_path() + + gc.set_fill_color((0., 0., 0., 1.0)) + x, y, w, h = gc.get_text_extent(text) + gc.show_text_at_point(text, -w/2, -h/2) + + +def draw_switch(gc, location, angle): + """ + Draws a switch at given location. Assumes location is the connected side + of the switch, and angle assumes orientation facing directly accross the + switch. + + Parameters + ---------- + gc : GraphicsContext + The graphics context doing the drawing. + location : 2-tuple + The point where the switch is to be drawn + angle : float + The angle of the switch + """ + with gc: + gc.translate_ctm(*location) + gc.rotate_ctm(angle) + gc.move_to(0, 0) + gc.line_to(30, 0) + gc.stroke_path() + + +def draw_battery(gc, location): + """ + Draws a battery at given location. Battery will extend down from the given + location. + + Parameters + ---------- + gc : GraphicsContext + The graphics context doing the drawing. + location : 2-tuple + The point where the switch is to be drawn + """ + with gc: + gc.translate_ctm(*location) + gc.move_to(0, 0) + thin_starts = [(-20, 0), (-20, -18)] + thin_ends = [(20,0), (20, -18)] + gc.line_set(thin_starts, thin_ends) + gc.stroke_path() + thick_starts = [(-8, -10), (-8, -28)] + thick_ends = [(8, -10), (8, -28)] + gc.set_line_width(8) + gc.set_line_cap(CAP_ROUND) + gc.line_set(thick_starts, thick_ends) + gc.stroke_path() + + +if __name__ == "__main__": + + gc = GraphicsContext((600, 300)) + + # step 1) Draw a skeleton of the circuit + component_locations = [ + ((260, 150), (340, 150)), + ((550, 130), (550, 100)), + ((550, 90), (550, 60)), + ((430, 50), (350, 50)), + ((230, 50), (150, 50)) + ] + draw_rect_wire_frame_with_components( + gc, 50, 50, 500, 100, component_locations + ) + draw_rect_wire_frame_with_components( + gc, 200, 200, 200, 50, [((340, 200), (260, 200))] + ) + draw_wire_with_components(gc, (200, 150), (200, 200), []) + draw_wire_with_components(gc, (400, 150), (400, 200), []) + + + # step 2) draw dots for wire connections + points = [(200, 150), (200, 200), (400, 150), (400, 200), (550, 130)] + draw_wire_connections_at_points(gc, points) + + # step 3) Draw the meters + draw_meter(gc, (50, 100), (.9, .9, 0.5, 1.0), 'A') # Ammeter + draw_meter(gc, (300, 250), (0.5, .9, 0.5, 1.0), 'V') # Voltmeter + + #step 4) Draw the resistors + resistor_path = create_resistor_path() + resistor_locations = [(150, 50), (350, 50), (260, 150), (260, 200)] + draw_resistors_at_points(gc, resistor_locations, resistor_path) + + # step 6) Draw the switch + draw_switch(gc, (550,100), tau/6) + + # step 7) Draw the battery + draw_battery(gc, (550,90)) + + gc.save("images/tutorial_advanced.png") diff --git a/docs/source/kiva_tutorial/tutorial_advanced_code.rst b/docs/source/kiva_tutorial/tutorial_advanced_code.rst new file mode 100644 index 000000000..5d3b96182 --- /dev/null +++ b/docs/source/kiva_tutorial/tutorial_advanced_code.rst @@ -0,0 +1,6 @@ +.. _tutorial_advanced_code: + +Advanced Tutorial Code +====================== + +.. literalinclude:: tutorial_advanced.py \ No newline at end of file diff --git a/docs/source/kiva_tutorial/tutorial_code.rst b/docs/source/kiva_tutorial/tutorial_code.rst new file mode 100644 index 000000000..3424515d2 --- /dev/null +++ b/docs/source/kiva_tutorial/tutorial_code.rst @@ -0,0 +1,6 @@ +.. _tutorial_code: + +Basic Tutorial Code +=================== + +.. literalinclude:: tutorial.py \ No newline at end of file