shades.shades

shades

contains classes and functions relating to Shades' shade object

  1"""
  2shades
  3
  4contains classes and functions relating to Shades' shade object
  5"""
  6from abc import ABC, abstractmethod
  7from typing import Tuple, List
  8
  9import numpy as np
 10
 11from PIL import Image
 12
 13from .noise_fields import NoiseField, noise_fields
 14from .utils import color_clamp
 15
 16
 17class Shade(ABC):
 18    """
 19    An Abstract base clase Shade.
 20    Methods are used to mark shapes onto images according to various color rules.
 21
 22    Initialisation parameters of warp_noise takes two noise_fields affecting how
 23    much a point is moved across x and y axis.
 24
 25    warp_size determines the amount that a warp_noise result of 1 (maximum perlin
 26    value) translates as
 27    """
 28
 29    def __init__(
 30            self,
 31            color: Tuple[int, int, int] = (0, 0, 0),
 32            warp_noise: Tuple[NoiseField] = noise_fields(channels=2),
 33            warp_size: float = 0,
 34        ):
 35        self.color = color
 36        self.warp_noise = warp_noise
 37        self.warp_size = warp_size
 38
 39    @abstractmethod
 40    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
 41        """
 42        Determines the shade/color for given xy coordinate.
 43        """
 44
 45    def adjust_point(self, xy_coords: Tuple[int, int]) -> Tuple[int, int]:
 46        """
 47        If warp is applied in shade, appropriately adjusts location of point.
 48        """
 49        if self.warp_size == 0:
 50            return xy_coords
 51        x_coord = xy_coords[0] + (self.warp_noise[0].noise(xy_coords) * self.warp_size)
 52        y_coord = xy_coords[1] + (self.warp_noise[1].noise(xy_coords) * self.warp_size)
 53        return (x_coord, y_coord)
 54
 55    def point(self, canvas: Image, xy_coords: Tuple[int, int]) -> None:
 56        """
 57        Determines colour and draws a point on an image.
 58        """
 59        color = self.determine_shade(xy_coords)
 60        if color is None:
 61            return
 62        xy_coords = self.adjust_point(xy_coords)
 63
 64        if self.in_bounds(canvas, xy_coords):
 65            canvas.putpixel((int(xy_coords[0]), int(xy_coords[1])), color)
 66
 67
 68    def in_bounds(self, canvas: Image, xy_coords: Tuple[int, int]) -> bool:
 69        """
 70        determined whether xy_coords are within the size of canvas image
 71        """
 72        if (xy_coords[0] < 0) or (xy_coords[0] >= canvas.width):
 73            return False
 74        if (xy_coords[1] < 0) or (xy_coords[1] >= canvas.height):
 75            return False
 76        return True
 77
 78
 79    def weighted_point(self, canvas: Image, xy_coords: Tuple[int, int], weight: int):
 80        """
 81        Determines colour and draws a weighted point on an image.
 82        """
 83        color = self.determine_shade(xy_coords)
 84        if self.warp_size != 0:
 85            xy_coords = self.adjust_point(xy_coords)
 86
 87        for x_coord in range(0, weight):
 88            for y_coord in range(0, weight):
 89                new_point = (int(xy_coords[0]+x_coord), int(xy_coords[1]+y_coord))
 90                if self.in_bounds(canvas, new_point):
 91                    canvas.putpixel(new_point, color)
 92
 93
 94    def pixels_inside_edge(self, edge_pixels: List) -> List:
 95        """
 96        Returns a list of  pixels from inside a edge of points using ray casting algorithm
 97        https://en.wikipedia.org/wiki/Point_in_polygon
 98        vertex correction requires improvements, unusual or particularly angular shapes may
 99        cause difficulties
100        """
101        inner_pixels = []
102        x_coords = {i[0] for i in edge_pixels}
103        for x_coord in range(min(x_coords), max(x_coords)+1):
104            y_coords = {i[1] for i in edge_pixels if i[0] == x_coord}
105            y_coords = [i for i in y_coords if i-1 not in y_coords]
106            ray_count = 0
107            for y_coord in range(min(y_coords), max(y_coords)+1):
108                if y_coord in y_coords and (x_coord, y_coord):
109                    ray_count += 1
110                if ray_count % 2 == 1:
111                    inner_pixels.append((x_coord, y_coord))
112
113        return list(set(inner_pixels + edge_pixels))
114
115
116    def pixels_between_two_points(self, xy_coord_1: Tuple, xy_coord_2: Tuple) -> List:
117        """
118        Returns a list of pixels that form a straight line between two points.
119
120        Parameters:
121        xy_coord_1 (int iterable): Coordinates for first point.
122        xy_coord_2 (int iterable): Coordinates for second point.
123
124        Returns:
125        pixels (int iterable): List of pixels between the two points.
126        """
127        if abs(xy_coord_1[0] - xy_coord_2[0]) > abs(xy_coord_1[1] - xy_coord_2[1]):
128            if xy_coord_1[0] > xy_coord_2[0]:
129                x_step = -1
130            else:
131                x_step = 1
132            y_step = (abs(xy_coord_1[1] - xy_coord_2[1]) / abs(xy_coord_1[0] - xy_coord_2[0]))
133            if xy_coord_1[1] > xy_coord_2[1]:
134                y_step *= -1
135            i_stop = abs(xy_coord_1[0] - xy_coord_2[0])
136        else:
137            if xy_coord_1[1] > xy_coord_2[1]:
138                y_step = -1
139            else:
140                y_step = 1
141            x_step = (abs(xy_coord_1[0] - xy_coord_2[0]) / abs(xy_coord_1[1] - xy_coord_2[1]))
142            if xy_coord_1[0] > xy_coord_2[0]:
143                x_step *= -1
144            i_stop = abs(xy_coord_1[1]-xy_coord_2[1])
145
146        pixels = []
147        x_coord, y_coord = xy_coord_1
148        for _ in range(0, int(i_stop) + 1):
149            pixels.append((int(x_coord), int(y_coord)))
150            x_coord += x_step
151            y_coord += y_step
152        return pixels
153
154
155    def line(
156            self,
157            canvas: Image,
158            xy_coords_1: Tuple[int, int],
159            xy_coords_2: Tuple[int, int],
160            weight: int = 2,
161        ) -> None:
162        """
163        Draws a weighted line on the image.
164        """
165        for pixel in self.pixels_between_two_points(xy_coords_1, xy_coords_2):
166            self.weighted_point(canvas, pixel, weight)
167
168
169    def fill(self, canvas: Image) -> None:
170        """
171        Fills the entire image with color.
172        """
173        # we'll temporarily turn off warping as it isn't needed here
174        warp_size_keeper = self.warp_size
175        self.warp_size = 0
176        for x_coord in range(0, canvas.width):
177            for y_coord in range(0, canvas.height):
178                self.point(canvas, (x_coord, y_coord))
179        #[[self.point(canvas, (x, y)) for x in range(0, canvas.width)]
180        # for y in range(0, canvas.height)]
181        self.warp_size = warp_size_keeper
182
183
184    def get_shape_edge(self, list_of_points: List[Tuple[int, int]]) -> List[Tuple]:
185        """
186        Returns list of coordinates making up the edge of a shape
187        """
188        edge = self.pixels_between_two_points(
189            list_of_points[-1], list_of_points[0])
190        for i in range(0, len(list_of_points)-1):
191            edge += self.pixels_between_two_points(
192                list_of_points[i], list_of_points[i+1])
193        return edge
194
195
196    def shape(self, canvas: Image, points: List[Tuple[int, int]]) -> None:
197        """
198        Draws a shape on an image based on a list of points.
199        """
200        edge = self.get_shape_edge(points)
201        for pixel in self.pixels_inside_edge(edge):
202            self.point(canvas, pixel)
203
204
205    def shape_outline(
206            self,
207            canvas: Image,
208            points: List[Tuple[int, int]],
209            weight: int = 2,
210        ) -> None:
211        """
212        Draws a shape outline on an image based on a list of points.
213        """
214        for pixel in self.get_shape_edge(points):
215            self.weighted_point(canvas, pixel, weight)
216
217
218    def rectangle(
219            self,
220            canvas: Image,
221            top_corner: Tuple[int, int],
222            width: int,
223            height: int,
224        ) -> None:
225        """
226        Draws a rectangle on the image.
227        """
228        for x_coord in range(top_corner[0], top_corner[0] + width):
229            for y_coord in range(top_corner[1], top_corner[1] + height):
230                self.point(canvas, (x_coord, y_coord))
231
232    def square(
233            self,
234            canvas: Image,
235            top_corner: Tuple[int, int],
236            size: int,
237        ) -> None:
238        """
239        Draws a square on the canvas
240        """
241        self.rectangle(canvas, top_corner, size, size)
242
243
244    def triangle(
245            self,
246            canvas,
247            xy1: Tuple[int, int],
248            xy2: Tuple[int, int],
249            xy3: Tuple[int, int],
250        ) -> None:
251        """
252        Draws a triangle on the image.
253        This is the same as calling Shade.shape with a list of three points.
254        """
255        self.shape(canvas, [xy1, xy2, xy3])
256
257
258    def triangle_outline(
259            self,
260            canvas,
261            xy1: Tuple[int, int],
262            xy2: Tuple[int, int],
263            xy3: Tuple[int, int],
264            weight: int = 2,
265        ) -> None:
266        """
267        Draws a triangle outline on the image.
268        Note that this is the same as calling Shade.shape_outline with a list of three points.
269        """
270        self.shape_outline(canvas, [xy1, xy2, xy3], weight)
271
272
273    def get_circle_edge(
274            self,
275            center: Tuple[int, int],
276            radius: int,
277        ) -> List[Tuple[int, int]]:
278        """
279        Returns the edge coordinates of a circle
280        """
281        edge_pixels = []
282        circumference = radius * 2 * np.pi
283        for i in range(0, int(circumference)+1):
284            angle = (i/circumference) * 360
285            opposite = np.sin(np.radians(angle)) * radius
286            adjacent = np.cos(np.radians(angle)) * radius
287            point = (int(center[0] + adjacent), int(center[1] + opposite))
288            edge_pixels.append(point)
289        return edge_pixels
290
291
292    def circle(
293            self,
294            canvas: Image,
295            center: Tuple[int, int],
296            radius: int,
297        ) -> None:
298        """
299        Draws a circle on the image.
300        """
301        edge_pixels = self.get_circle_edge(center, radius)
302        for pixel in self.pixels_inside_edge(edge_pixels):
303            self.point(canvas, pixel)
304
305
306    def circle_outline(
307            self,
308            canvas: Image,
309            center: Tuple[int, int],
310            radius: int,
311            weight: int = 2,
312        ) -> None:
313        """
314        Draws a circle outline on the image.
315        """
316        edge_pixels = self.get_circle_edge(center, radius)
317        for pixel in edge_pixels:
318            self.weighted_point(canvas, pixel, weight)
319
320
321    def circle_slice(
322            self,
323            canvas: Image,
324            center: Tuple[int, int],
325            radius: int,
326            start_angle: int,
327            degrees_of_slice: int,
328        ) -> None:
329        """
330        Draws a partial circle based on degrees.
331        (will have the appearance of a 'pizza slice' or 'pacman' depending on degrees).
332        """
333        # due to Shade.pixels_between_two_points vertex correction issues,
334        # breaks down shape into smaller parts
335        def _internal(canvas, center, radius, start_angle, degrees_of_slice):
336            circumference = radius * 2 * np.pi
337
338            start_point = int(
339                (((start_angle - 90) % 361) / 360) * circumference)
340            slice_length = int((degrees_of_slice / 360) * circumference)
341            end_point = start_point + slice_length
342            edge_pixels = []
343
344            for i in range(start_point, end_point + 1):
345                angle = (i/circumference) * 360
346                opposite = np.sin(np.radians(angle)) * radius
347                adjacent = np.cos(np.radians(angle)) * radius
348                point = (int(center[0] + adjacent), int(center[1] + opposite))
349                edge_pixels.append(point)
350                if i in [start_point, end_point]:
351                    edge_pixels += self.pixels_between_two_points(point, center)
352
353            for pixel in self.pixels_inside_edge(edge_pixels):
354                self.point(canvas, pixel)
355
356        if degrees_of_slice > 180:
357            _internal(canvas, center, radius, start_angle, 180)
358            _internal(canvas, center, radius, start_angle +
359                      180, degrees_of_slice - 180)
360        else:
361            _internal(canvas, center, radius, start_angle, degrees_of_slice)
362
363
364class BlockColor(Shade):
365    """
366    Type of shade that will always fill with defined color without variation.
367    """
368    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
369        """
370        Ignores xy coordinates and returns defined color.
371        """
372        return self.color
373
374
375class NoiseGradient(Shade):
376    """
377    Type of shade that will produce varying gradient based on noise fields.
378
379    Unique Parameters:
380    color_variance: How much noise is allowed to affect the color from the central shade
381    color_fields: A noise field for each channel (r,g,b)
382    """
383
384    def __init__(
385            self,
386            color: Tuple[int, int, int] = (0, 0, 0),
387            warp_noise: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
388            warp_size: int = 0,
389            color_variance: int = 70,
390            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
391        ):
392        super().__init__(color, warp_noise, warp_size)
393        self.color_variance = color_variance
394        self.color_fields = tuple(color_fields)
395
396
397    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
398        """
399        Measures noise from coordinates and affects color based upon return.
400        """
401        def apply_noise(i):
402            noise = self.color_fields[i].noise(xy_coords) - 0.5
403            color_affect = noise * (2*self.color_variance)
404            return self.color[i] + color_affect
405        return color_clamp([apply_noise(i) for i in range(len(self.color))])
406
407
408class DomainWarpGradient(Shade):
409    """
410    Type of shade that will produce varying gradient based on recursive noise fields.
411
412    Unique Parameters:
413    color_variance: How much noise is allowed to affect the color from the central shade
414    color_fields: A noise field for each channel (r,g,b)
415    depth: Number of recursions within noise to make
416    feedback: Affect of recursive calls, recomended around 0-2
417    """
418
419    def __init__(
420            self,
421            color: Tuple[int, int, int] = (0, 0, 0),
422            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
423            warp_size: int = 0,
424            color_variance: int = 70,
425            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
426            depth: int = 2,
427            feedback: float = 0.7,
428        ):
429        super().__init__(color, warp_noise, warp_size)
430        self.color_variance = color_variance
431        self.color_fields = tuple(color_fields)
432        self.depth = depth
433        self.feedback = feedback
434
435
436    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
437        """
438        Determines shade based on xy coordinates.
439        """
440        def apply_noise(i):
441            noise = self.color_fields[i].recursive_noise(
442                xy_coords, self.depth, self.feedback) - 0.5
443            color_affect = noise * (2*self.color_variance)
444            return self.color[i] + color_affect
445        return color_clamp([apply_noise(i) for i in range(len(self.color))])
446
447
448class SwirlOfShades(Shade):
449    """
450    Type of shade that will select from list of other shades based on recursive noise field.
451
452    Unique Parameters:
453    swirl_field: a NoiseField from which the selection of the shade is made
454    depth: Number of recursive calls to make from swirl_field.noise (defaults to 0)
455    feedback: Affect of recursive calls from swirl_field.noise
456    shades: this one is very specific, and determines when shades are used.
457            must be list of tuples of this form:
458            (lower_bound, upper_bound, Shade)
459
460    because the 'shades' arguments potentially confusing, here's an example.
461    The below will color white when noise of 0 - 0.5 is returned, and black if noise of 0.5 - 1
462    [(0, 0.5, shades.BlockColor((255, 255, 255)), (0.5, 1, shades.BlockColor((0, 0, 0)))]
463    """
464    def __init__(
465            self,
466            shades: List[Tuple[float, float, Shade]],
467            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
468            warp_size: int = 0,
469            color_variance: int = 70,
470            swirl_field: NoiseField = NoiseField(),
471            depth: int = 1,
472            feedback: float = 0.7,
473        ):
474        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
475        self.color_variance = color_variance
476        self.swirl_field = swirl_field
477        self.depth = depth
478        self.feedback = feedback
479        self.shades = shades
480
481
482    def determine_shade(self, xy_coords: Tuple[int, int]):
483        """
484        Determines shade based on xy coordinates.
485        """
486        noise = self.swirl_field.recursive_noise(xy_coords, self.depth, self.feedback)
487        shades = [i for i in self.shades if i[0] <= noise < i[1]]
488        if len(shades) > 0:
489            shade = shades[0][2]
490            return shade.determine_shade(xy_coords)
491        return None
492
493
494class LinearGradient(Shade):
495    """
496    Type of shade that will determine color based on transition between various 'color_points'
497
498    Unique Parameters:
499    color_points: Groups of colours and coordinate at which they should appear
500    axis: 0 for horizontal gradient, 1 for vertical
501
502    Here's an example of color_points
503    in this, anything before 50 (on whichever axis specified) will be black,
504    anything after 100 will be white
505    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
506    [((0, 0, 0), 50), ((250, 250, 250), 100)]
507    """
508
509    def __init__(
510            self,
511            color_points: List[Tuple[int, Tuple[int, int, int]]],
512            axis: int = 0,
513            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
514            warp_size: int = 0,
515        ):
516        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
517        self.color_points = color_points
518        self.axis = axis
519
520
521    def determine_shade(self, xy_coords):
522        """
523        Determines shade based on xy coordinates.
524
525        Parameters:
526        xy (iterable): xy coordinates
527
528        Returns:
529        color in form of tuple
530        """
531        larger = [i[1] for i in self.color_points if i[1] >= xy_coords[self.axis]]
532        smaller = [i[1] for i in self.color_points if i[1] < xy_coords[self.axis]]
533        if len(smaller) == 0:
534            next_item = min(larger)
535            next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
536            return next_color
537        if len(larger) == 0:
538            last_item = max(smaller)
539            last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
540            return last_color
541
542        next_item = min(larger)
543        last_item = max(smaller)
544
545        next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
546        last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
547        distance_from_next = abs(next_item - xy_coords[self.axis])
548        distance_from_last = abs(last_item - xy_coords[self.axis])
549        from_last_to_next = distance_from_last / (distance_from_next + distance_from_last)
550
551        color = [0 for i in len(next_color)]
552        for i, _ in enumerate(next_color):
553            color_difference = (
554                last_color[i] - next_color[i]) * from_last_to_next
555            color[i] = last_color[i] - color_difference
556
557        return color_clamp(color)
558
559
560class VerticalGradient(LinearGradient):
561    """
562    Type of shade that will determine color based on transition between various 'color_points'
563
564    Unique Parameters:
565    color_points: Groups of colours and coordinate at which they should appear
566
567    Here's an example of color_points
568    in this, anything before 50 (on y axis) will be black,
569    anything after 100 will be white
570    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
571    """
572    def __init__(
573        self,
574        color_points: List[Tuple[int, Tuple[int, int, int]]],
575        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
576        warp_size: int = 0,
577    ):
578        super().__init__(
579            color_points=color_points,
580            axis=1,
581            warp_noise=warp_noise,
582            warp_size=warp_size,
583        )
584
585
586class HorizontalGradient(LinearGradient):
587    """
588    Type of shade that will determine color based on transition between various 'color_points'
589
590    Unique Parameters:
591    color_points: Groups of colours and coordinate at which they should appear
592
593    Here's an example of color_points
594    in this, anything before 50 (on x axis) will be black,
595    anything after 100 will be white
596    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
597    """
598
599    def __init__(self,
600        color_points: List[Tuple[int, Tuple[int, int, int]]],
601        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
602        warp_size: int = 0,
603    ):
604        super().__init__(
605            color_points=color_points,
606            axis=0,
607            warp_noise=warp_noise,
608            warp_size=warp_size,
609        )
class Shade(abc.ABC):
 18class Shade(ABC):
 19    """
 20    An Abstract base clase Shade.
 21    Methods are used to mark shapes onto images according to various color rules.
 22
 23    Initialisation parameters of warp_noise takes two noise_fields affecting how
 24    much a point is moved across x and y axis.
 25
 26    warp_size determines the amount that a warp_noise result of 1 (maximum perlin
 27    value) translates as
 28    """
 29
 30    def __init__(
 31            self,
 32            color: Tuple[int, int, int] = (0, 0, 0),
 33            warp_noise: Tuple[NoiseField] = noise_fields(channels=2),
 34            warp_size: float = 0,
 35        ):
 36        self.color = color
 37        self.warp_noise = warp_noise
 38        self.warp_size = warp_size
 39
 40    @abstractmethod
 41    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
 42        """
 43        Determines the shade/color for given xy coordinate.
 44        """
 45
 46    def adjust_point(self, xy_coords: Tuple[int, int]) -> Tuple[int, int]:
 47        """
 48        If warp is applied in shade, appropriately adjusts location of point.
 49        """
 50        if self.warp_size == 0:
 51            return xy_coords
 52        x_coord = xy_coords[0] + (self.warp_noise[0].noise(xy_coords) * self.warp_size)
 53        y_coord = xy_coords[1] + (self.warp_noise[1].noise(xy_coords) * self.warp_size)
 54        return (x_coord, y_coord)
 55
 56    def point(self, canvas: Image, xy_coords: Tuple[int, int]) -> None:
 57        """
 58        Determines colour and draws a point on an image.
 59        """
 60        color = self.determine_shade(xy_coords)
 61        if color is None:
 62            return
 63        xy_coords = self.adjust_point(xy_coords)
 64
 65        if self.in_bounds(canvas, xy_coords):
 66            canvas.putpixel((int(xy_coords[0]), int(xy_coords[1])), color)
 67
 68
 69    def in_bounds(self, canvas: Image, xy_coords: Tuple[int, int]) -> bool:
 70        """
 71        determined whether xy_coords are within the size of canvas image
 72        """
 73        if (xy_coords[0] < 0) or (xy_coords[0] >= canvas.width):
 74            return False
 75        if (xy_coords[1] < 0) or (xy_coords[1] >= canvas.height):
 76            return False
 77        return True
 78
 79
 80    def weighted_point(self, canvas: Image, xy_coords: Tuple[int, int], weight: int):
 81        """
 82        Determines colour and draws a weighted point on an image.
 83        """
 84        color = self.determine_shade(xy_coords)
 85        if self.warp_size != 0:
 86            xy_coords = self.adjust_point(xy_coords)
 87
 88        for x_coord in range(0, weight):
 89            for y_coord in range(0, weight):
 90                new_point = (int(xy_coords[0]+x_coord), int(xy_coords[1]+y_coord))
 91                if self.in_bounds(canvas, new_point):
 92                    canvas.putpixel(new_point, color)
 93
 94
 95    def pixels_inside_edge(self, edge_pixels: List) -> List:
 96        """
 97        Returns a list of  pixels from inside a edge of points using ray casting algorithm
 98        https://en.wikipedia.org/wiki/Point_in_polygon
 99        vertex correction requires improvements, unusual or particularly angular shapes may
100        cause difficulties
101        """
102        inner_pixels = []
103        x_coords = {i[0] for i in edge_pixels}
104        for x_coord in range(min(x_coords), max(x_coords)+1):
105            y_coords = {i[1] for i in edge_pixels if i[0] == x_coord}
106            y_coords = [i for i in y_coords if i-1 not in y_coords]
107            ray_count = 0
108            for y_coord in range(min(y_coords), max(y_coords)+1):
109                if y_coord in y_coords and (x_coord, y_coord):
110                    ray_count += 1
111                if ray_count % 2 == 1:
112                    inner_pixels.append((x_coord, y_coord))
113
114        return list(set(inner_pixels + edge_pixels))
115
116
117    def pixels_between_two_points(self, xy_coord_1: Tuple, xy_coord_2: Tuple) -> List:
118        """
119        Returns a list of pixels that form a straight line between two points.
120
121        Parameters:
122        xy_coord_1 (int iterable): Coordinates for first point.
123        xy_coord_2 (int iterable): Coordinates for second point.
124
125        Returns:
126        pixels (int iterable): List of pixels between the two points.
127        """
128        if abs(xy_coord_1[0] - xy_coord_2[0]) > abs(xy_coord_1[1] - xy_coord_2[1]):
129            if xy_coord_1[0] > xy_coord_2[0]:
130                x_step = -1
131            else:
132                x_step = 1
133            y_step = (abs(xy_coord_1[1] - xy_coord_2[1]) / abs(xy_coord_1[0] - xy_coord_2[0]))
134            if xy_coord_1[1] > xy_coord_2[1]:
135                y_step *= -1
136            i_stop = abs(xy_coord_1[0] - xy_coord_2[0])
137        else:
138            if xy_coord_1[1] > xy_coord_2[1]:
139                y_step = -1
140            else:
141                y_step = 1
142            x_step = (abs(xy_coord_1[0] - xy_coord_2[0]) / abs(xy_coord_1[1] - xy_coord_2[1]))
143            if xy_coord_1[0] > xy_coord_2[0]:
144                x_step *= -1
145            i_stop = abs(xy_coord_1[1]-xy_coord_2[1])
146
147        pixels = []
148        x_coord, y_coord = xy_coord_1
149        for _ in range(0, int(i_stop) + 1):
150            pixels.append((int(x_coord), int(y_coord)))
151            x_coord += x_step
152            y_coord += y_step
153        return pixels
154
155
156    def line(
157            self,
158            canvas: Image,
159            xy_coords_1: Tuple[int, int],
160            xy_coords_2: Tuple[int, int],
161            weight: int = 2,
162        ) -> None:
163        """
164        Draws a weighted line on the image.
165        """
166        for pixel in self.pixels_between_two_points(xy_coords_1, xy_coords_2):
167            self.weighted_point(canvas, pixel, weight)
168
169
170    def fill(self, canvas: Image) -> None:
171        """
172        Fills the entire image with color.
173        """
174        # we'll temporarily turn off warping as it isn't needed here
175        warp_size_keeper = self.warp_size
176        self.warp_size = 0
177        for x_coord in range(0, canvas.width):
178            for y_coord in range(0, canvas.height):
179                self.point(canvas, (x_coord, y_coord))
180        #[[self.point(canvas, (x, y)) for x in range(0, canvas.width)]
181        # for y in range(0, canvas.height)]
182        self.warp_size = warp_size_keeper
183
184
185    def get_shape_edge(self, list_of_points: List[Tuple[int, int]]) -> List[Tuple]:
186        """
187        Returns list of coordinates making up the edge of a shape
188        """
189        edge = self.pixels_between_two_points(
190            list_of_points[-1], list_of_points[0])
191        for i in range(0, len(list_of_points)-1):
192            edge += self.pixels_between_two_points(
193                list_of_points[i], list_of_points[i+1])
194        return edge
195
196
197    def shape(self, canvas: Image, points: List[Tuple[int, int]]) -> None:
198        """
199        Draws a shape on an image based on a list of points.
200        """
201        edge = self.get_shape_edge(points)
202        for pixel in self.pixels_inside_edge(edge):
203            self.point(canvas, pixel)
204
205
206    def shape_outline(
207            self,
208            canvas: Image,
209            points: List[Tuple[int, int]],
210            weight: int = 2,
211        ) -> None:
212        """
213        Draws a shape outline on an image based on a list of points.
214        """
215        for pixel in self.get_shape_edge(points):
216            self.weighted_point(canvas, pixel, weight)
217
218
219    def rectangle(
220            self,
221            canvas: Image,
222            top_corner: Tuple[int, int],
223            width: int,
224            height: int,
225        ) -> None:
226        """
227        Draws a rectangle on the image.
228        """
229        for x_coord in range(top_corner[0], top_corner[0] + width):
230            for y_coord in range(top_corner[1], top_corner[1] + height):
231                self.point(canvas, (x_coord, y_coord))
232
233    def square(
234            self,
235            canvas: Image,
236            top_corner: Tuple[int, int],
237            size: int,
238        ) -> None:
239        """
240        Draws a square on the canvas
241        """
242        self.rectangle(canvas, top_corner, size, size)
243
244
245    def triangle(
246            self,
247            canvas,
248            xy1: Tuple[int, int],
249            xy2: Tuple[int, int],
250            xy3: Tuple[int, int],
251        ) -> None:
252        """
253        Draws a triangle on the image.
254        This is the same as calling Shade.shape with a list of three points.
255        """
256        self.shape(canvas, [xy1, xy2, xy3])
257
258
259    def triangle_outline(
260            self,
261            canvas,
262            xy1: Tuple[int, int],
263            xy2: Tuple[int, int],
264            xy3: Tuple[int, int],
265            weight: int = 2,
266        ) -> None:
267        """
268        Draws a triangle outline on the image.
269        Note that this is the same as calling Shade.shape_outline with a list of three points.
270        """
271        self.shape_outline(canvas, [xy1, xy2, xy3], weight)
272
273
274    def get_circle_edge(
275            self,
276            center: Tuple[int, int],
277            radius: int,
278        ) -> List[Tuple[int, int]]:
279        """
280        Returns the edge coordinates of a circle
281        """
282        edge_pixels = []
283        circumference = radius * 2 * np.pi
284        for i in range(0, int(circumference)+1):
285            angle = (i/circumference) * 360
286            opposite = np.sin(np.radians(angle)) * radius
287            adjacent = np.cos(np.radians(angle)) * radius
288            point = (int(center[0] + adjacent), int(center[1] + opposite))
289            edge_pixels.append(point)
290        return edge_pixels
291
292
293    def circle(
294            self,
295            canvas: Image,
296            center: Tuple[int, int],
297            radius: int,
298        ) -> None:
299        """
300        Draws a circle on the image.
301        """
302        edge_pixels = self.get_circle_edge(center, radius)
303        for pixel in self.pixels_inside_edge(edge_pixels):
304            self.point(canvas, pixel)
305
306
307    def circle_outline(
308            self,
309            canvas: Image,
310            center: Tuple[int, int],
311            radius: int,
312            weight: int = 2,
313        ) -> None:
314        """
315        Draws a circle outline on the image.
316        """
317        edge_pixels = self.get_circle_edge(center, radius)
318        for pixel in edge_pixels:
319            self.weighted_point(canvas, pixel, weight)
320
321
322    def circle_slice(
323            self,
324            canvas: Image,
325            center: Tuple[int, int],
326            radius: int,
327            start_angle: int,
328            degrees_of_slice: int,
329        ) -> None:
330        """
331        Draws a partial circle based on degrees.
332        (will have the appearance of a 'pizza slice' or 'pacman' depending on degrees).
333        """
334        # due to Shade.pixels_between_two_points vertex correction issues,
335        # breaks down shape into smaller parts
336        def _internal(canvas, center, radius, start_angle, degrees_of_slice):
337            circumference = radius * 2 * np.pi
338
339            start_point = int(
340                (((start_angle - 90) % 361) / 360) * circumference)
341            slice_length = int((degrees_of_slice / 360) * circumference)
342            end_point = start_point + slice_length
343            edge_pixels = []
344
345            for i in range(start_point, end_point + 1):
346                angle = (i/circumference) * 360
347                opposite = np.sin(np.radians(angle)) * radius
348                adjacent = np.cos(np.radians(angle)) * radius
349                point = (int(center[0] + adjacent), int(center[1] + opposite))
350                edge_pixels.append(point)
351                if i in [start_point, end_point]:
352                    edge_pixels += self.pixels_between_two_points(point, center)
353
354            for pixel in self.pixels_inside_edge(edge_pixels):
355                self.point(canvas, pixel)
356
357        if degrees_of_slice > 180:
358            _internal(canvas, center, radius, start_angle, 180)
359            _internal(canvas, center, radius, start_angle +
360                      180, degrees_of_slice - 180)
361        else:
362            _internal(canvas, center, radius, start_angle, degrees_of_slice)

An Abstract base clase Shade. Methods are used to mark shapes onto images according to various color rules.

Initialisation parameters of warp_noise takes two noise_fields affecting how much a point is moved across x and y axis.

warp_size determines the amount that a warp_noise result of 1 (maximum perlin value) translates as

color
warp_noise
warp_size
@abstractmethod
def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
40    @abstractmethod
41    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
42        """
43        Determines the shade/color for given xy coordinate.
44        """

Determines the shade/color for given xy coordinate.

def adjust_point(self, xy_coords: Tuple[int, int]) -> Tuple[int, int]:
46    def adjust_point(self, xy_coords: Tuple[int, int]) -> Tuple[int, int]:
47        """
48        If warp is applied in shade, appropriately adjusts location of point.
49        """
50        if self.warp_size == 0:
51            return xy_coords
52        x_coord = xy_coords[0] + (self.warp_noise[0].noise(xy_coords) * self.warp_size)
53        y_coord = xy_coords[1] + (self.warp_noise[1].noise(xy_coords) * self.warp_size)
54        return (x_coord, y_coord)

If warp is applied in shade, appropriately adjusts location of point.

def point( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, xy_coords: Tuple[int, int]) -> None:
56    def point(self, canvas: Image, xy_coords: Tuple[int, int]) -> None:
57        """
58        Determines colour and draws a point on an image.
59        """
60        color = self.determine_shade(xy_coords)
61        if color is None:
62            return
63        xy_coords = self.adjust_point(xy_coords)
64
65        if self.in_bounds(canvas, xy_coords):
66            canvas.putpixel((int(xy_coords[0]), int(xy_coords[1])), color)

