Skip to content

Instantly share code, notes, and snippets.

@abey79
Created May 4, 2020 15:42
Show Gist options
  • Save abey79/653ab8949623b47491349cb585fc7bd6 to your computer and use it in GitHub Desktop.
Save abey79/653ab8949623b47491349cb585fc7bd6 to your computer and use it in GitHub Desktop.
My pen is leaking (generative stop-motion animation with the axidraw)
import time
from pyaxidraw import axidraw
from gpiozero import LED
CM_TO_INCH = 1.0 / 2.54
trigger = LED(26)
ad = axidraw.AxiDraw()
ad.plot_setup()
ad.options.model = 2
ad.options.pen_pos_down = 40
ad.options.pen_pos_up = 60
ad.options.auto_rotate = False
def snap():
trigger.on()
time.sleep(0.1)
trigger.off()
def walk_x(x):
if x == 0:
return
ad.options.mode = "manual"
ad.options.manual_cmd = "walk_x"
ad.options.walk_dist = x * CM_TO_INCH
ad.plot_run()
def walk_y(y):
if y == 0:
return
ad.options.mode = "manual"
ad.options.manual_cmd = "walk_y"
ad.options.walk_dist = y * CM_TO_INCH
ad.plot_run()
def plot_svg(svg: str):
ad.plot_setup(svg)
ad.options.mode = "plot"
ad.plot_run()
def shutdown():
ad.options.mode = "manual"
ad.options.manual_cmd = "disable_xy"
ad.plot_run()
def penup():
ad.options.mode = "manual"
ad.options.manual_cmd = "raise_pen"
ad.plot_run()
def pendown():
ad.options.mode = "manual"
ad.options.manual_cmd = "lower_pen"
ad.plot_run()
import math
import random
from typing import Sequence, Optional, List
import matplotlib.pyplot as plt
import numpy as np
from shapely.geometry import Polygon, LineString, Point
__all__ = ["add", "step", "init_frame", "empty", "black_area"]
frame: Optional[Polygon] = None
black_area: Polygon = Polygon()
circle_generators = []
def _add_to_black(line):
p = Polygon([(pt.real, pt.imag) for pt in line])
global black_area
black_area = black_area.union(p)
def _remove_black(line):
global black_area, frame
res = LineString([(pt.real, pt.imag) for pt in line]).difference(black_area)
if frame:
res = res.intersection(frame)
if res.geom_type == "MultiLineString":
return [np.array(ls).view(dtype=complex).reshape(-1) for ls in res]
else:
return [np.array(res).view(dtype=complex).reshape(-1)]
def _circle(x, y, r, q):
n = math.ceil(2 * math.pi * r / q)
angle = (np.array(list(range(n)) + [0]) / n + random.random()) * 2 * math.pi
return r * (np.cos(angle) + 1j * np.sin(angle)) + complex(x, y)
def init_frame(x, y, w, h):
global frame, black_area
frame = Polygon([(x, y), (x + w, y), (x + w, y + h), (x, y + h), (x, y)])
black_area = Polygon()
def add(x: float, y: float, rr: Sequence[float], q: float) -> None:
"""Add a growing circle animation
Args:
x: X coordinate of circle center
y: Y coordinate of circle center
rr: list of (growing) radii
q: quantization
"""
circle_generators.append(_circle(x, y, r, q) for r in rr)
def step() -> Optional[List[np.ndarray]]:
"""Step all circle generators
Returns:
list of lines to be plotted
"""
if not circle_generators:
return None
lines = []
for gen in circle_generators:
try:
line = next(gen)
lines.extend(_remove_black(line))
_add_to_black(line)
except StopIteration:
circle_generators.remove(gen)
return lines
def empty() -> bool:
"""Are there any generator left?"""
return len(circle_generators) == 0
def test():
start_x, start_y = 1, 1
# width, height = 27.7, 19
width, height = 12.8, 8.5
init_frame(start_x, start_y, width, height)
num_circles = 15
pen_width = 0.03
# radii = [(i + 1) * pen_width for i in range(25)]
radii = np.cumsum(np.linspace(pen_width, 0.6 * pen_width, 150))
steps_per_new_circles = 10
counter = 0
circle_count = 0
while True:
if counter % steps_per_new_circles == 0 and circle_count < num_circles:
# find a location that is outside of the currently drawn area
tries = 0
while True:
x = start_x + np.random.uniform(0, width)
y = start_y + np.random.uniform(0, height)
tries += 1
if not Point(x, y).buffer(0.5).intersects(black_area) or tries > 100:
break
add(x, y, radii, 0.05)
circle_count += 1
lines = step()
if lines is None:
print("no more generator")
break
print(f"{counter}: {len(lines)} lines")
for line in lines:
plt.plot(
line.real, line.imag, 'k', lw=0.3
) # , c=(counter / 30, counter / 30, counter / 30))
counter += 1
plt.axis("square")
plt.show()
if __name__ == "__main__":
for _ in range(1):
test()
import io
import time
import axi
import numpy as np
from shapely.geometry import Point
from vpype import LineCollection, write_svg, VectorData
# import axy_stub as axy
import axy
import growcircle
# page layout
import textanim
CM_TO_PX = 96.0 / 2.54
PAGE_SIZE = (14.8, 10.5) # cm
MARGIN_X, MARGIN_Y = 1, 1 # cm
WIDTH, HEIGHT = (p - 2 for p in PAGE_SIZE)
position_x = 0 # cm
position_y = 0 # cm
def snap():
"""Take a photo"""
axy.snap()
def goto(x, y):
global position_x, position_y
dx = x - position_x
dy = y - position_y
axy.walk_x(dx)
axy.walk_y(dy)
position_x = x
position_y = y
def draw(lc: LineCollection):
"""
1) go to 0, 0
2) step circles and draw
3) go to previous position
"""
global position_x, position_y
cur_x, cur_y = position_x, position_y
goto(0, 0)
vd = VectorData()
lc.scale(CM_TO_PX)
vd.add(lc, 1)
svg = io.StringIO()
write_svg(
svg, vd, page_format=(PAGE_SIZE[0] * CM_TO_PX, PAGE_SIZE[1] * CM_TO_PX),
)
axy.plot_svg(svg.getvalue())
goto(cur_x, cur_y)
def draw_circle_and_snap():
lines = growcircle.step()
if not lines:
return
lc = LineCollection(lines)
lc.merge(0.05, True)
draw(lc)
snap()
def slide(x, y, steps, callback):
pos_x = np.linspace(position_x, x, steps)
pos_y = np.linspace(position_y, y, steps)
for px, py in zip(pos_x, pos_y):
goto(px, py)
if callback:
callback()
def test():
slide(5, 2, 3, snap)
growcircle.add(5, 2, [0.1, 0.2, 0.3], 0.05)
draw_circle_and_snap()
draw_circle_and_snap()
draw_circle_and_snap()
goto(0, 0)
axy.shutdown()
def movie():
growcircle.init_frame(MARGIN_X, MARGIN_Y, WIDTH, HEIGHT)
num_circles = 15
pen_width = 0.03
radii = np.cumsum(np.linspace(pen_width, 0.6 * pen_width, 150))
rest_pos = (-5, 0)
# ====================================================================================
# take a few frames at rest position
goto(*rest_pos)
for _ in range(10):
snap()
time.sleep(1)
# ====================================================================================
# slowly move to first circle
x = MARGIN_X + np.random.uniform(0, WIDTH)
y = MARGIN_Y + np.random.uniform(0, HEIGHT)
slide(x, y, 10, snap)
growcircle.add(x, y, radii, 0.05)
# ====================================================================================
# stay there for few frames
for _ in range(4):
draw_circle_and_snap()
# ====================================================================================
# start the other circles
for _ in range(num_circles - 1):
# find a location that is outside of the currently drawn area
tries = 0
while True:
x = MARGIN_X + np.random.uniform(0, WIDTH)
y = MARGIN_Y + np.random.uniform(0, HEIGHT)
tries += 1
if (
not Point(x, y).buffer(0.5).intersects(growcircle.black_area)
or tries > 100
):
break
slide(x, y, 5, draw_circle_and_snap)
growcircle.add(x, y, radii, 0.05)
for _ in range(5):
draw_circle_and_snap()
# ====================================================================================
# back to rest area
slide(*rest_pos, 8, draw_circle_and_snap)
# ====================================================================================
# let circles grow
while not growcircle.empty():
draw_circle_and_snap()
# ====================================================================================
# closing title
input("Change pen to white and press Enter...")
text_offset_x = 3.
text_offset_y = HEIGHT/2
textanim.add("The End.", 0.8, axi.ROWMANT)
while True:
line = textanim.step()
if line is None:
break
lc = LineCollection([line])
lc.translate(MARGIN_X + text_offset_x, MARGIN_Y + text_offset_y)
draw(lc)
snap()
if __name__ == "__main__":
try:
movie()
finally:
goto(0, 0)
axy.shutdown()
from typing import List, Optional
import axi
import numpy as np
line_list: List[np.ndarray] = []
def add(text: str, size: float, font: axi.FUTURAL) -> None:
lines = axi.text(text, font=font)
for ll in lines:
line = np.array([complex(x, y) for x, y in ll])
line *= size / 18.0
line_list.append(line)
def _line_length(line):
return np.sum(np.abs(np.diff(line)))
def step() -> Optional[np.ndarray]:
if not line_list:
return None
return line_list.pop(0)
def _test():
import matplotlib.pyplot as plt
add("The End.", 0.8, axi.ROWMANT)
while True:
line = step()
if line is None:
break
plt.plot(line.real, line.imag)
plt.axis("equal")
plt.gca().invert_yaxis()
plt.show()
if __name__ == "__main__":
_test()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment