mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 11:57:44 +00:00
Regions slightly integrated with scenes, and creating regions from lines is simple
This commit is contained in:
parent
f3f36c0a79
commit
3cba2b4813
6 changed files with 463 additions and 17 deletions
0
__init__.py
Normal file
0
__init__.py
Normal file
30
displayer.py
30
displayer.py
|
|
@ -4,15 +4,33 @@ import os
|
|||
from PIL import Image
|
||||
import subprocess
|
||||
import cv2
|
||||
from colour import Color
|
||||
|
||||
from animate import *
|
||||
from mobject_movement import *
|
||||
|
||||
def get_pixels(image_array):
|
||||
if image_array is None:
|
||||
return np.zeros(
|
||||
(DEFAULT_HEIGHT, DEFAULT_WIDTH, 3),
|
||||
dtype = 'uint8'
|
||||
)
|
||||
else:
|
||||
pixels = np.array(image_array).astype('uint8')
|
||||
assert len(pixels.shape) == 3 and pixels.shape[2] == 3
|
||||
return pixels
|
||||
|
||||
def paint_region(region, image_array = None, color = None):
|
||||
pixels = get_pixels(image_array)
|
||||
# assert region.shape == pixels.shape[:2]
|
||||
if color is None:
|
||||
rgb = np.random.random(3)
|
||||
else:
|
||||
rgb = np.array(Color(color).get_rgb())
|
||||
pixels[region.bool_grid] = (255*rgb).astype('uint8')
|
||||
return pixels
|
||||
|
||||
def paint_mobject(mobject, image_array = None):
|
||||
if image_array is None:
|
||||
pixels = np.zeros((DEFAULT_HEIGHT, DEFAULT_WIDTH, 3))
|
||||
else:
|
||||
pixels = np.array(image_array)
|
||||
assert len(pixels.shape) == 3 and pixels.shape[2] == 3
|
||||
pixels = get_pixels(image_array)
|
||||
height = pixels.shape[0]
|
||||
width = pixels.shape[1]
|
||||
space_height = SPACE_HEIGHT
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import os
|
|||
from PIL import Image
|
||||
from random import random
|
||||
|
||||
from animate import *
|
||||
from mobject_movement import *
|
||||
from tex_utils import *
|
||||
import displayer as disp
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ class Mobject(object):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def display(self):
|
||||
def show(self):
|
||||
Image.fromarray(disp.paint_mobject(self)).show()
|
||||
|
||||
def save_image(self, name = None):
|
||||
|
|
|
|||
345
mobject_movement.py
Normal file
345
mobject_movement.py
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
from PIL import Image
|
||||
from colour import Color
|
||||
import numpy as np
|
||||
import warnings
|
||||
import time
|
||||
import os
|
||||
import copy
|
||||
import progressbar
|
||||
import inspect
|
||||
from images2gif import writeGif
|
||||
|
||||
from helpers import *
|
||||
from mobject import *
|
||||
import displayer as disp
|
||||
|
||||
class MobjectMovement(object):
|
||||
def __init__(self,
|
||||
mobject,
|
||||
run_time = DEFAULT_ANIMATION_RUN_TIME,
|
||||
alpha_func = high_inflection_0_to_1,
|
||||
name = None):
|
||||
if isinstance(mobject, type) and issubclass(mobject, Mobject):
|
||||
self.mobject = mobject()
|
||||
elif isinstance(mobject, Mobject):
|
||||
self.mobject = mobject
|
||||
else:
|
||||
raise Exception("Invalid mobject parameter, must be \
|
||||
subclass or instance of Mobject")
|
||||
self.starting_mobject = copy.deepcopy(self.mobject)
|
||||
self.reference_mobjects = [self.starting_mobject]
|
||||
self.alpha_func = alpha_func or (lambda x : x)
|
||||
self.run_time = run_time
|
||||
#TODO, Adress the idea of filtering the mobmov
|
||||
self.filter_functions = []
|
||||
self.restricted_height = SPACE_HEIGHT
|
||||
self.restricted_width = SPACE_WIDTH
|
||||
self.spacial_center = np.zeros(3)
|
||||
self.name = name or self.__class__.__name__ + str(self.mobject)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_points_and_rgbs(self):
|
||||
"""
|
||||
It is the responsibility of this class to only emit points within
|
||||
the space. Returns np array of points and corresponding np array
|
||||
of rgbs
|
||||
"""
|
||||
#TODO, I don't think this should be necessary. This should happen
|
||||
#under the individual mobjects.
|
||||
points = self.mobject.points
|
||||
rgbs = self.mobject.rgbs
|
||||
#Filters out what is out of bounds.
|
||||
admissibles = (abs(points[:,0]) < self.restricted_width) * \
|
||||
(abs(points[:,1]) < self.restricted_height)
|
||||
for filter_function in self.filter_functions:
|
||||
admissibles *= ~filter_function(points)
|
||||
if any(self.spacial_center):
|
||||
points += self.spacial_center
|
||||
#Filter out points pushed off the edge
|
||||
admissibles *= (abs(points[:,0]) < SPACE_WIDTH) * \
|
||||
(abs(points[:,1]) < SPACE_HEIGHT)
|
||||
if rgbs.shape[0] < points.shape[0]:
|
||||
#TODO, this shouldn't be necessary, find what's happening.
|
||||
points = points[:rgbs.shape[0], :]
|
||||
admissibles = admissibles[:rgbs.shape[0]]
|
||||
return points[admissibles, :], rgbs[admissibles, :]
|
||||
|
||||
def update(self, alpha):
|
||||
if alpha < 0:
|
||||
alpha = 0
|
||||
if alpha > 1:
|
||||
alpha = 1
|
||||
self.update_mobject(self.alpha_func(alpha))
|
||||
|
||||
def filter_out(self, *filter_functions):
|
||||
self.filter_functions += filter_functions
|
||||
return self
|
||||
|
||||
def restrict_height(self, height):
|
||||
self.restricted_height = min(height, SPACE_HEIGHT)
|
||||
return self
|
||||
|
||||
def restrict_width(self, width):
|
||||
self.restricted_width = min(width, SPACE_WIDTH)
|
||||
return self
|
||||
|
||||
def shift(self, vector):
|
||||
self.spacial_center += vector
|
||||
return self
|
||||
|
||||
def set_run_time(self, time):
|
||||
self.run_time = time
|
||||
return self.reload()
|
||||
|
||||
def set_alpha_func(self, alpha_func):
|
||||
if alpha_func is None:
|
||||
alpha_func = lambda x : x
|
||||
self.alpha_func = alpha_func
|
||||
return self
|
||||
|
||||
def set_name(self, name):
|
||||
self.name = name
|
||||
return self
|
||||
|
||||
# def drag_pixels(self):
|
||||
# self.frames = drag_pixels(self.get_frames())
|
||||
# return self
|
||||
|
||||
# def reverse(self):
|
||||
# self.get_frames().reverse()
|
||||
# self.name = 'Reversed' + str(self)
|
||||
# return self
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
#Typically ipmlemented by subclass
|
||||
pass
|
||||
|
||||
def clean_up(self):
|
||||
pass
|
||||
|
||||
|
||||
###### Concrete MobjectMovement ########
|
||||
|
||||
class Rotating(MobjectMovement):
|
||||
def __init__(self,
|
||||
mobject,
|
||||
axis = None,
|
||||
axes = [[0, 0, 1], [0, 1, 0]],
|
||||
radians = 2 * np.pi,
|
||||
run_time = 20.0,
|
||||
alpha_func = None,
|
||||
*args, **kwargs):
|
||||
MobjectMovement.__init__(
|
||||
self, mobject,
|
||||
run_time = run_time,
|
||||
alpha_func = alpha_func,
|
||||
*args, **kwargs
|
||||
)
|
||||
self.axes = [axis] if axis else axes
|
||||
self.radians = radians
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
self.mobject.points = self.starting_mobject.points
|
||||
for axis in self.axes:
|
||||
self.mobject.rotate(
|
||||
self.radians * alpha,
|
||||
axis
|
||||
)
|
||||
|
||||
class RotationAsTransform(Rotating):
|
||||
def __init__(self, mobject, radians, axis = (0, 0, 1), axes = None,
|
||||
run_time = DEFAULT_ANIMATION_RUN_TIME,
|
||||
alpha_func = high_inflection_0_to_1,
|
||||
*args, **kwargs):
|
||||
Rotating.__init__(
|
||||
self,
|
||||
mobject,
|
||||
axis = axis,
|
||||
axes = axes,
|
||||
run_time = run_time,
|
||||
radians = radians,
|
||||
alpha_func = alpha_func,
|
||||
)
|
||||
|
||||
class FadeOut(MobjectMovement):
|
||||
def update_mobject(self, alpha):
|
||||
self.mobject.rgbs = self.starting_mobject.rgbs * (1 - alpha)
|
||||
|
||||
class Reveal(MobjectMovement):
|
||||
def update_mobject(self, alpha):
|
||||
self.mobject.rgbs = self.starting_mobject.rgbs * alpha
|
||||
if self.mobject.points.shape != self.starting_mobject.points.shape:
|
||||
self.mobject.points = self.starting_mobject.points
|
||||
#TODO, Why do you need to do this? Shouldn't points always align?
|
||||
|
||||
class Transform(MobjectMovement):
|
||||
def __init__(self, mobject1, mobject2,
|
||||
run_time = DEFAULT_TRANSFORM_RUN_TIME,
|
||||
*args, **kwargs):
|
||||
count1, count2 = mobject1.get_num_points(), mobject2.get_num_points()
|
||||
Mobject.align_data(mobject1, mobject2)
|
||||
MobjectMovement.__init__(self, mobject1, run_time = run_time, *args, **kwargs)
|
||||
self.ending_mobject = mobject2
|
||||
self.mobject.SHOULD_BUFF_POINTS = \
|
||||
mobject1.SHOULD_BUFF_POINTS and mobject2.SHOULD_BUFF_POINTS
|
||||
self.reference_mobjects.append(mobject2)
|
||||
self.name += "To" + str(mobject2)
|
||||
|
||||
if count2 < count1:
|
||||
#Ensure redundant pixels fade to black
|
||||
indices = self.non_redundant_m2_indices = \
|
||||
np.arange(0, count1-1, float(count1) / count2).astype('int')
|
||||
temp = np.zeros(mobject2.points.shape)
|
||||
temp[indices] = mobject2.rgbs[indices]
|
||||
mobject2.rgbs = temp
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
Mobject.interpolate(
|
||||
self.starting_mobject,
|
||||
self.ending_mobject,
|
||||
self.mobject,
|
||||
alpha
|
||||
)
|
||||
|
||||
def clean_up(self):
|
||||
if hasattr(self, "non_redundant_m2_indices"):
|
||||
#Reduce mobject (which has become identical to mobject2), as
|
||||
#well as mobject2 itself
|
||||
for mobject in [self.mobject, self.ending_mobject]:
|
||||
for attr in ['points', 'rgbs']:
|
||||
setattr(
|
||||
mobject, attr,
|
||||
getattr(
|
||||
self.ending_mobject,
|
||||
attr
|
||||
)[self.non_redundant_m2_indices]
|
||||
)
|
||||
|
||||
class ApplyMethod(Transform):
|
||||
def __init__(self, method, mobject, *args, **kwargs):
|
||||
"""
|
||||
Method is a method of Mobject
|
||||
"""
|
||||
method_args = ()
|
||||
if isinstance(method, tuple):
|
||||
method, method_args = method[0], method[1:]
|
||||
if not inspect.ismethod(method):
|
||||
raise "Not a valid Mobject method"
|
||||
Transform.__init__(
|
||||
self,
|
||||
mobject,
|
||||
method(copy.deepcopy(mobject), *method_args),
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
class ApplyFunction(Transform):
|
||||
def __init__(self, function, mobject, run_time = DEFAULT_ANIMATION_RUN_TIME,
|
||||
*args, **kwargs):
|
||||
map_image = copy.deepcopy(mobject)
|
||||
map_image.points = np.array(map(function, map_image.points))
|
||||
Transform.__init__(self, mobject, map_image, run_time = run_time,
|
||||
*args, **kwargs)
|
||||
self.name = "".join([
|
||||
"Apply",
|
||||
"".join([s.capitalize() for s in function.__name__.split("_")]),
|
||||
"To" + str(mobject)
|
||||
])
|
||||
|
||||
class ComplexFunction(ApplyFunction):
|
||||
def __init__(self, function, *args, **kwargs):
|
||||
def point_map(point):
|
||||
x, y, z = point
|
||||
c = np.complex(x, y)
|
||||
c = function(c)
|
||||
return c.real, c.imag, z
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
mobject = args.pop(0)
|
||||
elif "mobject" in kwargs:
|
||||
mobject = kwargs.pop("mobject")
|
||||
else:
|
||||
mobject = Grid()
|
||||
ApplyFunction.__init__(self, point_map, mobject, *args, **kwargs)
|
||||
self.name = "ComplexFunction" + to_cammel_case(function.__name__)
|
||||
#Todo, abstract away function naming'
|
||||
|
||||
class Homotopy(MobjectMovement):
|
||||
def __init__(self, homotopy, *args, **kwargs):
|
||||
"""
|
||||
Homotopy a function from (x, y, z, t) to (x', y', z')
|
||||
"""
|
||||
self.homotopy = homotopy
|
||||
MobjectMovement.__init__(self, *args, **kwargs)
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
self.mobject.points = np.array([
|
||||
self.homotopy((x, y, z, alpha))
|
||||
for x, y, z in self.starting_mobject.points
|
||||
])
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, *args, **kwargs):
|
||||
"""
|
||||
Complex Hootopy a function (z, t) to z'
|
||||
"""
|
||||
def homotopy((x, y, z, t)):
|
||||
c = complex_homotopy((complex(x, y), t))
|
||||
return (c.real, c.imag, z)
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
mobject = args.pop(0)
|
||||
elif "mobject" in kwargs:
|
||||
mobject = kwargs["mobject"]
|
||||
else:
|
||||
mobject = Grid()
|
||||
Homotopy.__init__(self, homotopy, mobject, *args, **kwargs)
|
||||
self.name = "ComplexHomotopy" + \
|
||||
to_cammel_case(complex_homotopy.__name__)
|
||||
|
||||
|
||||
class ShowCreation(MobjectMovement):
|
||||
def update_mobject(self, alpha):
|
||||
#TODO, shoudl I make this more efficient?
|
||||
new_num_points = int(alpha * self.starting_mobject.points.shape[0])
|
||||
for attr in ["points", "rgbs"]:
|
||||
setattr(
|
||||
self.mobject,
|
||||
attr,
|
||||
getattr(self.starting_mobject, attr)[:new_num_points, :]
|
||||
)
|
||||
|
||||
class Flash(MobjectMovement):
|
||||
def __init__(self, mobject, color = "white", slow_factor = 0.01,
|
||||
run_time = 0.1, alpha_func = None,
|
||||
*args, **kwargs):
|
||||
MobjectMovement.__init__(self, mobject, run_time = run_time,
|
||||
alpha_func = alpha_func,
|
||||
*args, **kwargs)
|
||||
self.intermediate = Mobject(color = color)
|
||||
self.intermediate.add_points([
|
||||
point + (x, y, 0)
|
||||
for point in self.mobject.points
|
||||
for x in [-1, 1]
|
||||
for y in [-1, 1]
|
||||
])
|
||||
self.reference_mobjects.append(self.intermediate)
|
||||
self.slow_factor = slow_factor
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
#Makes alpha go from 0 to slow_factor to 0 instead of 0 to 1
|
||||
alpha = self.slow_factor * (1.0 - 4 * (alpha - 0.5)**2)
|
||||
Mobject.interpolate(
|
||||
self.starting_mobject,
|
||||
self.intermediate,
|
||||
self.mobject,
|
||||
alpha
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
82
region.py
82
region.py
|
|
@ -2,13 +2,15 @@ import numpy as np
|
|||
import itertools as it
|
||||
from PIL import Image
|
||||
import cv2
|
||||
from copy import deepcopy
|
||||
|
||||
from constants import *
|
||||
import displayer as disp
|
||||
|
||||
class Region(object):
|
||||
def __init__(self,
|
||||
condition = lambda x, y : True,
|
||||
size = (DEFAULT_HEIGHT, DEFAULT_WIDTH)
|
||||
shape = (DEFAULT_HEIGHT, DEFAULT_WIDTH)
|
||||
):
|
||||
"""
|
||||
Condition must be a function which takes in two real
|
||||
|
|
@ -17,7 +19,10 @@ class Region(object):
|
|||
a function from R^2 to {True, False}, but & and | must be
|
||||
used in place of "and" and "or"
|
||||
"""
|
||||
self.size = (h, w) = size
|
||||
#TODO, maybe I want this to be flexible to resizing
|
||||
self.shape = shape
|
||||
# self.condition = condition
|
||||
(h, w) = self.shape
|
||||
scalar = 2*SPACE_HEIGHT / h
|
||||
xs = scalar*np.arange(-w/2, w/2)
|
||||
ys = -scalar*np.arange(-h/2, h/2)
|
||||
|
|
@ -31,9 +36,76 @@ class Region(object):
|
|||
)
|
||||
self.bool_grid = condition(self.xs, self.ys)
|
||||
|
||||
def show(self, color = None):
|
||||
Image.fromarray(disp.paint_region(self, color = color)).show()
|
||||
|
||||
def _combine(self, region, op):
|
||||
assert region.shape == self.shape
|
||||
self.bool_grid = op(self.bool_grid, region.bool_grid)
|
||||
|
||||
def union(self, region):
|
||||
self.bool_grid |= region.bool_grid
|
||||
self._combine(region, lambda bg1, bg2 : bg1 | bg2)
|
||||
return self
|
||||
|
||||
def intersect(self, region):
|
||||
self._combine(region, lambda bg1, bg2 : bg1 & bg2)
|
||||
return self
|
||||
|
||||
def complement(self):
|
||||
self.bool_grid = ~self.bool_grid
|
||||
return self
|
||||
|
||||
class HalfPlane(Region):
|
||||
def __init__(self, point_pair, upper_left = True, *args, **kwargs):
|
||||
"""
|
||||
point_pair of the form [(x_0, y_0), (x_1, y_1)]
|
||||
|
||||
Pf upper_left is True, the side of the region will be
|
||||
everything on the upper left side of the line through
|
||||
the point pair
|
||||
"""
|
||||
if not upper_left:
|
||||
point_pair = list(point_pair)
|
||||
point_pair.reverse()
|
||||
(x0, y0), (x1, y1) = point_pair
|
||||
def condition(x, y):
|
||||
return (x1 - x0)*(y - y0) > (y1 - y0)*(x - x0)
|
||||
Region.__init__(self, condition, *args, **kwargs)
|
||||
|
||||
def plane_partition(*lines):
|
||||
"""
|
||||
A 'line' is a pair of points [(x0, y0), (x1, y1)]
|
||||
|
||||
Returns the list of regions of the plane cut out by
|
||||
these lines
|
||||
"""
|
||||
result = []
|
||||
half_planes = [HalfPlane(line) for line in lines]
|
||||
complements = [deepcopy(hp).complement() for hp in half_planes]
|
||||
num_lines = len(lines)
|
||||
for bool_list in it.product(*[[True, False]]*num_lines):
|
||||
reg = Region()
|
||||
for i in range(num_lines):
|
||||
if bool_list[i]:
|
||||
reg.intersect(half_planes[i])
|
||||
else:
|
||||
reg.intersect(complements[i])
|
||||
if reg.bool_grid.any():
|
||||
result.append(reg)
|
||||
return result
|
||||
|
||||
def plane_partition_from_points(*points):
|
||||
"""
|
||||
Returns list of regions cut out by the complete graph
|
||||
with points from the argument as vertices.
|
||||
|
||||
Each point comes in the form (x, y)
|
||||
"""
|
||||
lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)]
|
||||
return plane_partition(*lines)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def intersection(self, region):
|
||||
self.bool_grid &= region.bool_grid
|
||||
|
||||
|
|
|
|||
19
scene.py
19
scene.py
|
|
@ -12,7 +12,7 @@ import inspect
|
|||
|
||||
from helpers import *
|
||||
from mobject import *
|
||||
from animate import *
|
||||
from mobject_movement import *
|
||||
import displayer as disp
|
||||
|
||||
class Scene(object):
|
||||
|
|
@ -26,11 +26,12 @@ class Scene(object):
|
|||
self.frames = []
|
||||
self.mobjects = []
|
||||
if background:
|
||||
self.background = np.array(background)
|
||||
self.original_background = np.array(background)
|
||||
#TODO, Error checking?
|
||||
else:
|
||||
self.background = np.zeros((height, width, 3))
|
||||
self.shape = self.background.shape[:2]mobmov
|
||||
self.original_background = np.zeros((height, width, 3))
|
||||
self.background = self.original_background
|
||||
self.shape = self.background.shape[:2]
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
|
|
@ -52,6 +53,16 @@ class Scene(object):
|
|||
while mobject in self.mobjects:
|
||||
self.mobjects.remove(mobject)
|
||||
|
||||
def highlight_region(self, region, color = None):
|
||||
self.background = disp.paint_region(
|
||||
region,
|
||||
image_array = self.background,
|
||||
color = color,
|
||||
)
|
||||
|
||||
def reset_background(self):
|
||||
self.background = self.original_background
|
||||
|
||||
def animate(self, *mobmovs):
|
||||
#Runtime is determined by the first mobmov
|
||||
run_time = mobmovs[0].run_time
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue