mirror of
https://github.com/3b1b/manim.git
synced 2025-08-01 17:29:06 +08:00
Better interpolation, bezier subpaths, and color bugfix
This commit is contained in:
12
camera.py
12
camera.py
@ -79,10 +79,8 @@ class Camera(object):
|
|||||||
mobject.points, mobject.rgbs,
|
mobject.points, mobject.rgbs,
|
||||||
self.adjusted_thickness(mobject.stroke_width)
|
self.adjusted_thickness(mobject.stroke_width)
|
||||||
)
|
)
|
||||||
else:
|
#TODO, more? Call out if it's unknown?
|
||||||
#TODO
|
|
||||||
print mobject
|
|
||||||
# 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
|
||||||
@ -113,11 +111,11 @@ class Camera(object):
|
|||||||
|
|
||||||
def get_pen_and_fill(self, vect_mobject):
|
def get_pen_and_fill(self, vect_mobject):
|
||||||
pen = aggdraw.Pen(
|
pen = aggdraw.Pen(
|
||||||
vect_mobject.get_stroke_color().get_web(),
|
vect_mobject.get_stroke_color().get_hex_l(),
|
||||||
vect_mobject.stroke_width
|
vect_mobject.stroke_width
|
||||||
)
|
)
|
||||||
fill = aggdraw.Brush(
|
fill = aggdraw.Brush(
|
||||||
vect_mobject.get_fill_color().get_web(),
|
vect_mobject.get_fill_color().get_hex_l(),
|
||||||
opacity = int(255*vect_mobject.get_fill_opacity())
|
opacity = int(255*vect_mobject.get_fill_opacity())
|
||||||
)
|
)
|
||||||
return (pen, fill)
|
return (pen, fill)
|
||||||
@ -126,6 +124,8 @@ class Camera(object):
|
|||||||
result = ""
|
result = ""
|
||||||
for mob in [vect_mobject]+vect_mobject.subpath_mobjects:
|
for mob in [vect_mobject]+vect_mobject.subpath_mobjects:
|
||||||
points = mob.points
|
points = mob.points
|
||||||
|
if len(points) == 0:
|
||||||
|
continue
|
||||||
coords = self.points_to_pixel_coords(points)
|
coords = self.points_to_pixel_coords(points)
|
||||||
start = "M%d %d"%tuple(coords[0])
|
start = "M%d %d"%tuple(coords[0])
|
||||||
#(handle1, handle2, anchor) tripletes
|
#(handle1, handle2, anchor) tripletes
|
||||||
|
21
helpers.py
21
helpers.py
@ -92,6 +92,25 @@ def compass_directions(n = 4, start_vect = UP):
|
|||||||
for k in range(n)
|
for k in range(n)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def partial_bezier_points(points, a, b):
|
||||||
|
"""
|
||||||
|
Given an array of points which define
|
||||||
|
a bezier curve, and two numbres 0<=a<b<=1,
|
||||||
|
return an array of the same size, which
|
||||||
|
describes the portion of the original bezier
|
||||||
|
curve on the interval [a, b].
|
||||||
|
|
||||||
|
This algorithm is pretty nifty, and pretty dense.
|
||||||
|
"""
|
||||||
|
a_to_1 = np.array([
|
||||||
|
bezier(points[i:])(a)
|
||||||
|
for i in range(len(points))
|
||||||
|
])
|
||||||
|
return np.array([
|
||||||
|
bezier(a_to_1[:i+1])(b)
|
||||||
|
for i in range(len(points))
|
||||||
|
])
|
||||||
|
|
||||||
def bezier(points):
|
def bezier(points):
|
||||||
n = len(points) - 1
|
n = len(points) - 1
|
||||||
return lambda t : sum([
|
return lambda t : sum([
|
||||||
@ -101,7 +120,7 @@ def bezier(points):
|
|||||||
|
|
||||||
def remove_list_redundancies(l):
|
def remove_list_redundancies(l):
|
||||||
"""
|
"""
|
||||||
Used instead of lsit(set(l)) to maintain order
|
Used instead of list(set(l)) to maintain order
|
||||||
"""
|
"""
|
||||||
return sorted(list(set(l)), lambda a, b : l.index(a) - l.index(b))
|
return sorted(list(set(l)), lambda a, b : l.index(a) - l.index(b))
|
||||||
|
|
||||||
|
@ -6,8 +6,9 @@ from random import random
|
|||||||
|
|
||||||
from helpers import *
|
from helpers import *
|
||||||
from mobject import Mobject
|
from mobject import Mobject
|
||||||
|
from point_cloud_mobject import PointCloudMobject
|
||||||
|
|
||||||
class ImageMobject(Mobject):
|
class ImageMobject(PointCloudMobject):
|
||||||
"""
|
"""
|
||||||
Automatically filters out black pixels
|
Automatically filters out black pixels
|
||||||
"""
|
"""
|
||||||
|
@ -415,17 +415,21 @@ class Mobject(object):
|
|||||||
elif diff > 0:
|
elif diff > 0:
|
||||||
larger, smaller = self, mobject
|
larger, smaller = self, mobject
|
||||||
for sub_mob in larger.sub_mobjects[-abs(diff):]:
|
for sub_mob in larger.sub_mobjects[-abs(diff):]:
|
||||||
smaller.add(sub_mob.get_point_mobject())
|
point_mob = sub_mob.get_point_mobject(
|
||||||
|
smaller.get_center()
|
||||||
|
)
|
||||||
|
smaller.add(point_mob)
|
||||||
for m1, m2 in zip(self.sub_mobjects, mobject.sub_mobjects):
|
for m1, m2 in zip(self.sub_mobjects, mobject.sub_mobjects):
|
||||||
m1.align_data(m2)
|
m1.align_data(m2)
|
||||||
|
|
||||||
def get_point_mobject(self):
|
def get_point_mobject(self, center):
|
||||||
"""
|
"""
|
||||||
The simplest mobject to be transformed to or from self.
|
The simplest mobject to be transformed to or from self.
|
||||||
Should by a point of the appropriate type
|
Should by a point of the appropriate type
|
||||||
"""
|
"""
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
|
|
||||||
def align_points(self, mobject):
|
def align_points(self, mobject):
|
||||||
count1 = self.get_num_points()
|
count1 = self.get_num_points()
|
||||||
count2 = mobject.get_num_points()
|
count2 = mobject.get_num_points()
|
||||||
|
@ -119,8 +119,10 @@ class PointCloudMobject(Mobject):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_point_mobject(self):
|
def get_point_mobject(self, center):
|
||||||
return Point(self.get_center())
|
if center is None:
|
||||||
|
center = self.get_center()
|
||||||
|
return Point(center)
|
||||||
|
|
||||||
def interpolate_color(self, mobject1, mobject2, alpha):
|
def interpolate_color(self, mobject1, mobject2, alpha):
|
||||||
self.rgbs = interpolate(
|
self.rgbs = interpolate(
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
from mobject import Mobject
|
from mobject import Mobject
|
||||||
|
from point_cloud_mobject import PointCloudMobject
|
||||||
from image_mobject import ImageMobject
|
from image_mobject import ImageMobject
|
||||||
from helpers import *
|
from helpers import *
|
||||||
|
|
||||||
#TODO, Cleanup and refactor this file.
|
#TODO, Cleanup and refactor this file.
|
||||||
|
|
||||||
class TexMobject(Mobject):
|
class TexMobject(PointCloudMobject):
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
"template_tex_file" : TEMPLATE_TEX_FILE,
|
"template_tex_file" : TEMPLATE_TEX_FILE,
|
||||||
"color" : WHITE,
|
"color" : WHITE,
|
||||||
|
@ -18,20 +18,30 @@ class VectorizedMobject(Mobject):
|
|||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
def init_colors(self):
|
def init_colors(self):
|
||||||
self.set_stroke_color(self.color)
|
self.set_stroke(color = self.color)
|
||||||
self.set_fill_color(self.fill_color)
|
self.set_fill(color = self.fill_color)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_fill_color(self, color):
|
def set_family_attr(self, attr, value):
|
||||||
self.fill_rgb = color_to_rgb(color)
|
for mob in self.submobject_family():
|
||||||
|
setattr(mob, attr, value)
|
||||||
|
|
||||||
|
def set_fill(self, color = None, opacity = 1.0):
|
||||||
|
if color is not None:
|
||||||
|
self.set_family_attr("fill_rgb", color_to_rgb(color))
|
||||||
|
self.set_family_attr("fill_opacity", opacity)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_stroke_color(self, color):
|
def set_stroke(self, color = None, width = None):
|
||||||
self.stroke_rgb = color_to_rgb(color)
|
if color is not None:
|
||||||
|
self.set_family_attr("stroke_rgb", color_to_rgb(color))
|
||||||
|
if width is not None:
|
||||||
|
self.set_family_attr("stroke_width", width)
|
||||||
|
return self
|
||||||
|
|
||||||
def highlight(self, color):
|
def highlight(self, color):
|
||||||
self.set_fill_color(color)
|
self.set_fill(color = color)
|
||||||
self.set_stroke_color(color)
|
self.set_stroke(color = color)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_fill_color(self):
|
def get_fill_color(self):
|
||||||
@ -47,19 +57,17 @@ class VectorizedMobject(Mobject):
|
|||||||
#is the predominant color attribute?
|
#is the predominant color attribute?
|
||||||
|
|
||||||
## Drawing
|
## Drawing
|
||||||
def init_points(self):
|
|
||||||
##Default to starting at origin
|
|
||||||
self.points = np.zeros((1, self.dim))
|
|
||||||
return self
|
|
||||||
|
|
||||||
def start_at(self, point):
|
def start_at(self, point):
|
||||||
|
if len(self.points) == 0:
|
||||||
|
self.points = np.zeros((1, 3))
|
||||||
self.points[0] = point
|
self.points[0] = point
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_point(self, handle1, handle2, point):
|
def add_control_points(self, control_points):
|
||||||
|
assert(len(control_points) % 3 == 0)
|
||||||
self.points = np.append(
|
self.points = np.append(
|
||||||
self.points,
|
self.points,
|
||||||
[handle1, handle2, point],
|
control_points,
|
||||||
axis = 0
|
axis = 0
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
@ -143,17 +151,17 @@ class VectorizedMobject(Mobject):
|
|||||||
## Information about line
|
## Information about line
|
||||||
|
|
||||||
def component_curves(self):
|
def component_curves(self):
|
||||||
for n in range(self.get_num_points()-1):
|
for n in range(self.get_num_anchor_points()-1):
|
||||||
yield self.get_nth_curve(n)
|
yield self.get_nth_curve(n)
|
||||||
|
|
||||||
def get_nth_curve(self, n):
|
def get_nth_curve(self, n):
|
||||||
return bezier(self.points[3*n:3*n+4])
|
return bezier(self.points[3*n:3*n+4])
|
||||||
|
|
||||||
def get_num_points(self):
|
def get_num_anchor_points(self):
|
||||||
return (len(self.points) - 1)/3 + 1
|
return (len(self.points) - 1)/3 + 1
|
||||||
|
|
||||||
def point_from_proportion(self, alpha):
|
def point_from_proportion(self, alpha):
|
||||||
num_cubics = self.get_num_points()-1
|
num_cubics = self.get_num_anchor_points()-1
|
||||||
interpoint_alpha = num_cubics*(alpha % (1./num_cubics))
|
interpoint_alpha = num_cubics*(alpha % (1./num_cubics))
|
||||||
index = 3*int(alpha*num_cubics)
|
index = 3*int(alpha*num_cubics)
|
||||||
cubic = bezier(self.points[index:index+4])
|
cubic = bezier(self.points[index:index+4])
|
||||||
@ -170,27 +178,31 @@ class VectorizedMobject(Mobject):
|
|||||||
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]])
|
||||||
target_len = larger_mobject.get_num_points()-1
|
target_len = larger_mobject.get_num_anchor_points()-1
|
||||||
num_curves = self.get_num_points()-1
|
num_curves = self.get_num_anchor_points()-1
|
||||||
#curves are buckets, and we need to know how many new
|
#Curves in self are buckets, and we need to know
|
||||||
#anchor points to put into each one
|
#how many new anchor points to put into each one.
|
||||||
|
#Each element of index_allocation is like a bucket,
|
||||||
|
#and its value tells you the appropriate index of
|
||||||
|
#the smaller curve.
|
||||||
index_allocation = (np.arange(target_len) * num_curves)/target_len
|
index_allocation = (np.arange(target_len) * num_curves)/target_len
|
||||||
for index, curve in enumerate(self.component_curves()):
|
for index in range(num_curves):
|
||||||
num_inter_points = sum(index_allocation == index)
|
curr_bezier_points = self.points[3*index:3*index+4]
|
||||||
step = 1./num_inter_points
|
num_inter_curves = sum(index_allocation == index)
|
||||||
|
step = 1./num_inter_curves
|
||||||
alphas = np.arange(0, 1+step, step)
|
alphas = np.arange(0, 1+step, step)
|
||||||
new_anchors = np.array(map(curve, alphas))
|
for a, b in zip(alphas, alphas[1:]):
|
||||||
h1, h2 = get_smooth_handle_points(new_anchors)
|
new_points = partial_bezier_points(curr_bezier_points, a, b)
|
||||||
new_points = np.array(
|
points = np.append(
|
||||||
zip(h1, h2, new_anchors[1:])
|
points, new_points[1:], axis = 0
|
||||||
)
|
)
|
||||||
new_points = new_points.reshape((new_points.size/3, 3))
|
self.set_points(points)
|
||||||
points = np.append(points, new_points, 0)
|
|
||||||
self.set_points(points, "handles_included")
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def get_point_mobject(self):
|
def get_point_mobject(self, center):
|
||||||
return VectorizedPoint(self.get_center())
|
if center is None:
|
||||||
|
center = self.get_center()
|
||||||
|
return VectorizedPoint(center)
|
||||||
|
|
||||||
def interpolate_color(self, mobject1, mobject2, alpha):
|
def interpolate_color(self, mobject1, mobject2, alpha):
|
||||||
attrs = [
|
attrs = [
|
||||||
@ -205,8 +217,6 @@ class VectorizedMobject(Mobject):
|
|||||||
getattr(mobject2, attr),
|
getattr(mobject2, attr),
|
||||||
alpha
|
alpha
|
||||||
))
|
))
|
||||||
self.closed = mobject1.is_closed() and mobject2.is_closed()
|
|
||||||
|
|
||||||
|
|
||||||
def become_partial(self, mobject, a, b):
|
def become_partial(self, mobject, a, b):
|
||||||
assert(isinstance(mobject, VectorizedMobject))
|
assert(isinstance(mobject, VectorizedMobject))
|
||||||
@ -214,32 +224,25 @@ class VectorizedMobject(Mobject):
|
|||||||
#-A middle section, which matches the curve exactly
|
#-A middle section, which matches the curve exactly
|
||||||
#-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()
|
|
||||||
if a <= 0 and b >= 1:
|
if a <= 0 and b >= 1:
|
||||||
if mobject.is_closed():
|
self.set_points(mobject.points)
|
||||||
self.close()
|
|
||||||
self.set_points(mobject.points, "handles_included")
|
|
||||||
return self
|
return self
|
||||||
num_cubics = mobject.get_num_points()-1
|
num_cubics = mobject.get_num_anchor_points()-1
|
||||||
lower_index = int(a*num_cubics)
|
lower_index = int(a*num_cubics)
|
||||||
upper_index = int(b*num_cubics)
|
upper_index = int(b*num_cubics)
|
||||||
points = np.array(
|
points = np.array(
|
||||||
mobject.points[3*lower_index:3*upper_index+4]
|
mobject.points[3*lower_index:3*upper_index+4]
|
||||||
)
|
)
|
||||||
if len(points) > 1:
|
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
|
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
|
b_residue = (num_cubics*b)%1
|
||||||
points[-4:] = [
|
points[:4] = partial_bezier_points(
|
||||||
bezier(points[-4:len(points)-3+i])(b_residue)
|
points[:4], a_residue, 1
|
||||||
for i in range(4)
|
)
|
||||||
]
|
points[-4:] = partial_bezier_points(
|
||||||
self.set_points(points, "handles_included")
|
points[-4:], 0, b_residue
|
||||||
|
)
|
||||||
|
self.set_points(points)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@ -256,8 +259,8 @@ class VectorizedMobjectFromSVGPathstring(VectorizedMobject):
|
|||||||
digest_locals(self)
|
digest_locals(self)
|
||||||
VectorizedMobject.__init__(self, **kwargs)
|
VectorizedMobject.__init__(self, **kwargs)
|
||||||
|
|
||||||
def generate_points(self):
|
def get_path_commands(self):
|
||||||
path_commands = [
|
return [
|
||||||
"M", #moveto
|
"M", #moveto
|
||||||
"L", #lineto
|
"L", #lineto
|
||||||
"H", #horizontal lineto
|
"H", #horizontal lineto
|
||||||
@ -269,15 +272,67 @@ class VectorizedMobjectFromSVGPathstring(VectorizedMobject):
|
|||||||
"A", #elliptical Arc
|
"A", #elliptical Arc
|
||||||
"Z", #closepath
|
"Z", #closepath
|
||||||
]
|
]
|
||||||
pattern = "[%s]"%("".join(path_commands))
|
|
||||||
|
def generate_points(self):
|
||||||
|
pattern = "[%s]"%("".join(self.get_path_commands()))
|
||||||
pairs = zip(
|
pairs = zip(
|
||||||
re.findall(pattern, self.pathstring),
|
re.findall(pattern, self.path_string),
|
||||||
re.split(pattern, self.path_string)
|
re.split(pattern, self.path_string)[1:]
|
||||||
)
|
)
|
||||||
|
#Which mobject should new points be added to
|
||||||
|
self.growing_path = self
|
||||||
for command, coord_string in pairs:
|
for command, coord_string in pairs:
|
||||||
pass
|
self.handle_command(command, coord_string)
|
||||||
#TODO
|
#people treat y-coordinate differently
|
||||||
|
self.rotate(np.pi, RIGHT)
|
||||||
|
|
||||||
|
def handle_command(self, command, coord_string):
|
||||||
|
#new_points are the points that will be added to the curr_points
|
||||||
|
#list. This variable may get modified in the conditionals below.
|
||||||
|
points = self.growing_path.points
|
||||||
|
new_points = self.string_to_points(coord_string)
|
||||||
|
if command == "M": #moveto
|
||||||
|
if len(points) > 0:
|
||||||
|
self.add_subpath(new_points)
|
||||||
|
self.growing_path = self.subpath_mobjects[-1]
|
||||||
|
else:
|
||||||
|
self.growing_path.start_at(new_points[0])
|
||||||
|
return
|
||||||
|
elif command in ["L", "H", "V"]: #lineto
|
||||||
|
if command == "H":
|
||||||
|
new_points[0,1] = points[-1,1]
|
||||||
|
elif command == "V":
|
||||||
|
new_points[0,1] = new_points[0,0]
|
||||||
|
new_points[0,0] = points[-1,0]
|
||||||
|
new_points = new_points[[0, 0, 0]]
|
||||||
|
elif command == "C": #curveto
|
||||||
|
pass #Yay! No action required
|
||||||
|
elif command in ["S", "T"]: #smooth curveto
|
||||||
|
handle1 = points[-1]+(points[-1]-points[-2])
|
||||||
|
new_points = np.append([handle1], new_points, axis = 0)
|
||||||
|
if command in ["Q", "T"]: #quadratic Bezier curve
|
||||||
|
#TODO, this is a suboptimal approximation
|
||||||
|
new_points = np.append([new_points[0]], new_points, axis = 0)
|
||||||
|
elif command == "A": #elliptical Arc
|
||||||
|
raise Exception("Not implemented")
|
||||||
|
elif command == "Z": #closepath
|
||||||
|
if not is_closed(points):
|
||||||
|
#Both handles and new anchor are the start
|
||||||
|
new_points = points[[0, 0, 0]]
|
||||||
|
self.growing_path.add_control_points(new_points)
|
||||||
|
|
||||||
|
def string_to_points(self, coord_string):
|
||||||
|
numbers = [
|
||||||
|
float(s)
|
||||||
|
for s in coord_string.split(" ")
|
||||||
|
if s is not ""
|
||||||
|
]
|
||||||
|
if len(numbers)%2 == 1:
|
||||||
|
numbers.append(0)
|
||||||
|
num_points = len(numbers)/2
|
||||||
|
result = np.zeros((num_points, self.dim))
|
||||||
|
result[:,:2] = np.array(numbers).reshape((num_points, 2))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user