Some example templates as SVG for laser cutting
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
4.9 KiB

#!/usr/bin/env python3
"""
An algorithm for generating SVG art by Cinnamon.
Original JavaScript code converted from https://github.com/cinnamon-bun/plotter-mushroom
"""
import svgwrite
import hashlib
import math
def posMod(x, m):
return ((x % m) + m) % m
def detRandom(s):
# return random-ish float between 0 and 1, deterministically derived from a hash of the string
m = hashlib.md5(s.encode("utf-8")).hexdigest()
return int(m[:16], 16) / int("ffffffffffffffff", 16)
def remap(x: float, oldMin: float, oldMax: float, newMin: float, newMax: float):
ii = (x - oldMin) / (oldMax - oldMin)
return ii * (newMax - newMin) + newMin
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def clone(self):
return Point(self.x, self.y)
def addPoint(self, p2):
return Point(self.x + p2.x, self.y + p2.y)
def subPoint(self, p2):
return Point(self.x - p2.x, self.y - p2.y)
def mulFloat(self, n: float):
return Point(self.x * n, self.y * n)
def divFloat(self, n: float):
return Point(self.x / n, self.y / n)
def len(self):
return math.sqrt(self.x * self.x + self.y * self.y)
def normalized(self):
if self.x == 0 and self.y == 0:
return self
return self.divFloat(self.len())
def distTo(self, p2):
return self.subPoint(p2).len()
def detRandomInSquare(seed: str):
return Point(detRandom(seed) * 2 - 1, detRandom(seed + "zzz") * 2 - 1)
def detRandomInCircle(seed: str):
ii = 0
while True:
p = Point.detRandomInSquare(f"{seed}{ii}")
if p.len() < 1:
return p
ii += 1
def toBrush(self):
return Brush(self.x, self.y)
class Brush(Point):
def __init__(self, x: float, y: float):
Point.__init__(self, x, y)
self.history = []
def stamp(self):
self.history.append(self.clone())
def move(self, newPoint: Point):
self.x = newPoint.x
self.y = newPoint.y
def make_mushroom():
# the wave
initialNumSeeds = 9
splitAt = 0.05
minRad = 0.05
maxRad = 1
radStep = 0.005
skipIter = 3
damp = 0.85
# 0 is no dampening, 1 is totally damp
seed = 9
jitter = 0.1
# safe range 0 to 1
splitJitter = 0.0001
wind = 0.003
gravity = 0.003
twist = 0.4
brushes = []
for ii in range(initialNumSeeds):
rand = remap(detRandom(f"circlejitter-{ii}-{seed}"), 0, 1, -0.4, 0.4)
brushes.append(
Brush(
math.sin((ii + rand) / initialNumSeeds * 2 * math.pi),
math.cos((ii + rand) / initialNumSeeds * 2 * math.pi),
)
)
rad = 0.0001
paths = {}
iter = 0
rad = minRad
while rad < maxRad:
# smooth
oldPoints = list(brush.clone() for brush in brushes)
iiToSplit = []
for ii, _ in enumerate(oldPoints):
prevPoint = oldPoints[posMod(ii - 1, len(brushes))]
selfBrush = brushes[ii]
nextPoint = oldPoints[posMod(ii + 1, len(brushes))]
jitterMix = remap(
detRandom(f"jittermix-{ii}-{rad}-{seed}"),
0,
1,
0.5 - jitter / 2,
0.5 + jitter / 2,
)
jitterMix += twist
newPoint = (
selfBrush.mulFloat(damp)
.addPoint(prevPoint.mulFloat((1 - damp) * (jitterMix)))
.addPoint(nextPoint.mulFloat((1 - damp) * (1 - jitterMix)))
.addPoint(Point(wind, -gravity))
)
selfBrush.move(newPoint)
space = min(selfBrush.distTo(prevPoint), selfBrush.distTo(nextPoint))
if space > splitAt:
iiToSplit.append(ii)
# split
brushesWithSplits = []
for ii, brush in enumerate(brushes):
brushesWithSplits.append(brush)
if ii in iiToSplit:
splitJitterP = Point.detRandomInCircle(
f"splitjitter-{ii}-{seed}"
).mulFloat(splitJitter)
brushesWithSplits.append(brush.addPoint(splitJitterP).toBrush())
brushes = brushesWithSplits
# normalize to new radius
for brush in brushes:
brush.move(brush.normalized().mulFloat(rad))
# stamp
if iter > skipIter:
for brush in brushes:
brush.stamp()
iter += 1
rad += radStep
for brush in brushes:
brush.stamp()
return list(brush.history for brush in brushes)
paths = make_mushroom()
dwg = svgwrite.Drawing(
"test.svg", ("210mm", "297mm"), profile="tiny", viewBox="0 0 210 297"
)
for path in paths:
d = "M"
for point in path:
d += f" {210/2 + point.x*30} {297/2 + point.y*30} "
obj = dwg.path(d, stroke="black", stroke_width=0.2, fill="none")
dwg.add(obj)
dwg.save()