Determines colour and draws a point on an image.

def in_bounds( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, xy_coords: Tuple[int, int]) -> bool:
69    def in_bounds(self, canvas: Image, xy_coords: Tuple[int, int]) -> bool:
70        """
71        determined whether xy_coords are within the size of canvas image
72        """
73        if (xy_coords[0] < 0) or (xy_coords[0] >= canvas.width):
74            return False
75        if (xy_coords[1] < 0) or (xy_coords[1] >= canvas.height):
76            return False
77        return True

determined whether xy_coords are within the size of canvas image

def weighted_point( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, xy_coords: Tuple[int, int], weight: int):
80    def weighted_point(self, canvas: Image, xy_coords: Tuple[int, int], weight: int):
81        """
82        Determines colour and draws a weighted point on an image.
83        """
84        color = self.determine_shade(xy_coords)
85        if self.warp_size != 0:
86            xy_coords = self.adjust_point(xy_coords)
87
88        for x_coord in range(0, weight):
89            for y_coord in range(0, weight):
90                new_point = (int(xy_coords[0]+x_coord), int(xy_coords[1]+y_coord))
91                if self.in_bounds(canvas, new_point):
92                    canvas.putpixel(new_point, color)

Determines colour and draws a weighted point on an image.

def pixels_inside_edge(self, edge_pixels: List) -> List:
 95    def pixels_inside_edge(self, edge_pixels: List) -> List:
 96        """
 97        Returns a list of  pixels from inside a edge of points using ray casting algorithm
 98        https://en.wikipedia.org/wiki/Point_in_polygon
 99        vertex correction requires improvements, unusual or particularly angular shapes may
100        cause difficulties
101        """
102        inner_pixels = []
103        x_coords = {i[0] for i in edge_pixels}
104        for x_coord in range(min(x_coords), max(x_coords)+1):
105            y_coords = {i[1] for i in edge_pixels if i[0] == x_coord}
106            y_coords = [i for i in y_coords if i-1 not in y_coords]
107            ray_count = 0
108            for y_coord in range(min(y_coords), max(y_coords)+1):
109                if y_coord in y_coords and (x_coord, y_coord):
110                    ray_count += 1
111                if ray_count % 2 == 1:
112                    inner_pixels.append((x_coord, y_coord))
113
114        return list(set(inner_pixels + edge_pixels))

