Created
April 29, 2022 06:21
-
-
Save bskari/faf7cc90ca6979d12d857bb6107675c2 to your computer and use it in GitHub Desktop.
Generative Art Bike Paint Job Inkscape plugin
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
<?xml version="1.0" encoding="UTF-8"?> | |
<!-- CC-BY-NC-SA https://creativecommons.org/licenses/by-nc-sa/4.0/ --> | |
<!-- Modified from the original script by Oliver Child, ollie242, from https://www.instructables.com/Turing-Pattern-Bike-Paint-Job/ --> | |
<!-- Put this file and diffusion_reaction.py into $HOME/.config/inkscape/extensions or whereever your Inkscape extensions directory is --> | |
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension"> | |
<_name>Diffusion_Reaction</_name> | |
<id>org.inkscape.template.effect</id> | |
<dependency type="executable" location="extensions">diffusion_reaction.py</dependency> | |
<param name="dpi" type="int" min="0.0" max="1000.0" gui-text="dpi">24</param> | |
<param name="iterations" type="int" min="0.0" max="500000.0" gui-text="iterations">24</param> | |
<param name="fk_rate_default" type="enum" _gui-text="Preset feed and kill rates"> | |
<item value="0" default="default">Default (0.06, 0.062)</item> | |
<item value="1">Solitons (0.03, 0.062)</item> | |
<item value="2">Pulsating Solitons (0.025, 0.06)</item> | |
<item value="3">Worms (0.078, 0.061)</item> | |
<item value="4">Mazes (0.029, 0.057)</item> | |
<item value="5">Holes (0.039, 0.058)</item> | |
<item value="6">Moving spots (0.014, 0.054)</item> | |
<item value="7">Spots and loops (0.018, 0.051)</item> | |
<item value="8">Waves (0.014, 0.045)</item> | |
<item value="9">The u-skate world (0.062, 0.06093)</item> | |
<item value="-1">Custom (see below)</item> | |
</param> | |
<param name="feed_rate" type="float" min="0.0" max="0.1" gui-text="Custom feed rate">0.06</param> | |
<param name="kill_rate" type="float" min="0.0" max="0.073" gui-text="Custom kill rate">0.062</param> | |
<effect> | |
<object-type>all</object-type> | |
<effects-menu> | |
<submenu _name="Diffusion Reaction"/> | |
</effects-menu> | |
</effect> | |
<script> | |
<command reldir="extensions" interpreter="python">diffusion_reaction.py</command> | |
</script> | |
</inkscape-extension> |
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/env python | |
# CC-BY-NC-SA https://creativecommons.org/licenses/by-nc-sa/4.0/ Modified from | |
# the original script by Oliver Child, ollie242, from | |
# https://www.instructables.com/Turing-Pattern-Bike-Paint-Job/ This script | |
# removes the matplotlib requirement and thus works with the Snap version of | |
# Inkscape. A few other things were changed too, such as the addition of default | |
# parameters. | |
# Put this file and diffusion_reaction.inx into $HOME/.config/inkscape/extensions | |
# or whereever your Inkscape extensions directory is | |
""" | |
Generates Turing diffusion reaction images. | |
""" | |
import inkex | |
import inkex.command | |
import numpy as np | |
import os | |
import os.path | |
from PIL import Image | |
import subprocess | |
import re | |
def tupleListToDict(l): | |
return {a: b for a, b in l} | |
def discrete_laplacian(M): | |
L = -4 * M | |
L += np.roll(M, (0, -1), (0, 1)) | |
L += np.roll(M, (0, +1), (0, 1)) | |
L += np.roll(M, (-1, 0), (0, 1)) | |
L += np.roll(M, (+1, 0), (0, 1)) | |
return L | |
def gray_scott_update(A, B, DA, DB, f, k, delta_t): | |
LA = discrete_laplacian(A) | |
LB = discrete_laplacian(B) | |
diff_A = (DA * LA - A * B**2 + f * (1 - A)) * delta_t | |
diff_B = (DB * LB + A * B**2 - (k + f) * B) * delta_t | |
A += diff_A | |
B += diff_B | |
return A, B | |
delta_t = 1.0 | |
DA = 0.16 | |
DB = 0.08 | |
# These presets taken from https://pmneila.github.io/jsexp/grayscott/ | |
DEFAULT_FEED_KILL_RATES = [ | |
# Default from original script | |
(0.06, 0.062), | |
# Solitons | |
(0.03, 0.062), | |
# Pulsating solitons | |
(0.025, 0.06), | |
# Worms | |
(0.078, 0.061), | |
# Mazes | |
(0.029, 0.057), | |
# Holes | |
(0.039, 0.058), | |
# Moving spots | |
(0.014, 0.054), | |
# Spots and loops | |
(0.018, 0.051), | |
# Waves | |
(0.014, 0.045), | |
# The U-Skate World | |
(0.062, 0.06093), | |
] | |
class DiffusionReaction(inkex.Effect): | |
def add_arguments(self, pars): | |
pars.add_argument("--dpi", type=int, default=24, help="dpi") | |
pars.add_argument("--iterations", type=int, default=24, help="iterations") | |
pars.add_argument("--fk_rate_default", type=int, default=0, help="Preset feed and kill rates") | |
pars.add_argument("--feed_rate", type=float, default=0.06, help="Feed rate") | |
pars.add_argument("--kill_rate", type=float, default=0.062, help="Kill rate") | |
def rescale(self, bb, svg_obj): | |
scaling = tupleListToDict(svg_obj.items()) | |
transform_matrix = svg_obj.getchildren()[1].get("transform") | |
values = list( | |
map(float, re.search(r"\((.*?)\)", transform_matrix).group(1).split()) | |
) | |
x_scale = values[0] * ((bb[1] - bb[0]) / float(scaling["width"][0:-2])) | |
y_scale = values[3] * ((bb[3] - bb[2]) / float(scaling["height"][0:-2])) | |
x_translate = bb[0] | |
y_translate = bb[3] | |
x_scale = str(round(x_scale, 3)) | |
y_scale = str(round(y_scale, 3)) | |
x_translate = str(round(x_translate, 3)) | |
y_translate = str(round(y_translate, 3)) | |
svg_obj.getchildren()[1].set( | |
"transform", | |
"translate(" | |
+ x_translate | |
+ "," | |
+ y_translate | |
+ ") scale(" | |
+ x_scale | |
+ "," | |
+ y_scale | |
+ ")", | |
) | |
return svg_obj | |
def effect(self): | |
bbs = [] | |
svg = self.options.input_file | |
png = os.path.splitext(svg)[0] + ".png" | |
for node in self.svg.selected.values(): | |
(x1, x2), (y1, y2) = node.bounding_box() | |
bbs.append([x1, x2, y1, y2]) | |
bbs = np.array(bbs) | |
try: | |
bb = (min(bbs[:, 0]), max(bbs[:, 1]), min(bbs[:, 2]), max(bbs[:, 3])) | |
except IndexError: | |
raise ValueError("Please select an item in Inkscape") | |
# I think we need to put the largest node last in the list | |
ids_and_size = [] | |
for node in self.svg.selected.values(): | |
(x1, x2), (y1, y2) = node.bounding_box() | |
ids_and_size.append((node.get_id(), (x2 - x1 + y2 - y1))) | |
ids_and_size.sort(key=lambda t: t[1]) | |
inkex.command.inkscape( | |
svg, | |
"--export-filename=" + png, | |
"--export-dpi=" + str(self.options.dpi), | |
"--export-id=" | |
+ "/;".join([id for id, _ in ids_and_size]), | |
) | |
image = Image.open(png) | |
flat_data = [[float(i) / 255.0 for i in t] for t in list(image.getdata())] | |
image_ = np.array([flat_data[i * image.width:(i + 1) * image.width] for i in range(image.height)]) | |
grey = (image_[:, :, 0] + image_[:, :, 1] + image_[:, :, 2]) > 0 | |
A = image_[:, :, 0] | |
B = image_[:, :, 1] | |
if self.options.fk_rate_default == "-1": | |
feed_rate = self.options.feed_rate | |
kill_rate = self.options.kill_rate | |
else: | |
feed_rate, kill_rate = DEFAULT_FEED_KILL_RATES[int(self.options.fk_rate_default)] | |
for t in range(self.options.iterations): | |
A, B = gray_scott_update(A, B, DA, DB, feed_rate, kill_rate, delta_t) | |
B *= grey | |
A = A > 0.6 | |
PIL_image = Image.fromarray(np.uint8(A * 255), "L") | |
PIL_image.save("/tmp/diffusion.bmp", format="BMP") | |
try: | |
subprocess.check_output( | |
"potrace /tmp/diffusion.bmp -s -o /tmp/diffusion.svg", shell=True | |
) | |
except Exception as exc: | |
raise ValueError("Is potrace installed? It needs to be.") from exc | |
svg_obj = inkex.elements.load_svg("/tmp/diffusion.svg").getroot() | |
svg_obj = self.rescale(bb, svg_obj) | |
children = svg_obj.getchildren()[1] | |
if len(children) == 0: | |
raise ValueError("No children items were created") | |
self.svg.add(children) | |
os.remove("/tmp/diffusion.bmp") | |
os.remove("/tmp/diffusion.svg") | |
if __name__ == "__main__": | |
DiffusionReaction().run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment