diff --git a/python_payload/mypystubs/ctx.pyi b/python_payload/mypystubs/ctx.pyi index 7e9647aa326dda2aa774025caf49f277fd102278..130cbd2b441dba8dd2a38c5c93514250327b19e1 100644 --- a/python_payload/mypystubs/ctx.pyi +++ b/python_payload/mypystubs/ctx.pyi @@ -67,12 +67,44 @@ class Context(Protocol): """ Stores the transform, clipping state, fill and stroke sources, font size, stroking and dashing options. + + This example draws red squares and then saves teh state, draws one green square, and restores the state: + + >>> ctx.rgb(200, 0, 0) + >>> ctx.rectangle(0, -40, 5, 5).fill() + >>> ctx.rectangle(0, -30, 5, 5).fill() + >>> ctx.rectangle(0, -20, 5, 5).fill() + >>> ctx.save() + >>> ctx.rgb(0, 200, 0).rectangle(0, -10, 5, 5).fill() + >>> # Restore to the state saved by the most recent call to save() + >>> ctx.restore() + >>> ctx.rectangle(0, 0, 5, 5).fill() + >>> ctx.rectangle(0, 10, 5, 5).fill() + >>> ctx.rectangle(0, 20, 5, 5).fill() + >>> ctx.rectangle(0, 30, 5, 5).fill() + >>> ctx.rectangle(0, 40, 5, 5).fill() """ pass def restore(self) -> "Context": """ Restores the state previously saved with save, calls to save/restore should be balanced. + + This example draws red squares and then saves teh state, draws one green square, and restores the state: + + >>> ctx.rgb(200, 0, 0) + >>> ctx.rectangle(0, -40, 5, 5).fill() + >>> ctx.rectangle(0, -30, 5, 5).fill() + >>> ctx.rectangle(0, -20, 5, 5).fill() + >>> ctx.save() + >>> ctx.rgb(0, 200, 0).rectangle(0, -10, 5, 5).fill() + >>> # Restore to the state saved by the most recent call to save() + >>> ctx.restore() + >>> ctx.rectangle(0, 0, 5, 5).fill() + >>> ctx.rectangle(0, 10, 5, 5).fill() + >>> ctx.rectangle(0, 20, 5, 5).fill() + >>> ctx.rectangle(0, 30, 5, 5).fill() + >>> ctx.rectangle(0, 40, 5, 5).fill() """ pass def clip(self) -> "Context": @@ -80,6 +112,18 @@ class Context(Protocol): Use the current path as a clipping mask, subsequent draw calls are limited by the path. The only way to increase the visible area is to first call save and then later restore to undo the clip. + + This example draws a blue circle and clips a square out of it and colors it red, resulting in a quarter of the circle being red. + + >>> # Blue circle + >>> ctx.arc(0, 0, 60, 0, 2 * math.pi, True) + >>> ctx.rgb(0, 0, 255).fill() + >>> # Define a clipping area for the circle + >>> ctx.arc(0, 0, 60, 0, 2 * math.pi, True) + >>> ctx.clip() + >>> # Add the shape into the clipping area + >>> ctx.rectangle(0, 0, 80, 80) + >>> ctx.rgb(255, 0, 0).fill() """ pass def rotate(self, x: float) -> "Context": @@ -121,11 +165,29 @@ class Context(Protocol): Adds a line segment to the path, from the position of the virtual pen to the given coordinates, the coordinates are the new position of the virtual pen. + + This example draws two lines on the screen perpendicular to each other: + + >>> ctx.rgb(0, 1, 0).begin_path() + >>> ctx.move_to(-120,0) + >>> ctx.line_to(120, 0) + >>> ctx.move_to(0, 120) + >>> ctx.line_to(0, -120) + >>> ctx.stroke() """ pass def move_to(self, x: float, y: float) -> "Context": """ Moves the virtual pen to the given coordinates without drawing anything. + + This example draws two lines on the screen perpendicular to each other: + + >>> ctx.rgb(0, 1, 0).begin_path() + >>> ctx.move_to(-120,0) + >>> ctx.line_to(120, 0) + >>> ctx.move_to(0, 120) + >>> ctx.line_to(0, -120) + >>> ctx.stroke() """ pass def curve_to( @@ -134,6 +196,28 @@ class Context(Protocol): """ Add a cubic bezier segment to current path, with control handles at cx0, cy0 and cx1,cy1, the virtual pens new position is x, y. + + This example draws a red Bézier curve based on start and end points and the control points. The start and end points are additionally drawn in blue and green for the visualization. + + >>> ctx.rgb(1, 0, 0).begin_path() + >>> # start point: (-80, -80) + >>> # first control point (a, b): (-70, -0) + >>> # second control point (c, d): (60, 20) + >>> # end point (e, f): (60, 80) + >>> # Add cubic bézier curve + >>> ctx.move_to(-80,-80) + >>> ctx.curve_to(-70, 0, 60, 20, 60, 80) + >>> ctx.stroke() + >>> # Show start and end points in blue + >>> ctx.rgb(0, 0, 1).begin_path() + >>> ctx.arc(-80,-80, 5, 0, 2 * math.pi, True) # start point + >>> ctx.arc(60, 80, 5, 0, 2 * math.pi, True) # end point + >>> ctx.fill() + >>> # Show control points in green + >>> ctx.rgb(0, 1, 0).begin_path() + >>> ctx.arc(-70, 0, 5, 0, 2 * math.pi, True) # first control point + >>> ctx.arc(60, 20, 5, 0, 2 * math.pi, True) # second control point + >>> ctx.fill() """ pass def quad_to(self, cx: float, cy: float, x: float, y: float) -> "Context": @@ -145,25 +229,45 @@ class Context(Protocol): def gray(self, a: float) -> "Context": """ Set current draw color to a floating point grayscale value from 0 to 1. + + This example places three circles next to each other with some overlap. One is black, the next gray, and the last one white. + + >>> ctx.gray(0.0).arc(-30, 0, 40, 0, 2 * math.pi, True).fill() + >>> ctx.gray(0.5).arc(0, 0, 40, 0, 2 * math.pi, True).fill() + >>> ctx.gray(1.0).arc(30, 0, 40, 0, 2 * math.pi, True).fill() """ pass def rgb(self, r: float, g: float, b: float) -> "Context": """ Set current draw color to an RGB color defined by component values from 0 to 1. + + This example places two circles next to each other with some overlap. One is colored red and the other blue. The blue circle is on top of the red circle and where they overlap, only the blue of the blue circle is visible. + + >>> # red left circle + >>> ctx.rgb(1.0, 0, 0).arc(-30, 0, 40, 0, 2 * math.pi, True).fill() + >>> # blue right circle + >>> ctx.rgb(0, 0, 1.0).arc(30, 0, 40, 0, 2 * math.pi, True).fill() """ pass def rgba(self, r: float, g: float, b: float, a: float) -> "Context": """ Set current draw color to an RGBA color defined by component values from 0 to 1. + + This example places two circles next to each other with some overlap. One is colored red and the other blue with 50% opacity. The blue circle is on top of the red circle and where they overlap, you can see the red through the blue. + + >>> # red left circle + >>> ctx.rgba(255, 0, 0, 1).arc(-30, 0, 40, 0, 2 * math.pi, True).fill() + >>> # blue right circle, 50% opacity + >>> ctx.rgba(0, 0, 100, 0.5).arc(30, 0, 40, 0, 2 * math.pi, True).fill() """ pass def arc_to( self, x1: float, y1: float, x2: float, y2: float, radius: float ) -> "Context": """ - TOD(q3k): document + TODO(q3k): document """ pass def rel_line_to(self, x: float, y: float) -> "Context": @@ -198,13 +302,18 @@ class Context(Protocol): self, x1: float, y1: float, x2: float, y2: float, radius: float ) -> "Context": """ - TOD(q3k): document + TODO(q3k): document """ pass def rectangle(self, x: float, y: float, w: float, h: float) -> "Context": """ Trace the outline of a rectangle with upper left coordinates at x,y which is w wide and h high. + + This is a simple example renders a red square: + + >>> ctx.rgb(0.8, 0, 0) + >>> ctx.rectangle(0, -40, 5, 5).fill() """ pass def round_rectangle( @@ -223,33 +332,63 @@ class Context(Protocol): radius: float, angle1: float, angle2: float, - direction: float, + direction: bool, ) -> "Context": """ - TOD(q3k): document + Add a circular arc to the current sub-path at the coordinates x,y with a radius of r for corners and angle1 in radians specifying the angle at which the arc starts and angle2 in radians specifying the angle at which the arc ends. A truthful value for the direction draws the arc counter-clockwise between the start and end angles. + + This example places two circles next to each other with some overlap. One is colored red and the other blue. The blue circle is on top of the red circle and where they overlap, only the blue of the blue circle is visible. + + >>> # red left circle + >>> ctx.rgb(1.0, 0, 0).arc(-30, 0, 40, 0, 2 * math.pi, True).fill() + >>> # blue right circle + >>> ctx.rgb(0, 0, 1.0).arc(30, 0, 40, 0, 2 * math.pi, True).fill() """ pass def close_path(self) -> "Context": """ Close the current open path with a curve back to where the current sequence of path segments started. + + This example draws a triangle: + + >>> ctx.rgb(1.0, 0, 0).begin_path() + >>> ctx.move_to(-30, -30) + >>> ctx.line_to(-30, 30) + >>> ctx.line_to(30, 0) + >>> ctx.close_path() + >>> ctx.stroke() """ pass def preserve(self) -> "Context": """ - TOD(q3k): document + TODO(q3k): document """ pass def fill(self) -> "Context": """ Fill the current path with the current source (color, gradient or texture). + + This is a simple example renders a red square: + + >>> ctx.rgb(0.8, 0, 0) + >>> ctx.rectangle(0, -40, 5, 5).fill() """ pass def stroke(self) -> "Context": """ Stroke the current path with the current source (color, gradient or texture), with current line_width + + This example draws a triangle: + + >>> ctx.rgb(1.0, 0, 0).begin_path() + >>> ctx.move_to(-30, -30) + >>> ctx.line_to(-30, 30) + >>> ctx.line_to(30, 0) + >>> ctx.close_path() + >>> ctx.stroke() """ pass def add_stop( @@ -258,6 +397,13 @@ class Context(Protocol): """ Adds a color stop for a linear or radial gradient. Pos is a position between 0.0 and 1.0. Should be called after linear_gradient or radial_gradient. + + This example adds a radial gradiant to a rectangle: + + >>> ctx.radial_gradient(30, 30, 80, -30, -50, 70) + >>> ctx.add_stop(0, (100,0,100), 0.5) + >>> ctx.add_stop(1, (100,0,0), 0.8) + >>> ctx.rectangle(-100, -100, 200, 200).fill() """ pass def linear_gradient(self, x0: float, y0: float, x1: float, y1: float) -> "Context": @@ -266,7 +412,7 @@ class Context(Protocol): an empty gradient from black to white exists, add stops with add_stop to specify a custom gradient. - This is a simple example rendering a rainbow gradient on the right side of the screen: + This is an example rendering a rainbow gradient on the right side of the screen: >>> ctx.linear_gradient(0.18*120,0.5*120,0.95*120,0.5*120) >>> ctx.add_stop(0.0, [1.0,0.0,0.0], 1.0) @@ -286,6 +432,13 @@ class Context(Protocol): Change the source to a radial gradient from a circle x0,y0 with radius0 to an outer circle x1,y1 with radidus r1. + This example adds a radial gradiant to a rectangle: + + >>> ctx.radial_gradient(30, 30, 80, -30, -50, 70) + >>> ctx.add_stop(0, (100,0,100), 0.5) + >>> ctx.add_stop(1, (100,0,0), 0.8) + >>> ctx.rectangle(-100, -100, 200, 200).fill() + NOTE: currently only the second circle's origin is used, but both radiuses are in use. """ @@ -318,6 +471,12 @@ class Context(Protocol): def text(self, text: str) -> "Context": """ Add a text fragment using the current fill source, font and font size. + + This example draws the text Hello world on a red rectangle: + + >>> ctx.font = ctx.get_font_name(5) + >>> ctx.rgb(0.4, 0, 0).rectangle(-120,-120,240,240).fill() + >>> ctx.rgb(1.0, 0, 0).move_to(-80,0).text("Hello world") """ pass def scope(self) -> "Context": diff --git a/sim/fakes/ctx.py b/sim/fakes/ctx.py index cef1f8c90d09ccebb508a245d9e6d06e98776073..f898517d632c7aa09e52eff162108d692296d3d7 100644 --- a/sim/fakes/ctx.py +++ b/sim/fakes/ctx.py @@ -304,20 +304,58 @@ class Context: self._emit(f"gray {v:.3f}") return self + def _value_range_rgb(self, value, value_str, low_limit, high_limit): + if value > high_limit: + print( + "{name} value should be below {limit}. Setting to {limit}.".format( + name=value_str, limit=high_limit + ), + file=sys.stderr, + ) + return high_limit + if value < low_limit: + print( + "{name} value should be above {limit}. Setting to {limit}.".format( + name=value_str, limit=low_limit + ), + file=sys.stderr, + ) + return low_limit + return value + def rgba(self, r, g, b, a): - # TODO(q3k): dispatch by type instead of value, warn on - # ambiguous/unexpected values for type. - if r > 1.0 or g > 1.0 or b > 1.0 or a > 1.0: + r = self._value_range_rgb(r, "r", 0.0, 255.0) + g = self._value_range_rgb(g, "g", 0.0, 255.0) + b = self._value_range_rgb(b, "b", 0.0, 255.0) + a = self._value_range_rgb(a, "a", 0.0, 1.0) + + # if one value is a float between 0 and 1, check that no value is above 1 + if (r > 0.0 and r < 1.0) or (g > 0.0 and g < 1.0) or (b > 0.0 and b < 1.0): + if r > 1.0 or g > 1.0 or b > 1.0: + print( + "r, g, and b values are using mixed ranges (0.0 to 1.0) and (0 - 255).", + file=sys.stderr, + ) + if r > 1.0 or g > 1.0 or b > 1.0: r /= 255.0 g /= 255.0 b /= 255.0 - a /= 255.0 + self._emit(f"rgba {r:.3f} {g:.3f} {b:.3f} {a:.3f}") return self def rgb(self, r, g, b): - # TODO(q3k): dispatch by type instead of value, warn on - # ambiguous/unexpected values for type. + r = self._value_range_rgb(r, "r", 0.0, 255.0) + g = self._value_range_rgb(g, "g", 0.0, 255.0) + b = self._value_range_rgb(b, "b", 0.0, 255.0) + + # if one value is a float between 0 and 1, check that no value is above 1 + if (r > 0.0 and r < 1.0) or (g > 0.0 and g < 1.0) or (b > 0.0 and b < 1.0): + if r > 1.0 or g > 1.0 or b > 1.0: + print( + "r, g, and b values are using mixed ranges (0.0 to 1.0) and (0 - 255).", + file=sys.stderr, + ) if r > 1.0 or g > 1.0 or b > 1.0: r /= 255.0 g /= 255.0