Skip to content

Instantly share code, notes, and snippets.

@bskari
Created April 29, 2022 06:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bskari/faf7cc90ca6979d12d857bb6107675c2 to your computer and use it in GitHub Desktop.
Save bskari/faf7cc90ca6979d12d857bb6107675c2 to your computer and use it in GitHub Desktop.
Generative Art Bike Paint Job Inkscape plugin
<?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>
#!/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