diff --git a/docs/source/index.rst b/docs/source/index.rst index 5f2e6ad88..6113098c1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,6 +17,7 @@ Enable Documentation kiva/quickref.rst kiva/fonts.rst kiva/compiled_path.rst + kiva/state.rst credits.rst diff --git a/docs/source/kiva/compiled_path_ex.py b/docs/source/kiva/compiled_path_ex.py index 59aa6a095..e90558b27 100644 --- a/docs/source/kiva/compiled_path_ex.py +++ b/docs/source/kiva/compiled_path_ex.py @@ -1,5 +1,5 @@ from math import pi -from kiva.agg import GraphicsContextArray as GraphicsContext +from kiva.image import GraphicsContext gc = GraphicsContext((600, 600)) @@ -17,4 +17,4 @@ gc.add_path(path) gc.fill_path() -gc.save("example.png") +gc.save("compiled_path_ex.png") diff --git a/docs/source/kiva/images/state_ex.png b/docs/source/kiva/images/state_ex.png new file mode 100644 index 000000000..5d325dd5f Binary files /dev/null and b/docs/source/kiva/images/state_ex.png differ diff --git a/docs/source/kiva/state.rst b/docs/source/kiva/state.rst new file mode 100644 index 000000000..96f3898d6 --- /dev/null +++ b/docs/source/kiva/state.rst @@ -0,0 +1,95 @@ +Kiva State +========== +Kiva is a "stateful" drawing API. What this means is that the graphics context +has a collection of state which affects the results of its drawing actions. +Furthermore, Kiva enables this state to be managed with a stack such that state +can be "pushed" onto the stack before making some temporary changes and then +"popped" off the stack to restore the state to a version which no longer +includes those changes. + +State Components +---------------- +Here is a list of all the pieces of state tracked by a Kiva graphics context, +along with the methods which operate on them: + +* Affine transformation (:py:meth:`translate_ctm`, :py:meth:`rotate_ctm`, + :py:meth:`scale_ctm`, :py:meth:`concat_ctm`, :py:meth:`set_ctm`, + :py:meth:`get_ctm`) +* Clipping (:py:meth:`clip_to_rect`, :py:meth:`clip_to_rects`, :py:meth:`clip`, + :py:meth:`even_odd_clip`) +* Fill color (:py:meth:`set_fill_color`, :py:meth:`get_fill_color`, + :py:meth:`linear_gradient`, :py:meth:`radial_gradient`) +* Stroke color (:py:meth:`set_stroke_color`, :py:meth:`get_stroke_color`) +* Line width (:py:meth:`set_line_width`) +* Line join style (:py:meth:`set_line_join`) +* Line cap style (:py:meth:`set_line_cap`) +* Line dashing (:py:meth:`set_line_dash`) +* Global transparency (:py:meth:`set_alpha`, :py:meth:`get_alpha`) +* Anti-aliasing (:py:meth:`set_antialias`, :py:meth:`get_antialias`) +* Miter limit (:py:meth:`set_miter_limit`) +* Flatness (:py:meth:`set_flatness`) +* Image interpolation (:py:meth:`set_image_interpolation`, :py:meth:`get_image_interpolation`) + +State Stack Management +---------------------- +Graphics context instances have two methods for saving and restoring the state, +:py:meth:`save_state` ("push") and :py:meth:`restore_state` ("pop"). That said, +it isn't recommended practice to call the methods directly. Instead, you can +treat the graphics context object as a +`context manager `_ +and use the ``with`` keyword to create a block of code where the graphics state +is temporarily modified. Using the context manager approach provides safety from +"temporary" modifications becoming permanent if an uncaught exception is raised +while drawing. + +In Enable and Chaco, it is frequently the case that a graphics context instance +will be passed into a method for the purpose of some drawing. Because it is not +reasonable to push the responsibility of state management "up" the call stack, +the onus is on the code making state modifications to do them safely so that +other changes don't leak into other code. + +**Well behaved code should take care to only modify graphics state inside a** +``with`` **block**. + +Example +------- +.. image:: images/state_ex.png + :width: 300 + :height: 300 + +First, the whole example: + +.. literalinclude:: state_ex.py + :linenos: + +The first part sets up the default graphics state. Here, that includes a scale +of 2 in X and Y, a translation of (150, 150) which is affected by the +preceeding scale transformation, and some line properties: stroke color, width, +join, and cap: + +.. literalinclude:: state_ex.py + :lines: 7-13 + :linenos: + :lineno-match: + +Then in a loop, we draw twice (the two :py:meth:`stroke_path` calls). The first +draw uses a ``with`` block to temporarily modify the drawing state. It adds more +affine transformations: a rotate and a translate. It also changes some line +properties: stroke color, width, and cap. A rectangle is then added to the +current path and stroked. + +.. literalinclude:: state_ex.py + :lines: 17-24 + :linenos: + :lineno-match: + +After leaving the first ``with`` block, the state is now restored to its +default. A new ``with`` block is entered and the current transformation matrix +is modified with the same rotation as the first drawing block, but a +*different* translation is applied. The line properties are unchanged +and so use the defaults set at the top. + +.. literalinclude:: state_ex.py + :lines: 26-31 + :linenos: + :lineno-match: diff --git a/docs/source/kiva/state_ex.py b/docs/source/kiva/state_ex.py new file mode 100644 index 000000000..73ac5e22c --- /dev/null +++ b/docs/source/kiva/state_ex.py @@ -0,0 +1,33 @@ +import math +from kiva import CAP_ROUND, CAP_SQUARE, JOIN_ROUND +from kiva.image import GraphicsContext + +gc = GraphicsContext((600, 600)) + +gc.scale_ctm(2, 2) +gc.translate_ctm(150, 150) + +gc.set_stroke_color((0.66, 0.88, 0.66)) +gc.set_line_width(7.0) +gc.set_line_join(JOIN_ROUND) +gc.set_line_cap(CAP_SQUARE) + +for i in range(0, 12): + theta = i*2*math.pi / 12.0 + with gc: + gc.rotate_ctm(theta) + gc.translate_ctm(105, 0) + gc.set_stroke_color((1 - (i / 12), math.fmod(i / 6, 1), i / 12)) + gc.set_line_width(10.0) + gc.set_line_cap(CAP_ROUND) + gc.rect(0, 0, 25, 25) + gc.stroke_path() + + with gc: + gc.rotate_ctm(theta) + gc.translate_ctm(20, 0) + gc.move_to(0, 0) + gc.line_to(80, 0) + gc.stroke_path() + +gc.save("state_ex.png")