mirror of
https://github.com/3b1b/manim.git
synced 2025-07-31 05:52:34 +08:00
Introduced notion of a subpath
This commit is contained in:
61
camera.py
61
camera.py
@ -84,27 +84,28 @@ class Camera(object):
|
|||||||
print mobject
|
print mobject
|
||||||
# raise Exception("I don't know how to display that")
|
# raise Exception("I don't know how to display that")
|
||||||
|
|
||||||
# def display_region(self, region):
|
def display_region(self, region):
|
||||||
# (h, w) = self.pixel_shape
|
(h, w) = self.pixel_shape
|
||||||
# scalar = 2*self.space_shape[0] / h
|
scalar = 2*self.space_shape[0] / h
|
||||||
# xs = scalar*np.arange(-w/2, w/2)+self.space_center[0]
|
xs = scalar*np.arange(-w/2, w/2)+self.space_center[0]
|
||||||
# ys = -scalar*np.arange(-h/2, h/2)+self.space_center[1]
|
ys = -scalar*np.arange(-h/2, h/2)+self.space_center[1]
|
||||||
# x_array = np.dot(np.ones((h, 1)), xs.reshape((1, w)))
|
x_array = np.dot(np.ones((h, 1)), xs.reshape((1, w)))
|
||||||
# y_array = np.dot(ys.reshape(h, 1), np.ones((1, w)))
|
y_array = np.dot(ys.reshape(h, 1), np.ones((1, w)))
|
||||||
# covered = region.condition(x_array, y_array)
|
covered = region.condition(x_array, y_array)
|
||||||
# rgb = np.array(Color(region.color).get_rgb())
|
rgb = np.array(Color(region.color).get_rgb())
|
||||||
# rgb = (255*rgb).astype('uint8')
|
rgb = (255*rgb).astype('uint8')
|
||||||
# self.pixel_array[covered] = rgb
|
self.pixel_array[covered] = rgb
|
||||||
|
|
||||||
|
|
||||||
def display_vectorized(self, vect_mobject):
|
def display_vectorized(self, vect_mobject):
|
||||||
|
if vect_mobject.is_subpath:
|
||||||
|
#Subpath vectorized mobjects are taken care
|
||||||
|
#of by their parent
|
||||||
|
return
|
||||||
im = Image.fromarray(self.pixel_array, mode = "RGB")
|
im = Image.fromarray(self.pixel_array, mode = "RGB")
|
||||||
canvas = aggdraw.Draw(im)
|
canvas = aggdraw.Draw(im)
|
||||||
pen, fill = self.get_pen_and_fill(vect_mobject)
|
pen, fill = self.get_pen_and_fill(vect_mobject)
|
||||||
pathstring = self.get_pathstring(
|
pathstring = self.get_pathstring(vect_mobject)
|
||||||
self.points_to_pixel_coords(vect_mobject.points),
|
|
||||||
closed = vect_mobject.is_closed()
|
|
||||||
)
|
|
||||||
symbol = aggdraw.Symbol(pathstring)
|
symbol = aggdraw.Symbol(pathstring)
|
||||||
canvas.symbol((0, 0), symbol, pen, fill)
|
canvas.symbol((0, 0), symbol, pen, fill)
|
||||||
canvas.flush()
|
canvas.flush()
|
||||||
@ -121,19 +122,23 @@ class Camera(object):
|
|||||||
)
|
)
|
||||||
return (pen, fill)
|
return (pen, fill)
|
||||||
|
|
||||||
def get_pathstring(self, cubic_bezier_points, closed = False):
|
def get_pathstring(self, vect_mobject):
|
||||||
start = "m%d,%d"%tuple(cubic_bezier_points[0])
|
result = ""
|
||||||
#(handle1, handle2, anchor) tripletes
|
for mob in [vect_mobject]+vect_mobject.subpath_mobjects:
|
||||||
triplets = zip(*[
|
points = mob.points
|
||||||
cubic_bezier_points[i+1::3]
|
coords = self.points_to_pixel_coords(points)
|
||||||
for i in range(3)
|
start = "M%d %d"%tuple(coords[0])
|
||||||
])
|
#(handle1, handle2, anchor) tripletes
|
||||||
cubics = [
|
triplets = zip(*[
|
||||||
"C" + ",".join(map(str, it.chain(*triplet)))
|
coords[i+1::3]
|
||||||
for triplet in triplets
|
for i in range(3)
|
||||||
]
|
])
|
||||||
end = "z" if closed else ""
|
cubics = [
|
||||||
return " ".join([start] + cubics + [end])
|
"C" + " ".join(map(str, it.chain(*triplet)))
|
||||||
|
for triplet in triplets
|
||||||
|
]
|
||||||
|
result += " ".join([start] + cubics)
|
||||||
|
return result
|
||||||
|
|
||||||
def display_point_cloud(self, points, rgbs, thickness):
|
def display_point_cloud(self, points, rgbs, thickness):
|
||||||
if len(points) == 0:
|
if len(points) == 0:
|
||||||
|
14
helpers.py
14
helpers.py
@ -9,8 +9,10 @@ import string
|
|||||||
import re
|
import re
|
||||||
from scipy import linalg
|
from scipy import linalg
|
||||||
|
|
||||||
|
from constants import *
|
||||||
|
|
||||||
def get_smooth_handle_points(points, closed = False):
|
|
||||||
|
def get_smooth_handle_points(points):
|
||||||
num_handles = len(points) - 1
|
num_handles = len(points) - 1
|
||||||
dim = points.shape[1]
|
dim = points.shape[1]
|
||||||
if num_handles < 1:
|
if num_handles < 1:
|
||||||
@ -30,8 +32,8 @@ def get_smooth_handle_points(points, closed = False):
|
|||||||
diag[2,1:-2:2] = -2
|
diag[2,1:-2:2] = -2
|
||||||
diag[3,0:-3:2] = 1
|
diag[3,0:-3:2] = 1
|
||||||
#last
|
#last
|
||||||
diag[2,-2] = 2
|
diag[2,-2] = -1
|
||||||
diag[1,-1] = -1
|
diag[1,-1] = 2
|
||||||
#This is the b as in Ax = b, where we are solving for x,
|
#This is the b as in Ax = b, where we are solving for x,
|
||||||
#and A is represented using diag. However, think of entries
|
#and A is represented using diag. However, think of entries
|
||||||
#to x and b as being points in space, not numbers
|
#to x and b as being points in space, not numbers
|
||||||
@ -42,7 +44,7 @@ def get_smooth_handle_points(points, closed = False):
|
|||||||
solve_func = lambda b : linalg.solve_banded(
|
solve_func = lambda b : linalg.solve_banded(
|
||||||
(l, u), diag, b
|
(l, u), diag, b
|
||||||
)
|
)
|
||||||
if closed:
|
if is_closed(points):
|
||||||
#Get equations to relate first and last points
|
#Get equations to relate first and last points
|
||||||
matrix = diag_to_matrix((l, u), diag)
|
matrix = diag_to_matrix((l, u), diag)
|
||||||
#last row handles second derivative
|
#last row handles second derivative
|
||||||
@ -74,8 +76,8 @@ def diag_to_matrix(l_and_u, diag):
|
|||||||
)
|
)
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
from constants import *
|
def is_closed(points):
|
||||||
|
return np.all(points[0] == points[-1])
|
||||||
|
|
||||||
def color_to_rgb(color):
|
def color_to_rgb(color):
|
||||||
return np.array(Color(color).get_rgb())
|
return np.array(Color(color).get_rgb())
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from .mobject import Mobject
|
from .mobject import Mobject
|
||||||
|
|
||||||
@ -6,10 +6,15 @@ from helpers import *
|
|||||||
|
|
||||||
class VectorizedMobject(Mobject):
|
class VectorizedMobject(Mobject):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"closed" : False,
|
"fill_color" : BLACK,
|
||||||
"fill_color" : BLACK,
|
"fill_opacity" : 0.0,
|
||||||
"fill_opacity" : 0.0
|
#Indicates that it will not be displayed, but
|
||||||
|
#that it should count in parent mobject's path
|
||||||
|
"is_subpath" : False,
|
||||||
}
|
}
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.subpath_mobjects = []
|
||||||
|
Mobject.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
def init_colors(self):
|
def init_colors(self):
|
||||||
@ -51,17 +56,6 @@ class VectorizedMobject(Mobject):
|
|||||||
self.points[0] = point
|
self.points[0] = point
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.closed = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def open(self):
|
|
||||||
self.closed = False
|
|
||||||
return self
|
|
||||||
|
|
||||||
def is_closed(self):
|
|
||||||
return self.closed
|
|
||||||
|
|
||||||
def add_point(self, handle1, handle2, point):
|
def add_point(self, handle1, handle2, point):
|
||||||
self.points = np.append(
|
self.points = np.append(
|
||||||
self.points,
|
self.points,
|
||||||
@ -70,6 +64,9 @@ class VectorizedMobject(Mobject):
|
|||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def is_closed(self):
|
||||||
|
return is_closed(self.points)
|
||||||
|
|
||||||
def set_anchors_and_handles(self, anchors, handles1, handles2):
|
def set_anchors_and_handles(self, anchors, handles1, handles2):
|
||||||
assert(len(anchors) == len(handles1)+1)
|
assert(len(anchors) == len(handles1)+1)
|
||||||
assert(len(anchors) == len(handles2)+1)
|
assert(len(anchors) == len(handles2)+1)
|
||||||
@ -84,7 +81,6 @@ class VectorizedMobject(Mobject):
|
|||||||
def set_points_as_corners(self, points):
|
def set_points_as_corners(self, points):
|
||||||
if len(points) <= 1:
|
if len(points) <= 1:
|
||||||
return self
|
return self
|
||||||
points = self.close_if_needed(points)
|
|
||||||
handles1 = points[:-1]
|
handles1 = points[:-1]
|
||||||
handles2 = points[1:]
|
handles2 = points[1:]
|
||||||
self.set_anchors_and_handles(points, handles1, handles2)
|
self.set_anchors_and_handles(points, handles1, handles2)
|
||||||
@ -93,28 +89,23 @@ class VectorizedMobject(Mobject):
|
|||||||
def set_points_smoothly(self, points):
|
def set_points_smoothly(self, points):
|
||||||
if len(points) <= 1:
|
if len(points) <= 1:
|
||||||
return self
|
return self
|
||||||
points = self.close_if_needed(points)
|
h1, h2 = get_smooth_handle_points(points)
|
||||||
h1, h2 = get_smooth_handle_points(points, self.is_closed())
|
|
||||||
self.set_anchors_and_handles(points, h1, h2)
|
self.set_anchors_and_handles(points, h1, h2)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def close_if_needed(self, points):
|
def set_points(self, points):
|
||||||
if self.is_closed() and not np.all(points[0] == points[-1]):
|
self.points = points
|
||||||
points = np.append(
|
return self
|
||||||
points,
|
|
||||||
[points[0]],
|
|
||||||
axis = 0
|
|
||||||
)
|
|
||||||
return points
|
|
||||||
|
|
||||||
def set_points(self, points, mode = "smooth"):
|
def set_anchor_points(self, points, mode = "smooth"):
|
||||||
points = np.array(points)
|
if not isinstance(points, np.ndarray):
|
||||||
|
points = np.array(points)
|
||||||
|
if self.closed and not is_closed(points):
|
||||||
|
points = np.append(points, [points[0]], axis = 0)
|
||||||
if mode == "smooth":
|
if mode == "smooth":
|
||||||
self.set_points_smoothly(points)
|
self.set_points_smoothly(points)
|
||||||
elif mode == "corners":
|
elif mode == "corners":
|
||||||
self.set_points_as_corners(points)
|
self.set_points_as_corners(points)
|
||||||
elif mode == "handles_included":
|
|
||||||
self.points = points
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown mode")
|
raise Exception("Unknown mode")
|
||||||
return self
|
return self
|
||||||
@ -130,6 +121,25 @@ class VectorizedMobject(Mobject):
|
|||||||
def make_jagged(self):
|
def make_jagged(self):
|
||||||
return self.change_mode("corners")
|
return self.change_mode("corners")
|
||||||
|
|
||||||
|
def add_subpath(self, points):
|
||||||
|
"""
|
||||||
|
A VectorizedMobject is meant to represnt
|
||||||
|
a single "path", in the svg sense of the word.
|
||||||
|
However, one such path may really consit of separate
|
||||||
|
continuous components if there is a move_to command.
|
||||||
|
|
||||||
|
These other portions of the path will be treated as submobjects,
|
||||||
|
but will be tracked in a separate special list for when
|
||||||
|
it comes time to display.
|
||||||
|
"""
|
||||||
|
subpath_mobject = VectorizedMobject(
|
||||||
|
is_subpath = True
|
||||||
|
)
|
||||||
|
subpath_mobject.set_points(points)
|
||||||
|
self.subpath_mobjects.append(subpath_mobject)
|
||||||
|
self.add(subpath_mobject)
|
||||||
|
return self
|
||||||
|
|
||||||
## Information about line
|
## Information about line
|
||||||
|
|
||||||
def component_curves(self):
|
def component_curves(self):
|
||||||
@ -157,24 +167,6 @@ class VectorizedMobject(Mobject):
|
|||||||
|
|
||||||
## Alignment
|
## Alignment
|
||||||
|
|
||||||
# def align_points_with_larger(self, larger_mobject):
|
|
||||||
# assert(isinstance(larger_mobject, VectorizedMobject))
|
|
||||||
# anchors, handles1, handles2 = self.get_anchors_and_handles()
|
|
||||||
# old_n = len(anchors)
|
|
||||||
# new_n = larger_mobject.get_num_points()
|
|
||||||
# #Buff up list of anchor points to appropriate length
|
|
||||||
# new_anchors = anchors[old_n*np.arange(new_n)/new_n]
|
|
||||||
# #At first, handles are on anchor points
|
|
||||||
# #the [2:] is because start has no handles
|
|
||||||
# new_points = new_anchors.repeat(3, axis = 0)[2:]
|
|
||||||
# #These indices indicate the spots between genuinely
|
|
||||||
# #different anchor points in new_points list
|
|
||||||
# indices = 3*(np.arange(old_n) * new_n / old_n)[1:]
|
|
||||||
# new_points[indices+1] = handles1
|
|
||||||
# new_points[indices+2] = handles2
|
|
||||||
# self.set_points(new_points, mode = "handles_included")
|
|
||||||
# return self
|
|
||||||
|
|
||||||
def align_points_with_larger(self, larger_mobject):
|
def align_points_with_larger(self, larger_mobject):
|
||||||
assert(isinstance(larger_mobject, VectorizedMobject))
|
assert(isinstance(larger_mobject, VectorizedMobject))
|
||||||
points = np.array([self.points[0]])
|
points = np.array([self.points[0]])
|
||||||
@ -213,39 +205,7 @@ class VectorizedMobject(Mobject):
|
|||||||
getattr(mobject2, attr),
|
getattr(mobject2, attr),
|
||||||
alpha
|
alpha
|
||||||
))
|
))
|
||||||
|
self.closed = mobject1.is_closed() and mobject2.is_closed()
|
||||||
|
|
||||||
def get_partial_bezier_points(self, x, pre_x = True):
|
|
||||||
"""
|
|
||||||
Input is a number number 0 <= x <= 1, which
|
|
||||||
corresponds to some point along the curve.
|
|
||||||
This point lies on some cubic. This function
|
|
||||||
return the four bezeir points giving a partial
|
|
||||||
version of that cubic. Either the part before x,
|
|
||||||
if pre_x == True, otherwise the part after x
|
|
||||||
"""
|
|
||||||
if x >= 1.0:
|
|
||||||
return np.array(4*[self.points[-1]])
|
|
||||||
num_cubics = self.get_num_points()-1
|
|
||||||
which_curve = int(x*num_cubics)
|
|
||||||
alpha = num_cubics*(x%(1./num_cubics))
|
|
||||||
cubic = self.get_nth_curve(which_curve)
|
|
||||||
new_anchor = cubic(alpha)
|
|
||||||
a1, h1, h2, a2 = self.points[3*which_curve:3*which_curve+4]
|
|
||||||
if pre_x:
|
|
||||||
return np.array([
|
|
||||||
a1,
|
|
||||||
interpolate(cubic(alpha/3), h1, alpha),
|
|
||||||
interpolate(cubic(2*alpha/3), h2, alpha),
|
|
||||||
new_anchor
|
|
||||||
])
|
|
||||||
else:
|
|
||||||
return np.array([
|
|
||||||
new_anchor,
|
|
||||||
interpolate(h1, cubic((1-alpha)/3), alpha),
|
|
||||||
interpolate(h2, cubic(2*(1-alpha/3), alpha)),
|
|
||||||
a2
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def become_partial(self, mobject, a, b):
|
def become_partial(self, mobject, a, b):
|
||||||
@ -255,34 +215,31 @@ class VectorizedMobject(Mobject):
|
|||||||
#-A start, which is some ending portion of an inner cubic
|
#-A start, which is some ending portion of an inner cubic
|
||||||
#-An end, which is the starting portion of a later inner cubic
|
#-An end, which is the starting portion of a later inner cubic
|
||||||
self.open()
|
self.open()
|
||||||
if a <= 0 and b >= 1 and mobject.is_closed():
|
if a <= 0 and b >= 1:
|
||||||
self.close()
|
if mobject.is_closed():
|
||||||
|
self.close()
|
||||||
|
self.set_points(mobject.points, "handles_included")
|
||||||
|
return self
|
||||||
num_cubics = mobject.get_num_points()-1
|
num_cubics = mobject.get_num_points()-1
|
||||||
if a <= 0:
|
lower_index = int(a*num_cubics)
|
||||||
lower_index = 0
|
upper_index = int(b*num_cubics)
|
||||||
start_points = np.zeros((0, 3))
|
points = np.array(
|
||||||
else:
|
mobject.points[3*lower_index:3*upper_index+4]
|
||||||
lower_index = 3*int(a*num_cubics)+4
|
|
||||||
start_points = mobject.get_partial_bezier_points(
|
|
||||||
a, pre_x = False
|
|
||||||
)
|
|
||||||
if b >= 1:
|
|
||||||
upper_index = len(mobject.points)+1
|
|
||||||
end_points = np.zeros((0, 3))
|
|
||||||
else:
|
|
||||||
upper_index = 3*int(b*num_cubics)
|
|
||||||
end_points = mobject.get_partial_bezier_points(
|
|
||||||
b, pre_x = True
|
|
||||||
)
|
|
||||||
new_points = reduce(
|
|
||||||
lambda a, b : np.append(a, b, axis = 0),
|
|
||||||
[
|
|
||||||
start_points,
|
|
||||||
mobject.points[lower_index:upper_index],
|
|
||||||
end_points
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
self.set_points(new_points, "handles_included")
|
if len(points) > 1:
|
||||||
|
#This is a kind of neat-but-dense algorithm
|
||||||
|
#for how to interpolate the handle points
|
||||||
|
a_residue = (num_cubics*a)%1
|
||||||
|
points[:4] = [
|
||||||
|
bezier(points[i:4])(a_residue)
|
||||||
|
for i in range(4)
|
||||||
|
]
|
||||||
|
b_residue = (num_cubics*b)%1
|
||||||
|
points[-4:] = [
|
||||||
|
bezier(points[-4:len(points)-3+i])(b_residue)
|
||||||
|
for i in range(4)
|
||||||
|
]
|
||||||
|
self.set_points(points, "handles_included")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@ -294,7 +251,32 @@ class VectorizedPoint(VectorizedMobject):
|
|||||||
VectorizedMobject.__init__(self, **kwargs)
|
VectorizedMobject.__init__(self, **kwargs)
|
||||||
self.set_points([location])
|
self.set_points([location])
|
||||||
|
|
||||||
|
class VectorizedMobjectFromSVGPathstring(VectorizedMobject):
|
||||||
|
def __init__(self, path_string, **kwargs):
|
||||||
|
digest_locals(self)
|
||||||
|
VectorizedMobject.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
def generate_points(self):
|
||||||
|
path_commands = [
|
||||||
|
"M", #moveto
|
||||||
|
"L", #lineto
|
||||||
|
"H", #horizontal lineto
|
||||||
|
"V", #vertical lineto
|
||||||
|
"C", #curveto
|
||||||
|
"S", #smooth curveto
|
||||||
|
"Q", #quadratic Bezier curve
|
||||||
|
"T", #smooth quadratic Bezier curveto
|
||||||
|
"A", #elliptical Arc
|
||||||
|
"Z", #closepath
|
||||||
|
]
|
||||||
|
pattern = "[%s]"%("".join(path_commands))
|
||||||
|
pairs = zip(
|
||||||
|
re.findall(pattern, self.pathstring),
|
||||||
|
re.split(pattern, self.path_string)
|
||||||
|
)
|
||||||
|
for command, coord_string in pairs:
|
||||||
|
pass
|
||||||
|
#TODO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user