Returns a list of pixels from inside a edge of points using ray casting algorithm https://en.wikipedia.org/wiki/Point_in_polygon vertex correction requires improvements, unusual or particularly angular shapes may cause difficulties

def pixels_between_two_points(self, xy_coord_1: Tuple, xy_coord_2: Tuple) -> List:
117    def pixels_between_two_points(self, xy_coord_1: Tuple, xy_coord_2: Tuple) -> List:
118        """
119        Returns a list of pixels that form a straight line between two points.
120
121        Parameters:
122        xy_coord_1 (int iterable): Coordinates for first point.
123        xy_coord_2 (int iterable): Coordinates for second point.
124
125        Returns:
126        pixels (int iterable): List of pixels between the two points.
127        """
128        if abs(xy_coord_1[0] - xy_coord_2[0]) > abs(xy_coord_1[1] - xy_coord_2[1]):
129            if xy_coord_1[0] > xy_coord_2[0]:
130                x_step = -1
131            else:
132                x_step = 1
133            y_step = (abs(xy_coord_1[1] - xy_coord_2[1]) / abs(xy_coord_1[0] - xy_coord_2[0]))
134            if xy_coord_1[1] > xy_coord_2[1]:
135                y_step *= -1
136            i_stop = abs(xy_coord_1[0] - xy_coord_2[0])
137        else:
138            if xy_coord_1[1] > xy_coord_2[1]:
139                y_step = -1
140            else:
141                y_step = 1
142            x_step = (abs(xy_coord_1[0] - xy_coord_2[0]) / abs(xy_coord_1[1] - xy_coord_2[1]))
143            if xy_coord_1[0] > xy_coord_2[0]:
144                x_step *= -1
145            i_stop = abs(xy_coord_1[1]-xy_coord_2[1])
146
147        pixels = []
148        x_coord, y_coord = xy_coord_1
149        for _ in range(0, int(i_stop) + 1):
150            pixels.append((int(x_coord), int(y_coord)))
151            x_coord += x_step
152            y_coord += y_step
153        return pixels

