Files
OmniLottie/lottie/parsers/svg/builder.py
T
2026-03-01 21:36:54 +08:00

720 lines
28 KiB
Python

import re
import math
from xml.etree import ElementTree
from .handler import SvgHandler, NameMode
from ... import objects
from ...nvector import NVector
from ...utils import restructure
from ...utils.transform import TransformMatrix
try:
from ...utils import font
has_font = True
except ImportError:
has_font = False
class PrecompTime:
def __init__(self, pcl: objects.PreCompLayer):
self.pcl = pcl
def get_time_offset(self, time, lot):
remap = time
if self.pcl.time_remapping:
remapf = self.pcl.time_remapping.get_value(time)
remap = lot.in_point * (1-remapf) + lot.out_point * remapf
return remap - self.pcl.start_time
class SvgBuilder(SvgHandler, restructure.AbstractBuilder):
merge_paths = True
namestart = (
r":_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF" +
r"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" +
r"\uFDF0-\uFFFD\U00010000-\U000EFFFF"
)
namenostart = r"-.0-9\xB7\u0300-\u036F\u203F-\u2040"
id_re = re.compile("^[%s][%s%s]*$" % (namestart, namenostart, namestart))
def __init__(self, time=0):
super().__init__()
self.svg = ElementTree.Element("svg")
self.dom = ElementTree.ElementTree(self.svg)
self.svg.attrib["xmlns"] = self.ns_map["svg"]
self.ids = set()
self.idc = 0
self.name_mode = NameMode.Inkscape
self.actual_time = time
self.precomp_times = []
self._precomps = {}
self._assets = {}
self._current_layer = []
@property
def time(self):
time = self.actual_time
if self.precomp_times:
for pct in self.precomp_times:
time = pct.get_time_offset(time, self._current_layer[-1])
return time
def gen_id(self, prefix="id"):
while True:
self.idc += 1
id = "%s_%s" % (prefix, self.idc)
if id not in self.ids:
break
self.ids.add(id)
return id
def set_clean_id(self, dom, n):
idn = n.replace(" ", "_")
if self.id_re.match(idn) and idn not in self.ids:
self.ids.add(idn)
else:
idn = self.gen_id(dom.tag)
dom.attrib["id"] = idn
return idn
def set_id(self, dom, lottieobj, inkscape_qual=None, force=False):
n = getattr(lottieobj, "name", None)
if n is None or self.name_mode == NameMode.NoName:
if force:
id = self.gen_id(dom.tag)
dom.attrib["id"] = id
return id
return None
idn = self.set_clean_id(dom, n)
if inkscape_qual is None:
inkscape_qual = self.qualified("inkscape", "label")
if inkscape_qual:
dom.attrib[inkscape_qual] = n
return idn
def _on_animation(self, animation: objects.Animation):
self.svg.attrib["width"] = str(animation.width)
self.svg.attrib["height"] = str(animation.height)
self.svg.attrib["viewBox"] = "0 0 %s %s" % (animation.width, animation.height)
self.svg.attrib["version"] = "1.1"
self.set_id(self.svg, animation, self.qualified("sodipodi", "docname"))
self.defs = ElementTree.SubElement(self.svg, "defs")
if self.name_mode == NameMode.Inkscape:
self.svg.attrib[self.qualified("inkscape", "export-xdpi")] = "96"
self.svg.attrib[self.qualified("inkscape", "export-ydpi")] = "96"
namedview = ElementTree.SubElement(self.svg, self.qualified("sodipodi", "namedview"))
namedview.attrib[self.qualified("inkscape", "pagecheckerboard")] = "true"
namedview.attrib["borderlayer"] = "true"
namedview.attrib["bordercolor"] = "#666666"
namedview.attrib["pagecolor"] = "#ffffff"
self.svg.attrib["style"] = "fill: none; stroke: none"
self._current_layer = [animation]
return self.svg
def _mask_to_def(self, mask):
svgmask = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
svgmask.attrib["id"] = mask_id
svgmask.attrib["mask-type"] = "alpha"
path = ElementTree.SubElement(svgmask, "path")
path.attrib["d"] = self._bezier_to_d(mask.shape.get_value(self.time))
path.attrib["fill"] = "#fff"
path.attrib["fill-opacity"] = str(mask.opacity.get_value(self.time) / 100)
return mask_id
def _matte_source_to_def(self, layer_builder):
svgmask = ElementTree.SubElement(self.defs, "mask")
if not layer_builder.matte_id:
layer_builder.matte_id = self.gen_id()
svgmask.attrib["id"] = layer_builder.matte_id
matte_mode = layer_builder.matte_target.lottie.matte_mode
mask_type = "alpha"
if matte_mode == objects.MatteMode.Luma:
mask_type = "luminance"
svgmask.attrib["mask-type"] = mask_type
return svgmask
def _on_masks(self, masks):
if len(masks) == 1:
return self._mask_to_def(masks[0])
mask_ids = list(map(self._mask_to_def, masks))
mask_def = ElementTree.SubElement(self.defs, "mask")
mask_id = self.gen_id()
mask_def.attrib["id"] = mask_id
g = mask_def
for mid in mask_ids:
g = ElementTree.SubElement(g, "g")
g.attrib["mask"] = "url(#%s)" % mid
full = ElementTree.SubElement(g, "rect")
full.attrib["fill"] = "#fff"
full.attrib["width"] = self.svg.attrib["width"]
full.attrib["height"] = self.svg.attrib["height"]
full.attrib["x"] = "0"
full.attrib["y"] = "0"
return mask_id
def _on_layer(self, layer_builder, dom_parent):
lot = layer_builder.lottie
self._current_layer.append(lot)
if not self.precomp_times and (lot.in_point > self.time or lot.out_point < self.time):
self._current_layer.pop()
return None
if layer_builder.matte_target:
dom_parent = self._matte_source_to_def(layer_builder)
g = self.group_from_lottie(lot, dom_parent, True)
if lot.masks:
g.attrib["mask"] = "url(#%s)" % self._on_masks(lot.masks)
elif layer_builder.matte_source:
matte_id = layer_builder.matte_source.matte_id
if not matte_id:
matte_id = layer_builder.matte_source.matte_id = self.gen_id()
g.attrib["mask"] = "url(#%s)" % matte_id
if isinstance(lot, objects.PreCompLayer):
self.precomp_times.append(PrecompTime(lot))
for layer in self._precomps.get(lot.reference_id, []):
self.process_layer(layer, g)
self.precomp_times.pop()
elif isinstance(lot, objects.NullLayer):
g.attrib["opacity"] = "1"
elif isinstance(lot, objects.ImageLayer):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + self._assets[lot.image_id]
elif isinstance(lot, objects.TextLayer):
self._on_text_layer(g, lot)
elif isinstance(lot, objects.SolidColorLayer):
rect = ElementTree.SubElement(g, "rect")
rect.attrib["width"] = str(lot.width)
rect.attrib["height"] = str(lot.height)
rect.attrib["fill"] = lot.color
if not lot.name:
g.attrib[self.qualified("inkscape", "label")] = lot.__class__.__name__
if layer_builder.shapegroup:
g.attrib["style"] = self.group_to_style(layer_builder.shapegroup)
self._split_stroke(layer_builder.shapegroup, g, dom_parent)
#if lot.hidden:
#g.attrib.setdefault("style", "")
#g.attrib["style"] += "display: none;"
return g
def _on_text_layer(self, g, lot):
text = ElementTree.SubElement(g, "text")
doc = lot.data.get_value(self.time)
if doc:
text.attrib["font-family"] = doc.font_family
text.attrib["font-size"] = str(doc.font_size)
if doc.line_height:
text.attrib["line-height"] = "%s%%" % doc.line_height
if doc.justify == objects.text.TextJustify.Left:
text.attrib["text-align"] = "start"
elif doc.justify == objects.text.TextJustify.Center:
text.attrib["text-align"] = "center"
elif doc.justify == objects.text.TextJustify.Right:
text.attrib["text-align"] = "end"
text.attrib["fill"] = color_to_css(doc.color)
text.text = doc.text
def _on_layer_end(self, out_layer):
self._current_layer.pop()
def _on_precomp(self, id, dom_parent, layers):
self._precomps[id] = layers
def _on_asset(self, asset):
if isinstance(asset, objects.assets.Image):
img = ElementTree.SubElement(self.defs, "image")
xmlid = self.set_clean_id(img, asset.id)
self._assets[asset.id] = xmlid
if asset.is_embedded:
url = asset.image
else:
url = asset.image_path + asset.image
img.attrib[self.qualified("xlink", "href")] = url
img.attrib["width"] = str(asset.width)
img.attrib["height"] = str(asset.height)
def _get_value(self, prop, default=NVector(0, 0)):
if prop:
v = prop.get_value(self.time)
else:
v = default
if v is None:
return default
if isinstance(v, NVector):
return v.clone()
return v
def set_transform(self, dom, transform, auto_orient=False):
mat = transform.to_matrix(self.time, auto_orient)
dom.attrib["transform"] = mat.to_css_2d()
if transform.opacity is not None:
op = transform.opacity.get_value(self.time)
if op != 100:
dom.attrib["opacity"] = str(op/100)
def _get_group_stroke(self, group):
style = {}
if group.stroke:
if isinstance(group.stroke, objects.GradientStroke):
style["stroke"] = "url(#%s)" % self.process_gradient(group.stroke)
else:
style["stroke"] = color_to_css(group.stroke.color.get_value(self.time))
style["stroke-opacity"] = group.stroke.opacity.get_value(self.time) / 100
style["stroke-width"] = group.stroke.width.get_value(self.time)
if group.stroke.miter_limit is not None:
style["stroke-miterlimit"] = group.stroke.miter_limit
if group.stroke.line_cap == objects.LineCap.Round:
style["stroke-linecap"] = "round"
elif group.stroke.line_cap == objects.LineCap.Butt:
style["stroke-linecap"] = "butt"
elif group.stroke.line_cap == objects.LineCap.Square:
style["stroke-linecap"] = "square"
if group.stroke.line_join == objects.LineJoin.Round:
style["stroke-linejoin"] = "round"
elif group.stroke.line_join == objects.LineJoin.Bevel:
style["stroke-linejoin"] = "bevel"
elif group.stroke.line_join == objects.LineJoin.Miter:
style["stroke-linejoin"] = "miter"
if group.stroke.dashes:
dasharray = []
last = 0
last_mode = objects.StrokeDashType.Dash
for dash in group.stroke.dashes:
if last_mode == dash.type:
last += dash.length.get_value(self.time)
else:
if last_mode != objects.StrokeDashType.Offset:
dasharray.append(str(last))
last = 0
last_mode = dash.type
style["stroke-dasharray"] = " ".join(dasharray)
return style
def _style_to_css(self, style):
return ";".join(map(
lambda x: ":".join(map(str, x)),
style.items()
))
def _split_stroke(self, group, fill_layer, out_parent):
if not group.stroke:# or group.stroke_above:
return
style = self._get_group_stroke(group)
if style.get("stroke-width", 0) <= 0 or style["stroke-opacity"] <= 0:
return
if group.stroke_above:
if fill_layer.attrib.get("style", ""):
fill_layer.attrib["style"] += ";"
else:
fill_layer.attrib["style"] = ""
fill_layer.attrib["style"] += self._style_to_css(style)
return fill_layer
g = ElementTree.Element("g")
self.set_clean_id(g, "stroke")
use = ElementTree.Element("use")
for i, e in enumerate(out_parent):
if e is fill_layer:
out_parent.insert(i, g)
out_parent.remove(fill_layer)
break
else:
return
g.append(use)
g.append(fill_layer)
use.attrib[self.qualified("xlink", "href")] = "#" + fill_layer.attrib["id"]
use.attrib["style"] = self._style_to_css(style)
return g
def group_to_style(self, group):
style = {}
if group.fill:
style["fill-opacity"] = group.fill.opacity.get_value(self.time) / 100
if isinstance(group.fill, objects.GradientFill):
style["fill"] = "url(#%s)" % self.process_gradient(group.fill)
else:
style["fill"] = color_to_css(group.fill.color.get_value(self.time))
if group.fill.fill_rule:
style["fill-rule"] = "evenodd" if group.fill.fill_rule == objects.FillRule.EvenOdd else "nonzero"
if group.lottie.hidden:
style["display"] = "none"
#if group.stroke_above:
#style.update(self._get_group_stroke(group))
return self._style_to_css(style)
def process_gradient(self, gradient):
spos = gradient.start_point.get_value(self.time)
epos = gradient.end_point.get_value(self.time)
if gradient.gradient_type == objects.GradientType.Linear:
dom = ElementTree.SubElement(self.defs, "linearGradient")
dom.attrib["x1"] = str(spos[0])
dom.attrib["y1"] = str(spos[1])
dom.attrib["x2"] = str(epos[0])
dom.attrib["y2"] = str(epos[1])
elif gradient.gradient_type == objects.GradientType.Radial:
dom = ElementTree.SubElement(self.defs, "radialGradient")
dom.attrib["cx"] = str(spos[0])
dom.attrib["cy"] = str(spos[1])
dom.attrib["r"] = str((epos-spos).length)
a = gradient.highlight_angle.get_value(self.time) * math.pi / 180
l = gradient.highlight_length.get_value(self.time)
dom.attrib["fx"] = str(spos[0] + math.cos(a) * l)
dom.attrib["fy"] = str(spos[1] + math.sin(a) * l)
id = self.set_id(dom, gradient, force=True)
dom.attrib["gradientUnits"] = "userSpaceOnUse"
for off, color in gradient.colors.stops_at(self.time):
stop = ElementTree.SubElement(dom, "stop")
stop.attrib["offset"] = "%s%%" % (off * 100)
stop.attrib["stop-color"] = color_to_css(color[:3])
if len(color) > 3:
stop.attrib["stop-opacity"] = str(color[3])
return id
def group_from_lottie(self, lottie, dom_parent, layer):
g = ElementTree.SubElement(dom_parent, "g")
if layer and self.name_mode == NameMode.Inkscape:
g.attrib[self.qualified("inkscape", "groupmode")] = "layer"
self.set_id(g, lottie, force=True)
self.set_transform(g, lottie.transform, getattr(lottie, "auto_orient", False))
return g
def _on_shapegroup(self, group, dom_parent):
if group.empty():
return
if len(group.children) == 1 and isinstance(group.children[0], restructure.RestructuredPathMerger):
path = self.build_path(group.paths.paths, dom_parent)
self.set_id(path, group.paths.paths[0], force=True)
path.attrib["style"] = self.group_to_style(group)
self.set_transform(path, group.lottie.transform)
return self._split_stroke(group, path, dom_parent)
g = self.group_from_lottie(group.lottie, dom_parent, group.layer)
g.attrib["style"] = self.group_to_style(group)
self.shapegroup_process_children(group, g)
return self._split_stroke(group, g, dom_parent)
def _on_merged_path(self, shape, shapegroup, out_parent):
path = self.build_path(shape.paths, out_parent)
self.set_id(path, shape.paths[0])
path.attrib["style"] = self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, path, out_parent)
return path
def _on_shape(self, shape, shapegroup, out_parent):
if isinstance(shape, objects.Rect):
svgshape = self.build_rect(shape, out_parent)
elif isinstance(shape, objects.Ellipse):
svgshape = self.build_ellipse(shape, out_parent)
elif isinstance(shape, objects.Star):
svgshape = self.build_path([shape.to_bezier()], out_parent)
elif isinstance(shape, objects.Path):
svgshape = self.build_path([shape], out_parent)
elif has_font and isinstance(shape, font.FontShape):
svgshape = self.build_text(shape, out_parent)
else:
return
self.set_id(svgshape, shape, force=True)
if "style" not in svgshape.attrib:
svgshape.attrib["style"] = ""
svgshape.attrib["style"] += self.group_to_style(shapegroup)
#self._split_stroke(shapegroup, svgshape, out_parent)
if shape.hidden:
svgshape.attrib["style"] += "display: none;"
return svgshape
def build_rect(self, shape, parent):
rect = ElementTree.SubElement(parent, "rect")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
rect.attrib["width"] = str(size[0])
rect.attrib["height"] = str(size[1])
rect.attrib["x"] = str(pos[0] - size[0] / 2)
rect.attrib["y"] = str(pos[1] - size[1] / 2)
rect.attrib["rx"] = str(shape.rounded.get_value(self.time))
return rect
def build_ellipse(self, shape, parent):
ellipse = ElementTree.SubElement(parent, "ellipse")
size = shape.size.get_value(self.time)
pos = shape.position.get_value(self.time)
ellipse.attrib["rx"] = str(size[0] / 2)
ellipse.attrib["ry"] = str(size[1] / 2)
ellipse.attrib["cx"] = str(pos[0])
ellipse.attrib["cy"] = str(pos[1])
return ellipse
def build_path(self, shapes, parent):
path = ElementTree.SubElement(parent, "path")
d = ""
for shape in shapes:
bez = shape.shape.get_value(self.time)
if isinstance(bez, list):
bez = bez[0]
if not bez.vertices:
continue
if d:
d += "\n"
d += self._bezier_to_d(bez)
path.attrib["d"] = d
return path
def _bezier_tangent(self, tangent):
_tangent_threshold = 0.5
if tangent.length < _tangent_threshold:
return NVector(0, 0)
return tangent
def _bezier_to_d(self, bez):
d = "M %s,%s " % tuple(bez.vertices[0].components[:2])
for i in range(1, len(bez.vertices)):
qfrom = bez.vertices[i-1]
h1 = self._bezier_tangent(bez.out_tangents[i-1]) + qfrom
qto = bez.vertices[i]
h2 = self._bezier_tangent(bez.in_tangents[i]) + qto
d += "C %s,%s %s,%s %s,%s " % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
if bez.closed:
qfrom = bez.vertices[-1]
h1 = self._bezier_tangent(bez.out_tangents[-1]) + qfrom
qto = bez.vertices[0]
h2 = self._bezier_tangent(bez.in_tangents[0]) + qto
d += "C %s,%s %s,%s %s,%s Z" % (
h1[0], h1[1],
h2[0], h2[1],
qto[0], qto[1],
)
return d
def _on_shape_modifier(self, shape, shapegroup, out_parent):
if isinstance(shape.lottie, objects.Repeater):
svgshape = self.build_repeater(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.RoundedCorners):
svgshape = self.build_rouded_corners(shape.lottie, shape.child, shapegroup, out_parent)
elif isinstance(shape.lottie, objects.Trim):
svgshape = self.build_trim_path(shape.lottie, shape.child, shapegroup, out_parent)
else:
return self.shapegroup_process_child(shape.child, shapegroup, out_parent)
return svgshape
def build_repeater(self, shape, child, shapegroup, out_parent):
original = self.shapegroup_process_child(child, shapegroup, out_parent)
if not original:
return
ncopies = int(round(shape.copies.get_value(self.time)))
if ncopies == 1:
return
out_parent.remove(original)
g = ElementTree.SubElement(out_parent, "g")
self.set_clean_id(g, "repeater")
for copy in range(ncopies-1):
use = ElementTree.SubElement(g, "use")
use.attrib[self.qualified("xlink", "href")] = "#" + original.attrib["id"]
orig_wrapper = ElementTree.SubElement(g, "g")
orig_wrapper.append(original)
transform = objects.Transform()
so = shape.transform.start_opacity.get_value(self.time)
eo = shape.transform.end_opacity.get_value(self.time)
position = shape.transform.position.get_value(self.time)
rotation = shape.transform.rotation.get_value(self.time)
anchor_point = shape.transform.anchor_point.get_value(self.time)
for i in range(ncopies-1, -1, -1):
of = i / (ncopies-1)
transform.opacity.value = so * of + eo * (1 - of)
self.set_transform(g[i], transform)
transform.position.value += position
transform.rotation.value += rotation
transform.anchor_point.value += anchor_point
return g
def build_rouded_corners(self, shape, child, shapegroup, out_parent):
round_amount = shape.radius.get_value(self.time)
return self._modifier_process(child, shapegroup, out_parent, self._build_rouded_corners_shape, round_amount)
def _build_rouded_corners_shape(self, shape, round_amount):
if not isinstance(shape, objects.Shape):
return [shape]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time).rounded(round_amount)
path.shape.clear_animation(bezier)
return [path]
def build_trim_path(self, shape, child, shapegroup, out_parent):
start = max(0, min(1, shape.start.get_value(self.time) / 100))
end = max(0, min(1, shape.end.get_value(self.time) / 100))
offset = shape.offset.get_value(self.time) / 360 % 1
multidata = {}
length = 0
if shape.multiple == objects.TrimMultipleShapes.Individually:
for visishape in reversed(list(self._modifier_foreach_shape(child))):
bez = visishape.to_bezier().shape.get_value(self.time)
local_length = bez.rough_length()
multidata[visishape] = (bez, length, local_length)
length += local_length
return self._modifier_process(
child, shapegroup, out_parent, self._build_trim_path_shape,
start+offset, end+offset, multidata, length
)
def _modifier_foreach_shape(self, shape):
if isinstance(shape, restructure.RestructuredShapeGroup):
for child in shape.children:
for chsh in self._modifier_foreach_shape(child):
yield chsh
elif isinstance(shape, restructure.RestructuredPathMerger):
for p in shape.paths:
yield p
elif isinstance(shape, objects.Shape):
yield shape
def _modifier_process(self, child, shapegroup, out_parent, callback, *args):
children = self._modifier_process_child(child, shapegroup, out_parent, callback, *args)
return [self.shapegroup_process_child(ch, shapegroup, out_parent) for ch in children]
def _trim_offlocal(self, t, local_start, local_length, total_length):
gt = (t * total_length - local_start) / local_length
return max(0, min(1, gt))
def _build_trim_path_shape(self, shape, start, end, multidata, total_length):
if not isinstance(shape, objects.Shape):
return [shape]
if multidata:
bezier, local_start, local_length = multidata[shape]
if end > 1:
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end-1, local_start, local_length, total_length)
out = []
if lstart < 1:
out.append(objects.Path(bezier.segment(lstart, 1)))
if lend > 0:
out.append(objects.Path(bezier.segment(0, lend)))
return out
lstart = self._trim_offlocal(start, local_start, local_length, total_length)
lend = self._trim_offlocal(end, local_start, local_length, total_length)
if lend <= 0 or lstart >= 1:
return []
if lstart <= 0 and lend >= 1:
return [objects.Path(bezier)]
seg = bezier.segment(lstart, lend)
return [objects.Path(seg)]
path = shape.to_bezier()
bezier = path.shape.get_value(self.time)
if end > 1:
bez1 = bezier.segment(start, 1)
bez2 = bezier.segment(0, end-1)
return [objects.Path(bez1), objects.Path(bez2)]
else:
seg = bezier.segment(start, end)
return [objects.Path(seg)]
def _modifier_process_children(self, shapegroup, out_parent, callback, *args):
children = []
for shape in shapegroup.children:
children.extend(self._modifier_process_child(shape, shapegroup, out_parent, callback, *args))
shapegroup.children = children
def _modifier_process_child(self, shape, shapegroup, out_parent, callback, *args):
if isinstance(shape, restructure.RestructuredShapeGroup):
self._modifier_process_children(shape, out_parent, callback, *args)
return [shape]
elif isinstance(shape, restructure.RestructuredPathMerger):
paths = []
for p in shape.paths:
paths.extend(callback(p, *args))
shape.paths = paths
if paths:
return [shape]
return []
else:
return callback(shape, *args)
def _custom_object_supported(self, shape):
if has_font and isinstance(shape, font.FontShape):
return True
return False
def build_text(self, shape, parent):
text = ElementTree.SubElement(parent, "text")
if "family" in shape.query:
text.attrib["font-family"] = shape.query["family"]
if "weight" in shape.query:
text.attrib["font-weight"] = str(shape.query.weight_to_css())
slant = int(shape.query.get("slant", 0))
if slant > 0 and slant < 110:
text.attrib["font-style"] = "italic"
elif slant >= 110:
text.attrib["font-style"] = "oblique"
text.attrib["font-size"] = str(shape.size)
text.attrib["white-space"] = "pre"
pos = shape.style.position
text.attrib["x"] = str(pos.x)
text.attrib["y"] = str(pos.y)
text.text = shape.text
return text
def color_to_css(color):
#if len(color) == 4:
#return ("rgba(%s, %s, %s" % tuple(map(lambda c: int(round(c*255)), color[:3]))) + ", %s)" % color[3]
return "rgb(%s, %s, %s)" % tuple(map(lambda c: int(round(c*255)), color[:3]))
def to_svg(animation, time):
builder = SvgBuilder(time)
builder.process(animation)
return builder.dom