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 )
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
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.
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.
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.
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
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.
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
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.
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.
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.
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
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.
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.
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.
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
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.
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.
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
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.
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.
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).
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.
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.
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)
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.
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
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
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.
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)))]
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
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.
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)]
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
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
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
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 )
Inherited Members
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
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 )