optigami / sim /animate.py
ianalin123's picture
feat: Python 3D origami mass-spring simulator (Ghassaei 2018)
94ab3fc
raw
history blame
5.11 kB
"""
Matplotlib 3D animation of origami folding using OrigamiSimulator.
Usage:
python -m sim.animate [target_name]
target_name defaults to 'half_horizontal', resolved against
env/targets/<target_name>.fold relative to this file's parent directory.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from .simulator import OrigamiSimulator
# ── Design system colours ─────────────────────────────────────────────────────
BG_COLOR = '#0d0d14'
AX_COLOR = '#13131d'
PAPER_FACE = '#fafaf5'
PAPER_EDGE = '#2a2a3a'
MOUNTAIN_CLR = '#f59e0b' # amber
VALLEY_CLR = '#38bdf8' # sky
# ── Public API ────────────────────────────────────────────────────────────────
def animate_fold(fold_file: str,
n_frames: int = 80,
steps_per_frame: int = 40,
target_name: str = 'origami') -> None:
"""
Animate folding from 0% β†’ 100% β†’ 0% in a triangle-wave loop.
Parameters
----------
fold_file : str
Path to the .fold JSON file.
n_frames : int
Total animation frames (default 80 β†’ ~40 in, 40 out).
steps_per_frame : int
Physics steps executed per frame.
target_name : str
Display name shown in the title.
"""
fold_data = json.loads(Path(fold_file).read_text())
sim = OrigamiSimulator(fold_data, subdivisions=2)
# Triangle-wave fold percents: 0 β†’ 1 β†’ 0
half = n_frames // 2
fold_percents = np.concatenate([
np.linspace(0.0, 1.0, half),
np.linspace(1.0, 0.0, n_frames - half),
])
# ── Figure setup ──────────────────────────────────────────────────────────
fig = plt.figure(figsize=(9, 7), facecolor=BG_COLOR)
ax = fig.add_subplot(111, projection='3d')
ax.set_facecolor(AX_COLOR)
ax.xaxis.pane.fill = False
ax.yaxis.pane.fill = False
ax.zaxis.pane.fill = False
ax.grid(False)
ax.set_axis_off()
def update(frame: int) -> list:
pct = fold_percents[frame]
sim.set_fold_percent(pct)
sim.step(steps_per_frame)
ax.clear()
ax.set_facecolor(AX_COLOR)
ax.xaxis.pane.fill = False
ax.yaxis.pane.fill = False
ax.zaxis.pane.fill = False
ax.grid(False)
ax.set_axis_off()
# ── Paper surface ─────────────────────────────────────────────────────
verts = [sim.pos[tri] for tri in sim.triangles]
poly = Poly3DCollection(
verts,
alpha=0.85,
facecolor=PAPER_FACE,
edgecolor=PAPER_EDGE,
linewidth=0.2,
zorder=1,
)
ax.add_collection3d(poly)
# ── Crease / fold edges ───────────────────────────────────────────────
for i in range(len(sim._crease_a)):
if sim._crease_assign[i] not in ('M', 'V'):
continue
a, b = sim._crease_a[i], sim._crease_b[i]
color = MOUNTAIN_CLR if sim._crease_assign[i] == 'M' else VALLEY_CLR
ax.plot(
[sim.pos[a, 0], sim.pos[b, 0]],
[sim.pos[a, 1], sim.pos[b, 1]],
[sim.pos[a, 2], sim.pos[b, 2]],
color=color,
linewidth=2.5,
zorder=2,
)
# ── Axis limits & style ───────────────────────────────────────────────
ax.set_xlim(-0.2, 1.2)
ax.set_ylim(-0.2, 1.2)
ax.set_zlim(-0.6, 0.6)
ax.set_box_aspect([1.4, 1.4, 1.0])
ax.set_title(
f'OPTIGAMI β€” {target_name} fold: {pct * 100:.0f}%',
color='#e0e0f0',
fontsize=13,
pad=10,
)
return []
ani = animation.FuncAnimation(
fig,
update,
frames=n_frames,
interval=40, # ms between frames (~25 fps)
blit=False,
)
plt.tight_layout()
plt.show()
def main() -> None:
target = sys.argv[1] if len(sys.argv) > 1 else 'half_horizontal'
fold_file = Path(__file__).parent.parent / 'env' / 'targets' / f'{target}.fold'
if not fold_file.exists():
print(f'Error: fold file not found: {fold_file}', file=sys.stderr)
sys.exit(1)
animate_fold(str(fold_file), target_name=target)
if __name__ == '__main__':
main()