Returns a list of pixels that form a straight line between two points.

Parameters: xy_coord_1 (int iterable): Coordinates for first point. xy_coord_2 (int iterable): Coordinates for second point.

Returns: pixels (int iterable): List of pixels between the two points.

def line( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, xy_coords_1: Tuple[int, int], xy_coords_2: Tuple[int, int], weight: int = 2) -> None:
156    def line(
157            self,
158            canvas: Image,
159            xy_coords_1: Tuple[int, int],
160            xy_coords_2: Tuple[int, int],
161            weight: int = 2,
162        ) -> None:
163        """
164        Draws a weighted line on the image.
165        """
166        for pixel in self.pixels_between_two_points(xy_coords_1, xy_coords_2):
167            self.weighted_point(canvas, pixel, weight)

Draws a weighted line on the image.

def fill( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>) -> None:
170    def fill(self, canvas: Image) -> None:
171        """
172        Fills the entire image with color.
173        """
174        # we'll temporarily turn off warping as it isn't needed here
175        warp_size_keeper = self.warp_size
176        self.warp_size = 0
177        for x_coord in range(0, canvas.width):
178            for y_coord in range(0, canvas.height):
179                self.point(canvas, (x_coord, y_coord))
180        #[[self.point(canvas, (x, y)) for x in range(0, canvas.width)]
181        # for y in range(0, canvas.height)]
182        self.warp_size = warp_size_keeper

Fills the entire image with color.

