368 lines
12 KiB
Python
368 lines
12 KiB
Python
import cmath
|
|
from math import atan, cos, sin, pi
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
"""
|
|
A lib to draw fractals on pillow image
|
|
|
|
>>> img = Image.new('RGB', (5000, 5000), (0, 0, 0))
|
|
>>> figures = Figures(im=img)
|
|
>>> figures.von_koch_curve_flake((2500, 2500), 2000,6)
|
|
>>> img.save("test.bmp")
|
|
"""
|
|
|
|
|
|
class State:
|
|
"""State of Lsystem"""
|
|
width: int
|
|
color: tuple[int, int, int]
|
|
angle: int
|
|
y: int
|
|
x: int
|
|
|
|
def __init__(self):
|
|
"""Initialisation of state
|
|
|
|
>>> State().x
|
|
0
|
|
>>> State().y
|
|
0
|
|
>>> State().angle
|
|
0
|
|
>>> State().color
|
|
(255, 255, 255)
|
|
>>> State().width
|
|
0"""
|
|
self.x = 0
|
|
self.y = 0
|
|
self.angle = 0
|
|
self.color = (255, 255, 255)
|
|
self.width = 0
|
|
|
|
|
|
class Lsystem(ImageDraw.ImageDraw):
|
|
"""Draw a L system"""
|
|
state: State
|
|
states: list[State]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialisation
|
|
|
|
Parameters are the same than ImageDraw.__init__"""
|
|
super().__init__(*args, **kwargs)
|
|
self.states = []
|
|
self.state = State()
|
|
|
|
def _right(self, angle):
|
|
"""Turn pen to right of angle
|
|
|
|
:param angle: Angle to rotate
|
|
:type angle: float
|
|
"""
|
|
self.state.angle -= angle
|
|
|
|
def _left(self, angle):
|
|
"""Turn pen to left of angle
|
|
|
|
:param angle: Angle to rotate
|
|
:type angle: float
|
|
"""
|
|
self.state.angle += angle
|
|
|
|
def _forward(self, distance):
|
|
"""Forward pen of distance
|
|
|
|
:param distance: Distance to forward
|
|
:type distance: float
|
|
"""
|
|
x_2: float = (distance * cos(self.state.angle)) + self.state.x
|
|
y_2: float = (distance * sin(self.state.angle)) + self.state.y
|
|
self.line(((self.state.x, self.state.y), (x_2, y_2)), self.state.color, self.state.width)
|
|
self.state.x, self.state.y = x_2, y_2
|
|
|
|
def _backward(self, distance):
|
|
"""Backward pen of distance
|
|
|
|
:param distance: Distance to backward
|
|
:type distance: float
|
|
"""
|
|
self._forward(-distance)
|
|
|
|
def _save(self):
|
|
"""Save state of pen"""
|
|
self.states.append(self.state)
|
|
|
|
def _restore(self):
|
|
"""Restore last pen state"""
|
|
self.state = self.states[-1]
|
|
del self.states[-1]
|
|
|
|
def draw_l(self, start, replacement, constants, nb_recursive, color=(255, 255, 255), width=0):
|
|
"""Draw a L system
|
|
|
|
:param start: Axiome
|
|
:param replacement: Dictionary which contain replacement values (F->F+F-F-F+F)
|
|
:param constants: Dictionary which contain all elements with there function
|
|
:param nb_recursive: Number of recursion
|
|
:param color: Color to use for the drawing
|
|
:param width: The line width, in pixels
|
|
:type start: str
|
|
:type replacement: dict
|
|
:type constants: dict
|
|
:type nb_recursive: int
|
|
:type color: tuple(int, int, int)
|
|
:type width: int
|
|
"""
|
|
self.state.color = color
|
|
self.state.width = width
|
|
for i in range(nb_recursive):
|
|
for key, value in replacement.items():
|
|
start = start.replace(key, value)
|
|
for item in start:
|
|
constants[item]()
|
|
|
|
def right(self, angle):
|
|
"""Return a lambda function which make pen turning of angle radians to right
|
|
|
|
:param angle: Angle to build function
|
|
:type angle: float
|
|
|
|
:return: lambda function to make pen turning right
|
|
:rtype: lambda"""
|
|
return lambda: self._right(angle)
|
|
|
|
def left(self, angle):
|
|
"""Return a lambda function which make pen turning of angle radians to left
|
|
|
|
:param angle: Angle to build function
|
|
:type angle: float
|
|
|
|
:return: lambda function to make pen turning left
|
|
:rtype: lambda"""
|
|
return lambda: self._left(angle)
|
|
|
|
def forward(self, distance):
|
|
"""Return a lambda function which make pen forward of distance
|
|
|
|
:param distance: Distance to build function
|
|
:type distance: float
|
|
|
|
:return: lambda function to make pen forward
|
|
:rtype: lambda"""
|
|
return lambda: self._forward(distance)
|
|
|
|
def backward(self, distance):
|
|
"""Return a lambda function which make pen backward of distance
|
|
|
|
:param distance: Distance to build function
|
|
:type distance: float
|
|
|
|
:return: lambda function to make pen backward
|
|
:rtype: lambda"""
|
|
return lambda: self._backward(distance)
|
|
|
|
def save(self):
|
|
"""Return a lambda function which save state of pen
|
|
|
|
:return: lambda function to save pen state
|
|
:rtype: lambda"""
|
|
return lambda: self._save()
|
|
|
|
def restore(self):
|
|
"""Return a lambda function which restore state of pen
|
|
|
|
:return: lambda function to restore pen state
|
|
:rtype: lambda"""
|
|
return lambda: self._restore()
|
|
|
|
|
|
class Figures(ImageDraw.ImageDraw):
|
|
"""A lot of function to create some well-know shapes"""
|
|
|
|
@staticmethod
|
|
def point_to_complex(point):
|
|
"""Transform tuple to complex
|
|
|
|
:param point: Point to convert
|
|
:type point: tuple(float, float)
|
|
|
|
:return: Complex representation of point
|
|
:rtype: complex"""
|
|
return complex(point[0], point[1])
|
|
|
|
@staticmethod
|
|
def complex_to_point(point):
|
|
"""Transform tuple to complex
|
|
|
|
:param point: Point to convert
|
|
:type point: complex
|
|
|
|
:return: tuple representation of point
|
|
:rtype: tuple(float, float)"""
|
|
return point.real, point.imag
|
|
|
|
def rotation(self, point, center=0j, angle=0):
|
|
"""Rotate point in complex plane
|
|
|
|
:param point: point (or list of point) to rotate
|
|
:type point: tuple or complex
|
|
:param center: center of rotation
|
|
:type center: tuple or complex
|
|
:param angle: angle of rotation
|
|
:type angle: float
|
|
|
|
:return: Rotated point (or list of rotated points)
|
|
:rtype: tuple(int, int) or list of tuples"""
|
|
if type(center) != complex:
|
|
center = self.point_to_complex(center)
|
|
if type(point) == list:
|
|
return [self.rotation(p, center, angle) for p in point]
|
|
if type(point) != complex:
|
|
point = self.point_to_complex(point)
|
|
return self.complex_to_point(cmath.exp(complex(0, 1) * angle) * (point - center) + center)
|
|
|
|
def homothety(self, point, center=0j, size=0):
|
|
"""Homothety of point in complex plane
|
|
|
|
:param point: point (or list of point) to make homothety
|
|
:type point: tuple or complex
|
|
:param center: center of homothety
|
|
:type center: tuple or complex
|
|
:param size: size of homothety
|
|
:type size: float
|
|
|
|
:return: Homothety of point (or list of homothety of points)
|
|
:rtype: tuple(int, int) or list of tuples"""
|
|
if type(center) != complex:
|
|
center = self.point_to_complex(center)
|
|
if type(point) == list:
|
|
return [self.homothety(p, center, size) for p in point]
|
|
if type(point) != complex:
|
|
point = self.point_to_complex(point)
|
|
return self.complex_to_point(size * (point - center) + center)
|
|
|
|
def translation(self, point, vect):
|
|
"""Translate point in complex plane
|
|
|
|
:param point: point (or list of point) to translate
|
|
:type point: tuple or complex
|
|
:param vect: vector of translation
|
|
:type vect: tuple or complex
|
|
|
|
:return: Translated point (or list of translated points)
|
|
:rtype: tuple(int, int) or list of tuples"""
|
|
if type(vect) != complex:
|
|
vect = self.point_to_complex(vect)
|
|
if type(point) == list:
|
|
return [self.translation(p, vect) for p in point]
|
|
if type(point) != complex:
|
|
point = self.point_to_complex(point)
|
|
return self.complex_to_point(point + vect)
|
|
|
|
def blanc_manger(self, origin, finish, iterations, color=None, width=0):
|
|
"""Trace blanc manger curve
|
|
|
|
:param origin: coordinate of the starting point
|
|
:param finish: coordinate of the ending point
|
|
:param iterations: iterations for the drawings
|
|
:param color: color to use for the lines
|
|
:param width: the line width, in pixels
|
|
:type origin: tuple
|
|
:type finish: tuple
|
|
:type iterations: int
|
|
:type color: tuple
|
|
:type width: int"""
|
|
lenght_theoric = 2 ** iterations
|
|
length = (((origin[0] - finish[0]) ** 2 + (origin[1] - finish[1]) ** 2) ** 0.5)
|
|
|
|
def sawtooth(x):
|
|
d = x - int(x)
|
|
if d <= 1 / 2:
|
|
return d
|
|
return 1 - d
|
|
|
|
def blanc_manger(x, iterations=1):
|
|
return sum([1 / (2 ** k) * sawtooth((2 ** k) * x) for k in range(iterations)])
|
|
|
|
points = [
|
|
((i / lenght_theoric) * length,
|
|
(blanc_manger(i / lenght_theoric, iterations)) * length)
|
|
for i in range(lenght_theoric + 1)]
|
|
|
|
points = self.rotation(points, (0, 0), pi / 4)
|
|
points = self.translation(points, origin)
|
|
self.line(points, color, width)
|
|
|
|
def von_koch_curve_flake(self, origin, radius, iterations, angle=0, color=None, width=0):
|
|
"""Draw thee von koch flake on image.
|
|
|
|
:param origin: coordinate of the center of circumscribed circle of main triangle
|
|
:param radius: radius of circumscribed circle of main triangle
|
|
:param iterations: iterations for the drawings
|
|
:param angle: rotation of main triangle
|
|
:param color: color to use for the lines
|
|
:param width: the line width, in pixels
|
|
:type radius: float
|
|
:type origin: tuple
|
|
:type iterations: int
|
|
:type angle: float
|
|
:type color: tuple
|
|
:type width: int"""
|
|
angle = angle + pi / 2
|
|
summit_1 = (origin[0] + cos(angle) * radius, origin[1] + sin(angle) * radius)
|
|
summit_2 = (origin[0] + cos(angle + 2 / 3 * pi) * radius, origin[1] + sin(angle + 2 / 3 * pi) * radius)
|
|
summit_3 = (origin[0] + cos(angle - 2 / 3 * pi) * radius, origin[1] + sin(angle - 2 / 3 * pi) * radius)
|
|
self.von_koch_curve(summit_2, summit_1, iterations, color, width)
|
|
self.von_koch_curve(summit_3, summit_2, iterations, color, width)
|
|
self.von_koch_curve(summit_1, summit_3, iterations, color, width)
|
|
|
|
@staticmethod
|
|
def _int(value):
|
|
"""Make a tuple of float coordinate into tuple of int coordinate
|
|
|
|
:param value: Tuple to convert
|
|
:type value: tuple(float, float)
|
|
|
|
:return: new tuple with int values
|
|
:rtype: tuple(int, int)"""
|
|
return int(value[0]), int(value[1])
|
|
|
|
def von_koch_curve(self, origin, finish, iterations=1, color=None, width=0):
|
|
"""Draw thee von koch flake on image.
|
|
|
|
:param origin: coordinate of the starting point
|
|
:param finish: coordinate of the ending point
|
|
:param iterations: iterations for the drawings
|
|
:param color: color to use for the lines
|
|
:param width: the line width, in pixels
|
|
:type origin: tuple
|
|
:type finish: tuple
|
|
:type iterations: int
|
|
:type color: tuple
|
|
:type width: int"""
|
|
third = origin[0] + (finish[0] - origin[0]) * 1 / 3, origin[1] + (finish[1] - origin[1]) * 1 / 3
|
|
two_third = origin[0] + (finish[0] - origin[0]) * 2 / 3, origin[1] + (finish[1] - origin[1]) * 2 / 3
|
|
|
|
length = (((origin[0] - finish[0]) ** 2 + (origin[1] - finish[1]) ** 2) ** 0.5) / 3
|
|
angle = atan((finish[1] - origin[1]) / (finish[0] - origin[0]))
|
|
angle_total = angle + pi / 3
|
|
if origin[0] > finish[0]:
|
|
angle_total += pi
|
|
summit = (cos(angle_total) * length + third[0], sin(angle_total) * length + third[1])
|
|
if iterations <= 1:
|
|
self.line([self._int(origin), self._int(third), self._int(summit), self._int(two_third), self._int(finish)],
|
|
color, width)
|
|
else:
|
|
self.von_koch_curve(self._int(origin), self._int(third), iterations - 1, color, width)
|
|
self.von_koch_curve(self._int(third), self._int(summit), iterations - 1, color, width)
|
|
self.von_koch_curve(self._int(summit), self._int(two_third), iterations - 1, color, width)
|
|
self.von_koch_curve(self._int(two_third), self._int(finish), iterations - 1, color, width)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
img = Image.new('RGB', (5000, 5000), (255, 255, 255))
|
|
figures = Figures(im=img)
|
|
figures.blanc_manger((2000, 2000), (3000, 3000), 7, color=(0, 0, 0), width=2)
|
|
img.save("D:\\Users\\louis chauvet\\Documents\\GitHub\\fractale\\test.bmp")
|