PK!$S@@ CHANGES.rstv0.9.2 - 2019-01-06 ------------------- Changed ^^^^^^^ - Most commonly used objects have been imported into the top level namespace. This means that it is now possible to write code like the following. .. code-block:: python import stylo as st black = st.FillColor() circle = st.Circle(fill=True) image = st.SimpleImage(circle, black) - The way stylo has been packaged has been changed. It now comes with a couple of "extras". Instead of requiring dependencies for everything, the default installation now only contains the packages that are absolutely required to run stylo. The other dependencies have been split into a couple of extras + :code:`testing`: The dependencies required to import items from the `stylo.testing` package. + :code:`jupyer`: Dependencies required to use stylo interactively in a jupyter notebook. v0.9.1 - 2018-12-27 ------------------- There are no changes. This release is a test to ensure that internal changes to how :code:`stylo` is packaged and deployed are working correctly. v0.9.0 - 2018-11-11 ------------------- Added ^^^^^ - New :code:`stylo.math` module! Currently it contains a :code:`lerp` function to do linear implementation between two values :code:`a` and :code:`b` - New :code:`stylo.design` module! This is the start of the "next level" in styo's API abstracting away from the lower level objects such as shapes and colormaps. - This module adds the notion of a parameter group, this is a collection of values that can be passed into functions as a single object using the dictionary unpacking syntax (:code:`**params`) Parameter groups are defined using the :code:`define_parameter_group` function and taking a name and a comma separated string of parameter names. There is also :code:`define_time_dependent_parameter_group` that can be used to define a parameter group that depends on time. Currently there are two pre-defined paramters groups, :code:`Position` and :code:`Trajectory`. They both combine the :code:`x` and :code:`y` values into a single object, with the second being the time dependent version of the first. Finally there are two built-in implementations of these parameter groups. :code:`StaticPosition` and :code:`ParametricPosition` the first takes two values and returns them. The second takes two functions in time and calls them at each supplied time value. v0.8.0 - 2018-11-07 ------------------- Added ^^^^^ - New :code:`Timeline` system! This finally introduces explicit support for animations to :code:`stylo.` v0.7.0 - 2018-10-25 ------------------- Added ^^^^^ - New :code:`Line` shape! - New :code:`ImplicitXY` shape! Draw any curve that is implicitly defined by a function :math:`f(x, y)` Changed ^^^^^^^ - The :code:`Circle` and :code:`Ellipse` shapes now take more arguments. By default the shapes will now draw an outline rather than a filled in shape. v0.6.1 - 2018-10-20 ------------------- Added ^^^^^ - New :code:`preview` keyword argument to images, set this to :code:`False` if you don't want a matplotlib figure returned. - New :code:`encode` keyword argument to images, setting this to :code:`True` will return a base64 encoded string representation of the image in PNG format. Fixed ^^^^^ - Preview images are no longer displayed twice in jupyter notebooks - Preview images no longer display the x and y axis numbers. v0.6.0 - 2018-10-07 ------------------- Added ^^^^^ Users """"" - New :code:`Triangle` shape - Shapes can now be inverted using the :code:`~` operator. Contributors """""""""""" - Added new shape :code:`InvertedShape` which handles the inversion of a shape behind the scenes. - Tests for all the composite shapes and operators. - More documentation on how to get involved Changed ^^^^^^^ Users """"" - Shapes now have defined :code:`__repr__` methods, including shapes that have been combined, where a representation of a tree will be produced showing how the various shapes have been combined together. - Preview images in Jupyter notebooks are now larger by default This release of :code:`stylo` was brought to you thanks to contributions from the following awesome people! - `mvinoba `_ v0.5.0 - 2018-09-27 ------------------- Added ^^^^^ Users """"" - New Image object :code:`LayeredImage` object that can now draw more than one object - Added an introductory tutorial for first time users to the documentation - Functions from the :code:`stylo.domain.transform` package can now be applied to shapes, meaning that most images can now be made without handling domains directly. Contributors """""""""""" - Added a :code:`Drawable` class, this allows a domain, shape and colormap to be treated as a single entity. - Added a :code:`render_drawable` function that takes a drawable and some existing image data and applies it to the data. - Added a :code:`get_real_domain` function that given a width, height and scale returns a :code:`RectangularDomain` with appropriate aspect ratio, :math:`(0, 0)` at the centre of the image and the scale corresponding to the interval :math:`[ymin, ymax]` - We now make use of the :code:`[scripts]` section of :code:`Pipfile` so running common commands is now easier to remember + :code:`pipenv run test`: to run the test suite + :code:`pipenv run lint`: to lint the codebase + :code:`pipenv run docs`: to run a full build of the documentation + :code:`pipenv run docs_fast`: to run a less complete but faster build of the documentation. Changed ^^^^^^^ Users """"" - Altered :code:`SimpleImage` to no longer take a domain, reducing the cognitive load on first time users. It now instead takes an optional :code:`scale` variable to control the size of the domain underneath. This also means that the domain now automatically matches the aspect ratio of the image so no more distortion in non-square images. Contributors """""""""""" - The tests now take advantage of multi-core machines and should now run much faster - Building the docs now takes advantage of multi-core machines and should now run much faster. Fixed ^^^^^ Contributors """""""""""" - Fixed crashes in :code:`exampledoc.py` and :code:`apidoc.py` for first time users - Fixed issue with :code:`sed` on a Mac for people running the :code:`devenv-setup.sh` script This release of :code:`stylo` was brought to you thanks to contributions from the following awesome people! - `mvinoba `_ - `LordTandy `_ - `StephanieAngharad `_ v0.4.2 - 2018-09-17 ------------------- Added ^^^^^ - :code:`Image` objects can now take a :code:`size` keyword argument to adjust the size of the matplotlib preview plots v0.4.1 - 2018-09-17 ------------------- Fixed ^^^^^ - Fixed an issue with :code:`setup.py` that meant most of the code wasn't published to PyPi! v0.4.0 - 2018-09-16 ------------------- Out of the ashes of the previous version rises the biggest release to date! Stylo has been rewritten from the ground up and should now be easier to use, more modular and easier to extend! None (or very little) of the original code remains and not everything has been reimplemented yet so some of the features listed below may not be available in this version. There is a lot more work to be done particularly in the tests and docs departments however core functionality is now in place and it's been long enough since the previous release. I'm hoping that from now on releases will be smaller and more frequent as what is now here is refined and tested to create a stable base from which Stylo can be extended. Added ^^^^^ Users """"" One of the main ideas behind the latest incarnation of stylo is the idea of interfaces borrowed from Java. Where you have an object such as :code:`Shape` and all shapes have certain behaviors in common represented by methods on an interface. Then there are a number of implementations that provide the details specific to each shape. In stylo this is modelled by having a number of abstract classes that define the interfaces that represent different parts of the stylo image creation process. Then regular classes inherit from these to provide the details. With that in mind this release provides the following "interfaces". - New :code:`RealDomain` and :code:`RealDomainTransform` interfaces, these model the mapping of a continuous mathematical domain :math:`D \subset \mathbb{R}^2` onto a discrete grid of pixels. - New :code:`Shape` interface this models the mapping of the grid of values generated by a domain into a boolean numpy array representing which pixels are a part of the shape. - New :code:`ColorSpace` system this currently doesn't do much but should allow support for the use of different color representations. Current only 8-bit RGB values are supported. - New :code:`ColorMap` interface, this represents the mapping of the boolean numpy array generated by the :code:`Shape` interface into a numpy array containing the color values that will be eventually interpreted as an image. - New :code:`Image` interface. Implementations of this interface will implement common image creation workflows as well as providing a unified way to preview and save images to a file. With the main interfaces introduced here is a (very) brief introduction to each of the implementations provided in this release **RealDomain** - :code:`RectangularDomain`: Models a rectangular subset of the :math`xy`-plane :math:`[a, b] \times [c, d] \subset \mathbb{R}^2` - :code:`SquareDomain`: Similar to above but in the cases where :math:`c = a` and :math:`d = b` - :code:`UnitSquare`: Similar to above but the case where :math:`a = 0` and :math:`b = 1` **RealDomainTransform** - :code:`HorizontalShear`: Given a domain this applies a horizontal shear to it - :code:`Rotation`: Given a domain this rotates it by a given angle - :code:`Translation`: Given a domain this applies a translation to it - :code:`VerticalShear`: Given a domain this applies a vertical shear to it **Shape** - :code:`Square` - :code:`Rectangle` - :code:`Circle` - :code:`Ellipse` **ColorSpace** - :code:`RGB8`: 8-bit RGB valued colors **ColorMap** - :code:`FillColor`: Given a background and a foreground color. Color all :code:`False` pixels with the background color and color all the :code:`True` pixels the foreground color. **Image** - :code:`SimpleImage`: Currently the only image implementation, this implements one of the simplest workflows that can result in an interesting image. Take a :code:`Domain`, pass it to a :code:`Shape` and then apply a :code:`ColorMap` to the result. Extenders/Contributors """""""""""""""""""""" From the beginning this new attempt at :code:`stylo` has been designed with extensibility in mind so included in the library are also a number of utilities aimed to help you develop your own tools that integrate well with the rest of stylo. **Domains** and **DomainTransforms** While :code:`stylo` only currently ships with :code:`RealDomain` and :code:`RealDomainTransform` interfaces it is developed in a way to allow the addition of new "families" of domain. If you want to create your own stylo provides the following functions: - :code:`define_domain`: This will write your base domain class (like the :code:`RealDomain`) just give it a name and a list of parameters. - :code:`define_domain_transform`: The will write the :code:`DomainTransform` base class for you. In addition to defining new families :code:`stylo` provides a few helper classes to help you write your own domains and transforms for the existing :code:`RealDomain` family - :code:`PolarConversion`: If your domain is only "interesting" in cartesian coordinates this helper class will automatically write the conversion to polar coordinates for you. - :code:`CartesianConversion`: If your domain is only "interesting" in polar coordinates this helper class will automatically write the conversion to cartesian coordinates for you. **stylo.testing** :code:`stylo` also comes with a testing package that provides a number of utilities to help you ensure that any extensions you write will integrate well with the rest of :code:`stylo` - :code:`BaseRealDomainTest`: This is a class that you can base your test case on for any domains in the :code:`RealDomain` family to ensure that they function as expected. - :code:`define_domain_test`: Similar to the :code:`define_domain` and :code:`define_domain_transform` functions this defines a base test class to ensure that domains in your new family work as expected. - :code:`BaseShapeTest` Basing your test case on this for any new shapes will ensure that your shapes will function as expected by the rest of :code:`stylo` - :code:`define_benchmarked_example`: This is for those of you wishing to contribute an example to the documentation, using this function with your example code will ensure that your example is automatically included in the documentation when it is next built. **stylo.testing.strategies** This module defines a number of hypothesis strategies for common data types in :code:`stylo`. Using these (and hypothesis) in your test cases where possible will ensure that your objects will work with the same kind of data as :code:`stylo` itself. Removed ^^^^^^^ Everything mentioned below. v0.3.0 - 2017-12-09 -------------------- Added ^^^^^ - New Domain class, it is responsible for generating the grids of numbers passed to Drawables when they are mapped onto Images. It replaces most of the old decorators. - Drawables are now classes! Any drawable is now a class that inherits from Drawable, it brings back much of the old Puppet functionality with some improvements. - More tests! Changed ^^^^^^^ - ANDing Images (a & b) has been reimplemented so that it hopefully makes more sense. The alpha value of b is used to scale the color values of a. - Along with the new Domain system mapping Drawables onto Images has been reworked to hopefully make coordinate calculations faster Removed ^^^^^^^ - stylo/coords.py has been deleted, this means the following functions and decorators no longer exist + mk_domain - Domains are now a class + cartesian (now built into the new Domain object) + polar (now built into the new Domain object) + extend_periocally (now the .repeat() method on the new Domain object) + translate (now the .transform() method on the new Domain object) + reflect (not yet implemented in the new system) v0.2.3 - 2017-11-15 ------------------- Added ^^^^^ - Image objects can now be added together, this is simply the sum of the color values at each pixel - Image objects can now be subtracted, which is simply the difference of the colour values at each pixel Changed ^^^^^^^ - Renamed hex_to_rgb to hexcolor. It now also can cope with rgb and rgba arguments, with the ability to promote rgb to rgba colors v0.2.2 - 2017-10-30 ------------------- Added ^^^^^ - Keyword argument 'only' to the 'polar' decorator which allows you to ignore the x and y variables if you dont need them Fixed ^^^^^ - Forgot to expose the objects from interpolate.py to the top level stylo import - Examples in the documentation and enabled doctests for them v0.2.1 - 2017-10-29 ------------------- Fixed ^^^^^ - Stylo should now also work on python 3.5 Removed ^^^^^^^ - Deleted stylo/motion.py as its something better suited to a plugin - Deleted Pupptet, PuppetMaster and supporting functions as they are broken and better to be rewritten from scratch v0.2.0 - 2017-10-27 ------------------- Added ^^^^^ - Sampler object which forms the basis of the new Driver implementations - Channel object which can manage many Sampler-like objects to form a single 'track' of animation data - A very simple Driver object which allows you to collect multiple Channel objects into a single place - linear, quad_ease_in, quad_ease_out interpolation functions Docs """" - Added the following reference pages + Image + Drawable + Primitive + Sampler - A How-To section - How-To invert the colours of an Image Changed ^^^^^^^ - Image.__and__() now uses a new method which produces better results with colour images Fixed ^^^^^ - Numpy shape error in Image.__neg__() Removed ^^^^^^^ - stylo.prims.thicken was redundant so it has been removed v0.1.0 - 2017-08-02 ------------------- Initial Release PK!s5Ystylo/__init__.pyfrom .color import FillColor # noqa: F401 from .domain.transform import ( # noqa: F401 horizontal_shear, rotate, translate, vertical_shear, ) from .image import LayeredImage, SimpleImage # noqa: F401 from .math import anded, lerp # noqa: F401 from .shape import ( # noqa: F401 Circle, Ellipse, ImplicitXY, Rectangle, Shape, Square, Triangle, ) from .time import Timeline # noqa: F401 from ._version import __version__ # noqa: F401 PK! ostylo/_version.py__version__ = "0.9.2" PK![VVstylo/color/__init__.pyfrom .colorspaces import RGB8 # noqa: F401 from .fill import FillColor # noqa: F401 PK!lgstylo/color/colormap.pyfrom abc import ABC, abstractmethod from stylo.color.colorspaces import ColorSpace, RGB8 class ColorMap(ABC): def __init__(self, colorspace=None): if colorspace is None: colorspace = RGB8 self.colorspace = colorspace def __call__(self, shape, image_data=None): return self._paint(shape, image_data) def _parse_color(self, color): return self.colorspace.parse(color) @abstractmethod def _paint(self, shape, image_data): pass @property def colorspace(self): return self._colorspace @colorspace.setter def colorspace(self, value): if not issubclass(value, (ColorSpace,)): raise TypeError("Expected ColorSpace") self._colorspace = value PK!))stylo/color/colorspaces.pyimport re import struct from abc import ABC, abstractmethod rgb8_pattern = re.compile(r"\A[a-fA-F0-9]{6}\Z") class ColorSpace(ABC): @staticmethod @abstractmethod def parse(string): """Parse the color from its representation.""" pass class RGB8(ColorSpace): """Represents the 8-bit RGB colorspace.""" @staticmethod def parse(string): if rgb8_pattern.match(string): return struct.unpack("BBB", bytes.fromhex(string)) raise ValueError("String does not represent a valid color.") PK!V}ttstylo/color/fill.pyfrom stylo.color.colormap import ColorMap class FillColor(ColorMap): def __init__(self, color=None, colorspace=None): super().__init__(colorspace) if color is None: color = "000000" self.color = self._parse_color(color) def _paint(self, shape, image_data): image_data[shape] = self.color return image_data PK!`rrstylo/design/__init__.pyfrom .params import ( # noqa: F401 Position, StaticPosition, Trajectory, ParametricTrajectory, ) PK!stylo/design/_param_factory.py"""A :code:`ParameterGroup` is used to help define and manage a related set of parameters. For example a "Position" parameter group could contain values for the :code:`x` and :code:`y` position of an object. Another "Stickman" property group could contain all the values required to define a stickman with a particular size and shape. This module much like the :code:`stylo.domain._factory` module defines a number of functions for declaring new parameter groups of various types. By writing the base classes using code, we can ensure that all property groups follow the same interface. Currently the following functions are defined - :code:`define_parameter_group`: This defines a base, static parameter group. - :code:`define_time_dependent_parameter_group`: This defines a time-dependent parameter group, that can take some time :code:`t` and vary in time. It also provides some utilities for visualising these changes over time. """ from abc import ABC, abstractmethod import numpy as np from stylo.error import MissingDependencyError try: import matplotlib.pyplot as plt MATPLOTLIB = True except ImportError: MATPLOTLIB = False def pgroup_keys(params): """Provide the implementation of the :code:`keys` method for this parameter group One of the key features of a parameter group is the ability to use the dictionary unpacking syntax (:code:`**params`) to conveniently pass all the values into a function as a single unit. This syntax is enabled by implementing two methods :code:`keys` and :code:`__getitem__`, this function provides the implementation of the former. :param list params: The list of strings containing the names of the parameters. """ def keys(self): return params return keys def pgroup_parameters_prop(): """Provide the implementation of the :code:`parameters` property. The :code:`parameters` property returns a list of all the parameter names in the group. """ @property def parameters(self): return self.keys() return parameters def pgroup_getitem(): """Provide the implementation of the :code:`__getitem__` method. One of the key features of a parameter group is the ability to use the dictionary unpacking syntax (:code:`**params`) to conveniently pass all the values into a function as a single unit. This syntax is enabled by implementing two methods :code:`keys` and :code:`__getitem__`, this function provides the implementation of the latter. """ def __getitem__(self, key): return self._values[key] return __getitem__ def pgroup_init(): """Provide the implementation of the :code:`__init__` method.""" def __init__(self): self._values = self.calculate() return __init__ def pgroup_parameter_prop(param): """Provide the implementation of the property getter for the given parameter name. This enables the :code:`group.param` syntax. :param str param: The parameter name. """ @property def parameter(self): return self._values[param] return parameter def pgroup_calculate(): """Define an abstract method for the user to implement. The user must define this method to return the values dictionary containing each of the parameters in the group,. """ @abstractmethod def calculate(self): pass return calculate def pgroup_repr(name, params): """Provide the implementation of the :code:`__repr__` method. :param str name: The name of the parameter group. :param: list params: The list of parameter names. """ offset = max([len(p) for p in params]) + 10 def __repr__(self): cls_name = self.__class__.__name__ values = [ p + ":" + str(val).rjust(offset - len(p)) for p, val in self._values.items() ] ps = "\n ".join(values) return "{}: {}\n {}".format(cls_name, name, ps) return __repr__ def define_base_attributes(name, params): """Define the attributes, methods and properties that are common to every type of parameter group. :param str name: The name of the parameter group :param list params: The list of strings containing the parameter names. """ attributes = { "__doc__": "TODO: Make this docstring useful.", "__getitem__": pgroup_getitem(), "__repr__": pgroup_repr(name, params), "keys": pgroup_keys(params), "parameters": pgroup_parameters_prop(), "_parameters": params, } for p in params: attributes[p] = pgroup_parameter_prop(p) return attributes def get_parameter_names(parameters): """Convert the parameter string supplied by the user into a list of parameter names. :param list parameters: The comma separated string of parameter names """ return parameters.replace(" ", "").split(",") def define_parameter_group(name, parameters): """Define a new parameter group. :param str name: The name of the parameter group. :param list parameters: A list of strings containing the parameter names for the group. """ params = get_parameter_names(parameters) attributes = define_base_attributes(name, params) attributes["__init__"] = pgroup_init() attributes["calculate"] = pgroup_calculate() return type(name, (ABC,), attributes) def tdpgroup_calculate(): """Define an abstract :code:`calculate` method for users to implement. This method will serve the same role as the calculate method in a standard parameter group, but this one is dependent on time :code:`t`. """ @abstractmethod def calculate(self, t): pass return calculate def tdpgroup_init(): """Provide the implementation of the :code:`__init__` method.""" def __init__(self): self() return __init__ def tdpgroup_call(): """Provide the implementation of the :code:`__call__` method. This enables the :code:`**params(t)` functionality and use the parameter group as a time dependent function. """ def __call__(self, t=0): self._values = self.calculate(t) return self return __call__ def tdpgroup_plot(): """Provide the implementation of the :code:`plot` method. This enables users to plot the values as they change over time. """ def plot(self, start=0, stop=1, N=128, plot_size=8): if not MATPLOTLIB: message = ( "Unable to plot parameter group. Run `pip install stylo[jupyter]`" " to install the required dependencies." ) raise MissingDependencyError(message) cls = self.__class__ name = cls.__name__ base_name = cls.__base__.__name__ ts = np.linspace(start, stop, N) values = self.calculate(ts) fig, ax = plt.subplots(1, figsize=(plot_size, plot_size)) for pname, vals in values.items(): ax.plot(ts, vals, label=pname) # Include a legend, title and label the time axis. ax.legend() ax.set_title("{}: {}".format(name, base_name)) ax.set_xlabel("Time (seconds)") return ax return plot def define_time_dependent_parameter_group(name, parameters): """Define a new time dependent parameter group. :param str name: The name of the parameter group :param list parameters: A list of strings containing the parameter names for the group. """ params = get_parameter_names(parameters) attributes = define_base_attributes(name, params) attributes["__call__"] = tdpgroup_call() attributes["__init__"] = tdpgroup_init() attributes["calculate"] = tdpgroup_calculate() attributes["plot"] = tdpgroup_plot() return type(name, (ABC,), attributes) PK!P\\stylo/design/params.pyfrom ._param_factory import ( # noqa: F401 define_parameter_group, define_time_dependent_parameter_group, ) Position = define_parameter_group("Position", "x, y") Trajectory = define_time_dependent_parameter_group("Trajectory", "x, y") class StaticPosition(Position): """A basic implementation of the :code:`Position` parameter group. It takes an x and y and returns them. """ def __init__(self, x=0, y=0): self.x_pos = x self.y_pos = y super().__init__() def calculate(self): return {"x": self.x_pos, "y": self.y_pos} def id_func(t): return t class ParametricTrajectory(Trajectory): """A basic implementation of the :code:`Trajectory` time dependent parameter group. It takes two functions :code:`x(t)` and :code:`y(t)` and returns them. """ def __init__(self, x=None, y=None): if x is None: x = id_func if y is None: y = id_func self.x_t = x self.y_t = y super().__init__() def calculate(self, t): return {"x": self.x_t(t), "y": self.y_t(t)} PK!׃stylo/domain/__init__.pyfrom ._factory import define_domain RealDomain = define_domain("RealDomain", "x,y,r,t") from .rectangular import RectangularDomain, get_real_domain # noqa: F401 from .square import SquareDomain, UnitSquare # noqa: F401 PK!6rstylo/domain/_factory.py"""A :code:`Domain` is responsible for converting some abstract notion of a mathematical space into a discrete representation. Since :code:`stylo` is about creating images this representation is typically a 2D grid of numbers. This document details the machinery behind the :code:`Domain` system. More specifically how the :code:`define_domain` and :code:`define_domain_transform` functions work. It assumes you are already familiar with how the user interacts with :code:`Domain` objects and is written for someone who wishes to understand how they work behind the scenes. This module contains all the code that is required to automatically write both the definition of a new Domain type and the test case that users can use to verify that their implementations of said domains adhere to the expected interface. """ from abc import ABC, abstractmethod from textwrap import indent def domain_getitem(self, key): """Enable the domain[...] syntax for domains. Domains support the ability to return a function in :code:`(width, height)` of multiple parameters, for example for a :code:`RealDomain` you might want a function in both the :math:`x` and :math:`y` coordinates in which case you would write .. code-block:: python >>> values = domain['xy'] >>> values(N, N) (array([[x0, x1, ..., xN], [x0, x1, ..., XN], ... [x0, x1, ..., xN]]), array([[y0, y0, ..., y0], [y1, y1, ..., y1], ..., [yN, yN, ..., yN]])) .. note:: The string argument :code:`'xy'` only works in the case of single letter parameters. For multi-character parameters such as :code:`pt` this argument must be some other iterable such as a tuple e.g. :code:`('x', 'pt')`. This function constructs the implementation for the :code:`__getitem__` method that will be added to new :code:`Domain` definitions :param self: The standard python instance argument for methods :param key: The input that is given to the getitem syntax e.g. :code:`'xy'` :type self: object :type key: iterable """ return lambda w, h: tuple( self.__getattribute__(p)(w, h) for p in key if p in self._parameters ) def domain_property(base): """Construct the domain property for the domain transform base class. :param base: The base Domain class :type base: class """ transform_name = base.__name__ + "Transform" def getter(self): return self._domain def setter(self, value): if not isinstance(value, (base,)): message = "{transform}: Expected {base} instance." raise TypeError( message.format(transform=transform_name, base=base.__name__) ) self._domain = value return property(fget=getter, fset=setter) def domain_call(self, width, height): return lambda parameters: self[parameters](width, height) def parameters_property(parameters): """Given the list of parameters this function constructs a property that simply returns the given list. It doesn't provide a setter so that the list of parameters cannot be overridden.""" def getter(self): return parameters return property(fget=getter) def parameter_property(name): """Given the name of the parameter, construct the property definition that calls the abstract :code:`_get_name()` method that users will implement. :param name: The name of the parameter :type name: str """ def getter(self): return self.__getattribute__("_get_" + name)() return property(fget=getter) def mk_abstractmethod(): """Construct an abstractmethod that user must override when they create an instance of a class. """ def method(self): pass return abstractmethod(method) def transform_init(self, domain): """This implements the :code:`__init__` method for DomainTransform classes.""" self.domain = domain def transform_repr(self): """This implements the :code:`__repr__` method for DomainTransform classes.""" transform = self._repr() domain = repr(self.domain) return transform + "\n" + indent(domain, " ") def define_domain(name, parameters): """Given the name and the input parameters, construct the Domain definition. :param name: The name of the :code:`Domain` :param parameters: A list of strings that encode the names of the parameters within the domain :type name: str :type parameters: list(str) """ params = parameters.split(",") attributes = { "_parameters": params, "parameters": parameters_property(params), "__doc__": "TODO: Make this docstring useful.", "__getitem__": domain_getitem, "__call__": domain_call, } for p in params: attributes[p] = parameter_property(p) attributes["_get_" + p] = mk_abstractmethod() return type(name, (ABC,), attributes) def define_domain_transform(base): """This constructs the class definition for a domain transform. :param name: The name of the base domain :param base: The class definition of the base domain :type name: str :type base: class """ transform_name = base.__name__ + "Transform" attributes = { "__init__": transform_init, "__repr__": transform_repr, "_repr": mk_abstractmethod(), "domain": domain_property(base), } return type(transform_name, (base, ABC), attributes) PK!^6$$stylo/domain/helpers.pyimport numpy as np class PolarConversion: """A helper class for defining new domain classes. Say you are implementing a new class, perhaps the :code:`Translation` domain transform where the interesting part of that class is implementing the shift in :code:`x` or the shift in :code:`y`. The implementation of the :code:`r` and :code:`t` is then done with respect to the shifted :code:`x` and :code:`y` This class provides implementations of the :code:`_get_rs` and :code:`_get_ts` that handle this conversion for you automatically, leaving you to focus on implementing the transform that interest you. To use this class, simply include it in your :code:`class` definition as follows. .. code-block:: python class MyDomain(PolarConversion, RealDomain): def _get_xs(self): ... def _get_ys(self): ... .. note:: The order in which the classes are listed is *very* important. """ def _get_r(self): """The conversion to the radial component in terms of :math:`x` and :math:`y`. .. math:: r = \\sqrt{x^2 + y^2} """ xs = self._get_x() ys = self._get_y() def mk_rs(width, height): x = xs(width, height) y = ys(width, height) return np.sqrt(x * x + y * y) return mk_rs def _get_t(self): """The conversion to the angular component in terms of :math:`x` and :math:`y`. .. math:: \\theta = \\tan^{-1}{\\left(\\frac{y}{x}\\right)} """ xs = self._get_x() ys = self._get_y() def mk_ts(width, height): x = xs(width, height) y = ys(width, height) return np.arctan2(y, x) return mk_ts class CartesianConversion: """A helper class for defining new domain classes. Say you are implementing a new class, perhaps the :code:`Rotation` domain transform where the interesting part is the adjustments to the :code:`r` and :code:`t` coordinates. The implementation of :code:`x` and :code:`y` is then done with respect to the transformed :code:`r` and :code:`t` variables. This class provides implementations of the :code:`_get_xs` and :code:`_get_ys` methods to handle this conversion for you automatically, leaving you to focus on implementing the transform that interest you. To use this class, simply include it in your :code:`class` definition as follows .. code-block:: python class MyDomain(CartesianConversion, RealDomain): def _get_xs(self): ... def _get_ys(self): ... .. note:: The order in which the classes are listed is *very* important. """ def _get_x(self): """The conversion to the :math:`x` component in terms of :math:`r` and :math:`\\theta` .. math:: x = r\\cos{(\\theta)} """ rs = self._get_r() ts = self._get_t() def mk_xs(width, height): r = rs(width, height) t = ts(width, height) return r * np.cos(t) return mk_xs def _get_y(self): """The conversion to the :math:`y` component in terms of :math:`r` and :math:`\\theta` .. math:: y = r\\sin{(\\theta)} """ rs = self._get_r() ts = self._get_t() def mk_ys(width, height): r = rs(width, height) t = ts(width, height) return r * np.sin(t) return mk_ys PK!zY# # stylo/domain/rectangular.pyimport numpy as np from stylo.domain import RealDomain from stylo.domain.helpers import PolarConversion from stylo.utils import bounded_property class RectangularDomain(PolarConversion, RealDomain): """A domain of the form :math:`[a, b] \\times [c, d] \\subset \\mathbb{R}^2`""" xmin = bounded_property("xmin", bounded_above_by="xmax") xmax = bounded_property("xmax", bounded_below_by="xmin") ymin = bounded_property("ymin", bounded_above_by="ymax") ymax = bounded_property("ymax", bounded_below_by="ymin") def __init__(self, xmin, xmax, ymin, ymax): if xmin >= xmax: raise ValueError( "The value of xmin must be strictly less than the " "value of xmax" ) if ymin >= ymax: raise ValueError( "The value of ymin must be strictly less than the " "value of ymax" ) self._xmin = xmin self._ymin = ymin self._xmax = xmax self._ymax = ymax def __repr__(self): name = self.__class__.__name__ return "{1}: [{0.xmin}, {0.xmax}] x [{0.ymin}, {0.ymax}]".format(self, name) def _get_x(self): def mk_xs(width, height): xs = np.linspace(self.xmin, self.xmax, width) xs = np.array([xs for _ in range(height)]) return xs return mk_xs def _get_y(self): def mk_ys(width, height): ys = np.linspace(self.ymax, self.ymin, height) ys = np.array([ys for _ in range(width)]) ys = ys.transpose() return ys return mk_ys def get_real_domain(width, height, scale=2): """Given the width and height for an image return a rectangular domain with appropriate bounds for the image's aspect ratio. So that shapes are not distorted when mapped onto non-square images the domain they are mapped onto needs to match the aspect ratio of the image otherwise the shape will be distorted in some way. This function will construct a RectangularDomain domain with :math:`(0, 0)` at the centre of the image, the overall size of the domain can be controlled with the :code:`scale` parameter :param width: The width of the image in pixels :param height: The height of the image in pixels :param scale: This constrols the size of the domain and corresponds to the length of the interval :math:`[ymin, ymax]` :type width: int :type height: int :type scale: float :returns: An appropriately sized domain. :rtype: RectangularDomain """ if height <= 0: raise ValueError("The height of the image must be a positive number.") if width <= 0: raise ValueError("The width of the image must be a positive number.") if scale <= 0: raise ValueError("The scale of the image must be strictly positive.") aspect_ratio = width / height ylength = scale / 2 xlength = ylength * aspect_ratio ymin, ymax = -ylength, ylength xmin, xmax = -xlength, xlength return RectangularDomain(xmin, xmax, ymin, ymax) PK!] stylo/domain/square.pyfrom .rectangular import RectangularDomain class SquareDomain(RectangularDomain): """ A domain of the form :math:`[a, b] \\times [a, b] \\subset \\mathbb{R}^2`.""" def __init__(self, x_min, x_max): super().__init__(x_min, x_max, x_min, x_max) @RectangularDomain.xmin.setter def xmin(self, value): RectangularDomain.xmin.fset(self, value) RectangularDomain.ymin.fset(self, value) @RectangularDomain.ymin.setter def ymin(self, value): RectangularDomain.ymin.fset(self, value) RectangularDomain.xmin.fset(self, value) @RectangularDomain.xmax.setter def xmax(self, value): RectangularDomain.xmax.fset(self, value) RectangularDomain.ymax.fset(self, value) @RectangularDomain.ymax.setter def ymax(self, value): RectangularDomain.ymax.fset(self, value) RectangularDomain.xmax.fset(self, value) class UnitSquare(SquareDomain): """The domain :math:`[0, 1] \\times [0, 1] \\subset \\mathbb{R}^2`.""" def __init__(self): super().__init__(0, 1) @SquareDomain.xmax.setter def xmax(self, value): raise AttributeError("can't set attribute") @SquareDomain.xmin.setter def xmin(self, value): raise AttributeError("can't set attribute") @SquareDomain.ymin.setter def ymin(self, value): raise AttributeError("can't set attribute") @SquareDomain.ymax.setter def ymax(self, value): raise AttributeError("can't set attribute") PK!k^h"stylo/domain/transform/__init__.pyfrom .transform import RealDomainTransform, define_transform # noqa: F401 from .rotation import rotate # noqa: F401 from .shear import horizontal_shear, vertical_shear # noqa: F401 from .translation import translate # noqa: F401 PK!kK"stylo/domain/transform/rotation.pyfrom stylo.domain.helpers import CartesianConversion from stylo.domain.transform import RealDomainTransform, define_transform class Rotation(CartesianConversion, RealDomainTransform): """A domain that applies a rotation to the domain provided to it.""" def __init__(self, domain, angle): super().__init__(domain) self.angle = angle def _repr(self): return "Rotation: {}".format(self.angle) def _get_r(self): return self.domain.r def _get_t(self): ts = self.domain.t def mk_ts(width, height): return ts(width, height) + self.angle return mk_ts rotate = define_transform(Rotation) PK!;stylo/domain/transform/shear.pyfrom stylo.domain.helpers import PolarConversion from stylo.domain.transform import RealDomainTransform, define_transform class HorizontalShear(PolarConversion, RealDomainTransform): """A horizontal shear transform.""" def __init__(self, domain, k=0): super().__init__(domain) self.k = -k def _repr(self): return "HorizontalShear: {}".format(-self.k) def _get_y(self): return self.domain.y def _get_x(self): xs = self.domain.x ys = self.domain.y def mk_xs(width, height): return xs(width, height) + self.k * ys(width, height) return mk_xs class VerticalShear(PolarConversion, RealDomainTransform): """A vertical shear transform.""" def __init__(self, domain, k=0): super().__init__(domain) self.k = -k def _repr(self): return "VerticalShear: {}".format(-self.k) def _get_x(self): return self.domain.x def _get_y(self): xs = self.domain.x ys = self.domain.y def mk_ys(width, height): return ys(width, height) + self.k * xs(width, height) return mk_ys vertical_shear = define_transform(VerticalShear) horizontal_shear = define_transform(HorizontalShear) PK!I//#stylo/domain/transform/transform.pyfrom stylo.domain import RealDomain from stylo.domain._factory import define_domain_transform from stylo.shape import Shape RealDomainTransform = define_domain_transform(RealDomain) def find_base_transform(transform): """Given a domain transform, find its base class""" for base in transform.__bases__: if hasattr(base, "domain"): return base name = transform.__name__ raise TypeError("{} is not a domain transform".format(name)) def find_base_domain(base_transform): """Given a base domain transform class, find the base domain that it transform.""" for base in base_transform.__bases__: if hasattr(base, "_parameters") and not hasattr(base, "domain"): return base name = base_transform.__name__ raise TypeError("{} is not a base domain transform".format(name)) class DomainTransformer: """A class used to apply transformations, it enables us to do the syntactic nicities when applying domain transforms.""" def __init__(self, transform, *args, **kwargs): self.transform = transform self.args = args self.kwargs = kwargs self.base_transform = find_base_transform(transform) self.base_domain = find_base_domain(self.base_transform) self.name = transform.__name__.lower def __rrshift__(self, other): return self.apply_transform(other) def apply_transform(self, obj): transform = self.transform args = self.args kwargs = self.kwargs if isinstance(obj, (self.base_domain,)): return transform(obj, *args, **kwargs) if isinstance(obj, (Shape,)): obj._add_transform(lambda domain: self.transform(domain, *args, **kwargs)) return obj obj_name = obj.__class__.__name__ message = "Unable to perform a {} to a {}." raise TypeError(message.format(self.name, obj_name)) def define_transform(transform): def transform_func(*args, **kwargs): return DomainTransformer(transform, *args, **kwargs) return transform_func PK!uQ@33%stylo/domain/transform/translation.pyfrom stylo.domain.helpers import PolarConversion from stylo.domain.transform import RealDomainTransform, define_transform class Translation(PolarConversion, RealDomainTransform): """A domain that applies a translation to the domain provided to it.""" def __init__(self, domain, dx, dy): super().__init__(domain) self.dx = -dx self.dy = -dy def _repr(self): return "Translation: ({0}, {1})".format(-self.dx, -self.dy) def _get_x(self): xs = self.domain.x def mk_xs(width, height): return xs(width, height) + self.dx return mk_xs def _get_y(self): ys = self.domain.y def mk_ys(width, height): return ys(width, height) + self.dy return mk_ys translate = define_transform(Translation) PK!pstylo/error.pyclass StyloError(Exception): """An exception to group all stylo related errors under.""" pass class MissingDependencyError(StyloError): """An exception to represent a missing dependency.""" pass PK!R)stylo/image/__init__.pyfrom .image import Image # noqa: F401 from .layered import LayeredImage # noqa: F401 from .simple import SimpleImage # noqa: F401 PK!stylo/image/image.pyimport base64 import io import PIL.Image from abc import ABC, abstractmethod from stylo.error import MissingDependencyError try: import matplotlib.pyplot as plt MATPLOTLIB = True except ImportError: MATPLOTLIB = False class Drawable: def __init__(self, domain, shape, color): self.domain = domain self.shape = shape self.color = color def __iter__(self): return iter([self.domain, self.shape, self.color]) class Image(ABC): def __call__( self, width, height, filename=None, plot_size=None, encode=None, preview=MATPLOTLIB, ): self.data = self._render(width, height) if filename: self.save(filename) return if encode: return self.encode() if preview: return self.preview(plot_size) def preview(self, plot_size): """Generate a matplotlib plot of the image data which can then be viewed from a Jupyter Notebook. :param plot_size: This controls how large the generated plot is :type plot_size: int :returns: A matplotlib AxesImage object. """ if not MATPLOTLIB: message = ( "Unable to generate image preview. Run `pip install stylo[jupyter]`" " to install the required dependencies." ) raise MissingDependencyError(message) if plot_size is None: plot_size = 12 fig, ax = plt.subplots(1, figsize=(plot_size, plot_size)) # Hide the axis - show just the image fig.axes[0].get_yaxis().set_visible(False) fig.axes[0].get_xaxis().set_visible(False) # Draw the image ax.imshow(self.data) # Return just the axis, jupyter notebooks will capture the figure and # display it anyway. return ax def encode(self): """Encode the image as a PNG represented by a base64 string. :return: The image encoded as a base64 string. """ image = self._to_pil_image() with io.BytesIO() as byte_stream: image.save(byte_stream, "PNG") image_bytes = byte_stream.getvalue() return base64.b64encode(image_bytes) def save(self, filename): """Save the image to file as a PNG image. If the given filename already exists the existing image will be overwritten. :param filename: The file to save the image to. :type filename: str """ image = self._to_pil_image() with open(filename, "wb") as f: image.save(f) def _to_pil_image(self): """Convert the numpy representation of the image into a PIL.Image object.""" height, width, _ = self.data.shape return PIL.Image.frombuffer( "RGB", (width, height), self.data, "raw", "RGB", 0, 1 ) @property def data(self): return self._data @data.setter def data(self, value): self._data = value @abstractmethod def _render(self, width, height): pass def render_drawable(drawable, image_data): """Given a drawable, render it onto the given image data.""" domain, shape, color = drawable domain = shape._apply_transform(domain) height, width, depth = image_data.shape parameters = shape.parameters values = domain[parameters](width, height) coords = {k: v for k, v in zip(parameters, values)} mask = shape(**coords) return color(mask, image_data=image_data) PK!)Mstylo/image/layered.pyimport numpy as np from stylo.color import RGB8 from stylo.domain import get_real_domain from stylo.image import Image from stylo.image.image import Drawable, render_drawable class LayeredImage(Image): def __init__(self, background=None, scale=2, colorspace=None): self.scale = scale if background is None: background = "ffffff" if colorspace is None: colorspace = RGB8 self.background = colorspace.parse(background) self.colorspace = colorspace self.layers = [] def add_layer(self, shape, color, domain=None): # Make sure everyone uses the same colorspace. color.colorspace = self.colorspace self.layers.append(Drawable(domain, shape, color)) def _render(self, width, height): domain = get_real_domain(width, height, self.scale) dimensions = (height, width, len(self.background)) image_data = np.full(dimensions, self.background, dtype=np.uint8) for drawable in self.layers: if drawable.domain is None: drawable.domain = domain image_data = render_drawable(drawable, image_data) return image_data PK!*bAstylo/image/simple.pyimport numpy as np from stylo.domain import get_real_domain from stylo.image.image import Drawable, Image, render_drawable class SimpleImage(Image): def __init__(self, shape, color, background=None, scale=2): self.shape = shape self.color = color self.scale = scale if background is None: background = "ffffff" self.background = color._parse_color(background) def _render(self, width, height): domain = get_real_domain(width, height, scale=self.scale) drawable = Drawable(domain, self.shape, self.color) dimensions = (height, width, len(self.background)) image_data = np.full(dimensions, self.background, dtype=np.uint8) return render_drawable(drawable, image_data) PK!ntUUstylo/math/__init__.pyfrom .logic import anded # noqa: F401 from .interpolation import lerp # noqa: F401 PK!Cstylo/math/interpolation.pydef lerp(a, b): """Return a function that will linearly interpolate between :code:`a` and :code:`b`. The returned function will take the value :code:`a` when passed the value :code:`0` and will increase/decrease linearly to the value :code:`b`, taking that value at the point :code:`1`. :param float a: The value the function should take at :code:`0` :param float b: The value the function should take at :code:`1` """ def s(t): return (1 - t) * a + t * b return s PK!stylo/math/logic.pyimport numpy as np class LogicalAnd: """Represents a logical AND between N values. Yes, there is currently no reason to use a class but where I want to take this in the near future will require a class. """ def __init__(self, values): self.values = values def __call__(self): result = True for v in self.values: result = np.logical_and(result, v) return result def anded(*args): logical_and = LogicalAnd(args) return logical_and() PK!*stylo/shape/__init__.pyfrom .basic import Rectangle, Square, Ellipse, Circle, Triangle, Line # noqa: F401 from .curves import ImplicitXY # noqa: F401 from .shape import Shape # noqa: F401 PK!X-I3I3stylo/shape/basic.py"""This module defines the standard shapes you would expect to see from any graphics package such as lines, simple curves, circles, squares, ellipses etc. """ import numpy as np from stylo.shape.shape import Shape def define_ellipse(a, b): """An ellipse can be implicitly defined by the equation. .. math:: \\frac{x^2}{a^2} + \\frac{y^2}{b^2} = r^2 where: - :math:`a` is known as the semi-major axis. Larger values stretch the ellipse along the :math:`x`-direction - :math:`b` is known as the semi-minor axis. Larger values stretch the ellipse along the :math:`y`-direction - :math:`r` is the radius and controls the overall size. This function will calculate the value of the left hand side of the above equation. :param a: The value of the semi-major axis :param b: The value of the semi-minor axis. :type a: float :type b: float :returns: The function in :code:`(x, y)` for the left hand side of the equation above. """ a = a * a b = b * b def ellipse(x, y): return (x * x) / a + (y * y) / b return ellipse def define_line(p1, p2): """Return a function than can be used to determine if a given point lies on the line joining the points :math:`p_1` and :math:`p_2`. This function assumes that your line is **not** vertical. The function returned will be the implicit form of a straight line .. math:: f(x, y) = \\frac{(y_2 - y_1)}{(x_2 - x_1)}(x - x_1) + (y_1 - y) :param p1: The :math:`(x_1, y_1)` coordinate of :math:`p_1` :param p2: The :math:`(x_2, y_2)` coordinate of :math:`p2` :type p1: tuple :type p2: tuple """ x1, y1 = p1 x2, y2 = p2 m = (y2 - y1) / (x2 - x1) def line(x, y): return m * (x - x1) + (y1 - y) return line class Line(Shape): """We can define a line joining two points :math:`p_1, p_2` implicitly using the following equation. .. math:: f(x, y) = \\frac{(y_2 - y_1)}{(x_2 - x_1)}(x - x_1) + (y_1 - y) = 0 where: - :math:`x_2 \\neq x_1` - :math:`p_1 = (x_1, y_1)` - :math:`p_2 = (x_2, y_2)` However since a line has no area we would never see it so we need to introduce an error margin :math:`e > 0`. We will say a point is on the line if .. math:: |f(x, y)| \\leq e """ def __init__( self, p1=None, p2=None, pt=0.01, extend=False, above=False, below=False ): """Construct an instance of :code:`Line` depending on the the given parameters. By default only a line segment is drawn joining the two points :code:`p1, p2`. However if :code:`extend=True` then the line will be extended off to infinity. The thickness of the line can be controlled with the parameter :code:`pt`, which corresponds to :math:`e` in the equations above. :param p1: The first coordinate used to define the line. (Default :code:`(0, 0)`) :param p2: The second coordinate used to define the line. (Default :code:`(1, 1)`) :param pt: This defines the thickness of the line. (Default :code:`0.01`) :param extend: If :code:`True` the line will be extended off to infinity. :param above: If :code:`True` then the area above the line is shaded. Only takes effect when :code:`extend=True` :param below: If code:`True` the the area below the line is shaded. Only takes effect when :code:`extend=True` :type p1: tuple :type p2: tuple :type pt: float :type extend: bool :type above: bool :type below: bool """ p1 = (0, 0) if p1 is None else p1 p2 = (1, 1) if p2 is None else p2 # It makes things easier if we can assume x1 < x2 points = sorted([p1, p2], key=lambda p: p[0]) self.p1 = points[0] self.p2 = points[1] self.pt = pt self.extend = extend self.above = above self.below = below def draw(self): pt = self.pt line_definition = define_line(self.p1, self.p2) if self.extend and self.above: def above_line(x, y): return line_definition(x, y) <= 0 return above_line if self.extend and self.below: def below_line(x, y): return line_definition(x, y) >= 0 return below_line if self.extend: def line(x, y): error = np.abs(line_definition(x, y)) return error <= pt return line x1, _ = self.p1 x2, _ = self.p2 def line_segment(x, y): error = np.abs(line_definition(x, y)) <= pt after = x1 < x before = x < x2 in_bounds = np.logical_and(after, before) return np.logical_and(in_bounds, error) return line_segment class Ellipse(Shape): """One way of defining an ellipse centered at :math:`(x_0, y_0)` is through the implicit equation. .. math:: \\frac{(x - x_0)^2}{a^2} + \\frac{(y - y_0)^2}{b^2} - r^2 = 0 where: - :math:`a` is known as the semi-major axis. Larger values stretch the ellipse along the :math:`x`-direction - :math:`b` is known as the semi-minor axis. Larger values stretch the ellipse along the :math:`y`-direction - :math:`r` is the radius and controls the overall size. By playing around with this definition we can draw just the outline of the ellipse with a given thickness or a filled in version of the ellipse. """ def __init__( self, x=0, y=0, a=2, b=1, r=0.5, pt=0.01, fill=False, start_angle=None, end_angle=None, ): """Construct an instance of :code:`Ellipse` based on given parameters. By default this will define an ellipse that only draws it's outline. If you want a filled in ellipse then see the :code:`fill` parameter. If values for :code:`start_angle` and :code:`end_angle` are provided the portion of the ellipse drawn will be restricted to the portion between those two angles. If only one of the the values are set then only an upper/lower bound will be imposed. .. note:: All angles are in :term:`radians`. :param x: The :math:`x`-coordinate of the center of the ellipse. (Default: :code:`0`) :param y: The :math:`y`-coordinate of the center of the ellipse. (Default: :code:`0`) :param a: The length of the semi-major axis (Default: :code:`2`) :param b: The length of the semi-minor axis. (Default: :code:`1`) :param r: The radius of the ellipse. (Default: :code:`0.5`) :param pt: This controls the thickness of the line used to draw the ellipse. (Default: :code:`0.01`). Has no effect if :code:`fill` is used. :param fill: If true the ellipse is drawn as a shaded region instead of a curve. (Default: :code:`False`) :param start_angle: If set only draw the ellipse for values of :math:`\\theta \\geq t` (Default: :code:`None`) :param end_angle: If set only draw the ellipse for values of :math:`\\theta \\leq t` (Default: :code:`None`) :type x: float :type y: float :type a: float :type b: float :type r: float :type pt: float :type fill: bool :type start_angle: float :type end_angle: float :raises ValueError: If the provided arguments are inconsistent in some way. """ # These define the properties of the ellipse itself. self.x = x self.y = y self.a = a self.b = b self.r = r # These define how the ellipse is actually drawn. self.pt = pt self.fill = fill if start_angle and end_angle and end_angle <= start_angle: raise ValueError("start_angle must be strictly less than end_angle") self.start_angle = start_angle self.end_angle = end_angle def __repr__(self): return "Ellipse(x={0.x}, y={0.y}, a={0.a}, b={0.b}, r={0.r})".format(self) def _get_filled_ellipse(self): """Return a function that will draw a filled in ellipse.""" x0 = self.x y0 = self.y r = self.r * self.r ellipse = define_ellipse(self.a, self.b) def ellipse_fill(x, y): return ellipse(x - x0, y - y0) <= r return ellipse_fill def _get_ellipse(self): """Return a function that will draw an ellipse as a curve.""" x0 = self.x y0 = self.y pt = self.pt r = self.r * self.r ellipse = define_ellipse(self.a, self.b) def ellipse_curve(x, y): error = ellipse(x - x0, y - y0) - r return np.abs(error) <= pt return ellipse_curve def draw(self): if self.fill: return self._get_filled_ellipse() return self._get_ellipse() class Circle(Ellipse): """ Mathematically a circle can be defined as the set of all points :math:`(x, y)` that satisfy .. math:: (x - x_0)^2 + (y - y_0)^2 = r^2 This function returns another function which when given a point :code:`(x, y)` will return :code:`True` if that point is in the circle """ def __init__(self, x=0, y=0, r=0.5, *args, **kwargs): super().__init__(x, y, 1, 1, r, *args, **kwargs) def __repr__(self): return "Circle(x={0.x},y={0.y},r={0.r})".format(self) class Rectangle(Shape): """ It's quite simple to define a rectangle, simply pick a point :math:`(x_0,y_0)` that you want to be the center and then two numbers which will represent the width and height of the rectangle. """ def __init__(self, x, y, width, height): self.x = x self.y = y self.width = width self.height = height def __repr__(self): arg_string = "x={0.x},y={0.y},width={0.width},height={0.height}".format(self) return "Rectangle({})".format(arg_string) def draw(self): left = self.x - (self.width / 2) right = self.x + (self.width / 2) top = self.y + (self.height / 2) bottom = self.y - (self.height / 2) def rectangle(x, y): xs = np.logical_and(left < x, x < right) ys = np.logical_and(bottom < y, y < top) return np.logical_and(xs, ys) return rectangle class Square(Rectangle): def __init__(self, x, y, size): super().__init__(x, y, size, size) def __repr__(self): return "Square(x={0.x},y={0.y},size={0.width})".format(self) class Triangle(Shape): """ A Triangle can be defined by picking three non-collinear points :math:`(a, b, c)` in the form of a tuple each. Points inside the triangle are determined using the barycentric coordinate system method, which is further explained `in this answer `_ and the first method `here `_. """ # noqa E501 def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __repr__(self): return "Triangle(a={0.a}, b={0.b}, c={0.c})".format(self) def get_q(self): """ Returns the denominator for calculating both the barycentric coordinates, which absolute value is the double of the area of the triangle. """ a = self.a b = self.b c = self.c q = -b[1] * c[0] + a[1] * (-b[0] + c[0]) + a[0] * (b[1] - c[1]) + b[0] * c[1] return q def get_s(self, x, y): """ Returns one of the barycentric coordinates of the triangle using both :math:`a` and :math:`c` points. """ a = self.a c = self.c sign = -1 if self.get_q() < 0 else 1 s = (a[1] * c[0] - a[0] * c[1] + (c[1] - a[1]) * x + (a[0] - c[0]) * y) * sign return s def get_t(self, x, y): """ Returns one of the barycentric coordinates of the triangle using both :math:`a` and :math:`b` points. """ a = self.a b = self.b sign = -1 if self.get_q() < 0 else 1 t = (a[0] * b[1] - a[1] * b[0] + (a[1] - b[1]) * x + (b[0] - a[0]) * y) * sign return t def draw(self): def triangle(x, y): sign = -1 if self.get_q() < 0 else 1 first_condition = self.get_s(x, y) > 0 second_condition = self.get_t(x, y) > 0 third_condition = self.get_s(x, y) + self.get_t(x, y) < self.get_q() * sign return np.logical_and( first_condition, np.logical_and(second_condition, third_condition) ) return triangle PK!ostylo/shape/curves.pyimport numpy as np from stylo.shape.shape import Shape class ImplicitXY(Shape): """Define a curve using an implicit function in the :math:`x` and :math:`y` coordinates. A point :math:`(x, y)` lies on the curve defined by the function :math:`f(x, y)` if the following holds. .. math:: f(x, y) = 0 However since paths are one dimensional objects and have no area, if we used this rule as it stands we would never see the curve in question. So instead we introduce some error margin :math:`e > 0` and say that a point is on the curve if the following holds. .. math:: |f(x, y)| \\leq e """ def __init__(self, f, pt=0.01, above=False, below=False): """Construct an instance of :code:`ImplicitXY` according to the given parameters. :param f: The function that defines the curve. :param pt: The error margin, corresponds to :math:`e` in the above. (Default: 0.01) :param above: If :code:`True` draw the area above the curve as a shaded region (Default: :code:`False`) :param below: If :code:`True` draw the area underneath the curve as a shaded region (Default: False) """ self.f = f self.pt = pt self.above = above self.below = below def draw(self): if self.below: def below_curve(x, y): return self.f(x, y) >= 0 return below_curve if self.above: def above_curve(x, y): return self.f(x, y) <= 0 return above_curve def curve(x, y): error = self.f(x, y) return np.abs(error) <= self.pt return curve PK![!stylo/shape/shape.pyimport numpy as np from abc import ABC, abstractmethod from textwrap import indent from stylo.domain import RealDomain from stylo.utils import get_parameters class Shape(ABC): """A shape constructs a boolean 2D array that represents some shape or region.""" def __new__(cls, *args, **kwargs): instance = super().__new__(cls) instance._transforms = [] return instance def __invert__(self): return InvertedShape(self) def __and__(self, other): return self._logical_op(ANDedShape, other) def __or__(self, other): return self._logical_op(ORedShape, other) def __xor__(self, other): return self._logical_op(XORedShape, other) def __call__(self, *args, **kwargs): return self._render(*args, **kwargs) def _logical_op(self, cls, other): if not isinstance(other, (Shape,)): raise TypeError("Shape: Expected Shape instance.") return cls(self, other) def _add_transform(self, transform_func): self._transforms.insert(0, transform_func) def _apply_transform(self, domain): if not self._transforms: return domain for transform in self._transforms: domain = transform(domain) return domain def _render(self, *args, **kwargs): """Override this function if you want to change the default evaluation rules.""" if len(args) > 0 and isinstance(args[0], (RealDomain,)): return self._render_domain(*args) return self._render_args(**kwargs) def _render_domain(self, *args): domain = args[0] shape = self.draw() coordinates = get_parameters(shape) values = domain[coordinates] width, height = args[1], args[2] return shape(*values(width, height)) def _render_args(self, **kwargs): shape = self.draw() return shape(**kwargs) @property def parameters(self): return get_parameters(self.draw()) @abstractmethod def draw(self): pass class InvertedShape(Shape): """A shape that has been inverted.""" def __init__(self, shape): self.shape = shape def __repr__(self): return "NOT [ " + repr(self.shape) + " ]" def _render_domain(self, *args): mask = self.shape._render_domain(*args) return np.logical_not(mask) def _render_args(self, **kwargs): mask = self.shape._render_args(**kwargs) return np.logical_not(mask) def draw(self): pass @property def parameters(self): return self.shape.parameters def composite_shape_factory(op, name, op_name): """A factory function that returns class definitions for composite shapes constructed using binary logical operations such as AND, OR, etc. :param op: The function that implements the operation in question :param name: The name to give the composite class. :param op_name: The name of the logical operation, this will be used in the object's __repr__ :type op: callable :type name: str :type op_name: str :rtype: class """ class CompositeShape(Shape): """Represents a shape that has been anded together.""" def __init__(self, a, b): self.a = a self.b = b def __repr__(self): a_repr = indent("-- " + repr(self.a), "| ") b_repr = indent("-- " + repr(self.b), "| ") return "\n".join([op_name, a_repr, b_repr]) def _render_domain(self, *args): a = self.a._render_domain(*args) b = self.b._render_domain(*args) return op(a, b) def _render_args(self, **kwargs): a_params = self.a.parameters b_params = self.b.parameters a_values = {k: v for k, v in kwargs.items() if k in a_params} b_values = {k: v for k, v in kwargs.items() if k in b_params} aas = self.a._render_args(**a_values) bbs = self.b._render_args(**b_values) return op(aas, bbs) @property def parameters(self): a = {*self.a.parameters} b = {*self.b.parameters} return tuple(a | b) def draw(self): pass CompositeShape.__name__ = name return CompositeShape ANDedShape = composite_shape_factory(np.logical_and, "ANDedShape", "AND") ORedShape = composite_shape_factory(np.logical_or, "ORedShape", "OR") XORedShape = composite_shape_factory(np.logical_xor, "XORedShape", "XOR") PK!stylo/testing/__init__.pyfrom .strategies import ( # noqa: F401 real, angle, dimension, small_dimension, image_size, domain_values, shape_mask, ) from .color import BaseColorMapTest # noqa: F401 from .design import ( # noqa: F401 define_parameter_group_test, define_time_dependent_parameter_group_test, BasePositionTest, BaseTrajectoryTest, ) from .domain import define_domain_test, BaseRealDomainTest # noqa: F401 from .image import BaseImageTest # noqa: F401 from .shape import BaseShapeTest # noqa: F401 PK!V'' stylo/testing/_domain_factory.py"""This module contains functions that automate the process of writing base test classes for :code:`Domain` objects. """ import pytest import numpy as np from stylo.error import MissingDependencyError try: from hypothesis import given from stylo.testing import dimension except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err DOMAIN_TEST_DOCSTRING = """ A base class to use when testing {0} implementations. When writing your test case declare it as follows: .. code-block:: python from unittest import TestCase from stylo.testing.domain import Base{0}Test class TestMyDomain(TestCase, Base{0}Test): ... .. note:: The order in which you include these classes is *very* important. You also need to define a :code:`setUp()` method to set the :code:`domain` attribute to be an instance of your class. .. code-block:: python def setUp(self): self.domain = MyDomain() If you follow these steps in addition to the test cases you write checking the specifics of your class it will also be checked to see that it conforms to the behavior that is expected from every {0} instance. """ PARAMETER_READ_ONLY_DOCSTRING = """Ensure that the :code:`{0}` property is read only.""" PARAMETER_DOCSTRING = """ Ensure that the :code:`{0}` property works as expected. The :code:`{0}` property should: - Return a function in width and height. - When called it should return a numpy array. - The array should have shape :code:`(height, width)` """ def parameter_read_only_check(name): """Given the name of a parameter to check, make sure that the corresponding attribute is read-only. :param name: The name of the parameter :type name: str """ def check(self): with pytest.raises(AttributeError) as err: self.domain.__setattr__(name, 2) self.assertIn("can't set attribute", str(err.value)) check.__doc__ = PARAMETER_READ_ONLY_DOCSTRING.format(name) return check def parameter_check(name): """Given the name of a parameter to check, make sure that the corresponding attribute returns a function that works as expected. :param name: The name of the parameter :type name: str """ context = given(width=dimension, height=dimension) def check(self, width, height): param = self.domain.__getattribute__(name) self.assertTrue( callable(param), "The {} property should return a function".format(name) ) ps = param(width, height) self.assertTrue( isinstance(ps, (np.ndarray,)), "The function should return a numpy array" ) self.assertEqual( (height, width), ps.shape, "The resulting array should have shape (height, width)", ) check.__doc__ = PARAMETER_DOCSTRING.format(name) return context(check) def define_domain_test(domain): """Given a domain definition, write the test class that will automatically verify that a given subclass follows the expected interface. """ test_name = "Base" + domain.__name__ + "Test" attributes = {"__doc__": DOMAIN_TEST_DOCSTRING.format(domain.__name__)} for param in domain._parameters: read_only = "test_parameter_{}_read_only".format(param) p = "test_parameter_{}".format(param) attributes[read_only] = parameter_read_only_check(param) attributes[p] = parameter_check(param) return type(test_name, (), attributes) PK!s"u<stylo/testing/_param_factory.py"""This module contains functions that automate the process of writing base test classes for :code:`ParameterGroup` objects. """ import pytest import numpy as np from stylo.error import MissingDependencyError try: from hypothesis import given from stylo.testing.strategies import real except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err PARAM_TEST_DOCSTRING = """ A base class to use when testing {0} implementations. When writing your test case, declare it as follows: .. code-block:: python from unittest import TestCase from stylo.testing.design import Base{0}Test class TestMyGroup(TestCase, Base{0}Test): ... .. note:: The order in which you include these classes is *very* important. You also need to define a :code:`setUp()` method to set the :code:`params` attribute to be an instance of your class. .. code-block:: python def setUp(self): self.params = MyParams() If you follow these steps in addition to the test cases you write your class will be automatically checked to see that it conforms to the behavior expected from every {0}. """ def keys_check(expected_keys): """Parameter groups should define a :code:`keys()` method that returns the name of each of the parameters in the group. """ def check(self): assert self.params.keys() == expected_keys return check def getitem_check(keys): """Parameter groups should define :code:`__getitem__` and return a value for each parameter in the group.""" def check(self): for key in keys: self.params[key] return check def mapping_check(expected_keys): """Parameter groups should support the dictionary unpacking syntax :code:`**params`. """ def check(self): def map_func(**kwargs): assert set(kwargs.keys()) == set(expected_keys) map_func(**self.params) return check def parameters_check(expected_params): """Parameter groups should define a :code:`parameters` property that returns a list of the parameter names.""" def check(self): assert self.params.parameters == expected_params return check def parameters_check_read_only(): """The :code:`parameters` property should be read only.""" def check(self): with pytest.raises(AttributeError) as err: self.params.parameters = "2" assert "can't set attribute" in str(err.value) return check def parameter_check(parameter): """Parameter groups should define a property for each parameter name.""" def check(self): self.params.__getattribute__(parameter) return check def parameter_check_read_only(parameter): """The declared parameter should be read only.""" def check(self): with pytest.raises(AttributeError) as err: self.params.__setattr__(parameter, 2) assert "can't set attribute" in str(err.value) return check def define_base_tests(pgroup): """Define the test cases that are common to every type of parameter group. :param list pgroup: The parameter group to define test cases for. """ params = pgroup._parameters attributes = { "__doc__": PARAM_TEST_DOCSTRING.format(pgroup.__name__), "test_getitem": getitem_check(params), "test_keys": keys_check(params), "test_parameters": parameters_check(params), "test_parameters_read_only": parameters_check_read_only(), "test_mapping": mapping_check(params), } for pname in params: property_check = "test_{}_property".format(pname) read_only_check = "test_{}_property_read_only".format(pname) attributes[property_check] = parameter_check(pname) attributes[read_only_check] = parameter_check_read_only(pname) return attributes def define_parameter_group_test(pgroup): """Given a parameter group, write the test class that will automatically verify that a given subclass follows the expected interface. """ test_name = "Base" + pgroup.__name__ + "Test" attributes = define_base_tests(pgroup) return type(test_name, (), attributes) def call_check(expected_keys): """Time dependent parameter groups should be able to be called as a function, taking the time :code:`t` as an argument.""" context = given(t=real) def check(self, t): def map_check(**kwargs): assert set(kwargs.keys()) == set(expected_keys) result = self.params(t) # The parameter group should return itself. assert result is self.params map_check(**self.params(t)) return context(check) def call_check_numpy(): """Time dependent parameter groups should be able to take numpy arrays as an argument.""" ts = np.linspace(0, 1, 128) def check(self): self.params(ts) return check def define_time_dependent_parameter_group_test(pgroup): """Given a time dependent parameter group, write the test calss that will automatically verify that a given subclass follows the expected interface.""" test_name = "Base" + pgroup.__name__ + "Test" params = pgroup._parameters attributes = define_base_tests(pgroup) attributes["test_call"] = call_check(params) attributes["test_call_numpy"] = call_check_numpy() return type(test_name, (), attributes) PK!CB[<<stylo/testing/color.pyimport numpy as np from stylo.error import MissingDependencyError try: from hypothesis import given from stylo.testing import shape_mask except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err class BaseColorMapTest: """A base class for testing :code:`ColorMap` implementations. When writing your test case for a new :code:`Shape` implementation you need to declare it as follows. .. code-block:: python from unittest import TestCase from stylo.testing.color import BaseColorMapTest class TestMyColorMap(TestCase, BaseColorMapTest): ... .. note:: The order in which you write the classes is *very* important. You also need to define a :code:`setUp` method to set the :code:`colormap` attribute equal to an instance of your shape implementation .. code-block:: python def setUp(self): self.colormap = MyColorMap() Then in addition to any tests your write, your :code:`ColorMap` implementation will be automatically tested to see if it passes the checks defined below. """ @given(mask=shape_mask) def test_paint(self, mask): """Ensure that a colormap can be called with a mask produced by some shape and that the result is: - A numpy array with the same shape :code:`(height, width)` as the given mask. .. note:: Since :code:`ColorMaps` need to be independent of :code:`ColorSpace` we cannot enforce anything about the contents of the array """ colormap = self.colormap colorspace = colormap.colorspace background = colorspace.parse("ffffff") height, width = mask.shape dimensions = (height, width, len(background)) color = np.full(dimensions, background) color = colormap(mask, image_data=color) self.assertEqual(mask.shape[0], color.shape[0]) self.assertEqual(mask.shape[1], color.shape[1]) PK!L%%stylo/testing/design.pyfrom stylo.design import Position, Trajectory from ._param_factory import ( define_parameter_group_test, define_time_dependent_parameter_group_test, ) BasePositionTest = define_parameter_group_test(Position) BaseTrajectoryTest = define_time_dependent_parameter_group_test(Trajectory) PK!uٗstylo/testing/domain.pyfrom stylo.domain import RealDomain from stylo.testing._domain_factory import define_domain_test BaseRealDomainTest = define_domain_test(RealDomain) PK!/;p;;stylo/testing/image.pyimport base64 from stylo.error import MissingDependencyError try: from hypothesis import given from stylo.testing.strategies import dimension except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err class BaseImageTest: """A base class for writing :code:`Image` implementations. When writing your test case for a new :code:`Image` implementation you need to declare it as follows. .. code-block:: python from unittest import TestCase from stylo.testing.image import BaseImageTest class TestMyImage(TestCase, BaseImageTest): ... .. note:: The order in which you write the classes is *very* important. You also need to define the :code:`setUp` method to set the :code:`image` attribute equal to an instance of your image implementation. .. code-block:: python def setUp(self): self.image = MyImage() Then in addition to any tests you write, your :code:`Image` implementation will be automatically tested to see if passes the checks defined below. """ @given(width=dimension, height=dimension) def test_encode(self, width, height): """Ensure that if the :code:`encode=True` keyword argument is given then a base64 encoded string representing the image in PNG format is returned.""" # Every PNG image starts with the same magic number. # https://en.wikipedia.org/wiki/Portable_Network_Graphics magic = base64.b64encode(bytes.fromhex("89504e470d0a1a0a")) image_bytes = self.image(width, height, encode=True) assert isinstance(image_bytes, (bytes,)), "Expected byte string" assert magic[0:11] == image_bytes[0:11] PK!7SSstylo/testing/shape.pyimport numpy as np from stylo.error import MissingDependencyError from stylo.domain import UnitSquare, RealDomain from stylo.shape.shape import InvertedShape, ANDedShape, ORedShape, XORedShape from stylo.utils import get_parameters try: from hypothesis import given from stylo.testing import dimension except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err class BaseShapeTest: """A base class for testing :code:`Shape` implementations. When writing your test case for a new :code:`Shape` implementation you need to declare it as follows. .. code-block:: python from unittest import TestCase from stylo.testing.shape import BaseShapeTest class TestMyShape(TestCase, BaseShapeTest): ... .. note:: The order in which you write the classes is *very* important. You also need to define the :code:`setUp` method to set the :code:`shape` attribute equal to an instance of your shape implementation. .. code-block:: python def setUp(self): self.shape = MyShape() Then in addition to any tests you write, your :code:`Shape` implementation will be automatically tested to see if it passes the checks defined below. """ @given(width=dimension, height=dimension) def test_render_with_domain(self, width, height): """Ensure that a shape can be called with a domain and integers representing the width and height in pixels of an image. The result must: - Be numpy array - With shape :code:`(height, width)` - With elements of type bool. """ domain = UnitSquare() mask = self.shape(domain, width, height) self.assertTrue(isinstance(mask, (np.ndarray,)), "Expected numpy array.") self.assertEqual((height, width), mask.shape) self.assertTrue(mask.dtype == np.bool, "Expected boolean array") @given(width=dimension, height=dimension) def test_render_with_args(self, width, height): """Ensure that a shape can be called with keyword arguments. Each of the arguments should be numpy arrays of the same shape with names corresponding to each of the coordinate names the shape is defined with. The result must: - Be a numpy array - With shape :code:`(height, width)` - With elements of type bool. """ domain = UnitSquare() parameters = self.shape.parameters values = domain[parameters](width, height) params = {k: v for k, v in zip(parameters, values)} mask = self.shape(**params) self.assertTrue(isinstance(mask, (np.ndarray,)), "Expected numpy array.") self.assertEqual((height, width), mask.shape) self.assertTrue(mask.dtype == np.bool, "Expected boolean array.") def test_shape_can_be_inverted(self): """Ensure that a shape can be inverted by checking that an instance of :code:`InvertedShape` is returned when the :code:`~` operator is used on the shape. """ inverted_shape = ~self.shape assert isinstance(inverted_shape, (InvertedShape,)), "Expected inverted shape." assert inverted_shape.shape == self.shape def test_shape_can_be_anded(self): """Ensure that a shape can be ANDed by checking that an instance of :code:`ANDedShape` is returned when the :code:`&` operator is used.""" anded_shape = self.shape & self.shape assert isinstance(anded_shape, (ANDedShape,)), "Expected anded shape" assert anded_shape.a == self.shape assert anded_shape.b == self.shape def test_shape_can_be_ored(self): """Ensure that a shape can be ored by checking that an instance of :code:`ORedShape` is returned when the :code:`|` operator is used.""" ored_shape = self.shape | self.shape assert isinstance(ored_shape, (ORedShape,)), "Expected ored shape" assert ored_shape.a == self.shape assert ored_shape.b == self.shape def test_shape_can_be_xored(self): """Ensure that a shape can be xored by checking that an instance of :code:`XORedShape` is returned when the :code:`^` operator is used.""" xored_shape = self.shape ^ self.shape assert isinstance(xored_shape, (XORedShape,)), "Expected xored shape" assert xored_shape.a == self.shape assert xored_shape.b == self.shape def test_parameters_property(self): """Ensure that the :code:`parameters` property returns the coordinates that the shape is defined with. The parameters must - Be iterable - Be unique - Be declared on the :code:`RealDomain` interface. """ coordinates = self.shape.parameters unique = set(coordinates) self.assertEqual(len(unique), len(coordinates)) for c in coordinates: if c not in RealDomain._parameters: message = "{} is not a valid coordinate variable." raise ValueError(message.format(c)) def test_draw_method(self): """Ensure that the :code:`draw` method returns a valid shape function. A shape function must - Be callable - Only take coordinate arguments defined in :code:`RealDomain` - These arguments must match those defined in the :code:`parameters` property. """ method = self.shape.draw() self.assertTrue(callable(method), "Expected callable.") parameters = get_parameters(method) self.assertEqual(parameters, self.shape.parameters) def test_has_transforms(self): """Ensure that a shape has a :code:`_transforms` attribute that is equal to the empty list.""" assert self.shape._transforms == [] def test_add_transform(self): """Ensure that when transforms are added to the list that they are added to the front of the list.""" shape = self.shape shape._add_transform(1) shape._add_transform(2) assert [2, 1] == shape._transforms def test_apply_transform_none(self): """Ensure that when a shape carries no transforms that the domain is returned untouched.""" domain = UnitSquare() assert self.shape._apply_transform(domain) == domain def test_apply_transform(self): """Ensure that when a shape carries transforms that they are applied.""" self.shape._add_transform(lambda n: 2 * n) self.shape._add_transform(lambda n: n + 1) assert 22 == self.shape._apply_transform(10) PK! stylo/testing/strategies.py"""Specific hypothesis strategies that are useful for testing. It defines the following strategies: - :code:`dimension`: Represents a size or dimension e.g. for numpy arrays image sizes etc. - :code:`real`: Represents a real number in the range +/-1 million """ import numpy as np from math import pi from stylo.error import MissingDependencyError try: from hypothesis.strategies import integers, floats, tuples from hypothesis.extra.numpy import arrays except ImportError as err: raise MissingDependencyError( "The testing package requires additional dependencies." " Run `pip install stylo[testing]` to install them." ) from err # Basic types real = floats(min_value=-1e6, max_value=1e6) angle = floats(min_value=-pi, max_value=pi) dimension = integers(min_value=4, max_value=1024) small_dimension = integers(min_value=4, max_value=128) image_size = tuples(small_dimension, small_dimension) # Stylo data domain_values = arrays(np.float64, image_size, elements=real) shape_mask = arrays(np.bool_, image_size) PK!sԕ--stylo/time/__init__.pyfrom .timeline import Timeline # noqa: F401 PK!stylo/time/clock.pyfrom stylo.utils import get_message_bus class Clock: """This manages all the ticking etc.""" def __init__(self, fps=25): self.fps = fps self.timedelta = 1 / fps self.bus = get_message_bus() self.time = 0 self.tick = 0 self.event_id = self.bus.new_id() def on_tick(self, obj): self.bus.register(self.event_id, obj) def __call__(self): self.tick += 1 self.time += self.timedelta self.bus.send(self.event_id, f=self.tick, t=self.time) PK!Dstylo/time/event.pyfrom .clock import Clock from stylo.utils import get_parameters class Event: """A representation of an event that lasts for some duration.""" def __init__(self, duration, name=None, fps=25, timeline=None): self._clock = Clock(duration, fps) self._timeline = timeline self.duration = duration self.name = "Event" if name is None else name def __repr__(self): return "{0.name}: {0.duration}s".format(self) def __call__(self, f): """Use this to register a time dependent function with this event.""" return TimeDependent(self._clock, f) @property def started(self): return self._clock.started @property def finished(self): return self._clock.finished def start(self): self._clock.start() return self def tick(self): self._clock.tick() return self class TimeDependent: """This wraps a callable so that it is given the appropriate time value.""" def __init__(self, clock, f): self._params = get_parameters(f) if not set(self._params) <= set("ft"): raise TypeError("Expected time dependent function.") self.f = f self.clock = clock def __repr__(self): return "{0}\n{1}".format(repr(self.f), repr(self.clock)) def __call__(self): values = self.clock[self._params] return self.f(*values) PK!8d``stylo/time/timeline.pyfrom math import floor from pathlib import Path from tqdm import tqdm from .clock import Clock class Timeline: """All time begins here.""" def __init__(self, seconds=None, minutes=None, hours=None, fps=25): self._filename = None self._clock = Clock(fps) duration = 0 if hours is not None: duration += hours * 60 * 60 if minutes is not None: duration += minutes * 60 if seconds is not None: duration += seconds self.fps = fps self.framecount = floor(duration * fps) self.duration = duration def _build_filename(self, filename): path = Path(filename) pad = "-{{:0{}d}}".format(len(str(self.framecount))) fname = path.stem + pad + path.suffix fmt_string = str(path.parent.joinpath(fname)) self._filename = fmt_string @property def filename(self): if self._filename is not None: return self._filename.format(self._clock.tick) def main(self, f): self._clock.on_tick(f) return f def render(self, filename=None): if filename is not None: self._build_filename(filename) with tqdm(total=self.framecount) as pbar: while self._clock.tick < self.framecount: self._clock() pbar.update(1) PK!-stylo/utils.pyfrom uuid import uuid4 from inspect import signature def bounded_property( name, bounded_above=None, bounded_above_by=None, bounded_below=None, bounded_below_by=None, ): """Factory function to define a bounded property. This function writes a property definition for a numeric bounded property. It can be bounded above, below or both and the bound can either be a given number or another attribute/property on the same class. The bound can be specified by a constant numeric value e.g. :code:`1` or :code:`2.4` in which case you use the :code:`bounded_above` and :code:`bounded_below` arguments. Alternatively to specify the bound as the value of an attribute on the same class then use the :code:`bounded_above_by` and :code:`bounded_below_by` arugments and pass in the name of the attribute as a string. .. note:: You cannot simultaneously use the :code:`bounded_xxx` and :code:`bounded_xxx_by` version of an argument. :param name: The name of the property :param bounded_above: The value to bound the property above by. This cannot be used in conjunction with :code:`bounded_above_by` :param bounded_below: The value to bound the property below by. This cannot be used in conjunction with :code:`bounded_below_by` :param bounded_above_by: The name of the attribute to bound the value by. This cannot be used in conjunction with :code:`bounded_above` :param bounded_below_by: The name of the attribute to bound the value by. This cannot be used in conjunction with :code:`bounded_below`. :type name: str :type bounded_above: float, int :type bounded_below: float, int :type bounded_below_by: str :type bounded_above_by: str :raises ValueError: If the given arguments are inconsistent in some way. :raises TypeError: If the given arguments do not have their expected types :returns: The constructed property instance """ # First check that the given arguments make sense. if bounded_below is not None and bounded_below_by is not None: raise ValueError('You can only use "bounded_below" or "bounded_below_by"') if bounded_above is not None and bounded_above_by is not None: raise ValueError('You can only use "bounded_above" or "bounded_above_by"') if bounded_below is not None and not isinstance(bounded_below, (int, float)): raise TypeError("The value of bounded_below must be a number.") if bounded_above is not None and not isinstance(bounded_above, (int, float)): raise TypeError("The value of bounded_above must be a number.") if bounded_below_by is not None and not isinstance(bounded_below_by, (str,)): raise TypeError("The value of bounded_below_by must be a string") if bounded_above_by is not None and not isinstance(bounded_above_by, (str,)): raise TypeError("The value of bounded_above_by must be a string") hidden_name = "_" + name type_error_message = "Value of property {0} must be a number".format(name) bounded_below_message = "Value of property {0} must be strictly larger than {1}" bounded_below_by_message = ( "Value of property {0} must be strictly larger than the value of property {1}" ) bounded_above_message = "Value of property {0} must be strictly less than {1}" bounded_above_by_message = ( "Value of property {0} must be strictly less than the value of property {1}" ) checks = [(lambda s, v: isinstance(v, (int, float)), TypeError(type_error_message))] if bounded_above is not None: exception = ValueError(bounded_above_message.format(name, bounded_above)) checks.append((lambda s, v: v < bounded_above, exception)) if bounded_above_by is not None: exception = ValueError(bounded_above_by_message.format(name, bounded_above_by)) checks.append( (lambda s, v: v < s.__getattribute__(bounded_above_by), exception) ) if bounded_below is not None: exception = ValueError(bounded_below_message.format(name, bounded_below)) checks.append((lambda s, v: v > bounded_below, exception)) if bounded_below_by is not None: exception = ValueError(bounded_below_by_message.format(name, bounded_below_by)) checks.append( (lambda s, v: v > s.__getattribute__(bounded_below_by), exception) ) def getter(self): return self.__getattribute__(hidden_name) def setter(self, value): for (value_ok, err) in checks: if not value_ok(self, value): raise err self.__setattr__(hidden_name, value) return property(fget=getter, fset=setter) def get_parameters(f): """ Given a function f, return a tuple of all its parameters """ return tuple(signature(f).parameters.keys()) class MessageBus: """A class that is used behind the scenes to coordinate events and timings of animations. """ def __init__(self): self.subs = {} def new_id(self): """Use this to get a name to use for your events.""" return str(uuid4()) def register(self, event, obj): """Register to receive notifications of an event. :param event: The name of the kind of event to receive :param obj: The object to receive that kind of message. """ if event not in self.subs: self.subs[event] = [obj] return self.subs[event].append(obj) def send(self, event, **kwargs): """Send a message to whoever may be listening.""" if event not in self.subs: return for obj in self.subs[event]: params = get_parameters(obj) values = {k: v for k, v in kwargs.items() if k in params} obj(**values) _message_bus = MessageBus() def get_message_bus(): """A function that returns an instance of the message bus to ensure everyone uses the same instance.""" return _message_bus PK!HY11stylo-0.9.2.dist-info/LICENSEMIT License Copyright (c) 2017-2018 Alex Carney Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HnHTUstylo-0.9.2.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H:2~stylo-0.9.2.dist-info/METADATAXsFb]=l@qk(2hCC>"Իf㻻'ŲZ KNd‰Oi $x- 9\7q(os#\*JZ*\%7nKJ,S ~Ӆ*ԘڹΒF 2XDI.. T*K ^^Ok4kf4TR6--B*S&QTO5I88ͅj$}.72U!Kop Cؓmqo_Ӫ2z#3zF{OEAq;@|,rknq|ßxk0u\\->G9$u01sb^.fNkJM,%]]@{6J[ivVX % KUTGM%>;uer!}HѨ4eXtTZkTCqqx,U!;%tu|C'-sBtr‰glu0Ǔ_ CLq}~37!xԗ&=hA$p0MNHD2ڇiZh 1sq(5m)DLQ1%n<8'7v~/˺2 ϤE5#M_%J0˘ațUBI9-Wx94[@N{1lm{@6 (.uWIjk gaRVaVlj7=~:{S;<^T*,!"z;е93+YTN8|9 P_:":/\^a-5}B GmߞhVjĪЪrzמrdyޭf4M9nE.T܇h^+v 5wr/0Y\u{x%="'"6롳+;9-_wooҹ^»TgC,۠9}xK6&_ ^b@ n 'WNB] iG8" CaMߌyU'q>u9%ziF^WÊ1zoN>v{6 1}MuȌ Uf YfP/'c6q*2@AH4 ׄNJnOkR~?abaO@ F>]kV@,lcRZXUV R Yigʰxi@3)E))ܤ@0y YCzxV4<6& P@ C6o2X}(X 3#u|ˈHxR%Z ZT0扬`U#腃p`E9s+׆Yl )$c0+ Boܘ]ɰ[ %]Yc.kD{K!:Oe-J@c컴#N fc O-zx{fvl:niC9fu~=fVk8cIp 5y%6 7 NuKPX3мv ;yo K'\f|bFV|| ͒@BIM;X$Lϸ0{v1hA0-4.&$[ٕ'GlZ 9!~_(Q5|;W.ۙUa p#c"PSjƐ?_s~JHb9rK6r~c8ѓߞS;ƦŃ ,hHY$R|q/aNje1(-خ͇ nRnbZ/ܯc4s |%0 ,aZ|i>"y,_3уjA+~mqd 6{ph-|4Up__q 3x@;^mnF@ e,tHh =g|:y>dPY4MGIE'LXP_}Mqux1<<ͣQY*Tyo ka{# 1 9Sk`L%+\cl`7P4U/^  ry/pk';]'+/q3PSA,g6p=Rv95zīSk 8 aa^α]KOOx-ϫkc5>+b:3.{lcs~P^[W@. nFAD8yR$WM&SUioXY0 ~A<|G1=jq>j?A<u\7/0ݖN{!yhDIokN=c % ]qU]ًPK!$S@@ CHANGES.rstPK!s5YAstylo/__init__.pyPK! o)Cstylo/_version.pyPK![VVnCstylo/color/__init__.pyPK!lgCstylo/color/colormap.pyPK!))/Gstylo/color/colorspaces.pyPK!V}ttIstylo/color/fill.pyPK!`rr5Kstylo/design/__init__.pyPK!Kstylo/design/_param_factory.pyPK!P\\jstylo/design/params.pyPK!׃nostylo/domain/__init__.pyPK!6rpstylo/domain/_factory.pyPK!^6$$stylo/domain/helpers.pyPK!zY# # ؔstylo/domain/rectangular.pyPK!] 4stylo/domain/square.pyPK!k^h"Ostylo/domain/transform/__init__.pyPK!kK"ystylo/domain/transform/rotation.pyPK!;^stylo/domain/transform/shear.pyPK!I//#stylo/domain/transform/transform.pyPK!uQ@33%stylo/domain/transform/translation.pyPK!pqstylo/error.pyPK!R)tstylo/image/__init__.pyPK!.stylo/image/image.pyPK!)Mfstylo/image/layered.pyPK!*bAKstylo/image/simple.pyPK!ntUUstylo/math/__init__.pyPK!Cstylo/math/interpolation.pyPK!Istylo/math/logic.pyPK!*stylo/shape/__init__.pyPK!X-I3I3\stylo/shape/basic.pyPK!o stylo/shape/curves.pyPK![!stylo/shape/shape.pyPK!'stylo/testing/__init__.pyPK!V'' [)stylo/testing/_domain_factory.pyPK!s"u<7stylo/testing/_param_factory.pyPK!CB[<<}Mstylo/testing/color.pyPK!L%%Ustylo/testing/design.pyPK!uٗGWstylo/testing/domain.pyPK!/;p;;Xstylo/testing/image.pyPK!7SS_stylo/testing/shape.pyPK!  zstylo/testing/strategies.pyPK!sԕ--]~stylo/time/__init__.pyPK!~stylo/time/clock.pyPK!Dstylo/time/event.pyPK!8d``ˆstylo/time/timeline.pyPK!-_stylo/utils.pyPK!HY11stylo-0.9.2.dist-info/LICENSEPK!HnHTUstylo-0.9.2.dist-info/WHEELPK!H:2~stylo-0.9.2.dist-info/METADATAPK!Hystylo-0.9.2.dist-info/RECORDPK22