def get_shape_edge(self, list_of_points: List[Tuple[int, int]]) -> List[Tuple]:
185    def get_shape_edge(self, list_of_points: List[Tuple[int, int]]) -> List[Tuple]:
186        """
187        Returns list of coordinates making up the edge of a shape
188        """
189        edge = self.pixels_between_two_points(
190            list_of_points[-1], list_of_points[0])
191        for i in range(0, len(list_of_points)-1):
192            edge += self.pixels_between_two_points(
193                list_of_points[i], list_of_points[i+1])
194        return edge

Returns list of coordinates making up the edge of a shape

def shape( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, points: List[Tuple[int, int]]) -> None:
197    def shape(self, canvas: Image, points: List[Tuple[int, int]]) -> None:
198        """
199        Draws a shape on an image based on a list of points.
200        """
201        edge = self.get_shape_edge(points)
202        for pixel in self.pixels_inside_edge(edge):
203            self.point(canvas, pixel)

Draws a shape on an image based on a list of points.

def shape_outline( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, points: List[Tuple[int, int]], weight: int = 2) -> None:
206    def shape_outline(
207            self,
208            canvas: Image,
209            points: List[Tuple[int, int]],
210            weight: int = 2,
211        ) -> None:
212        """
213        Draws a shape outline on an image based on a list of points.
214        """
215        for pixel in self.get_shape_edge(points):
216            self.weighted_point(canvas, pixel, weight)

Draws a shape outline on an image based on a list of points.

def rectangle( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, top_corner: Tuple[int, int], width: int, height: int) -> None:
219    def rectangle(
220            self,
221            canvas: Image,
222            top_corner: Tuple[int, int],
223            width: int,
224            height: int,
225        ) -> None:
226        """
227        Draws a rectangle on the image.
228        """
229        for x_coord in range(top_corner[0], top_corner[0] + width):
230            for y_coord in range(top_corner[1], top_corner[1] + height):
231                self.point(canvas, (x_coord, y_coord))

Draws a rectangle on the image.

def square( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, top_corner: Tuple[int, int], size: int) -> None:
233    def square(
234            self,
235            canvas: Image,
236            top_corner: Tuple[int, int],
237            size: int,
238        ) -> None:
239        """
240        Draws a square on the canvas
241        """
242        self.rectangle(canvas, top_corner, size, size)

Draws a square on the canvas

def triangle( self, canvas, xy1: Tuple[int, int], xy2: Tuple[int, int], xy3: Tuple[int, int]) -> None:
245    def triangle(
246            self,
247            canvas,
248            xy1: Tuple[int, int],
249            xy2: Tuple[int, int],
250            xy3: Tuple[int, int],
251        ) -> None:
252        """
253        Draws a triangle on the image.
254        This is the same as calling Shade.shape with a list of three points.
255        """
256        self.shape(canvas, [xy1, xy2, xy3])

Draws a triangle on the image. This is the same as calling Shade.shape with a list of three points.

def triangle_outline( self, canvas, xy1: Tuple[int, int], xy2: Tuple[int, int], xy3: Tuple[int, int], weight: int = 2) -> None:
259    def triangle_outline(
260            self,
261            canvas,
262            xy1: Tuple[int, int],
263            xy2: Tuple[int, int],
264            xy3: Tuple[int, int],
265            weight: int = 2,
266        ) -> None:
267        """
268        Draws a triangle outline on the image.
269        Note that this is the same as calling Shade.shape_outline with a list of three points.
270        """
271        self.shape_outline(canvas, [xy1, xy2, xy3], weight)

Draws a triangle outline on the image. Note that this is the same as calling Shade.shape_outline with a list of three points.

def get_circle_edge(self, center: Tuple[int, int], radius: int) -> List[Tuple[int, int]]:
274    def get_circle_edge(
275            self,
276            center: Tuple[int, int],
277            radius: int,
278        ) -> List[Tuple[int, int]]:
279        """
280        Returns the edge coordinates of a circle
281        """
282        edge_pixels = []
283        circumference = radius * 2 * np.pi
284        for i in range(0, int(circumference)+1):
285            angle = (i/circumference) * 360
286            opposite = np.sin(np.radians(angle)) * radius
287            adjacent = np.cos(np.radians(angle)) * radius
288            point = (int(center[0] + adjacent), int(center[1] + opposite))
289            edge_pixels.append(point)
290        return edge_pixels

Returns the edge coordinates of a circle

def circle( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, center: Tuple[int, int], radius: int) -> None:
293    def circle(
294            self,
295            canvas: Image,
296            center: Tuple[int, int],
297            radius: int,
298        ) -> None:
299        """
300        Draws a circle on the image.
301        """
302        edge_pixels = self.get_circle_edge(center, radius)
303        for pixel in self.pixels_inside_edge(edge_pixels):
304            self.point(canvas, pixel)

Draws a circle on the image.

def circle_outline( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, center: Tuple[int, int], radius: int, weight: int = 2) -> None:
307    def circle_outline(
308            self,
309            canvas: Image,
310            center: Tuple[int, int],
311            radius: int,
312            weight: int = 2,
313        ) -> None:
314        """
315        Draws a circle outline on the image.
316        """
317        edge_pixels = self.get_circle_edge(center, radius)
318        for pixel in edge_pixels:
319            self.weighted_point(canvas, pixel, weight)

Draws a circle outline on the image.

def circle_slice( self, canvas: <module 'PIL.Image' from '/usr/lib/python3/dist-packages/PIL/Image.py'>, center: Tuple[int, int], radius: int, start_angle: int, degrees_of_slice: int) -> None:
322    def circle_slice(
323            self,
324            canvas: Image,
325            center: Tuple[int, int],
326            radius: int,
327            start_angle: int,
328            degrees_of_slice: int,
329        ) -> None:
330        """
331        Draws a partial circle based on degrees.
332        (will have the appearance of a 'pizza slice' or 'pacman' depending on degrees).
333        """
334        # due to Shade.pixels_between_two_points vertex correction issues,
335        # breaks down shape into smaller parts
336        def _internal(canvas, center, radius, start_angle, degrees_of_slice):
337            circumference = radius * 2 * np.pi
338
339            start_point = int(
340                (((start_angle - 90) % 361) / 360) * circumference)
341            slice_length = int((degrees_of_slice / 360) * circumference)
342            end_point = start_point + slice_length
343            edge_pixels = []
344
345            for i in range(start_point, end_point + 1):
346                angle = (i/circumference) * 360
347                opposite = np.sin(np.radians(angle)) * radius
348                adjacent = np.cos(np.radians(angle)) * radius
349                point = (int(center[0] + adjacent), int(center[1] + opposite))
350                edge_pixels.append(point)
351                if i in [start_point, end_point]:
352                    edge_pixels += self.pixels_between_two_points(point, center)
353
354            for pixel in self.pixels_inside_edge(edge_pixels):
355                self.point(canvas, pixel)
356
357        if degrees_of_slice > 180:
358            _internal(canvas, center, radius, start_angle, 180)
359            _internal(canvas, center, radius, start_angle +
360                      180, degrees_of_slice - 180)
361        else:
362            _internal(canvas, center, radius, start_angle, degrees_of_slice)

Draws a partial circle based on degrees. (will have the appearance of a 'pizza slice' or 'pacman' depending on degrees).

class BlockColor(Shade):
365class BlockColor(Shade):
366    """
367    Type of shade that will always fill with defined color without variation.
368    """
369    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
370        """
371        Ignores xy coordinates and returns defined color.
372        """
373        return self.color

Type of shade that will always fill with defined color without variation.

def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
369    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
370        """
371        Ignores xy coordinates and returns defined color.
372        """
373        return self.color

Ignores xy coordinates and returns defined color.

