-
-
Save thetanil/472b8025864fa6c47593b489bb105417 to your computer and use it in GitHub Desktop.
Procedural generation of circular patterns
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
__author__ = "Andrey Zamaraev (a5kin)" | |
import math | |
import random | |
import cairo | |
import numpy as np | |
CANVAS_WIDTH, CANVAS_HEIGHT = 1000, 1000 | |
LINE_WIDTH = 2 | |
NUM_CLONES = 23 | |
BACKGROUND_COLOR = (0, 0, 0) | |
FULL_COLOR1 = (1, 0.7, 0.1, 0.7) | |
FULL_COLOR2 = (1, 1, 1, 0.05) | |
EMPTY_COLOR1 = (0, 0, 0, 0.1) | |
EMPTY_COLOR2 = (0, 0, 0, 0.1) | |
RADIUS1 = 0.3 | |
RADIUS2 = RADIUS1 * 0.8 | |
MU1 = 0.75 # random.random() | |
MU2 = 0.6 # random.random() | |
SIGMA1 = 0.21 # random.random() / 4 + 0.2 | |
SIGMA2 = 0.2 # random.random() / 4 + 0.2 | |
NUM_POINTS1 = 666 | |
NUM_POINTS2 = 666 | |
class EntropyCanvas: | |
def __init__(self, width, height, color): | |
"""Create canvas.""" | |
self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) | |
self.ctx = cairo.Context(self.surface) | |
self.ctx.scale(width, height) | |
self.background(color) | |
def background(self, color): | |
"""Create canvas background.""" | |
pat = cairo.SolidPattern(*color) | |
self.ctx.rectangle(0, 0, 1, 1) | |
self.ctx.set_source(pat) | |
self.ctx.fill() | |
def shift(self, x, deviation): | |
"""Return deviated value.""" | |
return x + random.gauss(0.5, 0.2) * deviation * 2 - deviation / 2 | |
def distort(self, path, deviation): | |
"""Distort given path.""" | |
interpolated_path = [] | |
for p1, p2 in zip(path[:-1], path[1:]): | |
segment_len = math.hypot(p2[0] - p1[0], p2[1] - p1[1]) | |
num_subsegments = max(1, segment_len // 0.01) | |
segment = np.linspace(p1, p2, num_subsegments) | |
interpolated_path += segment.tolist()[:-1] | |
interpolated_path.append(path[-1]) | |
res_path = [] | |
for x, y in interpolated_path: | |
x, y = self.shift(x, deviation / 10), self.shift(y, deviation / 10) | |
res_path.append((x, y)) | |
return res_path | |
def line(self, path, color, width): | |
"""Draw humanized line.""" | |
dev = 0.01 | |
dpath = [(self.shift(x, dev), self.shift(y, dev)) for x, y in path] | |
for p1, p2 in zip(dpath[:-1], dpath[1:]): | |
subpath = self.distort((p1, p2), deviation=dev) | |
self.ctx.move_to(*subpath[0]) | |
for x, y in subpath[1:]: | |
self.ctx.line_to(x, y) | |
self.ctx.set_source_rgba(*color) | |
self.ctx.set_line_width(width) | |
self.ctx.stroke() | |
def multiline(self, path, color1, color2, num_clones, width): | |
"""Draw line several times with variations.""" | |
colors = np.linspace(color1, color2, num_clones).tolist() | |
for i in range(num_clones): | |
color = colors[i] | |
self.line(path, color, width) | |
def save_to(self, filename): | |
"""Save result to a file.""" | |
self.surface.write_to_png(filename) | |
def generate_path(r, mu, sigma, num_points): | |
"""Generate circular path.""" | |
angles = [0] + [random.gauss(mu, sigma) * math.pi for _ in range(num_points)] | |
angles += [2 * math.pi - a for a in angles[::-1]] | |
points = [(r * math.sin(fi) + 0.5, r * math.cos(fi) + 0.5) for fi in angles] | |
# points = [(0.5 * r * math.sin(fi) ** 3 + 0.5, r * math.cos(fi) + 0.5) for fi in angles] | |
path = [p for p in points] + [points[0]] | |
return path | |
def main(): | |
canvas = EntropyCanvas(CANVAS_WIDTH, CANVAS_HEIGHT, BACKGROUND_COLOR) | |
line_width = LINE_WIDTH / CANVAS_WIDTH | |
path = generate_path(RADIUS1, MU1, SIGMA1, NUM_POINTS1) | |
canvas.multiline(path, FULL_COLOR1, FULL_COLOR2, NUM_CLONES, line_width) | |
path = generate_path(RADIUS2, MU2, SIGMA2, NUM_POINTS2) | |
canvas.multiline(path, EMPTY_COLOR1, EMPTY_COLOR2, NUM_CLONES, line_width) | |
canvas.save_to("frame.png") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment