Created
May 4, 2020 15:42
-
-
Save abey79/653ab8949623b47491349cb585fc7bd6 to your computer and use it in GitHub Desktop.
My pen is leaking (generative stop-motion animation with the axidraw)
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
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() |
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
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() |
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
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() |
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
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