Reimplented svg rendering using cairo, and changed vmobject color model to allow for gradeints and strokes with opacities. Many errors associated with python 2 to python 3 conversion are likely still present at this point.

This commit is contained in:
Grant Sanderson
2018-08-10 15:12:49 -07:00
parent 858051a806
commit 087715e538
7 changed files with 296 additions and 204 deletions

View File

@ -10,6 +10,7 @@ import time
from PIL import Image from PIL import Image
from colour import Color from colour import Color
from scipy.spatial.distance import pdist from scipy.spatial.distance import pdist
import cairo
from constants import * from constants import *
from mobject.types.image_mobject import AbstractImageMobject from mobject.types.image_mobject import AbstractImageMobject
@ -50,6 +51,7 @@ class Camera(object):
# z_buff_func is only used if the flag above is set to True. # z_buff_func is only used if the flag above is set to True.
# round z coordinate to nearest hundredth when comparring # round z coordinate to nearest hundredth when comparring
"z_buff_func": lambda m: np.round(m.get_center()[2], 2), "z_buff_func": lambda m: np.round(m.get_center()[2], 2),
"cairo_line_width_multiple": 0.01,
} }
def __init__(self, background=None, **kwargs): def __init__(self, background=None, **kwargs):
@ -278,14 +280,25 @@ class Camera(object):
# Methods associated with svg rendering # Methods associated with svg rendering
def get_aggdraw_canvas(self): def get_cairo_context(self):
if not hasattr(self, "canvas") or not self.canvas: # TODO, make sure this isn't run too much
self.reset_aggdraw_canvas() pw = self.get_pixel_width()
return self.canvas ph = self.get_pixel_height()
fw = self.get_frame_width()
def reset_aggdraw_canvas(self): fh = self.get_frame_height()
image = Image.fromarray(self.pixel_array, mode=self.image_mode) surface = cairo.ImageSurface.create_for_data(
self.canvas = aggdraw.Draw(image) self.pixel_array,
cairo.FORMAT_ARGB32,
pw, ph
)
ctx = cairo.Context(surface)
ctx.scale(pw, ph)
ctx.set_matrix(cairo.Matrix(
fdiv(pw, fw), 0,
0, -fdiv(ph, fh),
pw / 2, ph / 2,
))
return ctx
def display_multiple_vectorized_mobjects(self, vmobjects): def display_multiple_vectorized_mobjects(self, vmobjects):
if len(vmobjects) == 0: if len(vmobjects) == 0:
@ -301,90 +314,87 @@ class Camera(object):
self.display_multiple_non_background_colored_vmobjects(batch) self.display_multiple_non_background_colored_vmobjects(batch)
def display_multiple_non_background_colored_vmobjects(self, vmobjects): def display_multiple_non_background_colored_vmobjects(self, vmobjects):
self.reset_aggdraw_canvas()
canvas = self.get_aggdraw_canvas()
for vmobject in vmobjects: for vmobject in vmobjects:
self.display_vectorized(vmobject, canvas) self.display_vectorized(vmobject)
canvas.flush()
def display_vectorized(self, vmobject, canvas=None): def display_vectorized(self, vmobject):
if vmobject.is_subpath: if vmobject.is_subpath:
# Subpath vectorized mobjects are taken care # Subpath vectorized mobjects are taken care
# of by their parent # of by their parent
return return
canvas = canvas or self.get_aggdraw_canvas() ctx = self.get_cairo_context()
pen, fill = self.get_pen_and_fill(vmobject) self.set_cairo_context_path(ctx, vmobject)
pathstring = self.get_pathstring(vmobject) self.apply_stroke(ctx, vmobject, background=True)
symbol = aggdraw.Symbol(pathstring) self.apply_fill(ctx, vmobject)
self.draw_background_stroke(canvas, vmobject, symbol) self.apply_stroke(ctx, vmobject)
canvas.symbol((0, 0), symbol, pen, fill) ctx.new_path()
return self
def draw_background_stroke(self, canvas, vmobject, symbol): def set_cairo_context_path(self, ctx, vmobject):
bs_width = vmobject.get_background_stroke_width() for vmob in it.chain([vmobject], vmobject.get_subpath_mobjects()):
if bs_width == 0: points = vmob.points
return ctx.new_sub_path()
bs_rgb = vmobject.get_background_stroke_rgb() ctx.move_to(*points[0][:2])
bs_hex = rgb_to_hex(bs_rgb) for triplet in zip(points[1::3], points[2::3], points[3::3]):
pen = aggdraw.Pen(bs_hex, bs_width) ctx.curve_to(*it.chain(*[
canvas.symbol((0, 0), symbol, pen, None) point[:2] for point in triplet
]))
if vmob.is_closed():
ctx.close_path()
return self
def get_pen_and_fill(self, vmobject): def set_cairo_context_color(self, ctx, rgbas, vmobject):
stroke_width = max(vmobject.get_stroke_width(), 0) if len(rgbas) == 0:
if stroke_width == 0: # Use reversed rgb because cairo surface is
pen = None # encodes it in reverse order
ctx.set_source_rgba(
*rgbas[0][2::-1], rgbas[0][3]
)
else: else:
stroke_rgb = self.get_stroke_rgb(vmobject) points = vmobject.get_gradient_start_and_end_points()
stroke_hex = rgb_to_hex(stroke_rgb) pat = cairo.LinearGradient(*it.chain(*[
pen = aggdraw.Pen(stroke_hex, stroke_width) point[:2] for point in points
]))
offsets = np.linspace(1, 0, len(rgbas))
for rgba, offset in zip(rgbas, offsets):
pat.add_color_stop_rgba(
offset, *rgba[2::-1], rgba[3]
)
ctx.set_source(pat)
return self
fill_opacity = int(self.rgb_max_val * vmobject.get_fill_opacity()) def apply_fill(self, ctx, vmobject):
if fill_opacity == 0: self.set_cairo_context_color(
fill = None ctx, self.get_fill_rgbas(vmobject), vmobject
else: )
fill_rgb = self.get_fill_rgb(vmobject) ctx.fill_preserve()
fill_hex = rgb_to_hex(fill_rgb) return self
fill = aggdraw.Brush(fill_hex, fill_opacity)
return (pen, fill) def apply_stroke(self, ctx, vmobject, background=False):
width = vmobject.get_stroke_width(background)
self.set_cairo_context_color(
ctx,
self.get_stroke_rgbas(vmobject, background=background),
vmobject
)
ctx.set_line_width(
width * self.cairo_line_width_multiple
)
ctx.stroke_preserve()
return self
def color_to_hex_l(self, color): def get_stroke_rgbas(self, vmobject, background=False):
try: return vmobject.get_stroke_rgbas(background)
return color.get_hex_l()
except:
return Color(BLACK).get_hex_l()
def get_stroke_rgb(self, vmobject): def get_fill_rgbas(self, vmobject):
return vmobject.get_stroke_rgb() return vmobject.get_fill_rgbas()
def get_fill_rgb(self, vmobject):
return vmobject.get_fill_rgb()
def get_pathstring(self, vmobject):
result = ""
for mob in [vmobject] + vmobject.get_subpath_mobjects():
points = mob.points
# points = self.adjust_out_of_range_points(points)
if len(points) == 0:
continue
coords = self.points_to_pixel_coords(points)
coord_strings = coords.flatten().astype(str)
# Start new path string with M
coord_strings[0] = "M" + coord_strings[0]
# The C at the start of every 6th number communicates
# that the following 6 define a cubic Bezier
coord_strings[2::6] = ["C" + str(s) for s in coord_strings[2::6]]
# Possibly finish with "Z"
if vmobject.mark_paths_closed:
coord_strings[-1] = coord_strings[-1] + " Z"
result += " ".join(coord_strings)
return result
def get_background_colored_vmobject_displayer(self): def get_background_colored_vmobject_displayer(self):
# Quite wordy to type out a bunch # Quite wordy to type out a bunch
long_name = "background_colored_vmobject_displayer" bcvd = "background_colored_vmobject_displayer"
if not hasattr(self, long_name): if not hasattr(self, bcvd):
setattr(self, long_name, BackgroundColoredVMobjectDisplayer(self)) setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self))
return getattr(self, long_name) return getattr(self, bcvd)
def display_multiple_background_colored_vmobject(self, cvmobjects): def display_multiple_background_colored_vmobject(self, cvmobjects):
displayer = self.get_background_colored_vmobject_displayer() displayer = self.get_background_colored_vmobject_displayer()
@ -595,6 +605,7 @@ class Camera(object):
return centered_space_coords return centered_space_coords
# TODO
class BackgroundColoredVMobjectDisplayer(object): class BackgroundColoredVMobjectDisplayer(object):
def __init__(self, camera): def __init__(self, camera):
self.camera = camera self.camera = camera

View File

@ -878,7 +878,7 @@ class Mobject(Container):
self.add(self.copy()) self.add(self.copy())
n -= 1 n -= 1
curr += 1 curr += 1
indices = curr * np.arange(curr + n) / (curr + n) indices = curr * np.arange(curr + n) // (curr + n)
new_submobjects = [] new_submobjects = []
for index in indices: for index in indices:
submob = self.submobjects[index] submob = self.submobjects[index]

View File

@ -375,7 +375,7 @@ class VMobjectFromSVGPathstring(VMobject):
numbers = string_to_numbers(coord_string) numbers = string_to_numbers(coord_string)
if len(numbers) % 2 == 1: if len(numbers) % 2 == 1:
numbers.append(0) numbers.append(0)
num_points = len(numbers) / 2 num_points = len(numbers) // 2
result = np.zeros((num_points, self.dim)) result = np.zeros((num_points, self.dim))
result[:, :2] = np.array(numbers).reshape((num_points, 2)) result[:, :2] = np.array(numbers).reshape((num_points, 2))
return result return result

View File

@ -9,8 +9,11 @@ from utils.bezier import get_smooth_handle_points
from utils.bezier import interpolate from utils.bezier import interpolate
from utils.bezier import is_closed from utils.bezier import is_closed
from utils.bezier import partial_bezier_points from utils.bezier import partial_bezier_points
from utils.color import color_to_rgb from utils.color import color_to_rgba
from utils.color import interpolate_color
from utils.iterables import make_even from utils.iterables import make_even
from utils.iterables import tuplify
from utils.iterables import stretch_array_to_length
class VMobject(Mobject): class VMobject(Mobject):
@ -18,11 +21,21 @@ class VMobject(Mobject):
"fill_color": None, "fill_color": None,
"fill_opacity": 0.0, "fill_opacity": 0.0,
"stroke_color": None, "stroke_color": None,
"stroke_opacity": 1.0,
"stroke_width": DEFAULT_POINT_THICKNESS, "stroke_width": DEFAULT_POINT_THICKNESS,
# The purpose of background stroke is to have # The purpose of background stroke is to have
# something that won't overlap the fill # something that won't overlap the fill, e.g.
# For text against some textured background
"background_stroke_color": BLACK, "background_stroke_color": BLACK,
"background_stroke_opacity": 1.0,
"background_stroke_width": 0, "background_stroke_width": 0,
# When a color c is set, there will be a second color
# computed based on interpolating c to WHITE by with
# gradient_to_white_factor, and the display will
# gradient to this secondary color in the direction
# of color_gradient_direction.
"color_gradient_direction": UL,
"gradient_to_white_factor": 0.2,
# Indicates that it will not be displayed, but # Indicates that it will not be displayed, but
# that it should count in parent mobject's path # that it should count in parent mobject's path
"is_subpath": False, "is_subpath": False,
@ -39,95 +52,95 @@ class VMobject(Mobject):
# Colors # Colors
def init_colors(self): def init_colors(self):
self.set_style_data( self.set_fill(
fill_color=self.fill_color or self.color, color=self.fill_color or self.color,
fill_opacity=self.fill_opacity, opacity=self.fill_opacity,
stroke_color=self.stroke_color or self.color,
stroke_width=self.stroke_width,
background_stroke_color=self.background_stroke_color,
background_stroke_width=self.background_stroke_width,
family=self.propagate_style_to_family family=self.propagate_style_to_family
) )
self.set_stroke(
color=self.stroke_color or self.color,
width=self.stroke_width,
opacity=self.stroke_opacity,
family=self.propagate_style_to_family
)
self.set_background_stroke(
color=self.background_stroke_color,
width=self.background_stroke_width,
opacity=self.background_stroke_opacity,
family=self.propagate_style_to_family,
)
return self return self
def set_family_attr(self, attr, value): def get_rgbas_array(self, color=None, opacity=None):
for mob in self.submobject_family(): """
setattr(mob, attr, value) First arg can be either a color, or a tuple/list of colors.
Likewise, opacity can either be a float, or a tuple of floats.
If self.gradient_to_white_factor is not zero, and only
one color was passed in, a second slightly light color
will automatically be added for the gradient
"""
if color is None:
color = self.color
colors = list(tuplify(color))
opacities = list(tuplify(opacity))
g2w_factor = self.get_gradient_to_white_factor()
if g2w_factor != 0 and len(colors) == 1:
lighter_color = interpolate_color(
colors[0], WHITE, g2w_factor
)
colors.append(lighter_color)
def set_style_data(self, return np.array([
fill_color=None, color_to_rgba(c, o)
fill_opacity=None, for c, o in zip(*make_even(colors, opacities))
stroke_color=None, ])
stroke_width=None,
background_stroke_color=None,
background_stroke_width=None,
family=True
):
kwargs = {
"fill_color": fill_color,
"fill_opacity": fill_opacity,
"stroke_color": stroke_color,
"stroke_width": stroke_width,
"background_stroke_color": background_stroke_color,
"background_stroke_width": background_stroke_width,
"family": family,
}
for key in "fill_color", "stroke_color", "background_stroke_color":
# Instead of setting a self.fill_color attr,
# set a numerical self.fill_rgb to make
# interpolation easier
key_with_rgb = key.replace("color", "rgb")
color = kwargs[key]
if color is not None:
setattr(self, key_with_rgb, color_to_rgb(color))
for key in "fill_opacity", "stroke_width", "background_stroke_width":
if kwargs[key] is not None:
setattr(self, key, kwargs[key])
if family:
for mob in self.submobjects:
mob.set_style_data(**kwargs)
return self
def set_fill(self, color=None, opacity=None, family=True): def set_fill(self, color=None, opacity=None, family=True):
return self.set_style_data( if opacity is None:
fill_color=color, opacity = self.get_fill_opacity()
fill_opacity=opacity, self.fill_rgbas = self.get_rgbas_array(color, opacity)
family=family if family:
) for submobject in self.submobjects:
submobject.set_fill(color, opacity, family)
return self
def set_stroke(self, color=None, width=None, family=True): def set_stroke(self, color=None, width=None, opacity=None,
return self.set_style_data( background=False, family=True):
stroke_color=color, if opacity is None:
stroke_width=width, opacity = self.get_stroke_opacity(background)
family=family
)
def set_background_stroke(self, color=None, width=None, family=True): if background:
return self.set_style_data( array_name = "background_stroke_rgbas"
background_stroke_color=color, width_name = "background_stroke_width"
background_stroke_width=width, else:
family=family array_name = "stroke_rgbas"
) width_name = "stroke_width"
rgbas = self.get_rgbas_array(color, opacity)
setattr(self, array_name, rgbas)
if width is not None:
setattr(self, width_name, width)
if family:
for submobject in self.submobjects:
submobject.set_stroke(
color, width, opacity, background, family
)
return self
def set_background_stroke(self, **kwargs):
kwargs["background"] = True
self.set_stroke(**kwargs)
return self
def set_color(self, color, family=True): def set_color(self, color, family=True):
self.set_style_data( self.set_fill(color, family=family)
stroke_color=color, self.set_stroke(color, family=family)
fill_color=color,
family=family
)
self.color = color
return self return self
def match_style(self, vmobject): def match_style(self, vmobject):
self.set_style_data( for a_name in ["fill_rgbas", "stroke_rgbas", "background_stroke_rgbas"]:
fill_color=vmobject.get_fill_color(), setattr(self, np.array(get_attr(vmobject, a_name)))
fill_opacity=vmobject.get_fill_opacity(), self.stroke_width = vmobject.stroke_width
stroke_color=vmobject.get_stroke_color(), self.background_stroke_width = vmobject.background_stroke_width
stroke_width=vmobject.get_stroke_width(),
background_stroke_color=vmobject.get_background_stroke_color(),
background_stroke_width=vmobject.get_background_stroke_width(),
family=False
)
# Does its best to match up submobject lists, and # Does its best to match up submobject lists, and
# match styles accordingly # match styles accordingly
@ -151,52 +164,99 @@ class VMobject(Mobject):
) )
return self return self
def get_fill_rgb(self): def get_fill_rgbas(self):
return np.clip(self.fill_rgb, 0, 1) return np.clip(self.fill_rgbas, 0, 1)
def get_fill_color(self): def get_fill_color(self):
try: """
self.fill_rgb = np.clip(self.fill_rgb, 0.0, 1.0) If there are multiple colors (for gradient)
return Color(rgb=self.fill_rgb) this returns the first one
except: """
return Color(WHITE) return self.get_fill_colors()[0]
def get_fill_opacity(self): def get_fill_opacity(self):
return np.clip(self.fill_opacity, 0, 1) """
If there are multiple opacities, this returns the
first
"""
return self.get_fill_opacities()[0]
def get_stroke_rgb(self): def get_fill_colors(self):
return np.clip(self.stroke_rgb, 0, 1) return [
Color(rgb=rgba[:3])
for rgba in self.get_fill_rgbas()
]
def get_stroke_color(self): def get_fill_opacities(self):
try: return self.get_fill_rgbas()[:, 3]
self.stroke_rgb = np.clip(self.stroke_rgb, 0, 1)
return Color(rgb=self.stroke_rgb)
except:
return Color(WHITE)
def get_stroke_width(self): def get_stroke_rgbas(self, background=False):
return max(0, self.stroke_width) if background:
rgbas = self.background_stroke_rgbas
else:
rgbas = self.stroke_rgbas
return np.clip(rgbas, 0, 1)
def get_background_stroke_rgb(self): def get_stroke_color(self, background=False):
return np.clip(self.background_stroke_rgb, 0, 1) return self.get_stroke_colors(background)[0]
def get_background_stroke_color(self): def get_stroke_width(self, background=False):
try: if background:
self.background_stroke_rgb = np.clip( width = self.background_stroke_width
self.background_stroke_rgb, 0, 1 else:
) width = self.stroke_width
return Color(rgb=self.background_stroke_rgb) return max(0, width)
except:
return Color(WHITE)
def get_background_stroke_width(self): def get_stroke_opacity(self, background=False):
return max(0, self.background_stroke_width) return self.get_stroke_opacities(background)[0]
def get_stroke_colors(self, background=False):
return [
Color(rgb=rgba[:3])
for rgba in self.get_stroke_rgbas(background)
]
def get_stroke_opacities(self, background=False):
return self.get_stroke_rgbas(background)[:, 3]
def get_color(self): def get_color(self):
if self.fill_opacity == 0: if np.all(self.get_fill_opacities() == 0):
return self.get_stroke_color() return self.get_stroke_color()
return self.get_fill_color() return self.get_fill_color()
def set_color_gradient_direction(self, direction, family=True):
direction = np.array(direction)
if family:
for submob in self.submobject_family():
submob.color_gradient_direction = direction
else:
self.color_gradient_direction = direction
return self
def set_gradient_to_white_factor(self, factor, family=True):
if family:
for submob in self.submobject_family():
submob.gradient_to_white_factor = factor
else:
self.gradient_to_white_factor = factor
return self
def get_color_gradient_direction(self):
return np.array(self.color_gradient_direction)
def get_gradient_to_white_factor(self):
return self.gradient_to_white_factor
def get_gradient_start_and_end_points(self):
direction = self.get_color_gradient_direction()
c = self.get_center()
bases = np.array([
self.get_edge_center(vect) - c
for vect in [RIGHT, UP, OUT]
]).transpose()
offset = np.dot(bases, direction)
return (c + offset, c - offset)
def color_using_background_image(self, background_image_file): def color_using_background_image(self, background_image_file):
self.background_image_file = background_image_file self.background_image_file = background_image_file
self.set_color(WHITE) self.set_color(WHITE)
@ -357,7 +417,7 @@ class VMobject(Mobject):
return bezier(self.points[3 * n:3 * n + 4]) return bezier(self.points[3 * n:3 * n + 4])
def get_num_anchor_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_anchor_points() - 1 num_cubics = self.get_num_anchor_points() - 1
@ -379,12 +439,13 @@ class VMobject(Mobject):
return self.get_anchors() return self.get_anchors()
# Alignment # Alignment
def align_points(self, mobject): def align_points(self, vmobject):
Mobject.align_points(self, mobject) Mobject.align_points(self, vmobject)
is_subpath = self.is_subpath or mobject.is_subpath self.align_rgbas(vmobject)
self.is_subpath = mobject.is_subpath = is_subpath is_subpath = self.is_subpath or vmobject.is_subpath
mark_closed = self.mark_paths_closed and mobject.mark_paths_closed self.is_subpath = vmobject.is_subpath = is_subpath
self.mark_paths_closed = mobject.mark_paths_closed = mark_closed mark_closed = self.mark_paths_closed and vmobject.mark_paths_closed
self.mark_paths_closed = vmobject.mark_paths_closed = mark_closed
return self return self
def align_points_with_larger(self, larger_mobject): def align_points_with_larger(self, larger_mobject):
@ -411,7 +472,7 @@ class VMobject(Mobject):
# and its value tells you the appropriate index of # and its value tells you the appropriate index of
# the smaller curve. # the smaller curve.
index_allocation = (np.arange(curr + n - 1) * index_allocation = (np.arange(curr + n - 1) *
num_curves) / (curr + n - 1) num_curves) // (curr + n - 1)
for index in range(num_curves): for index in range(num_curves):
curr_bezier_points = self.points[3 * index:3 * index + 4] curr_bezier_points = self.points[3 * index:3 * index + 4]
num_inter_curves = sum(index_allocation == index) num_inter_curves = sum(index_allocation == index)
@ -427,6 +488,19 @@ class VMobject(Mobject):
self.set_points(points) self.set_points(points)
return self return self
def align_rgbas(self, vmobject):
attrs = ["fill_rgbas", "stroke_rgbas", "background_stroke_rgbas"]
for attr in attrs:
a1 = getattr(self, attr)
a2 = getattr(vmobject, attr)
if len(a1) > len(a2):
new_a2 = stretch_array_to_length(a2, len(a1))
setattr(vmobject, attr, new_a2)
elif len(a2) > len(a1):
new_a1 = stretch_array_to_length(a1, len(a2))
setattr(self, attr, new_a1)
return self
def get_point_mobject(self, center=None): def get_point_mobject(self, center=None):
if center is None: if center is None:
center = self.get_center() center = self.get_center()
@ -439,12 +513,13 @@ class VMobject(Mobject):
def interpolate_color(self, mobject1, mobject2, alpha): def interpolate_color(self, mobject1, mobject2, alpha):
attrs = [ attrs = [
"fill_rgb", "fill_rgbas",
"fill_opacity", "stroke_rgbas",
"stroke_rgb", "background_stroke_rgbas",
"stroke_width", "stroke_width",
"background_stroke_rgb",
"background_stroke_width", "background_stroke_width",
"color_gradient_direction",
"gradient_to_white_factor",
] ]
for attr in attrs: for attr in attrs:
setattr(self, attr, interpolate( setattr(self, attr, interpolate(
@ -547,6 +622,6 @@ class DashedMobject(VMobject):
for i in range(self.dashes_num): for i in range(self.dashes_num):
a = ((1 + buff) * i) / self.dashes_num a = ((1 + buff) * i) / self.dashes_num
b = 1 - ((1 + buff) * (self.dashes_num - 1 - i)) / self.dashes_num b = 1 - ((1 + buff) * (self.dashes_num - 1 - i)) / self.dashes_num
dash = VMobject(color=self.color) dash = VMobject(color=self.get_color())
dash.pointwise_become_partial(mobject, a, b) dash.pointwise_become_partial(mobject, a, b)
self.submobjects.append(dash) self.submobjects.append(dash)

View File

@ -6,4 +6,4 @@ progressbar==2.5
scipy==1.1.0 scipy==1.1.0
tqdm==4.24.0 tqdm==4.24.0
opencv-python==3.4.2.17 opencv-python==3.4.2.17
git+https://github.com/scottopell/aggdraw-64bits@c95aac4369038706943fd0effb7d888683860e5a#egg=aggdraw pycairo==1.17.1

View File

@ -72,7 +72,7 @@ def tuplify(obj):
return (obj,) return (obj,)
try: try:
return tuple(obj) return tuple(obj)
except: except TypeError:
return (obj,) return (obj,)
@ -90,8 +90,8 @@ def make_even(iterable_1, iterable_2):
list_1, list_2 = list(iterable_1), list(iterable_2) list_1, list_2 = list(iterable_1), list(iterable_2)
length = max(len(list_1), len(list_2)) length = max(len(list_1), len(list_2))
return ( return (
[list_1[(n * len(list_1)) / length] for n in range(length)], [list_1[(n * len(list_1)) // length] for n in range(length)],
[list_2[(n * len(list_2)) / length] for n in range(length)] [list_2[(n * len(list_2)) // length] for n in range(length)]
) )

View File

@ -1,11 +1,17 @@
import os import os
import hashlib
from constants import TEX_DIR from constants import TEX_DIR
from constants import TEX_TEXT_TO_REPLACE from constants import TEX_TEXT_TO_REPLACE
def tex_hash(expression, template_tex_file): def tex_hash(expression, template_tex_file):
return str(hash(expression + template_tex_file)) id_str = str(expression + template_tex_file)
hasher = hashlib.sha256()
hasher.update(id_str.encode())
# Truncating at 16 bytes for cleanliness
return hasher.hexdigest()[:16]
def tex_to_svg_file(expression, template_tex_file): def tex_to_svg_file(expression, template_tex_file):