class NoiseGradient(Shade):
376class NoiseGradient(Shade):
377    """
378    Type of shade that will produce varying gradient based on noise fields.
379
380    Unique Parameters:
381    color_variance: How much noise is allowed to affect the color from the central shade
382    color_fields: A noise field for each channel (r,g,b)
383    """
384
385    def __init__(
386            self,
387            color: Tuple[int, int, int] = (0, 0, 0),
388            warp_noise: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
389            warp_size: int = 0,
390            color_variance: int = 70,
391            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
392        ):
393        super().__init__(color, warp_noise, warp_size)
394        self.color_variance = color_variance
395        self.color_fields = tuple(color_fields)
396
397
398    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
399        """
400        Measures noise from coordinates and affects color based upon return.
401        """
402        def apply_noise(i):
403            noise = self.color_fields[i].noise(xy_coords) - 0.5
404            color_affect = noise * (2*self.color_variance)
405            return self.color[i] + color_affect
406        return color_clamp([apply_noise(i) for i in range(len(self.color))])

Type of shade that will produce varying gradient based on noise fields.

Unique Parameters: color_variance: How much noise is allowed to affect the color from the central shade color_fields: A noise field for each channel (r,g,b)

385    def __init__(
386            self,
387            color: Tuple[int, int, int] = (0, 0, 0),
388            warp_noise: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
389            warp_size: int = 0,
390            color_variance: int = 70,
391            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
392        ):
393        super().__init__(color, warp_noise, warp_size)
394        self.color_variance = color_variance
395        self.color_fields = tuple(color_fields)
color_variance
color_fields
def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
398    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
399        """
400        Measures noise from coordinates and affects color based upon return.
401        """
402        def apply_noise(i):
403            noise = self.color_fields[i].noise(xy_coords) - 0.5
404            color_affect = noise * (2*self.color_variance)
405            return self.color[i] + color_affect
406        return color_clamp([apply_noise(i) for i in range(len(self.color))])

Measures noise from coordinates and affects color based upon return.

class DomainWarpGradient(Shade):
409class DomainWarpGradient(Shade):
410    """
411    Type of shade that will produce varying gradient based on recursive noise fields.
412
413    Unique Parameters:
414    color_variance: How much noise is allowed to affect the color from the central shade
415    color_fields: A noise field for each channel (r,g,b)
416    depth: Number of recursions within noise to make
417    feedback: Affect of recursive calls, recomended around 0-2
418    """
419
420    def __init__(
421            self,
422            color: Tuple[int, int, int] = (0, 0, 0),
423            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
424            warp_size: int = 0,
425            color_variance: int = 70,
426            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
427            depth: int = 2,
428            feedback: float = 0.7,
429        ):
430        super().__init__(color, warp_noise, warp_size)
431        self.color_variance = color_variance
432        self.color_fields = tuple(color_fields)
433        self.depth = depth
434        self.feedback = feedback
435
436
437    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
438        """
439        Determines shade based on xy coordinates.
440        """
441        def apply_noise(i):
442            noise = self.color_fields[i].recursive_noise(
443                xy_coords, self.depth, self.feedback) - 0.5
444            color_affect = noise * (2*self.color_variance)
445            return self.color[i] + color_affect
446        return color_clamp([apply_noise(i) for i in range(len(self.color))])

Type of shade that will produce varying gradient based on recursive noise fields.

Unique Parameters: color_variance: How much noise is allowed to affect the color from the central shade color_fields: A noise field for each channel (r,g,b) depth: Number of recursions within noise to make feedback: Affect of recursive calls, recomended around 0-2

DomainWarpGradient( color: Tuple[int, int, int] = (0, 0, 0), warp_noise: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], warp_size: int = 0, color_variance: int = 70, color_fields: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], depth: int = 2, feedback: float = 0.7)
420    def __init__(
421            self,
422            color: Tuple[int, int, int] = (0, 0, 0),
423            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
424            warp_size: int = 0,
425            color_variance: int = 70,
426            color_fields: Tuple[NoiseField, NoiseField, NoiseField] = noise_fields(channels=3),
427            depth: int = 2,
428            feedback: float = 0.7,
429        ):
430        super().__init__(color, warp_noise, warp_size)
431        self.color_variance = color_variance
432        self.color_fields = tuple(color_fields)
433        self.depth = depth
434        self.feedback = feedback
color_variance
color_fields
depth
feedback
def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
437    def determine_shade(self, xy_coords: Tuple[int, int]) -> Tuple[int, int, int]:
438        """
439        Determines shade based on xy coordinates.
440        """
441        def apply_noise(i):
442            noise = self.color_fields[i].recursive_noise(
443                xy_coords, self.depth, self.feedback) - 0.5
444            color_affect = noise * (2*self.color_variance)
445            return self.color[i] + color_affect
446        return color_clamp([apply_noise(i) for i in range(len(self.color))])

Determines shade based on xy coordinates.

class SwirlOfShades(Shade):
449class SwirlOfShades(Shade):
450    """
451    Type of shade that will select from list of other shades based on recursive noise field.
452
453    Unique Parameters:
454    swirl_field: a NoiseField from which the selection of the shade is made
455    depth: Number of recursive calls to make from swirl_field.noise (defaults to 0)
456    feedback: Affect of recursive calls from swirl_field.noise
457    shades: this one is very specific, and determines when shades are used.
458            must be list of tuples of this form:
459            (lower_bound, upper_bound, Shade)
460
461    because the 'shades' arguments potentially confusing, here's an example.
462    The below will color white when noise of 0 - 0.5 is returned, and black if noise of 0.5 - 1
463    [(0, 0.5, shades.BlockColor((255, 255, 255)), (0.5, 1, shades.BlockColor((0, 0, 0)))]
464    """
465    def __init__(
466            self,
467            shades: List[Tuple[float, float, Shade]],
468            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
469            warp_size: int = 0,
470            color_variance: int = 70,
471            swirl_field: NoiseField = NoiseField(),
472            depth: int = 1,
473            feedback: float = 0.7,
474        ):
475        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
476        self.color_variance = color_variance
477        self.swirl_field = swirl_field
478        self.depth = depth
479        self.feedback = feedback
480        self.shades = shades
481
482
483    def determine_shade(self, xy_coords: Tuple[int, int]):
484        """
485        Determines shade based on xy coordinates.
486        """
487        noise = self.swirl_field.recursive_noise(xy_coords, self.depth, self.feedback)
488        shades = [i for i in self.shades if i[0] <= noise < i[1]]
489        if len(shades) > 0:
490            shade = shades[0][2]
491            return shade.determine_shade(xy_coords)
492        return None

Type of shade that will select from list of other shades based on recursive noise field.

Unique Parameters: swirl_field: a NoiseField from which the selection of the shade is made depth: Number of recursive calls to make from swirl_field.noise (defaults to 0) feedback: Affect of recursive calls from swirl_field.noise shades: this one is very specific, and determines when shades are used. must be list of tuples of this form: (lower_bound, upper_bound, Shade)

because the 'shades' arguments potentially confusing, here's an example. The below will color white when noise of 0 - 0.5 is returned, and black if noise of 0.5 - 1 [(0, 0.5, shades.BlockColor((255, 255, 255)), (0.5, 1, shades.BlockColor((0, 0, 0)))]

SwirlOfShades( shades: List[Tuple[float, float, Shade]], warp_noise: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], warp_size: int = 0, color_variance: int = 70, swirl_field: shades.noise_fields.NoiseField = <shades.noise_fields.NoiseField object>, depth: int = 1, feedback: float = 0.7)
465    def __init__(
466            self,
467            shades: List[Tuple[float, float, Shade]],
468            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
469            warp_size: int = 0,
470            color_variance: int = 70,
471            swirl_field: NoiseField = NoiseField(),
472            depth: int = 1,
473            feedback: float = 0.7,
474        ):
475        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
476        self.color_variance = color_variance
477        self.swirl_field = swirl_field
478        self.depth = depth
479        self.feedback = feedback
480        self.shades = shades
color_variance
swirl_field
depth
feedback
shades
def determine_shade(self, xy_coords: Tuple[int, int]):
483    def determine_shade(self, xy_coords: Tuple[int, int]):
484        """
485        Determines shade based on xy coordinates.
486        """
487        noise = self.swirl_field.recursive_noise(xy_coords, self.depth, self.feedback)
488        shades = [i for i in self.shades if i[0] <= noise < i[1]]
489        if len(shades) > 0:
490            shade = shades[0][2]
491            return shade.determine_shade(xy_coords)
492        return None

Determines shade based on xy coordinates.

class LinearGradient(Shade):
495class LinearGradient(Shade):
496    """
497    Type of shade that will determine color based on transition between various 'color_points'
498
499    Unique Parameters:
500    color_points: Groups of colours and coordinate at which they should appear
501    axis: 0 for horizontal gradient, 1 for vertical
502
503    Here's an example of color_points
504    in this, anything before 50 (on whichever axis specified) will be black,
505    anything after 100 will be white
506    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
507    [((0, 0, 0), 50), ((250, 250, 250), 100)]
508    """
509
510    def __init__(
511            self,
512            color_points: List[Tuple[int, Tuple[int, int, int]]],
513            axis: int = 0,
514            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
515            warp_size: int = 0,
516        ):
517        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
518        self.color_points = color_points
519        self.axis = axis
520
521
522    def determine_shade(self, xy_coords):
523        """
524        Determines shade based on xy coordinates.
525
526        Parameters:
527        xy (iterable): xy coordinates
528
529        Returns:
530        color in form of tuple
531        """
532        larger = [i[1] for i in self.color_points if i[1] >= xy_coords[self.axis]]
533        smaller = [i[1] for i in self.color_points if i[1] < xy_coords[self.axis]]
534        if len(smaller) == 0:
535            next_item = min(larger)
536            next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
537            return next_color
538        if len(larger) == 0:
539            last_item = max(smaller)
540            last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
541            return last_color
542
543        next_item = min(larger)
544        last_item = max(smaller)
545
546        next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
547        last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
548        distance_from_next = abs(next_item - xy_coords[self.axis])
549        distance_from_last = abs(last_item - xy_coords[self.axis])
550        from_last_to_next = distance_from_last / (distance_from_next + distance_from_last)
551
552        color = [0 for i in len(next_color)]
553        for i, _ in enumerate(next_color):
554            color_difference = (
555                last_color[i] - next_color[i]) * from_last_to_next
556            color[i] = last_color[i] - color_difference
557
558        return color_clamp(color)

Type of shade that will determine color based on transition between various 'color_points'

Unique Parameters: color_points: Groups of colours and coordinate at which they should appear axis: 0 for horizontal gradient, 1 for vertical

Here's an example of color_points in this, anything before 50 (on whichever axis specified) will be black, anything after 100 will be white between 50 and 100 will be grey, with tone based on proximity to 50 or 100 [((0, 0, 0), 50), ((250, 250, 250), 100)]

LinearGradient( color_points: List[Tuple[int, Tuple[int, int, int]]], axis: int = 0, warp_noise: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], warp_size: int = 0)
510    def __init__(
511            self,
512            color_points: List[Tuple[int, Tuple[int, int, int]]],
513            axis: int = 0,
514            warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
515            warp_size: int = 0,
516        ):
517        super().__init__(warp_noise=warp_noise, warp_size=warp_size)
518        self.color_points = color_points
519        self.axis = axis
color_points
axis
def determine_shade(self, xy_coords):
522    def determine_shade(self, xy_coords):
523        """
524        Determines shade based on xy coordinates.
525
526        Parameters:
527        xy (iterable): xy coordinates
528
529        Returns:
530        color in form of tuple
531        """
532        larger = [i[1] for i in self.color_points if i[1] >= xy_coords[self.axis]]
533        smaller = [i[1] for i in self.color_points if i[1] < xy_coords[self.axis]]
534        if len(smaller) == 0:
535            next_item = min(larger)
536            next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
537            return next_color
538        if len(larger) == 0:
539            last_item = max(smaller)
540            last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
541            return last_color
542
543        next_item = min(larger)
544        last_item = max(smaller)
545
546        next_color = [i[0] for i in self.color_points if i[1] == next_item][0]
547        last_color = [i[0] for i in self.color_points if i[1] == last_item][0]
548        distance_from_next = abs(next_item - xy_coords[self.axis])
549        distance_from_last = abs(last_item - xy_coords[self.axis])
550        from_last_to_next = distance_from_last / (distance_from_next + distance_from_last)
551
552        color = [0 for i in len(next_color)]
553        for i, _ in enumerate(next_color):
554            color_difference = (
555                last_color[i] - next_color[i]) * from_last_to_next
556            color[i] = last_color[i] - color_difference
557
558        return color_clamp(color)

Determines shade based on xy coordinates.

Parameters: xy (iterable): xy coordinates

Returns: color in form of tuple

class VerticalGradient(LinearGradient):
561class VerticalGradient(LinearGradient):
562    """
563    Type of shade that will determine color based on transition between various 'color_points'
564
565    Unique Parameters:
566    color_points: Groups of colours and coordinate at which they should appear
567
568    Here's an example of color_points
569    in this, anything before 50 (on y axis) will be black,
570    anything after 100 will be white
571    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
572    """
573    def __init__(
574        self,
575        color_points: List[Tuple[int, Tuple[int, int, int]]],
576        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
577        warp_size: int = 0,
578    ):
579        super().__init__(
580            color_points=color_points,
581            axis=1,
582            warp_noise=warp_noise,
583            warp_size=warp_size,
584        )

Type of shade that will determine color based on transition between various 'color_points'

Unique Parameters: color_points: Groups of colours and coordinate at which they should appear

Here's an example of color_points in this, anything before 50 (on y axis) will be black, anything after 100 will be white between 50 and 100 will be grey, with tone based on proximity to 50 or 100

VerticalGradient( color_points: List[Tuple[int, Tuple[int, int, int]]], warp_noise: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], warp_size: int = 0)
573    def __init__(
574        self,
575        color_points: List[Tuple[int, Tuple[int, int, int]]],
576        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
577        warp_size: int = 0,
578    ):
579        super().__init__(
580            color_points=color_points,
581            axis=1,
582            warp_noise=warp_noise,
583            warp_size=warp_size,
584        )
class HorizontalGradient(LinearGradient):
587class HorizontalGradient(LinearGradient):
588    """
589    Type of shade that will determine color based on transition between various 'color_points'
590
591    Unique Parameters:
592    color_points: Groups of colours and coordinate at which they should appear
593
594    Here's an example of color_points
595    in this, anything before 50 (on x axis) will be black,
596    anything after 100 will be white
597    between 50 and 100 will be grey, with tone based on proximity to 50 or 100
598    """
599
600    def __init__(self,
601        color_points: List[Tuple[int, Tuple[int, int, int]]],
602        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
603        warp_size: int = 0,
604    ):
605        super().__init__(
606            color_points=color_points,
607            axis=0,
608            warp_noise=warp_noise,
609            warp_size=warp_size,
610        )

Type of shade that will determine color based on transition between various 'color_points'

Unique Parameters: color_points: Groups of colours and coordinate at which they should appear

Here's an example of color_points in this, anything before 50 (on x axis) will be black, anything after 100 will be white between 50 and 100 will be grey, with tone based on proximity to 50 or 100

HorizontalGradient( color_points: List[Tuple[int, Tuple[int, int, int]]], warp_noise: Tuple[shades.noise_fields.NoiseField, shades.noise_fields.NoiseField] = [<shades.noise_fields.NoiseField object>, <shades.noise_fields.NoiseField object>], warp_size: int = 0)
600    def __init__(self,
601        color_points: List[Tuple[int, Tuple[int, int, int]]],
602        warp_noise: Tuple[NoiseField, NoiseField] = noise_fields(channels=2),
603        warp_size: int = 0,
604    ):
605        super().__init__(
606            color_points=color_points,
607            axis=0,
608            warp_noise=warp_noise,
609            warp_size=warp_size,
610        )